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