6871f072cfdd9980b839d0a8d1b9e7172058196a
[odoo/odoo.git] / addons / point_of_sale / point_of_sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import pdb
23 import openerp
24 import addons
25
26 import time
27 from datetime import datetime
28 from dateutil.relativedelta import relativedelta
29 import logging
30
31 import netsvc
32 from osv import fields, osv
33 import tools
34 from tools.translate import _
35 from decimal import Decimal
36 import decimal_precision as dp
37
38 _logger = logging.getLogger(__name__)
39
40 class pos_config(osv.osv):
41     _name = 'pos.config'
42
43     POS_CONFIG_STATE = [
44         ('active', 'Active'),
45         ('inactive', 'Inactive'),
46         ('deprecated', 'Deprecated')
47     ]
48
49     _columns = {
50         'name' : fields.char('Point of Sale Name', size=32, select=1,
51              required=True, help="An internal identification of the point of sale"),
52         'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', 
53              'pos_config_id', 'journal_id', 'Available Payment Methods',
54              domain="[('journal_user', '=', True )]",),
55         'shop_id' : fields.many2one('sale.shop', 'Shop',
56              required=True),
57         'journal_id' : fields.many2one('account.journal', 'Sale Journal',
58              domain=[('type', '=', 'sale')],
59              help="Accounting journal used to post sales entries."),
60         'iface_self_checkout' : fields.boolean('Self Checkout Mode',
61              help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, OpenERP uses the normal cashier mode by default."),
62         'iface_websql' : fields.boolean('WebSQL (Faster but Chrome Only)',
63             help="If have more than 200 products, it's highly suggested to use WebSQL "\
64                 "to store the data in the browser, instead of localStore mechanism. "\
65                 "It's more efficient but works on the Chrome browser only."
66             ),
67         'iface_led' : fields.boolean('Help Notification'),
68         'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'),
69         'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'),
70         'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'),
71         'iface_barscan' : fields.boolean('BarScan Interface'), 
72         'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'),
73         'iface_print_via_proxy' : fields.boolean('Print via Proxy'),
74
75         'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True),
76         'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True,
77             help="This sequence is automatically created by OpenERP but you can change it "\
78                 "to customize the reference numbers of your orders."),
79         'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'),
80         'group_by' : fields.boolean('Group By', help="Check this if you want to group the Journal Items by Product while closing a Session"),
81     }
82
83     def name_get(self, cr, uid, ids, context=None):
84         result = []
85         states = {
86             'opening_control': _('Opening Control'),
87             'opened': _('In Progress'),
88             'closing_control': _('Closing Control'),
89             'closed': _('Closed & Posted'),
90         }
91         for record in self.browse(cr, uid, ids, context=context):
92             if (not record.session_ids) or (record.session_ids[0].state=='closed'):
93                 result.append((record.id, record.name+' ('+_('not used')+')'))
94                 continue
95             session = record.session_ids[0]
96             result.append((record.id, record.name + ' ('+session.user_id.name+', '+states[session.state]+')'))
97         return result
98
99
100     def _default_payment_journal(self, cr, uid, context=None):
101         res = self.pool.get('account.journal').search(cr, uid, [('type', 'in', ('bank','cash'))], limit=2)
102         return res or []
103
104     def _default_sale_journal(self, cr, uid, context=None):
105         res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
106         return res and res[0] or False
107
108     def _default_shop(self, cr, uid, context=None):
109         res = self.pool.get('sale.shop').search(cr, uid, [])
110         return res and res[0] or False
111
112     _defaults = {
113         'state' : POS_CONFIG_STATE[0][0],
114         'shop_id': _default_shop,
115         'journal_id': _default_sale_journal,
116         'journal_ids': _default_payment_journal,
117         'group_by' : True,
118     }
119
120     def set_active(self, cr, uid, ids, context=None):
121         return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
122
123     def set_inactive(self, cr, uid, ids, context=None):
124         return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context)
125
126     def set_deprecate(self, cr, uid, ids, context=None):
127         return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context)
128
129     def create(self, cr, uid, values, context=None):
130         proxy = self.pool.get('ir.sequence')
131         sequence_values = dict(
132             name='PoS %s' % values['name'],
133             padding=5,
134             prefix="%s/"  % values['name'],
135         )
136         sequence_id = proxy.create(cr, uid, sequence_values, context=context)
137         values['sequence_id'] = sequence_id
138         return super(pos_config, self).create(cr, uid, values, context=context)
139
140     def unlink(self, cr, uid, ids, context=None):
141         for obj in self.browse(cr, uid, ids, context=context):
142             if obj.sequence_id:
143                 obj.sequence_id.unlink()
144         return super(pos_config, self).unlink(cr, uid, ids, context=context)
145
146 pos_config()
147
148 class pos_session(osv.osv):
149     _name = 'pos.session'
150     _order = 'id desc'
151
152     POS_SESSION_STATE = [
153         ('opening_control', 'Opening Control'),  # Signal open
154         ('opened', 'In Progress'),                    # Signal closing
155         ('closing_control', 'Closing Control'),  # Signal close
156         ('closed', 'Closed & Posted'),
157     ]
158
159     def _compute_cash_journal_id(self, cr, uid, ids, fieldnames, args, context=None):
160         result = dict.fromkeys(ids, False)
161         for record in self.browse(cr, uid, ids, context=context):
162             for st in record.statement_ids:
163                 if st.journal_id.type == 'cash':
164                     result[record.id] = st.journal_id.id
165                     break
166         return result
167
168     def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None):
169         result = dict.fromkeys(ids, False)
170         for record in self.browse(cr, uid, ids, context=context):
171             for st in record.statement_ids:
172                 if st.journal_id.type == 'cash':
173                     result[record.id] = st.id
174                     break
175         return result
176
177     def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None):
178         result = {}
179
180         for record in self.browse(cr, uid, ids, context=context):
181             has_opening_control = False
182             has_closing_control = False
183
184             for journal in record.config_id.journal_ids:
185                 if journal.opening_control == True:
186                     has_opening_control = True
187                 if journal.closing_control == True:
188                     has_closing_control = True
189
190                 if has_opening_control and has_closing_control:
191                     break
192
193             values = {
194                 'has_opening_control': has_opening_control,
195                 'has_closing_control': has_closing_control,
196             }
197             result[record.id] = values
198
199         return result
200
201     _columns = {
202         'config_id' : fields.many2one('pos.config', 'Point of Sale',
203                                       help="The physical point of sale you will use.",
204                                       required=True,
205                                       select=1,
206                                       domain="[('state', '=', 'active')]",
207                                      ),
208
209         'name' : fields.char('Session ID', size=32, required=True, readonly=True),
210         'user_id' : fields.many2one('res.users', 'Responsible',
211                                     required=True,
212                                     select=1,
213                                     readonly=True,
214                                     states={'opening_control' : [('readonly', False)]}
215                                    ),
216         'start_at' : fields.datetime('Opening Date', readonly=True), 
217         'stop_at' : fields.datetime('Closing Date', readonly=True),
218
219         'state' : fields.selection(POS_SESSION_STATE, 'State',
220                 required=True, readonly=True,
221                 select=1),
222
223         'cash_journal_id' : fields.function(_compute_cash_journal_id, method=True, 
224                 type='many2one', relation='account.journal',
225                 string='Cash Journal', store=True),
226         'cash_register_id' : fields.function(_compute_cash_register_id, method=True, 
227                 type='many2one', relation='account.bank.statement',
228                 string='Cash Register', store=True),
229
230         'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids', 
231                 type='one2many', relation='account.cashbox.line',
232                 string='Opening Cash Control'),
233         'details_ids' : fields.related('cash_register_id', 'details_ids', 
234                 type='one2many', relation='account.cashbox.line',
235                 string='Cash Control'),
236
237         'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real',
238                 type='float',
239                 digits_compute=dp.get_precision('Account'),
240                 string="Ending Balance",
241                 help="Computed using the cash control lines",
242                 readonly=True),
243         'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start',
244                 type='float',
245                 digits_compute=dp.get_precision('Account'),
246                 string="Starting Balance",
247                 help="Computed using the cash control at the opening.",
248                 readonly=True),
249         'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding',
250                 string='Total Cash Transaction',
251                 readonly=True),
252         'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end',
253                 type='float',
254                 digits_compute=dp.get_precision('Account'),
255                 string="Computed Balance",
256                 help="Computed with the initial cash control and the sum of all payments.",
257                 readonly=True),
258         'cash_register_difference' : fields.related('cash_register_id', 'difference',
259                 type='float',
260                 string='Difference',
261                 help="Difference between the counted cash control at the closing and the computed balance.",
262                 readonly=True),
263
264         'journal_ids' : fields.related('config_id', 'journal_ids',
265                                        type='many2many',
266                                        readonly=True,
267                                        relation='account.journal',
268                                        string='Available Payment Methods'),
269         'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
270
271         'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True),
272         'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'),
273         'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'),
274     }
275
276     _defaults = {
277         'name' : '/',
278         'user_id' : lambda obj, cr, uid, context: uid,
279         'state' : 'opening_control',
280     }
281
282     _sql_constraints = [
283         ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
284     ]
285
286     def _check_unicity(self, cr, uid, ids, context=None):
287         for session in self.browse(cr, uid, ids, context=None):
288             # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
289             domain = [
290                 ('state', 'not in', ('closed','closing_control')),
291                 ('user_id', '=', uid)
292             ]
293             count = self.search_count(cr, uid, domain, context=context)
294             if count>1:
295                 return False
296         return True
297
298     def _check_pos_config(self, cr, uid, ids, context=None):
299         for session in self.browse(cr, uid, ids, context=None):
300             domain = [
301                 ('state', '!=', 'closed'),
302                 ('config_id', '=', session.config_id.id)
303             ]
304             count = self.search_count(cr, uid, domain, context=context)
305             if count>1:
306                 return False
307         return True
308
309     _constraints = [
310         (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']),
311         (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']),
312     ]
313
314     def create(self, cr, uid, values, context=None):
315         config_id = values.get('config_id', False) or False
316         if config_id:
317             # journal_id is not required on the pos_config because it does not
318             # exists at the installation. If nothing is configured at the
319             # installation we do the minimal configuration. Impossible to do in
320             # the .xml files as the CoA is not yet installed.
321             jobj = self.pool.get('pos.config')
322             pos_config = jobj.browse(cr, uid, config_id, context=context)
323             if not pos_config.journal_id:
324                 jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id']
325                 if jid:
326                     jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context)
327                 else:
328                     raise osv.except_osv( _('error!'),
329                         _("Unable to open the session. You have to assign a sale journal to your point of sale."))
330
331             # define some cash journal if no payment method exists
332             if not pos_config.journal_ids:
333                 cashids = self.pool.get('account.journal').search(cr, uid, [('journal_user','=',True)], context=context)
334                 if not cashids:
335                     cashids = self.pool.get('account.journal').search(cr, uid, [('type','=','cash')], context=context)
336                     self.pool.get('account.journal').write(cr, uid, cashids, {'journal_user': True})
337                 jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]})
338
339
340             pos_config = jobj.browse(cr, uid, config_id, context=context)
341             bank_statement_ids = []
342             for journal in pos_config.journal_ids:
343                 bank_values = {
344                     'journal_id' : journal.id,
345                     'user_id' : uid,
346                 }
347                 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
348                 bank_statement_ids.append(statement_id)
349
350             values.update({
351                 'name' : pos_config.sequence_id._next(),
352                 'statement_ids' : [(6, 0, bank_statement_ids)],
353                 'config_id': config_id
354             })
355
356         return super(pos_session, self).create(cr, uid, values, context=context)
357
358     def unlink(self, cr, uid, ids, context=None):
359         for obj in self.browse(cr, uid, ids, context=context):
360             for statement in obj.statement_ids:
361                 statement.unlink(context=context)
362         return True
363
364     def wkf_action_open(self, cr, uid, ids, context=None):
365         # second browse because we need to refetch the data from the DB for cash_register_id
366         for record in self.browse(cr, uid, ids, context=context):
367             values = {}
368             if not record.start_at:
369                 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
370             values['state'] = 'opened'
371             record.write(values, context=context)
372             for st in record.statement_ids:
373                 st.button_open(context=context)
374
375         return self.open_frontend_cb(cr, uid, ids, context=context)
376
377     def wkf_action_opening_control(self, cr, uid, ids, context=None):
378         return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context)
379
380     def wkf_action_closing_control(self, cr, uid, ids, context=None):
381         for session in self.browse(cr, uid, ids, context=context):
382             for statement in session.statement_ids:
383                 if statement.id <> session.cash_register_id.id:
384                     if statement.balance_end<>statement.balance_end_real:
385                         self.pool.get('account.bank.statement').write(cr, uid,
386                             [statement.id], {'balance_end_real': statement.balance_end})
387         return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
388
389     def wkf_action_close(self, cr, uid, ids, context=None):
390         # Close CashBox
391         bsl = self.pool.get('account.bank.statement.line')
392         for record in self.browse(cr, uid, ids, context=context):
393             for st in record.statement_ids:
394                 if abs(st.difference) > st.journal_id.amount_authorized_diff:
395                     # The pos manager can close statements with maximums.
396                     if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"):
397                         raise osv.except_osv( _('Error!'),
398                             _("Your ending balance is too different from the theorical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff))
399                 if st.difference:
400                     if st.difference > 0.0:
401                         name= _('Point of Sale Profit')
402                         account_id = st.journal_id.profit_account_id.id
403                     else:
404                         account_id = st.journal_id.loss_account_id.id
405                         name= _('Point of Sale Loss')
406                     if not account_id:
407                         raise osv.except_osv( _('Error!'),
408                         _("Please set your profit and loss accounts on your payment method '%s'. This will allow OpenERP to post the difference of %.2f in your ending balance. To close this session, you can update the 'Closing Cash Control' to avoid any difference.") % (st.journal_id.name,st.difference))
409                     bsl.create(cr, uid, {
410                         'statement_id': st.id,
411                         'amount': st.difference,
412                         'ref': record.name,
413                         'name': name,
414                         'account_id': account_id
415                     }, context=context)
416
417                 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
418         self._confirm_orders(cr, uid, ids, context=context)
419         self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
420
421         obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1]
422         return {
423             'type' : 'ir.actions.client',
424             'name' : 'Point of Sale Menu',
425             'tag' : 'reload',
426             'params' : {'menu_id': obj},
427         }
428
429     def _confirm_orders(self, cr, uid, ids, context=None):
430         wf_service = netsvc.LocalService("workflow")
431
432         for session in self.browse(cr, uid, ids, context=context):
433             order_ids = [order.id for order in session.order_ids if order.state == 'paid']
434
435             move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context)
436
437             self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context)
438
439             for order in session.order_ids:
440                 if order.state != 'paid':
441                     raise osv.except_osv(
442                         _('Error!'),
443                         _("You cannot confirm all orders of this session, because they have not the 'paid' status"))
444                 else:
445                     wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
446
447         return True
448
449     def open_frontend_cb(self, cr, uid, ids, context=None):
450         if not context:
451             context = {}
452         if not ids:
453             return {}
454         context.update({'session_id' : ids[0]})
455         return {
456             'type' : 'ir.actions.client',
457             'name' : 'Start Point Of Sale',
458             'tag' : 'pos.ui',
459             'context' : context,
460         }
461
462 pos_session()
463
464 class pos_order(osv.osv):
465     _name = "pos.order"
466     _description = "Point of Sale"
467     _order = "id desc"
468
469     def create_from_ui(self, cr, uid, orders, context=None):
470         #_logger.info("orders: %r", orders)
471         order_ids = []
472         for tmp_order in orders:
473             order = tmp_order['data']
474             order_id = self.create(cr, uid, {
475                 'name': order['name'],
476                 'user_id': order['user_id'] or False,
477                 'session_id': order['pos_session_id'],
478                 'lines': order['lines']
479             }, context)
480
481             for payments in order['statement_ids']:
482                 payment = payments[2]
483                 self.add_payment(cr, uid, order_id, {
484                     'amount': payment['amount'] or 0.0,
485                     'payment_date': payment['name'],
486                     'payment_name': payment.get('note', False),
487                     'journal': payment['journal_id']
488                 }, context=context)
489
490             if order['amount_return']:
491                 session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
492                 self.add_payment(cr, uid, order_id, {
493                     'amount': -order['amount_return'],
494                     'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
495                     'payment_name': _('return'),
496                     'journal': session.cash_journal_id.id
497                 }, context=context)
498             order_ids.append(order_id)
499             wf_service = netsvc.LocalService("workflow")
500             wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
501         return order_ids
502
503     def unlink(self, cr, uid, ids, context=None):
504         for rec in self.browse(cr, uid, ids, context=context):
505             if rec.state not in ('draft','cancel'):
506                 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
507         return super(pos_order, self).unlink(cr, uid, ids, context=context)
508
509     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
510         if not part:
511             return {'value': {}}
512         pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
513         return {'value': {'pricelist_id': pricelist}}
514
515     def _amount_all(self, cr, uid, ids, name, args, context=None):
516         tax_obj = self.pool.get('account.tax')
517         cur_obj = self.pool.get('res.currency')
518         res = {}
519         for order in self.browse(cr, uid, ids, context=context):
520             res[order.id] = {
521                 'amount_paid': 0.0,
522                 'amount_return':0.0,
523                 'amount_tax':0.0,
524             }
525             val1 = val2 = 0.0
526             cur = order.pricelist_id.currency_id
527             for payment in order.statement_ids:
528                 res[order.id]['amount_paid'] +=  payment.amount
529                 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
530             for line in order.lines:
531                 val1 += line.price_subtotal_incl
532                 val2 += line.price_subtotal
533             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
534             res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
535         return res
536
537     def copy(self, cr, uid, id, default=None, context=None):
538         if not default:
539             default = {}
540         d = {
541             'state': 'draft',
542             'invoice_id': False,
543             'account_move': False,
544             'picking_id': False,
545             'statement_ids': [],
546             'nb_print': 0,
547             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
548         }
549         d.update(default)
550         return super(pos_order, self).copy(cr, uid, id, d, context=context)
551
552     _columns = {
553         'name': fields.char('Order Ref', size=64, required=True, readonly=True),
554         'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
555         'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True),
556         'date_order': fields.datetime('Order Date', readonly=True, select=True),
557         'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It can be a reliever, a student or an interim employee."),
558         'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
559         'amount_total': fields.function(_amount_all, string='Total', multi='all'),
560         'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
561         'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
562         'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
563         'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
564         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
565         'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
566
567         'session_id' : fields.many2one('pos.session', 'Session', 
568                                         #required=True,
569                                         select=1,
570                                         domain="[('state', '=', 'opened')]",
571                                         states={'draft' : [('readonly', False)]},
572                                         readonly=True),
573
574         'state': fields.selection([('draft', 'New'),
575                                    ('cancel', 'Cancelled'),
576                                    ('paid', 'Paid'),
577                                    ('done', 'Posted'),
578                                    ('invoiced', 'Invoiced')],
579                                   'Status', readonly=True),
580
581         'invoice_id': fields.many2one('account.invoice', 'Invoice'),
582         'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
583         'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
584         'note': fields.text('Internal Notes'),
585         'nb_print': fields.integer('Number of Print', readonly=True),
586
587         'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
588     }
589
590     def _default_session(self, cr, uid, context=None):
591         so = self.pool.get('pos.session')
592         session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
593         return session_ids and session_ids[0] or False
594
595     def _default_pricelist(self, cr, uid, context=None):
596         res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
597         if res:
598             shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
599             return shop.pricelist_id and shop.pricelist_id.id or False
600         return False
601
602     _defaults = {
603         'user_id': lambda self, cr, uid, context: uid,
604         'state': 'draft',
605         'name': '/', 
606         'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
607         'nb_print': 0,
608         'session_id': _default_session,
609         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
610         'pricelist_id': _default_pricelist,
611     }
612
613     def create(self, cr, uid, values, context=None):
614         values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
615         return super(pos_order, self).create(cr, uid, values, context=context)
616
617     def test_paid(self, cr, uid, ids, context=None):
618         """A Point of Sale is paid when the sum
619         @return: True
620         """
621         for order in self.browse(cr, uid, ids, context=context):
622             if order.lines and not order.amount_total:
623                 return True
624             if (not order.lines) or (not order.statement_ids) or \
625                 (abs(order.amount_total-order.amount_paid) > 0.00001):
626                 return False
627         return True
628
629     def create_picking(self, cr, uid, ids, context=None):
630         """Create a picking for each order and validate it."""
631         picking_obj = self.pool.get('stock.picking')
632         partner_obj = self.pool.get('res.partner')
633         move_obj = self.pool.get('stock.move')
634
635         for order in self.browse(cr, uid, ids, context=context):
636             if not order.state=='draft':
637                 continue
638             addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
639             picking_id = picking_obj.create(cr, uid, {
640                 'origin': order.name,
641                 'partner_id': addr.get('delivery',False),
642                 'type': 'out',
643                 'company_id': order.company_id.id,
644                 'move_type': 'direct',
645                 'note': order.note or "",
646                 'invoice_state': 'none',
647                 'auto_picking': True,
648             }, context=context)
649             self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
650             location_id = order.shop_id.warehouse_id.lot_stock_id.id
651             output_id = order.shop_id.warehouse_id.lot_output_id.id
652
653             for line in order.lines:
654                 if line.product_id and line.product_id.type == 'service':
655                     continue
656                 if line.qty < 0:
657                     location_id, output_id = output_id, location_id
658
659                 move_obj.create(cr, uid, {
660                     'name': line.name,
661                     'product_uom': line.product_id.uom_id.id,
662                     'product_uos': line.product_id.uom_id.id,
663                     'picking_id': picking_id,
664                     'product_id': line.product_id.id,
665                     'product_uos_qty': abs(line.qty),
666                     'product_qty': abs(line.qty),
667                     'tracking_id': False,
668                     'state': 'draft',
669                     'location_id': location_id,
670                     'location_dest_id': output_id,
671                 }, context=context)
672                 if line.qty < 0:
673                     location_id, output_id = output_id, location_id
674
675             wf_service = netsvc.LocalService("workflow")
676             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
677             picking_obj.force_assign(cr, uid, [picking_id], context)
678         return True
679
680     def cancel_order(self, cr, uid, ids, context=None):
681         """ Changes order state to cancel
682         @return: True
683         """
684         stock_picking_obj = self.pool.get('stock.picking')
685         for order in self.browse(cr, uid, ids, context=context):
686             wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
687             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
688                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
689         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
690         return True
691
692     def add_payment(self, cr, uid, order_id, data, context=None):
693         """Create a new payment for the order"""
694         if not context:
695             context = {}
696         statement_obj = self.pool.get('account.bank.statement')
697         statement_line_obj = self.pool.get('account.bank.statement.line')
698         prod_obj = self.pool.get('product.product')
699         property_obj = self.pool.get('ir.property')
700         curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
701         curr_company = curr_c.id
702         order = self.browse(cr, uid, order_id, context=context)
703         args = {
704             'amount': data['amount'],
705         }
706         if 'payment_date' in data:
707             args['date'] = data['payment_date']
708         args['name'] = order.name
709         if data.get('payment_name', False):
710             args['name'] = args['name'] + ': ' + data['payment_name']
711         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
712         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
713                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
714         args['partner_id'] = order.partner_id and order.partner_id.id or None
715
716         if not args['account_id']:
717             if not args['partner_id']:
718                 msg = _('There is no receivable account defined to make payment.')
719             else:
720                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
721             raise osv.except_osv(_('Configuration Error!'), msg)
722
723         context.pop('pos_session_id', False)
724
725         try:
726             journal_id = long(data['journal'])
727         except Exception:
728             journal_id = False
729
730         statement_id = False
731         for statement in order.session_id.statement_ids:
732             if statement.journal_id.id == journal_id:
733                 statement_id = statement.id
734                 break
735
736         if not statement_id:
737             raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
738
739         args.update({
740             'statement_id' : statement_id,
741             'pos_statement_id' : order_id,
742             'journal_id' : journal_id,
743             'type' : 'customer',
744             'ref' : order.name,
745         })
746
747         statement_line_obj.create(cr, uid, args, context=context)
748
749         wf_service = netsvc.LocalService("workflow")
750         wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
751         wf_service.trg_write(uid, 'pos.order', order_id, cr)
752
753         return statement_id
754
755     def refund(self, cr, uid, ids, context=None):
756         """Create a copy of order  for refund order"""
757         clone_list = []
758         line_obj = self.pool.get('pos.order.line')
759         for order in self.browse(cr, uid, ids, context=context):
760             clone_id = self.copy(cr, uid, order.id, {
761                 'name': order.name + ' REFUND',
762             }, context=context)
763             clone_list.append(clone_id)
764
765         for clone in self.browse(cr, uid, clone_list, context=context):
766             for order_line in clone.lines:
767                 line_obj.write(cr, uid, [order_line.id], {
768                     'qty': -order_line.qty
769                 }, context=context)
770
771         new_order = ','.join(map(str,clone_list))
772         abs = {
773             #'domain': "[('id', 'in', ["+new_order+"])]",
774             'name': _('Return Products'),
775             'view_type': 'form',
776             'view_mode': 'form',
777             'res_model': 'pos.order',
778             'res_id':clone_list[0],
779             'view_id': False,
780             'context':context,
781             'type': 'ir.actions.act_window',
782             'nodestroy': True,
783             'target': 'current',
784         }
785         return abs
786
787     def action_invoice_state(self, cr, uid, ids, context=None):
788         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
789
790     def action_invoice(self, cr, uid, ids, context=None):
791         wf_service = netsvc.LocalService("workflow")
792         inv_ref = self.pool.get('account.invoice')
793         inv_line_ref = self.pool.get('account.invoice.line')
794         product_obj = self.pool.get('product.product')
795         inv_ids = []
796
797         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
798             if order.invoice_id:
799                 inv_ids.append(order.invoice_id.id)
800                 continue
801
802             if not order.partner_id:
803                 raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.'))
804
805             acc = order.partner_id.property_account_receivable.id
806             inv = {
807                 'name': order.name,
808                 'origin': order.name,
809                 'account_id': acc,
810                 'journal_id': order.sale_journal.id or None,
811                 'type': 'out_invoice',
812                 'reference': order.name,
813                 'partner_id': order.partner_id.id,
814                 'comment': order.note or '',
815                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
816             }
817             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
818             if not inv.get('account_id', None):
819                 inv['account_id'] = acc
820             inv_id = inv_ref.create(cr, uid, inv, context=context)
821
822             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
823             inv_ids.append(inv_id)
824             for line in order.lines:
825                 inv_line = {
826                     'invoice_id': inv_id,
827                     'product_id': line.product_id.id,
828                     'quantity': line.qty,
829                 }
830                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
831                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
832                                                                line.product_id.id,
833                                                                line.product_id.uom_id.id,
834                                                                line.qty, partner_id = order.partner_id.id,
835                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
836                 if line.product_id.description_sale:
837                     inv_line['note'] = line.product_id.description_sale
838                 inv_line['price_unit'] = line.price_unit
839                 inv_line['discount'] = line.discount
840                 inv_line['name'] = inv_name
841                 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
842                     and [(6, 0, inv_line['invoice_line_tax_id'])] or []
843                 inv_line_ref.create(cr, uid, inv_line, context=context)
844             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
845             wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
846
847         if not inv_ids: return {}
848
849         mod_obj = self.pool.get('ir.model.data')
850         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
851         res_id = res and res[1] or False
852         return {
853             'name': _('Customer Invoice'),
854             'view_type': 'form',
855             'view_mode': 'form',
856             'view_id': [res_id],
857             'res_model': 'account.invoice',
858             'context': "{'type':'out_invoice'}",
859             'type': 'ir.actions.act_window',
860             'nodestroy': True,
861             'target': 'current',
862             'res_id': inv_ids and inv_ids[0] or False,
863         }
864
865     def create_account_move(self, cr, uid, ids, context=None):
866         return self._create_account_move_line(cr, uid, ids, None, None, context=context)
867
868     def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
869         # Tricky, via the workflow, we only have one id in the ids variable
870         """Create a account move line of order grouped by products or not."""
871         account_move_obj = self.pool.get('account.move')
872         account_move_line_obj = self.pool.get('account.move.line')
873         account_period_obj = self.pool.get('account.period')
874         account_tax_obj = self.pool.get('account.tax')
875         user_proxy = self.pool.get('res.users')
876         property_obj = self.pool.get('ir.property')
877
878         period = account_period_obj.find(cr, uid, context=context)[0]
879
880         #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
881
882         if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
883             raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!'))
884
885         current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
886
887         grouped_data = {}
888         have_to_group_by = session and session.config_id.group_by or False
889
890         def compute_tax(amount, tax, line):
891             if amount > 0:
892                 tax_code_id = tax['base_code_id']
893                 tax_amount = line.price_subtotal * tax['base_sign']
894             else:
895                 tax_code_id = tax['ref_base_code_id']
896                 tax_amount = line.price_subtotal * tax['ref_base_sign']
897
898             return (tax_code_id, tax_amount,)
899
900         for order in self.browse(cr, uid, ids, context=context):
901             if order.account_move:
902                 continue
903             if order.state != 'paid':
904                 continue
905
906             user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
907
908             group_tax = {}
909             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
910
911             order_account = order.partner_id and \
912                             order.partner_id.property_account_receivable and \
913                             order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
914
915             if move_id is None:
916                 # Create an entry for the sale
917                 move_id = account_move_obj.create(cr, uid, {
918                     'ref' : order.name,
919                     'journal_id': order.sale_journal.id,
920                 }, context=context)
921
922             def insert_data(data_type, values):
923                 # if have_to_group_by:
924
925                 sale_journal_id = order.sale_journal.id
926
927                 # 'quantity': line.qty,
928                 # 'product_id': line.product_id.id,
929                 values.update({
930                     'date': order.date_order[:10],
931                     'ref': order.name,
932                     'journal_id' : sale_journal_id,
933                     'period_id' : period,
934                     'move_id' : move_id,
935                     'company_id': user_company and user_company.id or False,
936                 })
937
938                 if data_type == 'product':
939                     key = ('product', values['product_id'],)
940                 elif data_type == 'tax':
941                     key = ('tax', values['tax_code_id'],)
942                 elif data_type == 'counter_part':
943                     key = ('counter_part', values['partner_id'], values['account_id'])
944                 else:
945                     return
946
947                 grouped_data.setdefault(key, [])
948
949                 # if not have_to_group_by or (not grouped_data[key]):
950                 #     grouped_data[key].append(values)
951                 # else:
952                 #     pass
953
954                 if have_to_group_by:
955                     if not grouped_data[key]:
956                         grouped_data[key].append(values)
957                     else:
958                         current_value = grouped_data[key][0]
959                         current_value['quantity'] = current_value.get('quantity', 0.0) +  values.get('quantity', 0.0)
960                         current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
961                         current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
962                         current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
963                 else:
964                     grouped_data[key].append(values)
965
966             # Create an move for each order line
967
968             for line in order.lines:
969                 tax_amount = 0
970                 taxes = [t for t in line.product_id.taxes_id]
971                 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
972
973                 for tax in computed_taxes:
974                     tax_amount += round(tax['amount'], 2)
975                     group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
976
977                     group_tax.setdefault(group_key, 0)
978                     group_tax[group_key] += round(tax['amount'], 2)
979
980                 amount = line.price_subtotal
981
982                 # Search for the income account
983                 if  line.product_id.property_account_income.id:
984                     income_account = line.product_id.property_account_income.id
985                 elif line.product_id.categ_id.property_account_income_categ.id:
986                     income_account = line.product_id.categ_id.property_account_income_categ.id
987                 else:
988                     raise osv.except_osv(_('Error!'), _('Please define income '\
989                         'account for this product: "%s" (id:%d).') \
990                         % (line.product_id.name, line.product_id.id, ))
991
992                 # Empty the tax list as long as there is no tax code:
993                 tax_code_id = False
994                 tax_amount = 0
995                 while computed_taxes:
996                     tax = computed_taxes.pop(0)
997                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
998
999                     # If there is one we stop
1000                     if tax_code_id:
1001                         break
1002
1003                 # Create a move for the line
1004                 insert_data('product', {
1005                     'name': line.product_id.name,
1006                     'quantity': line.qty,
1007                     'product_id': line.product_id.id,
1008                     'account_id': income_account,
1009                     'credit': ((amount>0) and amount) or 0.0,
1010                     'debit': ((amount<0) and -amount) or 0.0,
1011                     'tax_code_id': tax_code_id,
1012                     'tax_amount': tax_amount,
1013                     'partner_id': order.partner_id and order.partner_id.id or False
1014                 })
1015
1016                 # For each remaining tax with a code, whe create a move line
1017                 for tax in computed_taxes:
1018                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1019                     if not tax_code_id:
1020                         continue
1021
1022                     insert_data('tax', {
1023                         'name': _('Tax'),
1024                         'product_id':line.product_id.id,
1025                         'quantity': line.qty,
1026                         'account_id': income_account,
1027                         'credit': 0.0,
1028                         'debit': 0.0,
1029                         'tax_code_id': tax_code_id,
1030                         'tax_amount': tax_amount,
1031                     })
1032
1033             # Create a move for each tax group
1034             (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
1035
1036             for key, tax_amount in group_tax.items():
1037                 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
1038                 insert_data('tax', {
1039                     'name': _('Tax') + ' ' + tax.name,
1040                     'quantity': line.qty,
1041                     'product_id': line.product_id.id,
1042                     'account_id': key[account_pos],
1043                     'credit': ((tax_amount>0) and tax_amount) or 0.0,
1044                     'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1045                     'tax_code_id': key[tax_code_pos],
1046                     'tax_amount': tax_amount,
1047                 })
1048
1049             # counterpart
1050             insert_data('counter_part', {
1051                 'name': _("Trade Receivables"), #order.name,
1052                 'account_id': order_account,
1053                 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1054                 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1055                 'partner_id': order.partner_id and order.partner_id.id or False
1056             })
1057
1058             order.write({'state':'done', 'account_move': move_id})
1059
1060         for group_key, group_data in grouped_data.iteritems():
1061             for value in group_data:
1062                 account_move_line_obj.create(cr, uid, value, context=context)
1063
1064         return True
1065
1066     def action_payment(self, cr, uid, ids, context=None):
1067         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1068
1069     def action_paid(self, cr, uid, ids, context=None):
1070         self.create_picking(cr, uid, ids, context=context)
1071         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1072         return True
1073
1074     def action_cancel(self, cr, uid, ids, context=None):
1075         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1076         return True
1077
1078     def action_done(self, cr, uid, ids, context=None):
1079         self.create_account_move(cr, uid, ids, context=context)
1080         return True
1081
1082 pos_order()
1083
1084 class account_bank_statement(osv.osv):
1085     _inherit = 'account.bank.statement'
1086     _columns= {
1087         'user_id': fields.many2one('res.users', 'User', readonly=True),
1088     }
1089     _defaults = {
1090         'user_id': lambda self,cr,uid,c={}: uid
1091     }
1092 account_bank_statement()
1093
1094 class account_bank_statement_line(osv.osv):
1095     _inherit = 'account.bank.statement.line'
1096     _columns= {
1097         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1098     }
1099 account_bank_statement_line()
1100
1101 class pos_order_line(osv.osv):
1102     _name = "pos.order.line"
1103     _description = "Lines of Point of Sale"
1104     _rec_name = "product_id"
1105
1106     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1107         res = dict([(i, {}) for i in ids])
1108         account_tax_obj = self.pool.get('account.tax')
1109         cur_obj = self.pool.get('res.currency')
1110         for line in self.browse(cr, uid, ids, context=context):
1111             if line.product_id.taxes_id:
1112                 taxes = line.product_id.taxes_id
1113             else:
1114                 taxes = line.product_id.categ_id.taxes_id
1115             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1116             taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
1117
1118             cur = line.order_id.pricelist_id.currency_id
1119             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1120             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1121         return res
1122
1123     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1124        context = context or {}
1125        if not product_id:
1126             return {}
1127        if not pricelist:
1128            raise osv.except_osv(_('No Pricelist !'),
1129                _('You have to select a pricelist in the sale form !\n' \
1130                'Please set one before choosing a product.'))
1131
1132        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1133                product_id, qty or 1.0, partner_id)[pricelist]
1134
1135        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1136        result['value']['price_unit'] = price
1137        return result
1138
1139     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1140         result = {}
1141         if not product:
1142             return result
1143         account_tax_obj = self.pool.get('account.tax')
1144         cur_obj = self.pool.get('res.currency')
1145
1146         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1147
1148         taxes = prod.taxes_id
1149         price = price_unit * (1 - (discount or 0.0) / 100.0)
1150         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1151
1152         result['price_subtotal'] = taxes['total']
1153         result['price_subtotal_incl'] = taxes['total_included']
1154         return {'value': result}
1155
1156     _columns = {
1157         'company_id': fields.many2one('res.company', 'Company', required=True),
1158         'name': fields.char('Line No', size=32, required=True),
1159         'notice': fields.char('Discount Notice', size=128),
1160         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1161         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1162         'qty': fields.float('Quantity', digits=(16, 2)),
1163         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1164         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1165         'discount': fields.float('Discount (%)', digits=(16, 2)),
1166         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1167         'create_date': fields.datetime('Creation Date', readonly=True),
1168     }
1169
1170     _defaults = {
1171         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1172         'qty': lambda *a: 1,
1173         'discount': lambda *a: 0.0,
1174         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1175     }
1176
1177     def copy_data(self, cr, uid, id, default=None, context=None):
1178         if not default:
1179             default = {}
1180         default.update({
1181             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1182         })
1183         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1184
1185 pos_order_line()
1186
1187 class pos_category(osv.osv):
1188     _name = 'pos.category'
1189     _description = "Point of Sale Category"
1190     _order = "sequence, name"
1191     def _check_recursion(self, cr, uid, ids, context=None):
1192         level = 100
1193         while len(ids):
1194             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1195             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1196             if not level:
1197                 return False
1198             level -= 1
1199         return True
1200
1201     _constraints = [
1202         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1203     ]
1204
1205     def name_get(self, cr, uid, ids, context=None):
1206         if not len(ids):
1207             return []
1208         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1209         res = []
1210         for record in reads:
1211             name = record['name']
1212             if record['parent_id']:
1213                 name = record['parent_id'][1]+' / '+name
1214             res.append((record['id'], name))
1215         return res
1216
1217     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1218         res = self.name_get(cr, uid, ids, context=context)
1219         return dict(res)
1220
1221     def _get_image(self, cr, uid, ids, name, args, context=None):
1222         result = dict.fromkeys(ids, False)
1223         for obj in self.browse(cr, uid, ids, context=context):
1224             result[obj.id] = tools.image_get_resized_images(obj.image)
1225         return result
1226     
1227     def _set_image(self, cr, uid, id, name, value, args, context=None):
1228         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
1229
1230     _columns = {
1231         'name': fields.char('Name', size=64, required=True, translate=True),
1232         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1233         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1234         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1235         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1236         'image': fields.binary("Image",
1237             help="This field holds the image used for the category. "\
1238                  "The image is base64 encoded, and PIL-supported. "\
1239                  "It is limited to a 1024x1024 px image."),
1240         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
1241             string="Medium-sized image", type="binary", multi="_get_image",
1242             store = {
1243                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1244             },
1245             help="Medium-sized image of the category. It is automatically "\
1246                  "resized as a 180x180 px image, with aspect ratio preserved. "\
1247                  "Use this field in form views or some kanban views."),
1248         'image_small': fields.function(_get_image, fnct_inv=_set_image,
1249             string="Smal-sized image", type="binary", multi="_get_image",
1250             store = {
1251                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1252             },
1253             help="Small-sized image of the category. It is automatically "\
1254                  "resized as a 50x50 px image, with aspect ratio preserved. "\
1255                  "Use this field anywhere a small image is required."),
1256     }
1257
1258     def _get_default_image(self, cr, uid, context=None):
1259         image_path = openerp.modules.get_module_resource('point_of_sale', 'images', 'default_category_photo.png')
1260         return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
1261
1262     _defaults = {
1263         'image': _get_default_image,
1264     }
1265
1266 pos_category()
1267
1268 import io, StringIO
1269
1270 class product_product(osv.osv):
1271     _inherit = 'product.product'
1272
1273     _columns = {
1274         'income_pdt': fields.boolean('Point of Sale Cash In', help="This is a product you can use to put cash into a statement for the point of sale backend."),
1275         'expense_pdt': fields.boolean('Point of Sale Cash Out', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."),
1276         'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1277             help="If you want to sell this product through the point of sale, select the category it belongs to."),
1278         'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"),
1279     }
1280     _defaults = {
1281         'to_weight' : False,
1282     }
1283
1284 product_product()
1285
1286
1287 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: