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