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