1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
26 from datetime import datetime
27 from dateutil.relativedelta import relativedelta
32 from osv import fields, osv
33 from tools.translate import _
34 from decimal import Decimal
35 import decimal_precision as dp
37 _logger = logging.getLogger(__name__)
39 class pos_config(osv.osv):
44 ('inactive', 'Inactive'),
45 ('deprecated', 'Deprecated')
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',
56 'journal_id' : fields.many2one('account.journal', 'Sale Journal',
57 required=True, 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_websql' : fields.boolean('WebSQL (Faster but Chrome Only)',
62 help="If have more than 200 products, it's highly suggested to use WebSQL "\
63 "to store the data in the browser, instead of localStore mechanism. "\
64 "It's more efficient but works on the Chrome browser only."
66 'iface_led' : fields.boolean('Help Notification'),
67 'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'),
68 'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'),
69 'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'),
70 'iface_barscan' : fields.boolean('BarScan Interface'),
71 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'),
72 'iface_print_via_proxy' : fields.boolean('Print via Proxy'),
74 'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True),
75 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True,
76 help="This sequence is automatically created by OpenERP but you can change it "\
77 "to customize the reference numbers of your orders."),
78 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'),
79 'group_by' : fields.boolean('Group By', help="Check this if you want to group the Journal Items by Product while a Session"),
82 def name_get(self, cr, uid, ids, context=None):
85 'opening_control': _('Opening Control'),
86 'opened': _('In Progress'),
87 'closing_control': _('Closing Control'),
88 'closed': _('Closed & Posted'),
90 for record in self.browse(cr, uid, ids, context=context):
91 if (not record.session_ids) or (record.session_ids[0].state=='closed'):
92 result.append((record.id, record.name+' ('+_('not used')+')'))
94 session = record.session_ids[0]
95 result.append((record.id, record.name + ' ('+session.user_id.name+', '+states[session.state]+')'))
99 def _default_sale_journal(self, cr, uid, context=None):
100 res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
101 return res and res[0] or False
103 def _default_shop(self, cr, uid, context=None):
104 res = self.pool.get('sale.shop').search(cr, uid, [])
105 return res and res[0] or False
108 'state' : POS_CONFIG_STATE[0][0],
109 'shop_id': _default_shop,
110 'journal_id': _default_sale_journal,
114 def set_active(self, cr, uid, ids, context=None):
115 return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
117 def set_inactive(self, cr, uid, ids, context=None):
118 return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context)
120 def set_deprecate(self, cr, uid, ids, context=None):
121 return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context)
123 def create(self, cr, uid, values, context=None):
124 proxy = self.pool.get('ir.sequence')
125 sequence_values = dict(
126 name='PoS %s' % values['name'],
128 prefix="%s/" % values['name'],
130 sequence_id = proxy.create(cr, uid, sequence_values, context=context)
131 values['sequence_id'] = sequence_id
132 return super(pos_config, self).create(cr, uid, values, context=context)
134 def unlink(self, cr, uid, ids, context=None):
135 for obj in self.browse(cr, uid, ids, context=context):
137 obj.sequence_id.unlink()
138 return super(pos_config, self).unlink(cr, uid, ids, context=context)
142 class pos_session(osv.osv):
143 _name = 'pos.session'
146 POS_SESSION_STATE = [
147 ('opening_control', 'Opening Control'), # Signal open
148 ('opened', 'In Progress'), # Signal closing
149 ('closing_control', 'Closing Control'), # Signal close
150 ('closed', 'Closed & Posted'),
153 def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None):
154 result = dict.fromkeys(ids, False)
155 for record in self.browse(cr, uid, ids, context=context):
156 for st in record.statement_ids:
157 if st.journal_id.type == 'cash':
158 result[record.id] = st.id
162 def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None):
165 for record in self.browse(cr, uid, ids, context=context):
166 has_opening_control = False
167 has_closing_control = False
169 for journal in record.config_id.journal_ids:
170 if journal.opening_control == True:
171 has_opening_control = True
172 if journal.closing_control == True:
173 has_closing_control = True
175 if has_opening_control and has_closing_control:
179 'has_opening_control': has_opening_control,
180 'has_closing_control': has_closing_control,
182 result[record.id] = values
187 'config_id' : fields.many2one('pos.config', 'Point of Sale',
188 help="The physical point of sale you will use.",
191 domain="[('state', '=', 'active')]",
193 # states={'draft' : [('readonly', False)]}
196 'name' : fields.char('Session ID', size=32,
199 # states={'draft' : [('readonly', False)]}
201 'user_id' : fields.many2one('res.users', 'Responsible',
205 # states={'draft' : [('readonly', False)]}
207 'start_at' : fields.datetime('Opening Date'),
208 'stop_at' : fields.datetime('Closing Date'),
210 'state' : fields.selection(POS_SESSION_STATE, 'State',
211 required=True, readonly=True,
214 'cash_register_id' : fields.function(_compute_cash_register_id, method=True,
215 type='many2one', relation='account.bank.statement',
216 string='Cash Register', store=True),
218 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids',
219 type='one2many', relation='account.cashbox.line',
220 string='Opening Cash Control'),
221 'details_ids' : fields.related('cash_register_id', 'details_ids',
222 type='one2many', relation='account.cashbox.line',
223 string='Cash Control'),
225 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real',
227 digits_compute=dp.get_precision('Account'),
228 string="Ending Balance",
229 help="Computed using the cash control lines",
231 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start',
233 digits_compute=dp.get_precision('Account'),
234 string="Starting Balance",
235 help="Computed using the cash control at the opening.",
237 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding',
238 string='Total Cash Transaction',
240 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end',
242 digits_compute=dp.get_precision('Account'),
243 string="Computed Balance",
244 help="Computed with the initial cash control and the sum of all payments.",
246 'cash_register_difference' : fields.related('cash_register_id', 'difference',
249 help="Difference between the counted cash control at the closing and the computed balance.",
252 'journal_ids' : fields.related('config_id', 'journal_ids',
255 relation='account.journal',
256 string='Available Payment Methods'),
257 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
259 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True),
260 'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'),
261 'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'),
266 'user_id' : lambda obj, cr, uid, context: uid,
267 'state' : 'opening_control',
271 ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
274 def _check_unicity(self, cr, uid, ids, context=None):
275 for session in self.browse(cr, uid, ids, context=None):
276 # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
278 ('state', '!=', 'closed'),
279 ('user_id', '=', uid)
281 count = self.search_count(cr, uid, domain, context=context)
286 def _check_pos_config(self, cr, uid, ids, context=None):
287 for session in self.browse(cr, uid, ids, context=None):
289 ('state', '!=', 'closed'),
290 ('config_id', '=', session.config_id.id)
292 count = self.search_count(cr, uid, domain, context=context)
298 (_check_unicity, "You can not create two active sessions with the same responsible!", ['user_id', 'state']),
299 (_check_pos_config, "You can not create two active sessions related to the same point of sale!", ['config_id']),
302 def create(self, cr, uid, values, context=None):
303 config_id = values.get('config_id', False) or False
307 pos_config = self.pool.get('pos.config').browse(cr, uid, config_id, context=context)
309 bank_statement_ids = []
310 for journal in pos_config.journal_ids:
312 'journal_id' : journal.id,
315 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
316 bank_statement_ids.append(statement_id)
319 'name' : pos_config.sequence_id._next(),
320 'statement_ids' : [(6, 0, bank_statement_ids)]
323 return super(pos_session, self).create(cr, uid, values, context=context)
325 def unlink(self, cr, uid, ids, context=None):
326 for obj in self.browse(cr, uid, ids, context=context):
327 for statement in obj.statement_ids:
328 statement.unlink(context=context)
331 def wkf_action_open(self, cr, uid, ids, context=None):
332 # si pas de date start_at, je balance une date, sinon on utilise celle de l'utilisateur
333 for record in self.browse(cr, uid, ids, context=context):
335 if not record.start_at:
336 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
337 values['state'] = 'opened'
338 record.write(values, context=context)
339 for st in record.statement_ids:
340 st.button_open(context=context)
343 def wkf_action_opening_control(self, cr, uid, ids, context=None):
344 return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context)
346 def wkf_action_closing_control(self, cr, uid, ids, context=None):
347 for session in self.browse(cr, uid, ids, context=context):
348 for statement in session.statement_ids:
349 if not statement.journal_id.closing_control:
350 if statement.balance_end<>statement.balance_end_real:
351 self.pool.get('account.bank.statement').write(cr, uid,
352 [statement.id], {'balance_end_real': statement.balance_end})
353 return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
355 def wkf_action_close(self, cr, uid, ids, context=None):
357 bsl = self.pool.get('account.bank.statement.line')
358 for record in self.browse(cr, uid, ids, context=context):
359 for st in record.statement_ids:
360 if abs(st.difference) > st.journal_id.amount_authorized_diff:
361 # The pos manager can close statements with maximums.
362 if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"):
363 raise osv.except_osv( _('Error !'),
364 _("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))
366 if st.difference > 0.0:
367 name= _('Point of Sale Profit')
368 account_id = st.journal_id.profit_account_id.id
370 account_id = st.journal_id.loss_account_id.id
371 name= _('Point of Sale Loss')
373 raise osv.except_osv( _('Error !'),
374 _("Please set your profit and loss accounts on your payment method '%s'.") % (st.journal_id.name,))
375 bsl.create(cr, uid, {
376 'statement_id': st.id,
377 'amount': st.difference,
380 'account_id': account_id
383 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
384 self._confirm_orders(cr, uid, ids, context=context)
385 return self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
387 def _confirm_orders(self, cr, uid, ids, context=None):
388 wf_service = netsvc.LocalService("workflow")
390 for session in self.browse(cr, uid, ids, context=context):
391 order_ids = [order.id for order in session.order_ids if order.state == 'paid']
393 move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context)
395 self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context)
397 for order in session.order_ids:
398 if order.state != 'paid':
399 raise osv.except_osv(
401 _("You can not confirm all orders of this session, because they have not the 'paid' status"))
403 wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
407 def open_frontend_cb(self, cr, uid, ids, context=None):
414 context.update({'session_id' : ids[0]})
416 'type' : 'ir.actions.client',
417 'name' : 'Start Point Of Sale',
424 class pos_order(osv.osv):
426 _description = "Point of Sale"
429 def create_from_ui(self, cr, uid, orders, context=None):
430 #_logger.info("orders: %r", orders)
432 for tmp_order in orders:
433 order = tmp_order['data']
434 # order :: {'name': 'Order 1329148448062', 'amount_paid': 9.42, 'lines': [[0, 0, {'discount': 0, 'price_unit': 1.46, 'product_id': 124, 'qty': 5}], [0, 0, {'discount': 0, 'price_unit': 0.53, 'product_id': 62, 'qty': 4}]], 'statement_ids': [[0, 0, {'journal_id': 7, 'amount': 9.42, 'name': '2012-02-13 15:54:12', 'account_id': 12, 'statement_id': 21}]], 'amount_tax': 0, 'amount_return': 0, 'amount_total': 9.42}
435 # get statements out of order because they will be generated with add_payment to ensure
436 # the module behavior is the same when using the front-end or the back-end
437 statement_ids = order.get('statement_ids', [])
438 order_id = self.create(cr, uid, order, context)
439 order_ids.append(order_id)
440 # call add_payment; refer to wizard/pos_payment for data structure
441 # add_payment launches the 'paid' signal to advance the workflow to the 'paid' state
444 'journal': statement_ids[0][2]['journal_id'],
445 'amount': order['amount_paid'],
446 'payment_name': order['name'],
447 'payment_date': statement_ids[0][2]['name'],
449 wf_service = netsvc.LocalService("workflow")
450 wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
451 wf_service.trg_write(uid, 'pos.order', order_id, cr)
453 #self.add_payment(cr, uid, order_id, data, context=context)
456 def unlink(self, cr, uid, ids, context=None):
457 for rec in self.browse(cr, uid, ids, context=context):
458 if rec.state not in ('draft','cancel'):
459 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
460 return super(pos_order, self).unlink(cr, uid, ids, context=context)
462 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
465 pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
466 return {'value': {'pricelist_id': pricelist}}
468 def _amount_all(self, cr, uid, ids, name, args, context=None):
469 tax_obj = self.pool.get('account.tax')
470 cur_obj = self.pool.get('res.currency')
472 for order in self.browse(cr, uid, ids, context=context):
479 cur = order.pricelist_id.currency_id
480 for payment in order.statement_ids:
481 res[order.id]['amount_paid'] += payment.amount
482 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
483 for line in order.lines:
484 val1 += line.price_subtotal_incl
485 val2 += line.price_subtotal
486 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
487 res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
490 def copy(self, cr, uid, id, default=None, context=None):
496 'account_move': False,
500 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
503 return super(pos_order, self).copy(cr, uid, id, d, context=context)
506 'name': fields.char('Order Ref', size=64, required=True, readonly=True),
507 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
508 'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True),
509 'date_order': fields.datetime('Order Date', readonly=True, select=True),
510 'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It could be a reliever, a student or an interim employee."),
511 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
512 'amount_total': fields.function(_amount_all, string='Total', multi='all'),
513 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
514 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
515 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
516 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
517 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
518 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
520 'session_id' : fields.many2one('pos.session', 'Session',
523 domain="[('state', '=', 'opened')]",
524 states={'draft' : [('readonly', False)]},
527 'state': fields.selection([('draft', 'New'),
528 ('cancel', 'Cancelled'),
531 ('invoiced', 'Invoiced')],
532 'Status', readonly=True),
534 'invoice_id': fields.many2one('account.invoice', 'Invoice'),
535 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
536 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
537 'note': fields.text('Internal Notes'),
538 'nb_print': fields.integer('Number of Print', readonly=True),
540 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
543 def _default_session(self, cr, uid, context=None):
544 so = self.pool.get('pos.session')
545 session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
546 return session_ids and session_ids[0] or False
548 def _default_pricelist(self, cr, uid, context=None):
549 res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
551 shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
552 return shop.pricelist_id and shop.pricelist_id.id or False
556 'user_id': lambda self, cr, uid, context: uid,
559 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
561 'session_id': _default_session,
562 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
563 'pricelist_id': _default_pricelist,
566 def create(self, cr, uid, values, context=None):
567 values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
568 return super(pos_order, self).create(cr, uid, values, context=context)
570 def test_paid(self, cr, uid, ids, context=None):
571 """A Point of Sale is paid when the sum
574 for order in self.browse(cr, uid, ids, context=context):
575 if order.lines and not order.amount_total:
577 if (not order.lines) or (not order.statement_ids) or \
578 (abs(order.amount_total-order.amount_paid) > 0.00001):
582 def create_picking(self, cr, uid, ids, context=None):
583 """Create a picking for each order and validate it."""
584 picking_obj = self.pool.get('stock.picking')
585 partner_obj = self.pool.get('res.partner')
586 move_obj = self.pool.get('stock.move')
588 for order in self.browse(cr, uid, ids, context=context):
589 if not order.state=='draft':
591 addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
592 picking_id = picking_obj.create(cr, uid, {
593 'origin': order.name,
594 'partner_id': addr.get('delivery',False),
596 'company_id': order.company_id.id,
597 'move_type': 'direct',
598 'note': order.note or "",
599 'invoice_state': 'none',
600 'auto_picking': True,
602 self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
603 location_id = order.shop_id.warehouse_id.lot_stock_id.id
604 output_id = order.shop_id.warehouse_id.lot_output_id.id
606 for line in order.lines:
607 if line.product_id and line.product_id.type == 'service':
610 location_id, output_id = output_id, location_id
612 move_obj.create(cr, uid, {
614 'product_uom': line.product_id.uom_id.id,
615 'product_uos': line.product_id.uom_id.id,
616 'picking_id': picking_id,
617 'product_id': line.product_id.id,
618 'product_uos_qty': abs(line.qty),
619 'product_qty': abs(line.qty),
620 'tracking_id': False,
622 'location_id': location_id,
623 'location_dest_id': output_id,
626 location_id, output_id = output_id, location_id
628 wf_service = netsvc.LocalService("workflow")
629 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
630 picking_obj.force_assign(cr, uid, [picking_id], context)
633 def cancel_order(self, cr, uid, ids, context=None):
634 """ Changes order state to cancel
637 stock_picking_obj = self.pool.get('stock.picking')
638 for order in self.browse(cr, uid, ids, context=context):
639 wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
640 if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
641 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
642 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
645 def add_payment(self, cr, uid, order_id, data, context=None):
646 """Create a new payment for the order"""
649 statement_obj = self.pool.get('account.bank.statement')
650 statement_line_obj = self.pool.get('account.bank.statement.line')
651 prod_obj = self.pool.get('product.product')
652 property_obj = self.pool.get('ir.property')
653 curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
654 curr_company = curr_c.id
655 order = self.browse(cr, uid, order_id, context=context)
657 'amount': data['amount'],
659 if 'payment_date' in data:
660 args['date'] = data['payment_date']
661 args['name'] = order.name
662 if data.get('payment_name', False):
663 args['name'] = args['name'] + ': ' + data['payment_name']
664 account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
665 args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
666 and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
667 args['partner_id'] = order.partner_id and order.partner_id.id or None
669 if not args['account_id']:
670 if not args['partner_id']:
671 msg = _('There is no receivable account defined to make payment')
673 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d)') % (order.partner_id.name, order.partner_id.id,)
674 raise osv.except_osv(_('Configuration Error !'), msg)
676 context.pop('pos_session_id', False)
679 journal_id = long(data['journal'])
684 for statement in order.session_id.statement_ids:
685 if statement.journal_id.id == journal_id:
686 statement_id = statement.id
690 raise osv.except_osv(_('Error !'), _('You have to open at least one cashbox'))
693 'statement_id' : statement_id,
694 'pos_statement_id' : order_id,
695 'journal_id' : journal_id,
700 statement_line_obj.create(cr, uid, args, context=context)
702 wf_service = netsvc.LocalService("workflow")
703 wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
704 wf_service.trg_write(uid, 'pos.order', order_id, cr)
708 def refund(self, cr, uid, ids, context=None):
709 """Create a copy of order for refund order"""
711 line_obj = self.pool.get('pos.order.line')
712 for order in self.browse(cr, uid, ids, context=context):
713 clone_id = self.copy(cr, uid, order.id, {
714 'name': order.name + ' REFUND',
716 clone_list.append(clone_id)
718 for clone in self.browse(cr, uid, clone_list, context=context):
719 for order_line in clone.lines:
720 line_obj.write(cr, uid, [order_line.id], {
721 'qty': -order_line.qty
724 new_order = ','.join(map(str,clone_list))
726 #'domain': "[('id', 'in', ["+new_order+"])]",
727 'name': _('Return Products'),
730 'res_model': 'pos.order',
731 'res_id':clone_list[0],
734 'type': 'ir.actions.act_window',
740 def action_invoice_state(self, cr, uid, ids, context=None):
741 return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
743 def action_invoice(self, cr, uid, ids, context=None):
744 wf_service = netsvc.LocalService("workflow")
745 inv_ref = self.pool.get('account.invoice')
746 inv_line_ref = self.pool.get('account.invoice.line')
747 product_obj = self.pool.get('product.product')
750 for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
752 inv_ids.append(order.invoice_id.id)
755 if not order.partner_id:
756 raise osv.except_osv(_('Error'), _('Please provide a partner for the sale.'))
758 acc = order.partner_id.property_account_receivable.id
761 'origin': order.name,
763 'journal_id': order.sale_journal.id or None,
764 'type': 'out_invoice',
765 'reference': order.name,
766 'partner_id': order.partner_id.id,
767 'comment': order.note or '',
768 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
770 inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
771 if not inv.get('account_id', None):
772 inv['account_id'] = acc
773 inv_id = inv_ref.create(cr, uid, inv, context=context)
775 self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
776 inv_ids.append(inv_id)
777 for line in order.lines:
779 'invoice_id': inv_id,
780 'product_id': line.product_id.id,
781 'quantity': line.qty,
783 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
784 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
786 line.product_id.uom_id.id,
787 line.qty, partner_id = order.partner_id.id,
788 fposition_id=order.partner_id.property_account_position.id)['value'])
789 if line.product_id.description_sale:
790 inv_line['note'] = line.product_id.description_sale
791 inv_line['price_unit'] = line.price_unit
792 inv_line['discount'] = line.discount
793 inv_line['name'] = inv_name
794 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
795 and [(6, 0, inv_line['invoice_line_tax_id'])] or []
796 inv_line_ref.create(cr, uid, inv_line, context=context)
797 inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
798 wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
800 if not inv_ids: return {}
802 mod_obj = self.pool.get('ir.model.data')
803 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
804 res_id = res and res[1] or False
806 'name': _('Customer Invoice'),
810 'res_model': 'account.invoice',
811 'context': "{'type':'out_invoice'}",
812 'type': 'ir.actions.act_window',
815 'res_id': inv_ids and inv_ids[0] or False,
818 def create_account_move(self, cr, uid, ids, context=None):
819 return self._create_account_move_line(cr, uid, ids, None, None, context=context)
821 def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
822 # Tricky, via the workflow, we only have one id in the ids variable
823 """Create a account move line of order grouped by products or not."""
824 account_move_obj = self.pool.get('account.move')
825 account_move_line_obj = self.pool.get('account.move.line')
826 account_period_obj = self.pool.get('account.period')
827 account_tax_obj = self.pool.get('account.tax')
828 user_proxy = self.pool.get('res.users')
829 property_obj = self.pool.get('ir.property')
831 period = account_period_obj.find(cr, uid, context=context)[0]
833 #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
835 if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
836 raise osv.except_osv(_('Error!'), _('The selected orders do not have the same session !'))
838 current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
841 have_to_group_by = session and session.config_id.group_by or False
843 def compute_tax(amount, tax, line):
845 tax_code_id = tax['base_code_id']
846 tax_amount = line.price_subtotal * tax['base_sign']
848 tax_code_id = tax['ref_base_code_id']
849 tax_amount = line.price_subtotal * tax['ref_base_sign']
851 return (tax_code_id, tax_amount,)
853 for order in self.browse(cr, uid, ids, context=context):
854 if order.account_move:
856 if order.state != 'paid':
859 user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
862 account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
864 order_account = order.partner_id and \
865 order.partner_id.property_account_receivable and \
866 order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
869 # Create an entry for the sale
870 move_id = account_move_obj.create(cr, uid, {
872 'journal_id': order.sale_journal.id,
875 def insert_data(data_type, values):
876 # if have_to_group_by:
878 sale_journal_id = order.sale_journal.id
880 # 'quantity': line.qty,
881 # 'product_id': line.product_id.id,
883 'date': order.date_order[:10],
885 'journal_id' : sale_journal_id,
886 'period_id' : period,
888 'company_id': user_company and user_company.id or False,
891 if data_type == 'product':
892 key = ('product', values['product_id'],)
893 elif data_type == 'tax':
894 key = ('tax', values['tax_code_id'],)
895 elif data_type == 'counter_part':
896 key = ('counter_part', values['partner_id'], values['account_id'])
900 grouped_data.setdefault(key, [])
902 # if not have_to_group_by or (not grouped_data[key]):
903 # grouped_data[key].append(values)
908 if not grouped_data[key]:
909 grouped_data[key].append(values)
911 current_value = grouped_data[key][0]
912 current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0)
913 current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
914 current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
915 current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
917 grouped_data[key].append(values)
919 # Create an move for each order line
921 for line in order.lines:
923 taxes = [t for t in line.product_id.taxes_id]
924 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
926 for tax in computed_taxes:
927 tax_amount += round(tax['amount'], 2)
928 group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
930 group_tax.setdefault(group_key, 0)
931 group_tax[group_key] += round(tax['amount'], 2)
933 amount = line.price_subtotal
935 # Search for the income account
936 if line.product_id.property_account_income.id:
937 income_account = line.product_id.property_account_income.id
938 elif line.product_id.categ_id.property_account_income_categ.id:
939 income_account = line.product_id.categ_id.property_account_income_categ.id
941 raise osv.except_osv(_('Error !'), _('There is no income '\
942 'account defined for this product: "%s" (id:%d)') \
943 % (line.product_id.name, line.product_id.id, ))
945 # Empty the tax list as long as there is no tax code:
948 while computed_taxes:
949 tax = computed_taxes.pop(0)
950 tax_code_id, tax_amount = compute_tax(amount, tax, line)
952 # If there is one we stop
956 # Create a move for the line
957 insert_data('product', {
958 'name': line.product_id.name,
959 'quantity': line.qty,
960 'product_id': line.product_id.id,
961 'account_id': income_account,
962 'credit': ((amount>0) and amount) or 0.0,
963 'debit': ((amount<0) and -amount) or 0.0,
964 'tax_code_id': tax_code_id,
965 'tax_amount': tax_amount,
966 'partner_id': order.partner_id and order.partner_id.id or False
969 # For each remaining tax with a code, whe create a move line
970 for tax in computed_taxes:
971 tax_code_id, tax_amount = compute_tax(amount, tax, line)
977 'product_id':line.product_id.id,
978 'quantity': line.qty,
979 'account_id': income_account,
982 'tax_code_id': tax_code_id,
983 'tax_amount': tax_amount,
986 # Create a move for each tax group
987 (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
989 for key, tax_amount in group_tax.items():
990 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
992 'name': _('Tax') + ' ' + tax.name,
993 'quantity': line.qty,
994 'product_id': line.product_id.id,
995 'account_id': key[account_pos],
996 'credit': ((tax_amount>0) and tax_amount) or 0.0,
997 'debit': ((tax_amount<0) and -tax_amount) or 0.0,
998 'tax_code_id': key[tax_code_pos],
999 'tax_amount': tax_amount,
1003 insert_data('counter_part', {
1004 'name': _("Trade Receivables"), #order.name,
1005 'account_id': order_account,
1006 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1007 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1008 'partner_id': order.partner_id and order.partner_id.id or False
1011 order.write({'state':'done', 'account_move': move_id})
1013 for group_key, group_data in grouped_data.iteritems():
1014 for value in group_data:
1015 account_move_line_obj.create(cr, uid, value, context=context)
1019 def action_payment(self, cr, uid, ids, context=None):
1020 return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1022 def action_paid(self, cr, uid, ids, context=None):
1023 self.create_picking(cr, uid, ids, context=context)
1024 self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1027 def action_cancel(self, cr, uid, ids, context=None):
1028 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1031 def action_done(self, cr, uid, ids, context=None):
1032 self.create_account_move(cr, uid, ids, context=context)
1037 class account_bank_statement(osv.osv):
1038 _inherit = 'account.bank.statement'
1040 'user_id': fields.many2one('res.users', 'User', readonly=True),
1043 'user_id': lambda self,cr,uid,c={}: uid
1045 account_bank_statement()
1047 class account_bank_statement_line(osv.osv):
1048 _inherit = 'account.bank.statement.line'
1050 'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1052 account_bank_statement_line()
1054 class pos_order_line(osv.osv):
1055 _name = "pos.order.line"
1056 _description = "Lines of Point of Sale"
1057 _rec_name = "product_id"
1059 def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1060 res = dict([(i, {}) for i in ids])
1061 account_tax_obj = self.pool.get('account.tax')
1062 cur_obj = self.pool.get('res.currency')
1063 for line in self.browse(cr, uid, ids, context=context):
1064 taxes = line.product_id.taxes_id
1065 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1066 taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
1068 cur = line.order_id.pricelist_id.currency_id
1069 res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1070 res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1073 def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1074 context = context or {}
1078 raise osv.except_osv(_('No Pricelist !'),
1079 _('You have to select a pricelist in the sale form !\n' \
1080 'Please set one before choosing a product.'))
1082 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1083 product_id, qty or 1.0, partner_id)[pricelist]
1085 result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1086 result['value']['price_unit'] = price
1089 def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1093 account_tax_obj = self.pool.get('account.tax')
1094 cur_obj = self.pool.get('res.currency')
1096 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1098 taxes = prod.taxes_id
1099 price = price_unit * (1 - (discount or 0.0) / 100.0)
1100 taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1102 result['price_subtotal'] = taxes['total']
1103 result['price_subtotal_incl'] = taxes['total_included']
1104 return {'value': result}
1107 'company_id': fields.many2one('res.company', 'Company', required=True),
1108 'name': fields.char('Line No', size=32, required=True),
1109 'notice': fields.char('Discount Notice', size=128),
1110 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1111 'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1112 'qty': fields.float('Quantity', digits=(16, 2)),
1113 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1114 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1115 'discount': fields.float('Discount (%)', digits=(16, 2)),
1116 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1117 'create_date': fields.datetime('Creation Date', readonly=True),
1121 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1122 'qty': lambda *a: 1,
1123 'discount': lambda *a: 0.0,
1124 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1127 def copy_data(self, cr, uid, id, default=None, context=None):
1131 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1133 return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1137 class pos_category(osv.osv):
1138 _name = 'pos.category'
1139 _description = "Point of Sale Category"
1140 _order = "sequence, name"
1141 def _check_recursion(self, cr, uid, ids, context=None):
1144 cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1145 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1152 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1155 def name_get(self, cr, uid, ids, context=None):
1158 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1160 for record in reads:
1161 name = record['name']
1162 if record['parent_id']:
1163 name = record['parent_id'][1]+' / '+name
1164 res.append((record['id'], name))
1167 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1168 res = self.name_get(cr, uid, ids, context=context)
1171 def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1173 for obj in self.browse(cr, uid, ids, context=context):
1174 if not obj.category_image:
1175 result[obj.id] = False
1178 image_stream = io.BytesIO(obj.category_image.decode('base64'))
1179 img = Image.open(image_stream)
1180 img.thumbnail((120, 100), Image.ANTIALIAS)
1181 img_stream = StringIO.StringIO()
1182 img.save(img_stream, "JPEG")
1183 result[obj.id] = img_stream.getvalue().encode('base64')
1187 'name': fields.char('Name', size=64, required=True, translate=True),
1188 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1189 'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1190 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1191 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1192 'category_image': fields.binary('Image'),
1193 'category_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1195 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['category_image'], 10),
1203 class product_product(osv.osv):
1204 _inherit = 'product.product'
1205 def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1207 for obj in self.browse(cr, uid, ids, context=context):
1208 if not obj.product_image:
1209 result[obj.id] = False
1212 image_stream = io.BytesIO(obj.product_image.decode('base64'))
1213 img = Image.open(image_stream)
1214 img.thumbnail((120, 100), Image.ANTIALIAS)
1215 img_stream = StringIO.StringIO()
1216 img.save(img_stream, "JPEG")
1217 result[obj.id] = img_stream.getvalue().encode('base64')
1221 'income_pdt': fields.boolean('Point of Sale Cash In', help="This is a product you can use to put cash into a statement for the point of sale backend."),
1222 'expense_pdt': fields.boolean('Point of Sale Cash Out', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."),
1223 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1224 help="If you want to sell this product through the point of sale, select the category it belongs to."),
1225 'product_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1227 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['product_image'], 10),
1229 'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"),
1232 'to_weight' : False,
1238 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: