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