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