[FIX] webclient returns to database manager after 1st database creation
[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_config, 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         wf_service = netsvc.LocalService("workflow")
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'] = [(6, 0, [x.id for x in line.product_id.taxes_id] )]
858                 inv_line_ref.create(cr, uid, inv_line, context=context)
859             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
860             wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
861
862         if not inv_ids: return {}
863
864         mod_obj = self.pool.get('ir.model.data')
865         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
866         res_id = res and res[1] or False
867         return {
868             'name': _('Customer Invoice'),
869             'view_type': 'form',
870             'view_mode': 'form',
871             'view_id': [res_id],
872             'res_model': 'account.invoice',
873             'context': "{'type':'out_invoice'}",
874             'type': 'ir.actions.act_window',
875             'nodestroy': True,
876             'target': 'current',
877             'res_id': inv_ids and inv_ids[0] or False,
878         }
879
880     def create_account_move(self, cr, uid, ids, context=None):
881         return self._create_account_move_line(cr, uid, ids, None, None, context=context)
882
883     def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
884         # Tricky, via the workflow, we only have one id in the ids variable
885         """Create a account move line of order grouped by products or not."""
886         account_move_obj = self.pool.get('account.move')
887         account_move_line_obj = self.pool.get('account.move.line')
888         account_period_obj = self.pool.get('account.period')
889         account_tax_obj = self.pool.get('account.tax')
890         user_proxy = self.pool.get('res.users')
891         property_obj = self.pool.get('ir.property')
892
893         period = account_period_obj.find(cr, uid, context=context)[0]
894
895         #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
896
897         if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
898             raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!'))
899
900         current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
901
902         grouped_data = {}
903         have_to_group_by = session and session.config_id.group_by or False
904
905         def compute_tax(amount, tax, line):
906             if amount > 0:
907                 tax_code_id = tax['base_code_id']
908                 tax_amount = line.price_subtotal * tax['base_sign']
909             else:
910                 tax_code_id = tax['ref_base_code_id']
911                 tax_amount = line.price_subtotal * tax['ref_base_sign']
912
913             return (tax_code_id, tax_amount,)
914
915         for order in self.browse(cr, uid, ids, context=context):
916             if order.account_move:
917                 continue
918             if order.state != 'paid':
919                 continue
920
921             user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
922
923             group_tax = {}
924             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
925
926             order_account = order.partner_id and \
927                             order.partner_id.property_account_receivable and \
928                             order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
929
930             if move_id is None:
931                 # Create an entry for the sale
932                 move_id = account_move_obj.create(cr, uid, {
933                     'ref' : order.name,
934                     'journal_id': order.sale_journal.id,
935                 }, context=context)
936
937             def insert_data(data_type, values):
938                 # if have_to_group_by:
939
940                 sale_journal_id = order.sale_journal.id
941
942                 # 'quantity': line.qty,
943                 # 'product_id': line.product_id.id,
944                 values.update({
945                     'date': order.date_order[:10],
946                     'ref': order.name,
947                     'journal_id' : sale_journal_id,
948                     'period_id' : period,
949                     'move_id' : move_id,
950                     'company_id': user_company and user_company.id or False,
951                 })
952
953                 if data_type == 'product':
954                     key = ('product', values['product_id'],)
955                 elif data_type == 'tax':
956                     key = ('tax', values['tax_code_id'],)
957                 elif data_type == 'counter_part':
958                     key = ('counter_part', values['partner_id'], values['account_id'])
959                 else:
960                     return
961
962                 grouped_data.setdefault(key, [])
963
964                 # if not have_to_group_by or (not grouped_data[key]):
965                 #     grouped_data[key].append(values)
966                 # else:
967                 #     pass
968
969                 if have_to_group_by:
970                     if not grouped_data[key]:
971                         grouped_data[key].append(values)
972                     else:
973                         current_value = grouped_data[key][0]
974                         current_value['quantity'] = current_value.get('quantity', 0.0) +  values.get('quantity', 0.0)
975                         current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
976                         current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
977                         current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
978                 else:
979                     grouped_data[key].append(values)
980
981             #because of the weird way the pos order is written, we need to make sure there is at least one line, 
982             #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that 
983             #are set inside the for loop)
984             #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack
985             assert order.lines, _('The POS order must have lines when calling this method')
986             # Create an move for each order line
987             for line in order.lines:
988                 tax_amount = 0
989                 taxes = [t for t in line.product_id.taxes_id]
990                 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
991
992                 for tax in computed_taxes:
993                     tax_amount += round(tax['amount'], 2)
994                     group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
995
996                     group_tax.setdefault(group_key, 0)
997                     group_tax[group_key] += round(tax['amount'], 2)
998
999                 amount = line.price_subtotal
1000
1001                 # Search for the income account
1002                 if  line.product_id.property_account_income.id:
1003                     income_account = line.product_id.property_account_income.id
1004                 elif line.product_id.categ_id.property_account_income_categ.id:
1005                     income_account = line.product_id.categ_id.property_account_income_categ.id
1006                 else:
1007                     raise osv.except_osv(_('Error!'), _('Please define income '\
1008                         'account for this product: "%s" (id:%d).') \
1009                         % (line.product_id.name, line.product_id.id, ))
1010
1011                 # Empty the tax list as long as there is no tax code:
1012                 tax_code_id = False
1013                 tax_amount = 0
1014                 while computed_taxes:
1015                     tax = computed_taxes.pop(0)
1016                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1017
1018                     # If there is one we stop
1019                     if tax_code_id:
1020                         break
1021
1022                 # Create a move for the line
1023                 insert_data('product', {
1024                     'name': line.product_id.name,
1025                     'quantity': line.qty,
1026                     'product_id': line.product_id.id,
1027                     'account_id': income_account,
1028                     'credit': ((amount>0) and amount) or 0.0,
1029                     'debit': ((amount<0) and -amount) or 0.0,
1030                     'tax_code_id': tax_code_id,
1031                     'tax_amount': tax_amount,
1032                     'partner_id': order.partner_id and order.partner_id.id or False
1033                 })
1034
1035                 # For each remaining tax with a code, whe create a move line
1036                 for tax in computed_taxes:
1037                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1038                     if not tax_code_id:
1039                         continue
1040
1041                     insert_data('tax', {
1042                         'name': _('Tax'),
1043                         'product_id':line.product_id.id,
1044                         'quantity': line.qty,
1045                         'account_id': income_account,
1046                         'credit': 0.0,
1047                         'debit': 0.0,
1048                         'tax_code_id': tax_code_id,
1049                         'tax_amount': tax_amount,
1050                     })
1051
1052             # Create a move for each tax group
1053             (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
1054
1055             for key, tax_amount in group_tax.items():
1056                 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
1057                 insert_data('tax', {
1058                     'name': _('Tax') + ' ' + tax.name,
1059                     'quantity': line.qty,
1060                     'product_id': line.product_id.id,
1061                     'account_id': key[account_pos] or income_account,
1062                     'credit': ((tax_amount>0) and tax_amount) or 0.0,
1063                     'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1064                     'tax_code_id': key[tax_code_pos],
1065                     'tax_amount': tax_amount,
1066                 })
1067
1068             # counterpart
1069             insert_data('counter_part', {
1070                 'name': _("Trade Receivables"), #order.name,
1071                 'account_id': order_account,
1072                 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1073                 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1074                 'partner_id': order.partner_id and order.partner_id.id or False
1075             })
1076
1077             order.write({'state':'done', 'account_move': move_id})
1078
1079         for group_key, group_data in grouped_data.iteritems():
1080             for value in group_data:
1081                 account_move_line_obj.create(cr, uid, value, context=context)
1082
1083         return True
1084
1085     def action_payment(self, cr, uid, ids, context=None):
1086         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1087
1088     def action_paid(self, cr, uid, ids, context=None):
1089         self.create_picking(cr, uid, ids, context=context)
1090         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1091         return True
1092
1093     def action_cancel(self, cr, uid, ids, context=None):
1094         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1095         return True
1096
1097     def action_done(self, cr, uid, ids, context=None):
1098         self.create_account_move(cr, uid, ids, context=context)
1099         return True
1100
1101 class account_bank_statement(osv.osv):
1102     _inherit = 'account.bank.statement'
1103     _columns= {
1104         'user_id': fields.many2one('res.users', 'User', readonly=True),
1105     }
1106     _defaults = {
1107         'user_id': lambda self,cr,uid,c={}: uid
1108     }
1109 account_bank_statement()
1110
1111 class account_bank_statement_line(osv.osv):
1112     _inherit = 'account.bank.statement.line'
1113     _columns= {
1114         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1115     }
1116
1117 account_bank_statement_line()
1118
1119 class pos_order_line(osv.osv):
1120     _name = "pos.order.line"
1121     _description = "Lines of Point of Sale"
1122     _rec_name = "product_id"
1123
1124     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1125         res = dict([(i, {}) for i in ids])
1126         account_tax_obj = self.pool.get('account.tax')
1127         cur_obj = self.pool.get('res.currency')
1128         for line in self.browse(cr, uid, ids, context=context):
1129             taxes_ids = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ]
1130             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1131             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)
1132
1133             cur = line.order_id.pricelist_id.currency_id
1134             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1135             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1136         return res
1137
1138     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1139        context = context or {}
1140        if not product_id:
1141             return {}
1142        if not pricelist:
1143            raise osv.except_osv(_('No Pricelist !'),
1144                _('You have to select a pricelist in the sale form !\n' \
1145                'Please set one before choosing a product.'))
1146
1147        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1148                product_id, qty or 1.0, partner_id)[pricelist]
1149
1150        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1151        result['value']['price_unit'] = price
1152        return result
1153
1154     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1155         result = {}
1156         if not product:
1157             return result
1158         account_tax_obj = self.pool.get('account.tax')
1159         cur_obj = self.pool.get('res.currency')
1160
1161         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1162
1163         price = price_unit * (1 - (discount or 0.0) / 100.0)
1164         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1165
1166         result['price_subtotal'] = taxes['total']
1167         result['price_subtotal_incl'] = taxes['total_included']
1168         return {'value': result}
1169
1170     _columns = {
1171         'company_id': fields.many2one('res.company', 'Company', required=True),
1172         'name': fields.char('Line No', size=32, required=True),
1173         'notice': fields.char('Discount Notice', size=128),
1174         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1175         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1176         'qty': fields.float('Quantity', digits=(16, 2)),
1177         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1178         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1179         'discount': fields.float('Discount (%)', digits=(16, 2)),
1180         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1181         'create_date': fields.datetime('Creation Date', readonly=True),
1182     }
1183
1184     _defaults = {
1185         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1186         'qty': lambda *a: 1,
1187         'discount': lambda *a: 0.0,
1188         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1189     }
1190
1191     def copy_data(self, cr, uid, id, default=None, context=None):
1192         if not default:
1193             default = {}
1194         default.update({
1195             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1196         })
1197         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1198
1199 class pos_category(osv.osv):
1200     _name = 'pos.category'
1201     _description = "Point of Sale Category"
1202     _order = "sequence, name"
1203     def _check_recursion(self, cr, uid, ids, context=None):
1204         level = 100
1205         while len(ids):
1206             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1207             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1208             if not level:
1209                 return False
1210             level -= 1
1211         return True
1212
1213     _constraints = [
1214         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1215     ]
1216
1217     def name_get(self, cr, uid, ids, context=None):
1218         if not len(ids):
1219             return []
1220         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1221         res = []
1222         for record in reads:
1223             name = record['name']
1224             if record['parent_id']:
1225                 name = record['parent_id'][1]+' / '+name
1226             res.append((record['id'], name))
1227         return res
1228
1229     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1230         res = self.name_get(cr, uid, ids, context=context)
1231         return dict(res)
1232
1233     def _get_image(self, cr, uid, ids, name, args, context=None):
1234         result = dict.fromkeys(ids, False)
1235         for obj in self.browse(cr, uid, ids, context=context):
1236             result[obj.id] = tools.image_get_resized_images(obj.image)
1237         return result
1238     
1239     def _set_image(self, cr, uid, id, name, value, args, context=None):
1240         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
1241
1242     _columns = {
1243         'name': fields.char('Name', size=64, required=True, translate=True),
1244         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1245         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1246         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1247         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1248         
1249         # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
1250         # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
1251         # In this case, the default image is set by the js code.
1252         # NOTE2: image: all image fields are base64 encoded and PIL-supported
1253         'image': fields.binary("Image",
1254             help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
1255         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
1256             string="Medium-sized image", type="binary", multi="_get_image",
1257             store={
1258                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1259             },
1260             help="Medium-sized image of the category. It is automatically "\
1261                  "resized as a 128x128px image, with aspect ratio preserved. "\
1262                  "Use this field in form views or some kanban views."),
1263         'image_small': fields.function(_get_image, fnct_inv=_set_image,
1264             string="Smal-sized image", type="binary", multi="_get_image",
1265             store={
1266                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1267             },
1268             help="Small-sized image of the category. It is automatically "\
1269                  "resized as a 64x64px image, with aspect ratio preserved. "\
1270                  "Use this field anywhere a small image is required."),
1271     }
1272
1273 import io, StringIO
1274
1275 class ean_wizard(osv.osv_memory):
1276     _name = 'pos.ean_wizard'
1277     _columns = {
1278         'ean13_pattern': fields.char('Reference', size=32, required=True, translate=True),
1279     }
1280     def sanitize_ean13(self, cr, uid, ids, context):
1281         for r in self.browse(cr,uid,ids):
1282             ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern)
1283             m = context.get('active_model')
1284             m_id =  context.get('active_id')
1285             self.pool.get(m).write(cr,uid,[m_id],{'ean13':ean13})
1286         return { 'type' : 'ir.actions.act_window_close' }
1287
1288 class product_product(osv.osv):
1289     _inherit = 'product.product'
1290
1291
1292     #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1293     #    result = {}
1294     #    for obj in self.browse(cr, uid, ids, context=context):
1295     #        if not obj.product_image:
1296     #            result[obj.id] = False
1297     #            continue
1298
1299     #        image_stream = io.BytesIO(obj.product_image.decode('base64'))
1300     #        img = Image.open(image_stream)
1301     #        img.thumbnail((120, 100), Image.ANTIALIAS)
1302     #        img_stream = StringIO.StringIO()
1303     #        img.save(img_stream, "JPEG")
1304     #        result[obj.id] = img_stream.getvalue().encode('base64')
1305     #    return result
1306
1307     _columns = {
1308         '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."),
1309         '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."),
1310         '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'), 
1311         'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1312             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."),
1313         'to_weight' : fields.boolean('To Weight', help="Check if the product should be weighted (mainly used with self check-out interface)."),
1314     }
1315
1316     def _default_pos_categ_id(self, cr, uid, context=None):
1317         proxy = self.pool.get('ir.model.data')
1318
1319         try:
1320             category_id = proxy.get_object_reference(cr, uid, 'point_of_sale', 'categ_others')[1]
1321         except ValueError:
1322             values = {
1323                 'name' : 'Others',
1324             }
1325             category_id = self.pool.get('pos.category').create(cr, uid, values, context=context)
1326             values = {
1327                 'name' : 'categ_others',
1328                 'model' : 'pos.category',
1329                 'module' : 'point_of_sale',
1330                 'res_id' : category_id,
1331             }
1332             proxy.create(cr, uid, values, context=context)
1333
1334         return category_id
1335
1336     _defaults = {
1337         'to_weight' : False,
1338         'available_in_pos': True,
1339         'pos_categ_id' : _default_pos_categ_id,
1340     }
1341
1342     def edit_ean(self, cr, uid, ids, context):
1343         return {
1344             'name': _("Assign a Custom EAN"),
1345             'type': 'ir.actions.act_window',
1346             'view_type': 'form',
1347             'view_mode': 'form',
1348             'res_model': 'pos.ean_wizard',
1349             'target' : 'new',
1350             'view_id': False,
1351             'context':context,
1352         }
1353
1354 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: