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 ##############################################################################
27 from datetime import datetime
28 from dateutil.relativedelta import relativedelta
33 from osv import fields, osv
34 from tools.translate import _
35 from decimal import Decimal
36 import decimal_precision as dp
38 _logger = logging.getLogger(__name__)
40 class pos_config(osv.osv):
45 ('inactive', 'Inactive'),
46 ('deprecated', 'Deprecated')
50 'name' : fields.char('Point of Sale Name', size=32, select=1,
51 required=True, help="An internal identification of the point of sale"),
52 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel',
53 'pos_config_id', 'journal_id', 'Available Payment Methods',
54 domain="[('journal_user', '=', True )]",),
55 'shop_id' : fields.many2one('sale.shop', 'Shop',
57 'journal_id' : fields.many2one('account.journal', 'Sale Journal',
58 required=True, domain=[('type', '=', 'sale')],
59 help="Accounting journal used to post sales entries."),
60 'iface_self_checkout' : fields.boolean('Self Checkout Mode',
61 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."),
62 'iface_websql' : fields.boolean('WebSQL (Faster but Chrome Only)',
63 help="If have more than 200 products, it's highly suggested to use WebSQL "\
64 "to store the data in the browser, instead of localStore mechanism. "\
65 "It's more efficient but works on the Chrome browser only."
67 'iface_led' : fields.boolean('Help Notification'),
68 'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'),
69 'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'),
70 'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'),
71 'iface_barscan' : fields.boolean('BarScan Interface'),
72 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'),
73 'iface_print_via_proxy' : fields.boolean('Print via Proxy'),
75 'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True),
76 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True,
77 help="This sequence is automatically created by OpenERP but you can change it "\
78 "to customize the reference numbers of your orders."),
79 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'),
80 'group_by' : fields.boolean('Group By', help="Check this if you want to group the Journal Items by Product while closing a Session"),
83 def name_get(self, cr, uid, ids, context=None):
86 'opening_control': _('Opening Control'),
87 'opened': _('In Progress'),
88 'closing_control': _('Closing Control'),
89 'closed': _('Closed & Posted'),
91 for record in self.browse(cr, uid, ids, context=context):
92 if (not record.session_ids) or (record.session_ids[0].state=='closed'):
93 result.append((record.id, record.name+' ('+_('not used')+')'))
95 session = record.session_ids[0]
96 result.append((record.id, record.name + ' ('+session.user_id.name+', '+states[session.state]+')'))
100 def _default_payment_journal(self, cr, uid, context=None):
101 res = self.pool.get('account.journal').search(cr, uid, [('type', 'in', ('bank','cash'))], limit=2)
104 def _default_sale_journal(self, cr, uid, context=None):
105 res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
106 return res and res[0] or False
108 def _default_shop(self, cr, uid, context=None):
109 res = self.pool.get('sale.shop').search(cr, uid, [])
110 return res and res[0] or False
113 'state' : POS_CONFIG_STATE[0][0],
114 'shop_id': _default_shop,
115 'journal_id': _default_sale_journal,
116 'journal_ids': _default_payment_journal,
120 def set_active(self, cr, uid, ids, context=None):
121 return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
123 def set_inactive(self, cr, uid, ids, context=None):
124 return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context)
126 def set_deprecate(self, cr, uid, ids, context=None):
127 return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context)
129 def create(self, cr, uid, values, context=None):
130 proxy = self.pool.get('ir.sequence')
131 sequence_values = dict(
132 name='PoS %s' % values['name'],
134 prefix="%s/" % values['name'],
136 sequence_id = proxy.create(cr, uid, sequence_values, context=context)
137 values['sequence_id'] = sequence_id
138 return super(pos_config, self).create(cr, uid, values, context=context)
140 def unlink(self, cr, uid, ids, context=None):
141 for obj in self.browse(cr, uid, ids, context=context):
143 obj.sequence_id.unlink()
144 return super(pos_config, self).unlink(cr, uid, ids, context=context)
148 class pos_session(osv.osv):
149 _name = 'pos.session'
152 POS_SESSION_STATE = [
153 ('opening_control', 'Opening Control'), # Signal open
154 ('opened', 'In Progress'), # Signal closing
155 ('closing_control', 'Closing Control'), # Signal close
156 ('closed', 'Closed & Posted'),
159 def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None):
160 result = dict.fromkeys(ids, False)
161 for record in self.browse(cr, uid, ids, context=context):
162 for st in record.statement_ids:
163 if st.journal_id.type == 'cash':
164 result[record.id] = st.id
168 def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None):
171 for record in self.browse(cr, uid, ids, context=context):
172 has_opening_control = False
173 has_closing_control = False
175 for journal in record.config_id.journal_ids:
176 if journal.opening_control == True:
177 has_opening_control = True
178 if journal.closing_control == True:
179 has_closing_control = True
181 if has_opening_control and has_closing_control:
185 'has_opening_control': has_opening_control,
186 'has_closing_control': has_closing_control,
188 result[record.id] = values
193 'config_id' : fields.many2one('pos.config', 'Point of Sale',
194 help="The physical point of sale you will use.",
197 domain="[('state', '=', 'active')]",
199 # states={'draft' : [('readonly', False)]}
202 'name' : fields.char('Session ID', size=32,
205 # states={'draft' : [('readonly', False)]}
207 'user_id' : fields.many2one('res.users', 'Responsible',
211 # states={'draft' : [('readonly', False)]}
213 'start_at' : fields.datetime('Opening Date'),
214 'stop_at' : fields.datetime('Closing Date'),
216 'state' : fields.selection(POS_SESSION_STATE, 'State',
217 required=True, readonly=True,
220 'cash_register_id' : fields.function(_compute_cash_register_id, method=True,
221 type='many2one', relation='account.bank.statement',
222 string='Cash Register', store=True),
224 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids',
225 type='one2many', relation='account.cashbox.line',
226 string='Opening Cash Control'),
227 'details_ids' : fields.related('cash_register_id', 'details_ids',
228 type='one2many', relation='account.cashbox.line',
229 string='Cash Control'),
231 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real',
233 digits_compute=dp.get_precision('Account'),
234 string="Ending Balance",
235 help="Computed using the cash control lines",
237 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start',
239 digits_compute=dp.get_precision('Account'),
240 string="Starting Balance",
241 help="Computed using the cash control at the opening.",
243 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding',
244 string='Total Cash Transaction',
246 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end',
248 digits_compute=dp.get_precision('Account'),
249 string="Computed Balance",
250 help="Computed with the initial cash control and the sum of all payments.",
252 'cash_register_difference' : fields.related('cash_register_id', 'difference',
255 help="Difference between the counted cash control at the closing and the computed balance.",
258 'journal_ids' : fields.related('config_id', 'journal_ids',
261 relation='account.journal',
262 string='Available Payment Methods'),
263 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
265 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True),
266 'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'),
267 'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'),
272 'user_id' : lambda obj, cr, uid, context: uid,
273 'state' : 'opening_control',
277 ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
280 def _check_unicity(self, cr, uid, ids, context=None):
281 for session in self.browse(cr, uid, ids, context=None):
282 # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
284 ('state', '!=', 'closed'),
285 ('user_id', '=', uid)
287 count = self.search_count(cr, uid, domain, context=context)
292 def _check_pos_config(self, cr, uid, ids, context=None):
293 for session in self.browse(cr, uid, ids, context=None):
295 ('state', '!=', 'closed'),
296 ('config_id', '=', session.config_id.id)
298 count = self.search_count(cr, uid, domain, context=context)
304 (_check_unicity, "You can not create two active sessions with the same responsible!", ['user_id', 'state']),
305 (_check_pos_config, "You can not create two active sessions related to the same point of sale!", ['config_id']),
308 def create(self, cr, uid, values, context=None):
309 config_id = values.get('config_id', False) or False
313 pos_config = self.pool.get('pos.config').browse(cr, uid, config_id, context=context)
315 bank_statement_ids = []
316 for journal in pos_config.journal_ids:
318 'journal_id' : journal.id,
321 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
322 bank_statement_ids.append(statement_id)
325 'name' : pos_config.sequence_id._next(),
326 'statement_ids' : [(6, 0, bank_statement_ids)]
329 return super(pos_session, self).create(cr, uid, values, context=context)
331 def unlink(self, cr, uid, ids, context=None):
332 for obj in self.browse(cr, uid, ids, context=context):
333 for statement in obj.statement_ids:
334 statement.unlink(context=context)
337 def wkf_action_open(self, cr, uid, ids, context=None):
338 # si pas de date start_at, je balance une date, sinon on utilise celle de l'utilisateur
339 for record in self.browse(cr, uid, ids, context=context):
341 if not record.start_at:
342 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
343 values['state'] = 'opened'
344 record.write(values, context=context)
345 for st in record.statement_ids:
346 st.button_open(context=context)
349 def wkf_action_opening_control(self, cr, uid, ids, context=None):
350 return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context)
352 def wkf_action_closing_control(self, cr, uid, ids, context=None):
353 for session in self.browse(cr, uid, ids, context=context):
354 for statement in session.statement_ids:
355 if not statement.journal_id.closing_control:
356 if statement.balance_end<>statement.balance_end_real:
357 self.pool.get('account.bank.statement').write(cr, uid,
358 [statement.id], {'balance_end_real': statement.balance_end})
359 return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
361 def wkf_action_close(self, cr, uid, ids, context=None):
363 bsl = self.pool.get('account.bank.statement.line')
364 for record in self.browse(cr, uid, ids, context=context):
365 for st in record.statement_ids:
366 if abs(st.difference) > st.journal_id.amount_authorized_diff:
367 # The pos manager can close statements with maximums.
368 if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"):
369 raise osv.except_osv( _('Error !'),
370 _("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))
372 if st.difference > 0.0:
373 name= _('Point of Sale Profit')
374 account_id = st.journal_id.profit_account_id.id
376 account_id = st.journal_id.loss_account_id.id
377 name= _('Point of Sale Loss')
379 raise osv.except_osv( _('Error !'),
380 _("Please set your profit and loss accounts on your payment method '%s'.") % (st.journal_id.name,))
381 bsl.create(cr, uid, {
382 'statement_id': st.id,
383 'amount': st.difference,
386 'account_id': account_id
389 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
390 self._confirm_orders(cr, uid, ids, context=context)
391 return self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
393 def _confirm_orders(self, cr, uid, ids, context=None):
394 wf_service = netsvc.LocalService("workflow")
396 for session in self.browse(cr, uid, ids, context=context):
397 order_ids = [order.id for order in session.order_ids if order.state == 'paid']
399 move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context)
401 self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context)
403 for order in session.order_ids:
404 if order.state != 'paid':
405 raise osv.except_osv(
407 _("You can not confirm all orders of this session, because they have not the 'paid' status"))
409 wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
413 def open_frontend_cb(self, cr, uid, ids, context=None):
420 context.update({'session_id' : ids[0]})
422 'type' : 'ir.actions.client',
423 'name' : 'Start Point Of Sale',
430 class pos_order(osv.osv):
432 _description = "Point of Sale"
435 def create_from_ui(self, cr, uid, orders, context=None):
436 #_logger.info("orders: %r", orders)
438 for tmp_order in orders:
439 order = tmp_order['data']
440 # 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}
441 # get statements out of order because they will be generated with add_payment to ensure
442 # the module behavior is the same when using the front-end or the back-end
443 statement_ids = order.get('statement_ids', [])
444 order_id = self.create(cr, uid, order, context)
445 order_ids.append(order_id)
446 # call add_payment; refer to wizard/pos_payment for data structure
447 # add_payment launches the 'paid' signal to advance the workflow to the 'paid' state
450 'journal': statement_ids[0][2]['journal_id'],
451 'amount': order['amount_paid'],
452 'payment_name': order['name'],
453 'payment_date': statement_ids[0][2]['name'],
455 wf_service = netsvc.LocalService("workflow")
456 wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
457 wf_service.trg_write(uid, 'pos.order', order_id, cr)
459 #self.add_payment(cr, uid, order_id, data, context=context)
462 def unlink(self, cr, uid, ids, context=None):
463 for rec in self.browse(cr, uid, ids, context=context):
464 if rec.state not in ('draft','cancel'):
465 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
466 return super(pos_order, self).unlink(cr, uid, ids, context=context)
468 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
471 pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
472 return {'value': {'pricelist_id': pricelist}}
474 def _amount_all(self, cr, uid, ids, name, args, context=None):
475 tax_obj = self.pool.get('account.tax')
476 cur_obj = self.pool.get('res.currency')
478 for order in self.browse(cr, uid, ids, context=context):
485 cur = order.pricelist_id.currency_id
486 for payment in order.statement_ids:
487 res[order.id]['amount_paid'] += payment.amount
488 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
489 for line in order.lines:
490 val1 += line.price_subtotal_incl
491 val2 += line.price_subtotal
492 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
493 res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
496 def copy(self, cr, uid, id, default=None, context=None):
502 'account_move': False,
506 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
509 return super(pos_order, self).copy(cr, uid, id, d, context=context)
512 'name': fields.char('Order Ref', size=64, required=True, readonly=True),
513 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
514 'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True),
515 'date_order': fields.datetime('Order Date', readonly=True, select=True),
516 '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."),
517 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
518 'amount_total': fields.function(_amount_all, string='Total', multi='all'),
519 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
520 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
521 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
522 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
523 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
524 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
526 'session_id' : fields.many2one('pos.session', 'Session',
529 domain="[('state', '=', 'opened')]",
530 states={'draft' : [('readonly', False)]},
533 'state': fields.selection([('draft', 'New'),
534 ('cancel', 'Cancelled'),
537 ('invoiced', 'Invoiced')],
538 'Status', readonly=True),
540 'invoice_id': fields.many2one('account.invoice', 'Invoice'),
541 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
542 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
543 'note': fields.text('Internal Notes'),
544 'nb_print': fields.integer('Number of Print', readonly=True),
546 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
549 def _default_session(self, cr, uid, context=None):
550 so = self.pool.get('pos.session')
551 session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
552 return session_ids and session_ids[0] or False
554 def _default_pricelist(self, cr, uid, context=None):
555 res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
557 shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
558 return shop.pricelist_id and shop.pricelist_id.id or False
562 'user_id': lambda self, cr, uid, context: uid,
565 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
567 'session_id': _default_session,
568 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
569 'pricelist_id': _default_pricelist,
572 def create(self, cr, uid, values, context=None):
573 values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
574 return super(pos_order, self).create(cr, uid, values, context=context)
576 def test_paid(self, cr, uid, ids, context=None):
577 """A Point of Sale is paid when the sum
580 for order in self.browse(cr, uid, ids, context=context):
581 if order.lines and not order.amount_total:
583 if (not order.lines) or (not order.statement_ids) or \
584 (abs(order.amount_total-order.amount_paid) > 0.00001):
588 def create_picking(self, cr, uid, ids, context=None):
589 """Create a picking for each order and validate it."""
590 picking_obj = self.pool.get('stock.picking')
591 partner_obj = self.pool.get('res.partner')
592 move_obj = self.pool.get('stock.move')
594 for order in self.browse(cr, uid, ids, context=context):
595 if not order.state=='draft':
597 addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
598 picking_id = picking_obj.create(cr, uid, {
599 'origin': order.name,
600 'partner_id': addr.get('delivery',False),
602 'company_id': order.company_id.id,
603 'move_type': 'direct',
604 'note': order.note or "",
605 'invoice_state': 'none',
606 'auto_picking': True,
608 self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
609 location_id = order.shop_id.warehouse_id.lot_stock_id.id
610 output_id = order.shop_id.warehouse_id.lot_output_id.id
612 for line in order.lines:
613 if line.product_id and line.product_id.type == 'service':
616 location_id, output_id = output_id, location_id
618 move_obj.create(cr, uid, {
620 'product_uom': line.product_id.uom_id.id,
621 'product_uos': line.product_id.uom_id.id,
622 'picking_id': picking_id,
623 'product_id': line.product_id.id,
624 'product_uos_qty': abs(line.qty),
625 'product_qty': abs(line.qty),
626 'tracking_id': False,
628 'location_id': location_id,
629 'location_dest_id': output_id,
632 location_id, output_id = output_id, location_id
634 wf_service = netsvc.LocalService("workflow")
635 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
636 picking_obj.force_assign(cr, uid, [picking_id], context)
639 def cancel_order(self, cr, uid, ids, context=None):
640 """ Changes order state to cancel
643 stock_picking_obj = self.pool.get('stock.picking')
644 for order in self.browse(cr, uid, ids, context=context):
645 wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
646 if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
647 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
648 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
651 def add_payment(self, cr, uid, order_id, data, context=None):
652 """Create a new payment for the order"""
655 statement_obj = self.pool.get('account.bank.statement')
656 statement_line_obj = self.pool.get('account.bank.statement.line')
657 prod_obj = self.pool.get('product.product')
658 property_obj = self.pool.get('ir.property')
659 curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
660 curr_company = curr_c.id
661 order = self.browse(cr, uid, order_id, context=context)
663 'amount': data['amount'],
665 if 'payment_date' in data:
666 args['date'] = data['payment_date']
667 args['name'] = order.name
668 if data.get('payment_name', False):
669 args['name'] = args['name'] + ': ' + data['payment_name']
670 account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
671 args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
672 and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
673 args['partner_id'] = order.partner_id and order.partner_id.id or None
675 if not args['account_id']:
676 if not args['partner_id']:
677 msg = _('There is no receivable account defined to make payment')
679 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d)') % (order.partner_id.name, order.partner_id.id,)
680 raise osv.except_osv(_('Configuration Error !'), msg)
682 context.pop('pos_session_id', False)
685 journal_id = long(data['journal'])
690 for statement in order.session_id.statement_ids:
691 if statement.journal_id.id == journal_id:
692 statement_id = statement.id
696 raise osv.except_osv(_('Error !'), _('You have to open at least one cashbox'))
699 'statement_id' : statement_id,
700 'pos_statement_id' : order_id,
701 'journal_id' : journal_id,
706 statement_line_obj.create(cr, uid, args, context=context)
708 wf_service = netsvc.LocalService("workflow")
709 wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
710 wf_service.trg_write(uid, 'pos.order', order_id, cr)
714 def refund(self, cr, uid, ids, context=None):
715 """Create a copy of order for refund order"""
717 line_obj = self.pool.get('pos.order.line')
718 for order in self.browse(cr, uid, ids, context=context):
719 clone_id = self.copy(cr, uid, order.id, {
720 'name': order.name + ' REFUND',
722 clone_list.append(clone_id)
724 for clone in self.browse(cr, uid, clone_list, context=context):
725 for order_line in clone.lines:
726 line_obj.write(cr, uid, [order_line.id], {
727 'qty': -order_line.qty
730 new_order = ','.join(map(str,clone_list))
732 #'domain': "[('id', 'in', ["+new_order+"])]",
733 'name': _('Return Products'),
736 'res_model': 'pos.order',
737 'res_id':clone_list[0],
740 'type': 'ir.actions.act_window',
746 def action_invoice_state(self, cr, uid, ids, context=None):
747 return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
749 def action_invoice(self, cr, uid, ids, context=None):
750 wf_service = netsvc.LocalService("workflow")
751 inv_ref = self.pool.get('account.invoice')
752 inv_line_ref = self.pool.get('account.invoice.line')
753 product_obj = self.pool.get('product.product')
756 for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
758 inv_ids.append(order.invoice_id.id)
761 if not order.partner_id:
762 raise osv.except_osv(_('Error'), _('Please provide a partner for the sale.'))
764 acc = order.partner_id.property_account_receivable.id
767 'origin': order.name,
769 'journal_id': order.sale_journal.id or None,
770 'type': 'out_invoice',
771 'reference': order.name,
772 'partner_id': order.partner_id.id,
773 'comment': order.note or '',
774 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
776 inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
777 if not inv.get('account_id', None):
778 inv['account_id'] = acc
779 inv_id = inv_ref.create(cr, uid, inv, context=context)
781 self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
782 inv_ids.append(inv_id)
783 for line in order.lines:
785 'invoice_id': inv_id,
786 'product_id': line.product_id.id,
787 'quantity': line.qty,
789 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
790 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
792 line.product_id.uom_id.id,
793 line.qty, partner_id = order.partner_id.id,
794 fposition_id=order.partner_id.property_account_position.id)['value'])
795 if line.product_id.description_sale:
796 inv_line['note'] = line.product_id.description_sale
797 inv_line['price_unit'] = line.price_unit
798 inv_line['discount'] = line.discount
799 inv_line['name'] = inv_name
800 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
801 and [(6, 0, inv_line['invoice_line_tax_id'])] or []
802 inv_line_ref.create(cr, uid, inv_line, context=context)
803 inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
804 wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
806 if not inv_ids: return {}
808 mod_obj = self.pool.get('ir.model.data')
809 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
810 res_id = res and res[1] or False
812 'name': _('Customer Invoice'),
816 'res_model': 'account.invoice',
817 'context': "{'type':'out_invoice'}",
818 'type': 'ir.actions.act_window',
821 'res_id': inv_ids and inv_ids[0] or False,
824 def create_account_move(self, cr, uid, ids, context=None):
825 return self._create_account_move_line(cr, uid, ids, None, None, context=context)
827 def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
828 # Tricky, via the workflow, we only have one id in the ids variable
829 """Create a account move line of order grouped by products or not."""
830 account_move_obj = self.pool.get('account.move')
831 account_move_line_obj = self.pool.get('account.move.line')
832 account_period_obj = self.pool.get('account.period')
833 account_tax_obj = self.pool.get('account.tax')
834 user_proxy = self.pool.get('res.users')
835 property_obj = self.pool.get('ir.property')
837 period = account_period_obj.find(cr, uid, context=context)[0]
839 #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
841 if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
842 raise osv.except_osv(_('Error!'), _('The selected orders do not have the same session !'))
844 current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
847 have_to_group_by = session and session.config_id.group_by or False
849 def compute_tax(amount, tax, line):
851 tax_code_id = tax['base_code_id']
852 tax_amount = line.price_subtotal * tax['base_sign']
854 tax_code_id = tax['ref_base_code_id']
855 tax_amount = line.price_subtotal * tax['ref_base_sign']
857 return (tax_code_id, tax_amount,)
859 for order in self.browse(cr, uid, ids, context=context):
860 if order.account_move:
862 if order.state != 'paid':
865 user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
868 account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
870 order_account = order.partner_id and \
871 order.partner_id.property_account_receivable and \
872 order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
875 # Create an entry for the sale
876 move_id = account_move_obj.create(cr, uid, {
878 'journal_id': order.sale_journal.id,
881 def insert_data(data_type, values):
882 # if have_to_group_by:
884 sale_journal_id = order.sale_journal.id
886 # 'quantity': line.qty,
887 # 'product_id': line.product_id.id,
889 'date': order.date_order[:10],
891 'journal_id' : sale_journal_id,
892 'period_id' : period,
894 'company_id': user_company and user_company.id or False,
897 if data_type == 'product':
898 key = ('product', values['product_id'],)
899 elif data_type == 'tax':
900 key = ('tax', values['tax_code_id'],)
901 elif data_type == 'counter_part':
902 key = ('counter_part', values['partner_id'], values['account_id'])
906 grouped_data.setdefault(key, [])
908 # if not have_to_group_by or (not grouped_data[key]):
909 # grouped_data[key].append(values)
914 if not grouped_data[key]:
915 grouped_data[key].append(values)
917 current_value = grouped_data[key][0]
918 current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0)
919 current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
920 current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
921 current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
923 grouped_data[key].append(values)
925 # Create an move for each order line
927 for line in order.lines:
929 taxes = [t for t in line.product_id.taxes_id]
930 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
932 for tax in computed_taxes:
933 tax_amount += round(tax['amount'], 2)
934 group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
936 group_tax.setdefault(group_key, 0)
937 group_tax[group_key] += round(tax['amount'], 2)
939 amount = line.price_subtotal
941 # Search for the income account
942 if line.product_id.property_account_income.id:
943 income_account = line.product_id.property_account_income.id
944 elif line.product_id.categ_id.property_account_income_categ.id:
945 income_account = line.product_id.categ_id.property_account_income_categ.id
947 raise osv.except_osv(_('Error !'), _('There is no income '\
948 'account defined for this product: "%s" (id:%d)') \
949 % (line.product_id.name, line.product_id.id, ))
951 # Empty the tax list as long as there is no tax code:
954 while computed_taxes:
955 tax = computed_taxes.pop(0)
956 tax_code_id, tax_amount = compute_tax(amount, tax, line)
958 # If there is one we stop
962 # Create a move for the line
963 insert_data('product', {
964 'name': line.product_id.name,
965 'quantity': line.qty,
966 'product_id': line.product_id.id,
967 'account_id': income_account,
968 'credit': ((amount>0) and amount) or 0.0,
969 'debit': ((amount<0) and -amount) or 0.0,
970 'tax_code_id': tax_code_id,
971 'tax_amount': tax_amount,
972 'partner_id': order.partner_id and order.partner_id.id or False
975 # For each remaining tax with a code, whe create a move line
976 for tax in computed_taxes:
977 tax_code_id, tax_amount = compute_tax(amount, tax, line)
983 'product_id':line.product_id.id,
984 'quantity': line.qty,
985 'account_id': income_account,
988 'tax_code_id': tax_code_id,
989 'tax_amount': tax_amount,
992 # Create a move for each tax group
993 (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
995 for key, tax_amount in group_tax.items():
996 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
998 'name': _('Tax') + ' ' + tax.name,
999 'quantity': line.qty,
1000 'product_id': line.product_id.id,
1001 'account_id': key[account_pos],
1002 'credit': ((tax_amount>0) and tax_amount) or 0.0,
1003 'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1004 'tax_code_id': key[tax_code_pos],
1005 'tax_amount': tax_amount,
1009 insert_data('counter_part', {
1010 'name': _("Trade Receivables"), #order.name,
1011 'account_id': order_account,
1012 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1013 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1014 'partner_id': order.partner_id and order.partner_id.id or False
1017 order.write({'state':'done', 'account_move': move_id})
1019 for group_key, group_data in grouped_data.iteritems():
1020 for value in group_data:
1021 account_move_line_obj.create(cr, uid, value, context=context)
1025 def action_payment(self, cr, uid, ids, context=None):
1026 return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1028 def action_paid(self, cr, uid, ids, context=None):
1029 self.create_picking(cr, uid, ids, context=context)
1030 self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1033 def action_cancel(self, cr, uid, ids, context=None):
1034 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1037 def action_done(self, cr, uid, ids, context=None):
1038 self.create_account_move(cr, uid, ids, context=context)
1043 class account_bank_statement(osv.osv):
1044 _inherit = 'account.bank.statement'
1046 'user_id': fields.many2one('res.users', 'User', readonly=True),
1049 'user_id': lambda self,cr,uid,c={}: uid
1051 account_bank_statement()
1053 class account_bank_statement_line(osv.osv):
1054 _inherit = 'account.bank.statement.line'
1056 'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1058 account_bank_statement_line()
1060 class pos_order_line(osv.osv):
1061 _name = "pos.order.line"
1062 _description = "Lines of Point of Sale"
1063 _rec_name = "product_id"
1065 def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1066 res = dict([(i, {}) for i in ids])
1067 account_tax_obj = self.pool.get('account.tax')
1068 cur_obj = self.pool.get('res.currency')
1069 for line in self.browse(cr, uid, ids, context=context):
1070 taxes = line.product_id.taxes_id
1071 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1072 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)
1074 cur = line.order_id.pricelist_id.currency_id
1075 res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1076 res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1079 def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1080 context = context or {}
1084 raise osv.except_osv(_('No Pricelist !'),
1085 _('You have to select a pricelist in the sale form !\n' \
1086 'Please set one before choosing a product.'))
1088 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1089 product_id, qty or 1.0, partner_id)[pricelist]
1091 result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1092 result['value']['price_unit'] = price
1095 def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1099 account_tax_obj = self.pool.get('account.tax')
1100 cur_obj = self.pool.get('res.currency')
1102 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1104 taxes = prod.taxes_id
1105 price = price_unit * (1 - (discount or 0.0) / 100.0)
1106 taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1108 result['price_subtotal'] = taxes['total']
1109 result['price_subtotal_incl'] = taxes['total_included']
1110 return {'value': result}
1113 'company_id': fields.many2one('res.company', 'Company', required=True),
1114 'name': fields.char('Line No', size=32, required=True),
1115 'notice': fields.char('Discount Notice', size=128),
1116 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1117 'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1118 'qty': fields.float('Quantity', digits=(16, 2)),
1119 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1120 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1121 'discount': fields.float('Discount (%)', digits=(16, 2)),
1122 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1123 'create_date': fields.datetime('Creation Date', readonly=True),
1127 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1128 'qty': lambda *a: 1,
1129 'discount': lambda *a: 0.0,
1130 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1133 def copy_data(self, cr, uid, id, default=None, context=None):
1137 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1139 return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1143 class pos_category(osv.osv):
1144 _name = 'pos.category'
1145 _description = "Point of Sale Category"
1146 _order = "sequence, name"
1147 def _check_recursion(self, cr, uid, ids, context=None):
1150 cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1151 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1158 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1161 def name_get(self, cr, uid, ids, context=None):
1164 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1166 for record in reads:
1167 name = record['name']
1168 if record['parent_id']:
1169 name = record['parent_id'][1]+' / '+name
1170 res.append((record['id'], name))
1173 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1174 res = self.name_get(cr, uid, ids, context=context)
1177 def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1179 for obj in self.browse(cr, uid, ids, context=context):
1180 if not obj.category_image:
1181 result[obj.id] = False
1184 image_stream = io.BytesIO(obj.category_image.decode('base64'))
1185 img = Image.open(image_stream)
1186 img.thumbnail((120, 100), Image.ANTIALIAS)
1187 img_stream = StringIO.StringIO()
1188 img.save(img_stream, "JPEG")
1189 result[obj.id] = img_stream.getvalue().encode('base64')
1193 'name': fields.char('Name', size=64, required=True, translate=True),
1194 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1195 'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1196 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1197 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1198 'category_image': fields.binary('Image'),
1199 'category_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1201 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['category_image'], 10),
1205 def _get_default_image(self, cr, uid, context=None):
1206 image_path = openerp.modules.get_module_resource('point_of_sale', 'images', 'default_category_photo.png')
1207 return open(image_path, 'rb').read().encode('base64')
1211 'category_image': _get_default_image,
1218 class product_product(osv.osv):
1219 _inherit = 'product.product'
1220 def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1222 for obj in self.browse(cr, uid, ids, context=context):
1223 if not obj.product_image:
1224 result[obj.id] = False
1227 image_stream = io.BytesIO(obj.product_image.decode('base64'))
1228 img = Image.open(image_stream)
1229 img.thumbnail((120, 100), Image.ANTIALIAS)
1230 img_stream = StringIO.StringIO()
1231 img.save(img_stream, "JPEG")
1232 result[obj.id] = img_stream.getvalue().encode('base64')
1236 '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."),
1237 '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."),
1238 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1239 help="If you want to sell this product through the point of sale, select the category it belongs to."),
1240 'product_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1242 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['product_image'], 10),
1244 'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"),
1247 'to_weight' : False,
1253 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: