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