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
32 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 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_journal_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.journal_id.id
168 def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None):
169 result = dict.fromkeys(ids, False)
170 for record in self.browse(cr, uid, ids, context=context):
171 for st in record.statement_ids:
172 if st.journal_id.type == 'cash':
173 result[record.id] = st.id
177 def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None):
180 for record in self.browse(cr, uid, ids, context=context):
181 has_opening_control = False
182 has_closing_control = False
184 for journal in record.config_id.journal_ids:
185 if journal.opening_control == True:
186 has_opening_control = True
187 if journal.closing_control == True:
188 has_closing_control = True
190 if has_opening_control and has_closing_control:
194 'has_opening_control': has_opening_control,
195 'has_closing_control': has_closing_control,
197 result[record.id] = values
202 'config_id' : fields.many2one('pos.config', 'Point of Sale',
203 help="The physical point of sale you will use.",
206 domain="[('state', '=', 'active')]",
209 'name' : fields.char('Session ID', size=32, required=True, readonly=True),
210 'user_id' : fields.many2one('res.users', 'Responsible',
214 states={'opening_control' : [('readonly', False)]}
216 'start_at' : fields.datetime('Opening Date', readonly=True),
217 'stop_at' : fields.datetime('Closing Date', readonly=True),
219 'state' : fields.selection(POS_SESSION_STATE, 'State',
220 required=True, readonly=True,
223 'cash_journal_id' : fields.function(_compute_cash_journal_id, method=True,
224 type='many2one', relation='account.journal',
225 string='Cash Journal', store=True),
226 'cash_register_id' : fields.function(_compute_cash_register_id, method=True,
227 type='many2one', relation='account.bank.statement',
228 string='Cash Register', store=True),
230 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids',
231 type='one2many', relation='account.cashbox.line',
232 string='Opening Cash Control'),
233 'details_ids' : fields.related('cash_register_id', 'details_ids',
234 type='one2many', relation='account.cashbox.line',
235 string='Cash Control'),
237 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real',
239 digits_compute=dp.get_precision('Account'),
240 string="Ending Balance",
241 help="Computed using the cash control lines",
243 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start',
245 digits_compute=dp.get_precision('Account'),
246 string="Starting Balance",
247 help="Computed using the cash control at the opening.",
249 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding',
250 string='Total Cash Transaction',
252 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end',
254 digits_compute=dp.get_precision('Account'),
255 string="Computed Balance",
256 help="Computed with the initial cash control and the sum of all payments.",
258 'cash_register_difference' : fields.related('cash_register_id', 'difference',
261 help="Difference between the counted cash control at the closing and the computed balance.",
264 'journal_ids' : fields.related('config_id', 'journal_ids',
267 relation='account.journal',
268 string='Available Payment Methods'),
269 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
271 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True),
272 'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'),
273 'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'),
278 'user_id' : lambda obj, cr, uid, context: uid,
279 'state' : 'opening_control',
283 ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
286 def _check_unicity(self, cr, uid, ids, context=None):
287 for session in self.browse(cr, uid, ids, context=None):
288 # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
290 ('state', 'not in', ('closed','closing_control')),
291 ('user_id', '=', uid)
293 count = self.search_count(cr, uid, domain, context=context)
298 def _check_pos_config(self, cr, uid, ids, context=None):
299 for session in self.browse(cr, uid, ids, context=None):
301 ('state', '!=', 'closed'),
302 ('config_id', '=', session.config_id.id)
304 count = self.search_count(cr, uid, domain, context=context)
310 (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']),
311 (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']),
314 def create(self, cr, uid, values, context=None):
315 config_id = values.get('config_id', False) or False
317 # journal_id is not required on the pos_config because it does not
318 # exists at the installation. If nothing is configured at the
319 # installation we do the minimal configuration. Impossible to do in
320 # the .xml files as the CoA is not yet installed.
321 jobj = self.pool.get('pos.config')
322 pos_config = jobj.browse(cr, uid, config_id, context=context)
323 if not pos_config.journal_id:
324 jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id']
326 jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context)
328 raise osv.except_osv( _('error!'),
329 _("Unable to open the session. You have to assign a sale journal to your point of sale."))
331 # define some cash journal if no payment method exists
332 if not pos_config.journal_ids:
333 cashids = self.pool.get('account.journal').search(cr, uid, [('journal_user','=',True)], context=context)
335 cashids = self.pool.get('account.journal').search(cr, uid, [('type','=','cash')], context=context)
336 self.pool.get('account.journal').write(cr, uid, cashids, {'journal_user': True})
337 jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]})
340 pos_config = jobj.browse(cr, uid, config_id, context=context)
341 bank_statement_ids = []
342 for journal in pos_config.journal_ids:
344 'journal_id' : journal.id,
347 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
348 bank_statement_ids.append(statement_id)
351 'name' : pos_config.sequence_id._next(),
352 'statement_ids' : [(6, 0, bank_statement_ids)],
353 'config_id': config_id
356 return super(pos_session, self).create(cr, uid, values, context=context)
358 def unlink(self, cr, uid, ids, context=None):
359 for obj in self.browse(cr, uid, ids, context=context):
360 for statement in obj.statement_ids:
361 statement.unlink(context=context)
364 def wkf_action_open(self, cr, uid, ids, context=None):
365 # second browse because we need to refetch the data from the DB for cash_register_id
366 for record in self.browse(cr, uid, ids, context=context):
368 if not record.start_at:
369 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
370 values['state'] = 'opened'
371 record.write(values, context=context)
372 for st in record.statement_ids:
373 st.button_open(context=context)
375 return self.open_frontend_cb(cr, uid, ids, context=context)
377 def wkf_action_opening_control(self, cr, uid, ids, context=None):
378 return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context)
380 def wkf_action_closing_control(self, cr, uid, ids, context=None):
381 for session in self.browse(cr, uid, ids, context=context):
382 for statement in session.statement_ids:
383 if statement.id <> session.cash_register_id.id:
384 if statement.balance_end<>statement.balance_end_real:
385 self.pool.get('account.bank.statement').write(cr, uid,
386 [statement.id], {'balance_end_real': statement.balance_end})
387 return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
389 def wkf_action_close(self, cr, uid, ids, context=None):
391 bsl = self.pool.get('account.bank.statement.line')
392 for record in self.browse(cr, uid, ids, context=context):
393 for st in record.statement_ids:
394 if abs(st.difference) > st.journal_id.amount_authorized_diff:
395 # The pos manager can close statements with maximums.
396 if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"):
397 raise osv.except_osv( _('Error!'),
398 _("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))
400 if st.difference > 0.0:
401 name= _('Point of Sale Profit')
402 account_id = st.journal_id.profit_account_id.id
404 account_id = st.journal_id.loss_account_id.id
405 name= _('Point of Sale Loss')
407 raise osv.except_osv( _('Error!'),
408 _("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))
409 bsl.create(cr, uid, {
410 'statement_id': st.id,
411 'amount': st.difference,
414 'account_id': account_id
417 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
418 self._confirm_orders(cr, uid, ids, context=context)
419 self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
421 obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1]
423 'type' : 'ir.actions.client',
424 'name' : 'Point of Sale Menu',
426 'params' : {'menu_id': obj},
429 def _confirm_orders(self, cr, uid, ids, context=None):
430 wf_service = netsvc.LocalService("workflow")
432 for session in self.browse(cr, uid, ids, context=context):
433 order_ids = [order.id for order in session.order_ids if order.state == 'paid']
435 move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context)
437 self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context)
439 for order in session.order_ids:
440 if order.state != 'paid':
441 raise osv.except_osv(
443 _("You cannot confirm all orders of this session, because they have not the 'paid' status"))
445 wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
449 def open_frontend_cb(self, cr, uid, ids, context=None):
454 context.update({'session_id' : ids[0]})
456 'type' : 'ir.actions.client',
457 'name' : 'Start Point Of Sale',
464 class pos_order(osv.osv):
466 _description = "Point of Sale"
469 def create_from_ui(self, cr, uid, orders, context=None):
470 #_logger.info("orders: %r", orders)
472 for tmp_order in orders:
473 order = tmp_order['data']
474 order_id = self.create(cr, uid, {
475 'name': order['name'],
476 'user_id': order['user_id'] or False,
477 'session_id': order['pos_session_id'],
478 'lines': order['lines']
481 for payments in order['statement_ids']:
482 payment = payments[2]
483 self.add_payment(cr, uid, order_id, {
484 'amount': payment['amount'] or 0.0,
485 'payment_date': payment['name'],
486 'payment_name': payment.get('note', False),
487 'journal': payment['journal_id']
490 if order['amount_return']:
491 session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
492 self.add_payment(cr, uid, order_id, {
493 'amount': -order['amount_return'],
494 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
495 'payment_name': _('return'),
496 'journal': session.cash_journal_id.id
498 order_ids.append(order_id)
499 wf_service = netsvc.LocalService("workflow")
500 wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
503 def unlink(self, cr, uid, ids, context=None):
504 for rec in self.browse(cr, uid, ids, context=context):
505 if rec.state not in ('draft','cancel'):
506 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
507 return super(pos_order, self).unlink(cr, uid, ids, context=context)
509 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
512 pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
513 return {'value': {'pricelist_id': pricelist}}
515 def _amount_all(self, cr, uid, ids, name, args, context=None):
516 tax_obj = self.pool.get('account.tax')
517 cur_obj = self.pool.get('res.currency')
519 for order in self.browse(cr, uid, ids, context=context):
526 cur = order.pricelist_id.currency_id
527 for payment in order.statement_ids:
528 res[order.id]['amount_paid'] += payment.amount
529 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
530 for line in order.lines:
531 val1 += line.price_subtotal_incl
532 val2 += line.price_subtotal
533 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
534 res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
537 def copy(self, cr, uid, id, default=None, context=None):
543 'account_move': False,
547 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
550 return super(pos_order, self).copy(cr, uid, id, d, context=context)
553 'name': fields.char('Order Ref', size=64, required=True, readonly=True),
554 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
555 'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True),
556 'date_order': fields.datetime('Order Date', readonly=True, select=True),
557 '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."),
558 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
559 'amount_total': fields.function(_amount_all, string='Total', multi='all'),
560 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
561 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
562 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
563 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
564 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
565 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
567 'session_id' : fields.many2one('pos.session', 'Session',
570 domain="[('state', '=', 'opened')]",
571 states={'draft' : [('readonly', False)]},
574 'state': fields.selection([('draft', 'New'),
575 ('cancel', 'Cancelled'),
578 ('invoiced', 'Invoiced')],
579 'Status', readonly=True),
581 'invoice_id': fields.many2one('account.invoice', 'Invoice'),
582 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
583 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
584 'note': fields.text('Internal Notes'),
585 'nb_print': fields.integer('Number of Print', readonly=True),
587 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
590 def _default_session(self, cr, uid, context=None):
591 so = self.pool.get('pos.session')
592 session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
593 return session_ids and session_ids[0] or False
595 def _default_pricelist(self, cr, uid, context=None):
596 res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
598 shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
599 return shop.pricelist_id and shop.pricelist_id.id or False
603 'user_id': lambda self, cr, uid, context: uid,
606 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
608 'session_id': _default_session,
609 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
610 'pricelist_id': _default_pricelist,
613 def create(self, cr, uid, values, context=None):
614 values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
615 return super(pos_order, self).create(cr, uid, values, context=context)
617 def test_paid(self, cr, uid, ids, context=None):
618 """A Point of Sale is paid when the sum
621 for order in self.browse(cr, uid, ids, context=context):
622 if order.lines and not order.amount_total:
624 if (not order.lines) or (not order.statement_ids) or \
625 (abs(order.amount_total-order.amount_paid) > 0.00001):
629 def create_picking(self, cr, uid, ids, context=None):
630 """Create a picking for each order and validate it."""
631 picking_obj = self.pool.get('stock.picking')
632 partner_obj = self.pool.get('res.partner')
633 move_obj = self.pool.get('stock.move')
635 for order in self.browse(cr, uid, ids, context=context):
636 if not order.state=='draft':
638 addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
639 picking_id = picking_obj.create(cr, uid, {
640 'origin': order.name,
641 'partner_id': addr.get('delivery',False),
643 'company_id': order.company_id.id,
644 'move_type': 'direct',
645 'note': order.note or "",
646 'invoice_state': 'none',
647 'auto_picking': True,
649 self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
650 location_id = order.shop_id.warehouse_id.lot_stock_id.id
651 output_id = order.shop_id.warehouse_id.lot_output_id.id
653 for line in order.lines:
654 if line.product_id and line.product_id.type == 'service':
657 location_id, output_id = output_id, location_id
659 move_obj.create(cr, uid, {
661 'product_uom': line.product_id.uom_id.id,
662 'product_uos': line.product_id.uom_id.id,
663 'picking_id': picking_id,
664 'product_id': line.product_id.id,
665 'product_uos_qty': abs(line.qty),
666 'product_qty': abs(line.qty),
667 'tracking_id': False,
669 'location_id': location_id,
670 'location_dest_id': output_id,
673 location_id, output_id = output_id, location_id
675 wf_service = netsvc.LocalService("workflow")
676 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
677 picking_obj.force_assign(cr, uid, [picking_id], context)
680 def cancel_order(self, cr, uid, ids, context=None):
681 """ Changes order state to cancel
684 stock_picking_obj = self.pool.get('stock.picking')
685 for order in self.browse(cr, uid, ids, context=context):
686 wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
687 if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
688 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
689 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
692 def add_payment(self, cr, uid, order_id, data, context=None):
693 """Create a new payment for the order"""
696 statement_obj = self.pool.get('account.bank.statement')
697 statement_line_obj = self.pool.get('account.bank.statement.line')
698 prod_obj = self.pool.get('product.product')
699 property_obj = self.pool.get('ir.property')
700 curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
701 curr_company = curr_c.id
702 order = self.browse(cr, uid, order_id, context=context)
704 'amount': data['amount'],
706 if 'payment_date' in data:
707 args['date'] = data['payment_date']
708 args['name'] = order.name
709 if data.get('payment_name', False):
710 args['name'] = args['name'] + ': ' + data['payment_name']
711 account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
712 args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
713 and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
714 args['partner_id'] = order.partner_id and order.partner_id.id or None
716 if not args['account_id']:
717 if not args['partner_id']:
718 msg = _('There is no receivable account defined to make payment.')
720 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
721 raise osv.except_osv(_('Configuration Error!'), msg)
723 context.pop('pos_session_id', False)
726 journal_id = long(data['journal'])
731 for statement in order.session_id.statement_ids:
732 if statement.journal_id.id == journal_id:
733 statement_id = statement.id
737 raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
740 'statement_id' : statement_id,
741 'pos_statement_id' : order_id,
742 'journal_id' : journal_id,
747 statement_line_obj.create(cr, uid, args, context=context)
749 wf_service = netsvc.LocalService("workflow")
750 wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
751 wf_service.trg_write(uid, 'pos.order', order_id, cr)
755 def refund(self, cr, uid, ids, context=None):
756 """Create a copy of order for refund order"""
758 line_obj = self.pool.get('pos.order.line')
759 for order in self.browse(cr, uid, ids, context=context):
760 clone_id = self.copy(cr, uid, order.id, {
761 'name': order.name + ' REFUND',
763 clone_list.append(clone_id)
765 for clone in self.browse(cr, uid, clone_list, context=context):
766 for order_line in clone.lines:
767 line_obj.write(cr, uid, [order_line.id], {
768 'qty': -order_line.qty
771 new_order = ','.join(map(str,clone_list))
773 #'domain': "[('id', 'in', ["+new_order+"])]",
774 'name': _('Return Products'),
777 'res_model': 'pos.order',
778 'res_id':clone_list[0],
781 'type': 'ir.actions.act_window',
787 def action_invoice_state(self, cr, uid, ids, context=None):
788 return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
790 def action_invoice(self, cr, uid, ids, context=None):
791 wf_service = netsvc.LocalService("workflow")
792 inv_ref = self.pool.get('account.invoice')
793 inv_line_ref = self.pool.get('account.invoice.line')
794 product_obj = self.pool.get('product.product')
797 for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
799 inv_ids.append(order.invoice_id.id)
802 if not order.partner_id:
803 raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.'))
805 acc = order.partner_id.property_account_receivable.id
808 'origin': order.name,
810 'journal_id': order.sale_journal.id or None,
811 'type': 'out_invoice',
812 'reference': order.name,
813 'partner_id': order.partner_id.id,
814 'comment': order.note or '',
815 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
817 inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
818 if not inv.get('account_id', None):
819 inv['account_id'] = acc
820 inv_id = inv_ref.create(cr, uid, inv, context=context)
822 self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
823 inv_ids.append(inv_id)
824 for line in order.lines:
826 'invoice_id': inv_id,
827 'product_id': line.product_id.id,
828 'quantity': line.qty,
830 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
831 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
833 line.product_id.uom_id.id,
834 line.qty, partner_id = order.partner_id.id,
835 fposition_id=order.partner_id.property_account_position.id)['value'])
836 if line.product_id.description_sale:
837 inv_line['note'] = line.product_id.description_sale
838 inv_line['price_unit'] = line.price_unit
839 inv_line['discount'] = line.discount
840 inv_line['name'] = inv_name
841 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
842 and [(6, 0, inv_line['invoice_line_tax_id'])] or []
843 inv_line_ref.create(cr, uid, inv_line, context=context)
844 inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
845 wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
847 if not inv_ids: return {}
849 mod_obj = self.pool.get('ir.model.data')
850 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
851 res_id = res and res[1] or False
853 'name': _('Customer Invoice'),
857 'res_model': 'account.invoice',
858 'context': "{'type':'out_invoice'}",
859 'type': 'ir.actions.act_window',
862 'res_id': inv_ids and inv_ids[0] or False,
865 def create_account_move(self, cr, uid, ids, context=None):
866 return self._create_account_move_line(cr, uid, ids, None, None, context=context)
868 def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
869 # Tricky, via the workflow, we only have one id in the ids variable
870 """Create a account move line of order grouped by products or not."""
871 account_move_obj = self.pool.get('account.move')
872 account_move_line_obj = self.pool.get('account.move.line')
873 account_period_obj = self.pool.get('account.period')
874 account_tax_obj = self.pool.get('account.tax')
875 user_proxy = self.pool.get('res.users')
876 property_obj = self.pool.get('ir.property')
878 period = account_period_obj.find(cr, uid, context=context)[0]
880 #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
882 if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
883 raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!'))
885 current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
888 have_to_group_by = session and session.config_id.group_by or False
890 def compute_tax(amount, tax, line):
892 tax_code_id = tax['base_code_id']
893 tax_amount = line.price_subtotal * tax['base_sign']
895 tax_code_id = tax['ref_base_code_id']
896 tax_amount = line.price_subtotal * tax['ref_base_sign']
898 return (tax_code_id, tax_amount,)
900 for order in self.browse(cr, uid, ids, context=context):
901 if order.account_move:
903 if order.state != 'paid':
906 user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
909 account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
911 order_account = order.partner_id and \
912 order.partner_id.property_account_receivable and \
913 order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
916 # Create an entry for the sale
917 move_id = account_move_obj.create(cr, uid, {
919 'journal_id': order.sale_journal.id,
922 def insert_data(data_type, values):
923 # if have_to_group_by:
925 sale_journal_id = order.sale_journal.id
927 # 'quantity': line.qty,
928 # 'product_id': line.product_id.id,
930 'date': order.date_order[:10],
932 'journal_id' : sale_journal_id,
933 'period_id' : period,
935 'company_id': user_company and user_company.id or False,
938 if data_type == 'product':
939 key = ('product', values['product_id'],)
940 elif data_type == 'tax':
941 key = ('tax', values['tax_code_id'],)
942 elif data_type == 'counter_part':
943 key = ('counter_part', values['partner_id'], values['account_id'])
947 grouped_data.setdefault(key, [])
949 # if not have_to_group_by or (not grouped_data[key]):
950 # grouped_data[key].append(values)
955 if not grouped_data[key]:
956 grouped_data[key].append(values)
958 current_value = grouped_data[key][0]
959 current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0)
960 current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
961 current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
962 current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
964 grouped_data[key].append(values)
966 # Create an move for each order line
968 for line in order.lines:
970 taxes = [t for t in line.product_id.taxes_id]
971 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
973 for tax in computed_taxes:
974 tax_amount += round(tax['amount'], 2)
975 group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
977 group_tax.setdefault(group_key, 0)
978 group_tax[group_key] += round(tax['amount'], 2)
980 amount = line.price_subtotal
982 # Search for the income account
983 if line.product_id.property_account_income.id:
984 income_account = line.product_id.property_account_income.id
985 elif line.product_id.categ_id.property_account_income_categ.id:
986 income_account = line.product_id.categ_id.property_account_income_categ.id
988 raise osv.except_osv(_('Error!'), _('Please define income '\
989 'account for this product: "%s" (id:%d).') \
990 % (line.product_id.name, line.product_id.id, ))
992 # Empty the tax list as long as there is no tax code:
995 while computed_taxes:
996 tax = computed_taxes.pop(0)
997 tax_code_id, tax_amount = compute_tax(amount, tax, line)
999 # If there is one we stop
1003 # Create a move for the line
1004 insert_data('product', {
1005 'name': line.product_id.name,
1006 'quantity': line.qty,
1007 'product_id': line.product_id.id,
1008 'account_id': income_account,
1009 'credit': ((amount>0) and amount) or 0.0,
1010 'debit': ((amount<0) and -amount) or 0.0,
1011 'tax_code_id': tax_code_id,
1012 'tax_amount': tax_amount,
1013 'partner_id': order.partner_id and order.partner_id.id or False
1016 # For each remaining tax with a code, whe create a move line
1017 for tax in computed_taxes:
1018 tax_code_id, tax_amount = compute_tax(amount, tax, line)
1022 insert_data('tax', {
1024 'product_id':line.product_id.id,
1025 'quantity': line.qty,
1026 'account_id': income_account,
1029 'tax_code_id': tax_code_id,
1030 'tax_amount': tax_amount,
1033 # Create a move for each tax group
1034 (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
1036 for key, tax_amount in group_tax.items():
1037 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
1038 insert_data('tax', {
1039 'name': _('Tax') + ' ' + tax.name,
1040 'quantity': line.qty,
1041 'product_id': line.product_id.id,
1042 'account_id': key[account_pos],
1043 'credit': ((tax_amount>0) and tax_amount) or 0.0,
1044 'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1045 'tax_code_id': key[tax_code_pos],
1046 'tax_amount': tax_amount,
1050 insert_data('counter_part', {
1051 'name': _("Trade Receivables"), #order.name,
1052 'account_id': order_account,
1053 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1054 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1055 'partner_id': order.partner_id and order.partner_id.id or False
1058 order.write({'state':'done', 'account_move': move_id})
1060 for group_key, group_data in grouped_data.iteritems():
1061 for value in group_data:
1062 account_move_line_obj.create(cr, uid, value, context=context)
1066 def action_payment(self, cr, uid, ids, context=None):
1067 return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1069 def action_paid(self, cr, uid, ids, context=None):
1070 self.create_picking(cr, uid, ids, context=context)
1071 self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1074 def action_cancel(self, cr, uid, ids, context=None):
1075 self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1078 def action_done(self, cr, uid, ids, context=None):
1079 self.create_account_move(cr, uid, ids, context=context)
1084 class account_bank_statement(osv.osv):
1085 _inherit = 'account.bank.statement'
1087 'user_id': fields.many2one('res.users', 'User', readonly=True),
1090 'user_id': lambda self,cr,uid,c={}: uid
1092 account_bank_statement()
1094 class account_bank_statement_line(osv.osv):
1095 _inherit = 'account.bank.statement.line'
1097 'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1099 account_bank_statement_line()
1101 class pos_order_line(osv.osv):
1102 _name = "pos.order.line"
1103 _description = "Lines of Point of Sale"
1104 _rec_name = "product_id"
1106 def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1107 res = dict([(i, {}) for i in ids])
1108 account_tax_obj = self.pool.get('account.tax')
1109 cur_obj = self.pool.get('res.currency')
1110 for line in self.browse(cr, uid, ids, context=context):
1111 if line.product_id.taxes_id:
1112 taxes = line.product_id.taxes_id
1114 taxes = line.product_id.categ_id.taxes_id
1115 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1116 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)
1118 cur = line.order_id.pricelist_id.currency_id
1119 res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1120 res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1123 def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1124 context = context or {}
1128 raise osv.except_osv(_('No Pricelist !'),
1129 _('You have to select a pricelist in the sale form !\n' \
1130 'Please set one before choosing a product.'))
1132 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1133 product_id, qty or 1.0, partner_id)[pricelist]
1135 result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1136 result['value']['price_unit'] = price
1139 def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1143 account_tax_obj = self.pool.get('account.tax')
1144 cur_obj = self.pool.get('res.currency')
1146 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1148 taxes = prod.taxes_id
1149 price = price_unit * (1 - (discount or 0.0) / 100.0)
1150 taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1152 result['price_subtotal'] = taxes['total']
1153 result['price_subtotal_incl'] = taxes['total_included']
1154 return {'value': result}
1157 'company_id': fields.many2one('res.company', 'Company', required=True),
1158 'name': fields.char('Line No', size=32, required=True),
1159 'notice': fields.char('Discount Notice', size=128),
1160 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1161 'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1162 'qty': fields.float('Quantity', digits=(16, 2)),
1163 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1164 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1165 'discount': fields.float('Discount (%)', digits=(16, 2)),
1166 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1167 'create_date': fields.datetime('Creation Date', readonly=True),
1171 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1172 'qty': lambda *a: 1,
1173 'discount': lambda *a: 0.0,
1174 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1177 def copy_data(self, cr, uid, id, default=None, context=None):
1181 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1183 return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1187 class pos_category(osv.osv):
1188 _name = 'pos.category'
1189 _description = "Point of Sale Category"
1190 _order = "sequence, name"
1191 def _check_recursion(self, cr, uid, ids, context=None):
1194 cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1195 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1202 (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1205 def name_get(self, cr, uid, ids, context=None):
1208 reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1210 for record in reads:
1211 name = record['name']
1212 if record['parent_id']:
1213 name = record['parent_id'][1]+' / '+name
1214 res.append((record['id'], name))
1217 def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1218 res = self.name_get(cr, uid, ids, context=context)
1221 def _get_image(self, cr, uid, ids, name, args, context=None):
1222 result = dict.fromkeys(ids, False)
1223 for obj in self.browse(cr, uid, ids, context=context):
1224 result[obj.id] = tools.image_get_resized_images(obj.image)
1227 def _set_image(self, cr, uid, id, name, value, args, context=None):
1228 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
1231 'name': fields.char('Name', size=64, required=True, translate=True),
1232 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1233 'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1234 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1235 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1236 'image': fields.binary("Image",
1237 help="This field holds the image used for the category. "\
1238 "The image is base64 encoded, and PIL-supported. "\
1239 "It is limited to a 1024x1024 px image."),
1240 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
1241 string="Medium-sized image", type="binary", multi="_get_image",
1243 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1245 help="Medium-sized image of the category. It is automatically "\
1246 "resized as a 180x180 px image, with aspect ratio preserved. "\
1247 "Use this field in form views or some kanban views."),
1248 'image_small': fields.function(_get_image, fnct_inv=_set_image,
1249 string="Smal-sized image", type="binary", multi="_get_image",
1251 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1253 help="Small-sized image of the category. It is automatically "\
1254 "resized as a 50x50 px image, with aspect ratio preserved. "\
1255 "Use this field anywhere a small image is required."),
1258 def _get_default_image(self, cr, uid, context=None):
1259 image_path = openerp.modules.get_module_resource('point_of_sale', 'images', 'default_category_photo.png')
1260 return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
1263 'image': _get_default_image,
1270 class product_product(osv.osv):
1271 _inherit = 'product.product'
1274 '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."),
1275 '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."),
1276 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1277 help="If you want to sell this product through the point of sale, select the category it belongs to."),
1278 'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"),
1281 'to_weight' : False,
1287 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: