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