From d1c3c6518d8bcaf93b3b679cb836cbbdae75526f Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 17 Feb 2016 15:40:24 +0100 Subject: [PATCH 1/9] [REF] point_of_sale: move files to appropriate folders --- addons/point_of_sale/__init__.py | 6 +----- addons/point_of_sale/{ => data}/point_of_sale_data.xml | 0 addons/point_of_sale/{ => data}/point_of_sale_demo.xml | 0 .../point_of_sale/{ => data}/point_of_sale_workflow.xml | 0 addons/point_of_sale/models/__init__.py | 8 ++++++++ .../point_of_sale/{ => models}/account_bank_statement.py | 0 addons/point_of_sale/{ => models}/point_of_sale.py | 0 addons/point_of_sale/{ => models}/res_config.py | 0 addons/point_of_sale/{ => models}/res_partner.py | 0 addons/point_of_sale/{ => models}/res_users.py | 0 .../{ => views}/account_statement_report.xml | 0 .../point_of_sale/{ => views}/account_statement_view.xml | 0 .../point_of_sale/{ => views}/point_of_sale_dashboard.xml | 0 addons/point_of_sale/{ => views}/point_of_sale_report.xml | 0 .../point_of_sale/{ => views}/point_of_sale_sequence.xml | 0 .../point_of_sale_template.xml} | 0 addons/point_of_sale/{ => views}/point_of_sale_view.xml | 0 .../views/{templates.xml => pos_templates.xml} | 0 addons/point_of_sale/{ => views}/res_config_view.xml | 0 addons/point_of_sale/{ => views}/res_partner_view.xml | 0 addons/point_of_sale/{ => views}/res_users_view.xml | 0 21 files changed, 9 insertions(+), 5 deletions(-) rename addons/point_of_sale/{ => data}/point_of_sale_data.xml (100%) rename addons/point_of_sale/{ => data}/point_of_sale_demo.xml (100%) rename addons/point_of_sale/{ => data}/point_of_sale_workflow.xml (100%) create mode 100644 addons/point_of_sale/models/__init__.py rename addons/point_of_sale/{ => models}/account_bank_statement.py (100%) rename addons/point_of_sale/{ => models}/point_of_sale.py (100%) rename addons/point_of_sale/{ => models}/res_config.py (100%) rename addons/point_of_sale/{ => models}/res_partner.py (100%) rename addons/point_of_sale/{ => models}/res_users.py (100%) rename addons/point_of_sale/{ => views}/account_statement_report.xml (100%) rename addons/point_of_sale/{ => views}/account_statement_view.xml (100%) rename addons/point_of_sale/{ => views}/point_of_sale_dashboard.xml (100%) rename addons/point_of_sale/{ => views}/point_of_sale_report.xml (100%) rename addons/point_of_sale/{ => views}/point_of_sale_sequence.xml (100%) rename addons/point_of_sale/{point_of_sale.xml => views/point_of_sale_template.xml} (100%) rename addons/point_of_sale/{ => views}/point_of_sale_view.xml (100%) rename addons/point_of_sale/views/{templates.xml => pos_templates.xml} (100%) rename addons/point_of_sale/{ => views}/res_config_view.xml (100%) rename addons/point_of_sale/{ => views}/res_partner_view.xml (100%) rename addons/point_of_sale/{ => views}/res_users_view.xml (100%) diff --git a/addons/point_of_sale/__init__.py b/addons/point_of_sale/__init__.py index f1451fa6494..ff29fd64226 100644 --- a/addons/point_of_sale/__init__.py +++ b/addons/point_of_sale/__init__.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import account_bank_statement +import models import controllers -import point_of_sale import report -import res_users -import res_partner import wizard -import res_config diff --git a/addons/point_of_sale/point_of_sale_data.xml b/addons/point_of_sale/data/point_of_sale_data.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_data.xml rename to addons/point_of_sale/data/point_of_sale_data.xml diff --git a/addons/point_of_sale/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_demo.xml rename to addons/point_of_sale/data/point_of_sale_demo.xml diff --git a/addons/point_of_sale/point_of_sale_workflow.xml b/addons/point_of_sale/data/point_of_sale_workflow.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_workflow.xml rename to addons/point_of_sale/data/point_of_sale_workflow.xml diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py new file mode 100644 index 00000000000..d529cbf25e0 --- /dev/null +++ b/addons/point_of_sale/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import account_bank_statement +import point_of_sale +import res_config +import res_partner +import res_users diff --git a/addons/point_of_sale/account_bank_statement.py b/addons/point_of_sale/models/account_bank_statement.py similarity index 100% rename from addons/point_of_sale/account_bank_statement.py rename to addons/point_of_sale/models/account_bank_statement.py diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/models/point_of_sale.py similarity index 100% rename from addons/point_of_sale/point_of_sale.py rename to addons/point_of_sale/models/point_of_sale.py diff --git a/addons/point_of_sale/res_config.py b/addons/point_of_sale/models/res_config.py similarity index 100% rename from addons/point_of_sale/res_config.py rename to addons/point_of_sale/models/res_config.py diff --git a/addons/point_of_sale/res_partner.py b/addons/point_of_sale/models/res_partner.py similarity index 100% rename from addons/point_of_sale/res_partner.py rename to addons/point_of_sale/models/res_partner.py diff --git a/addons/point_of_sale/res_users.py b/addons/point_of_sale/models/res_users.py similarity index 100% rename from addons/point_of_sale/res_users.py rename to addons/point_of_sale/models/res_users.py diff --git a/addons/point_of_sale/account_statement_report.xml b/addons/point_of_sale/views/account_statement_report.xml similarity index 100% rename from addons/point_of_sale/account_statement_report.xml rename to addons/point_of_sale/views/account_statement_report.xml diff --git a/addons/point_of_sale/account_statement_view.xml b/addons/point_of_sale/views/account_statement_view.xml similarity index 100% rename from addons/point_of_sale/account_statement_view.xml rename to addons/point_of_sale/views/account_statement_view.xml diff --git a/addons/point_of_sale/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_dashboard.xml rename to addons/point_of_sale/views/point_of_sale_dashboard.xml diff --git a/addons/point_of_sale/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_report.xml rename to addons/point_of_sale/views/point_of_sale_report.xml diff --git a/addons/point_of_sale/point_of_sale_sequence.xml b/addons/point_of_sale/views/point_of_sale_sequence.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_sequence.xml rename to addons/point_of_sale/views/point_of_sale_sequence.xml diff --git a/addons/point_of_sale/point_of_sale.xml b/addons/point_of_sale/views/point_of_sale_template.xml similarity index 100% rename from addons/point_of_sale/point_of_sale.xml rename to addons/point_of_sale/views/point_of_sale_template.xml diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/views/point_of_sale_view.xml similarity index 100% rename from addons/point_of_sale/point_of_sale_view.xml rename to addons/point_of_sale/views/point_of_sale_view.xml diff --git a/addons/point_of_sale/views/templates.xml b/addons/point_of_sale/views/pos_templates.xml similarity index 100% rename from addons/point_of_sale/views/templates.xml rename to addons/point_of_sale/views/pos_templates.xml diff --git a/addons/point_of_sale/res_config_view.xml b/addons/point_of_sale/views/res_config_view.xml similarity index 100% rename from addons/point_of_sale/res_config_view.xml rename to addons/point_of_sale/views/res_config_view.xml diff --git a/addons/point_of_sale/res_partner_view.xml b/addons/point_of_sale/views/res_partner_view.xml similarity index 100% rename from addons/point_of_sale/res_partner_view.xml rename to addons/point_of_sale/views/res_partner_view.xml diff --git a/addons/point_of_sale/res_users_view.xml b/addons/point_of_sale/views/res_users_view.xml similarity index 100% rename from addons/point_of_sale/res_users_view.xml rename to addons/point_of_sale/views/res_users_view.xml From fcdd43010ac060b29901d7d8e1957c3545d7664e Mon Sep 17 00:00:00 2001 From: Darshan Kalola Date: Thu, 24 Sep 2015 14:43:25 +0530 Subject: [PATCH 2/9] [REF] point_of_sale: split files per model --- addons/point_of_sale/__openerp__.py | 39 +- addons/point_of_sale/models/__init__.py | 8 +- .../models/account_bank_statement.py | 15 +- .../point_of_sale/models/account_journal.py | 27 + addons/point_of_sale/models/barcode_rule.py | 20 + addons/point_of_sale/models/pos_category.py | 71 ++ addons/point_of_sale/models/pos_config.py | 324 ++++++ .../models/{point_of_sale.py => pos_order.py} | 750 +------------ addons/point_of_sale/models/pos_session.py | 307 ++++++ addons/point_of_sale/models/product.py | 27 + addons/point_of_sale/models/res_partner.py | 20 +- .../views/account_invoice_view.xml | 13 + .../views/account_journal_view.xml | 29 + .../views/point_of_sale_report.xml | 218 ++++ .../views/point_of_sale_view.xml | 984 ------------------ .../point_of_sale/views/pos_category_view.xml | 59 ++ .../point_of_sale/views/pos_config_view.xml | 147 +++ .../pos_order_report_view.xml | 0 addons/point_of_sale/views/pos_order_view.xml | 279 +++++ .../point_of_sale/views/pos_session_view.xml | 179 ++++ addons/point_of_sale/views/product_view.xml | 59 ++ addons/point_of_sale/wizard/pos_payment.py | 19 +- 22 files changed, 1818 insertions(+), 1776 deletions(-) create mode 100644 addons/point_of_sale/models/account_journal.py create mode 100644 addons/point_of_sale/models/barcode_rule.py create mode 100644 addons/point_of_sale/models/pos_category.py create mode 100644 addons/point_of_sale/models/pos_config.py rename addons/point_of_sale/models/{point_of_sale.py => pos_order.py} (53%) create mode 100644 addons/point_of_sale/models/pos_session.py create mode 100644 addons/point_of_sale/models/product.py create mode 100644 addons/point_of_sale/views/account_invoice_view.xml create mode 100644 addons/point_of_sale/views/account_journal_view.xml create mode 100644 addons/point_of_sale/views/pos_category_view.xml create mode 100644 addons/point_of_sale/views/pos_config_view.xml rename addons/point_of_sale/{report => views}/pos_order_report_view.xml (100%) create mode 100644 addons/point_of_sale/views/pos_order_view.xml create mode 100644 addons/point_of_sale/views/pos_session_view.xml create mode 100644 addons/point_of_sale/views/product_view.xml diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 6d34f8a5cc1..f3c455b9023 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -30,28 +30,35 @@ Main Features """, 'depends': ['sale_stock', 'barcodes'], 'data': [ - 'data/default_barcode_patterns.xml', 'security/point_of_sale_security.xml', 'security/ir.model.access.csv', + 'data/default_barcode_patterns.xml', 'wizard/pos_box.xml', 'wizard/pos_confirm.xml', 'wizard/pos_details.xml', 'wizard/pos_discount.xml', 'wizard/pos_open_statement.xml', 'wizard/pos_payment.xml', - 'views/templates.xml', - 'point_of_sale.xml', - 'point_of_sale_report.xml', - 'point_of_sale_view.xml', - 'point_of_sale_sequence.xml', - 'point_of_sale_data.xml', - 'report/pos_order_report_view.xml', - 'point_of_sale_workflow.xml', - 'account_statement_view.xml', - 'account_statement_report.xml', - 'res_users_view.xml', - 'res_partner_view.xml', - 'res_config_view.xml', + 'views/pos_templates.xml', + 'views/point_of_sale_template.xml', + 'views/point_of_sale_report.xml', + 'views/point_of_sale_view.xml', + 'views/pos_order_view.xml', + 'views/product_view.xml', + 'views/pos_category_view.xml', + 'views/account_journal_view.xml', + 'views/pos_config_view.xml', + 'views/pos_session_view.xml', + 'views/account_invoice_view.xml', + 'views/point_of_sale_sequence.xml', + 'data/point_of_sale_data.xml', + 'data/point_of_sale_workflow.xml', + 'views/pos_order_report_view.xml', + 'views/account_statement_view.xml', + 'views/account_statement_report.xml', + 'views/res_users_view.xml', + 'views/res_partner_view.xml', + 'views/res_config_view.xml', 'views/report_statement.xml', 'views/report_usersproduct.xml', 'views/report_saleslines.xml', @@ -60,10 +67,10 @@ Main Features 'views/report_sessionsummary.xml', 'views/report_userlabel.xml', 'views/point_of_sale.xml', - 'point_of_sale_dashboard.xml', + 'views/point_of_sale_dashboard.xml', ], 'demo': [ - 'point_of_sale_demo.xml', + 'data/point_of_sale_demo.xml', ], 'test': [ '../account/test/account_minimal_test.xml', diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index d529cbf25e0..7170dbcf350 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -2,7 +2,13 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import account_bank_statement -import point_of_sale +import account_journal +import barcode_rule +import pos_category +import pos_config +import pos_order +import pos_session +import product import res_config import res_partner import res_users diff --git a/addons/point_of_sale/models/account_bank_statement.py b/addons/point_of_sale/models/account_bank_statement.py index 5faa31b3007..098b750beba 100644 --- a/addons/point_of_sale/models/account_bank_statement.py +++ b/addons/point_of_sale/models/account_bank_statement.py @@ -5,14 +5,6 @@ from openerp.osv import fields, osv -class account_journal(osv.osv): - _inherit = 'account.journal' - _columns = { - 'journal_user': fields.boolean('Active in Point of Sale', help="Check this box if this journal define a payment method that can be used in a point of sale."), - - 'amount_authorized_diff' : fields.float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at the closing of his session saying that he needs to contact his manager."), - } - class account_bank_statement(osv.osv): _inherit = 'account.bank.statement' @@ -20,3 +12,10 @@ class account_bank_statement(osv.osv): 'pos_session_id' : fields.many2one('pos.session', string="Session", copy=False), 'account_id': fields.related('journal_id', 'default_debit_account_id', type='many2one', relation='account.account', readonly=True), } + + +class account_bank_statement_line(osv.osv): + _inherit = 'account.bank.statement.line' + _columns= { + 'pos_statement_id': fields.many2one('pos.order', string="POS statement", ondelete='cascade'), + } diff --git a/addons/point_of_sale/models/account_journal.py b/addons/point_of_sale/models/account_journal.py new file mode 100644 index 00000000000..f9310410467 --- /dev/null +++ b/addons/point_of_sale/models/account_journal.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Copyright (C) 2004-2008 PC Solutions (). All Rights Reserved +from openerp.osv import fields, osv + +class account_journal(osv.osv): + _inherit = 'account.journal' + _columns = { + 'journal_user': fields.boolean('Active in Point of Sale', help="Check this box if this journal define a payment method that can be used in a point of sale."), + + 'amount_authorized_diff' : fields.float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at the closing of his session saying that he needs to contact his manager."), + } + + def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): + if not context: + context = {} + session_id = context.get('pos_session_id', False) or False + + if session_id: + session = self.pool.get('pos.session').browse(cr, uid, session_id, context=context) + + if session: + journal_ids = [journal.id for journal in session.config_id.journal_ids] + args += [('id', 'in', journal_ids)] + + return super(account_journal, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) diff --git a/addons/point_of_sale/models/barcode_rule.py b/addons/point_of_sale/models/barcode_rule.py new file mode 100644 index 00000000000..f1f5e2abbba --- /dev/null +++ b/addons/point_of_sale/models/barcode_rule.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import sets +from openerp import models +from openerp.tools.translate import _ + + +class barcode_rule(models.Model): + _inherit = 'barcode.rule' + + def _get_type_selection(self): + types = sets.Set(super(barcode_rule,self)._get_type_selection()) + types.update([ + ('weight', _('Weighted Product')), + ('price', _('Priced Product')), + ('discount', _('Discounted Product')), + ('client', _('Client')), + ('cashier', _('Cashier')) + ]) + return list(types) diff --git a/addons/point_of_sale/models/pos_category.py b/addons/point_of_sale/models/pos_category.py new file mode 100644 index 00000000000..8c9d2ba5253 --- /dev/null +++ b/addons/point_of_sale/models/pos_category.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import tools +from openerp.osv import fields, osv + + +class pos_category(osv.osv): + _name = "pos.category" + _description = "Public Category" + _order = "sequence, name" + + _constraints = [ + (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) + ] + + def name_get(self, cr, uid, ids, context=None): + res = [] + for cat in self.browse(cr, uid, ids, context=context): + names = [cat.name] + pcat = cat.parent_id + while pcat: + names.append(pcat.name) + pcat = pcat.parent_id + res.append((cat.id, ' / '.join(reversed(names)))) + return res + + def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): + res = self.name_get(cr, uid, ids, context=context) + return dict(res) + + _columns = { + 'name': fields.char('Name', required=True, translate=True), + 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'), + 'parent_id': fields.many2one('pos.category','Parent Category', select=True), + 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), + 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), + + } + + # NOTE: there is no 'default image', because by default we don't show + # thumbnails for categories. However if we have a thumbnail for at least one + # category, then we display a default image on the other, so that the + # buttons have consistent styling. + # In this case, the default image is set by the js code. + image = openerp.fields.Binary("Image", attachment=True, + help="This field holds the image used as image for the cateogry, limited to 1024x1024px.") + image_medium = openerp.fields.Binary("Medium-sized image", + compute='_compute_images', inverse='_inverse_image_medium', store=True, attachment=True, + help="Medium-sized image of the category. It is automatically "\ + "resized as a 128x128px image, with aspect ratio preserved. "\ + "Use this field in form views or some kanban views.") + image_small = openerp.fields.Binary("Small-sized image", + compute='_compute_images', inverse='_inverse_image_small', store=True, attachment=True, + help="Small-sized image of the category. It is automatically "\ + "resized as a 64x64px image, with aspect ratio preserved. "\ + "Use this field anywhere a small image is required.") + + @openerp.api.depends('image') + def _compute_images(self): + for rec in self: + rec.image_medium = tools.image_resize_image_medium(rec.image) + rec.image_small = tools.image_resize_image_small(rec.image) + + def _inverse_image_medium(self): + for rec in self: + rec.image = tools.image_resize_image_big(rec.image_medium) + + def _inverse_image_small(self): + for rec in self: + rec.image = tools.image_resize_image_big(rec.image_small) diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py new file mode 100644 index 00000000000..dfa363d1745 --- /dev/null +++ b/addons/point_of_sale/models/pos_config.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import uuid + +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +class pos_config(osv.osv): + _name = 'pos.config' + + POS_CONFIG_STATE = [ + ('active', 'Active'), + ('inactive', 'Inactive'), + ('deprecated', 'Deprecated') + ] + + def _get_currency(self, cr, uid, ids, fieldnames, args, context=None): + result = dict.fromkeys(ids, False) + for pos_config in self.browse(cr, uid, ids, context=context): + if pos_config.journal_id: + currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id + else: + currency_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id + result[pos_config.id] = currency_id + return result + + def _get_current_session(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + session_id = record.session_ids.filtered(lambda r: r.user_id.id == uid and not r.state == 'closed') + result[record.id] = { + 'current_session_id': session_id, + 'current_session_state': session_id.state, + } + return result + + def _get_last_session(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + session_ids = self.pool['pos.session'].search_read( + cr, uid, + [('config_id', '=', record.id), ('state', '=', 'closed')], + ['cash_register_balance_end_real', 'stop_at'], + order="stop_at desc", limit=1, context=context) + if session_ids: + result[record.id] = { + 'last_session_closing_cash': session_ids[0]['cash_register_balance_end_real'], + 'last_session_closing_date': session_ids[0]['stop_at'], + } + else: + result[record.id] = { + 'last_session_closing_cash': 0, + 'last_session_closing_date': None, + } + return result + + def _get_current_session_user(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened').user_id.name + return result + + _columns = { + 'name' : fields.char('Point of Sale Name', select=1, + required=True, help="An internal identification of the point of sale"), + 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', + 'pos_config_id', 'journal_id', 'Available Payment Methods', + domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",), + 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'), + 'stock_location_id': fields.many2one('stock.location', 'Stock Location', domain=[('usage', '=', 'internal')], required=True), + 'journal_id' : fields.many2one('account.journal', 'Sale Journal', + domain=[('type', '=', 'sale')], + help="Accounting journal used to post sales entries."), + 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), + 'iface_cashdrawer' : fields.boolean('Cashdrawer', help="Automatically open the cashdrawer"), + 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), + 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), + 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard', help="Enables an integrated Virtual Keyboard"), + 'iface_print_via_proxy' : fields.boolean('Print via Proxy', help="Bypass browser printing and prints via the hardware proxy"), + 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), + 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), + 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), + # TODO master: remove the `iface_fullscreen` field. This is no longer used. + 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), + 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), + 'iface_print_skip_screen': fields.boolean('Skip Receipt Screen', help='The receipt screen will be skipped if the receipt can be printed automatically.'), + 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), + 'iface_tax_included': fields.boolean('Include Taxes in Prices', help='The displayed prices will always include all taxes, even if the taxes have been setup differently'), + 'iface_start_categ_id': fields.many2one('pos.category','Start Category', help='The point of sale will display this product category by default. If no category is specified, all available products will be shown'), + 'iface_display_categ_images': fields.boolean('Display Category Pictures', help="The product categories will be displayed with pictures."), + 'cash_control': fields.boolean('Cash Control', help="Check the amount of the cashbox at opening and closing."), + 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), + 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), + 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), + + 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), + 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), + 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, + help="This sequence is automatically created by Odoo but you can change it "\ + "to customize the reference numbers of your orders.", copy=False), + 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), + 'current_session_id': fields.function(_get_current_session, multi="session", type="many2one", relation="pos.session", string="Current Session"), + 'current_session_state': fields.function(_get_current_session, multi="session", type='char'), + 'last_session_closing_cash': fields.function(_get_last_session, multi="last_session", type='float'), + 'last_session_closing_date': fields.function(_get_last_session, multi="last_session", type='date'), + 'pos_session_username': fields.function(_get_current_session_user, type='char'), + 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), + 'pricelist_id': fields.many2one('product.pricelist','Pricelist', required=True), + 'company_id': fields.many2one('res.company', 'Company', required=True), + 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcodes', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers', required=True), + 'group_pos_manager_id': fields.many2one('res.groups','Point of Sale Manager Group', help='This field is there to pass the id of the pos manager group to the point of sale client'), + 'group_pos_user_id': fields.many2one('res.groups','Point of Sale User Group', help='This field is there to pass the id of the pos user group to the point of sale client'), + 'tip_product_id': fields.many2one('product.product','Tip Product', help="The product used to encode the customer tip. Leave empty if you do not accept tips."), + 'fiscal_position_ids': fields.many2many('account.fiscal.position', string='Fiscal Positions') + } + + def _check_company_location(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + if config.stock_location_id.company_id and config.stock_location_id.company_id.id != config.company_id.id: + return False + return True + + def _check_company_journal(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + if config.journal_id and config.journal_id.company_id.id != config.company_id.id: + return False + return True + + def _check_company_payment(self, cr, uid, ids, context=None): + for config in self.browse(cr, uid, ids, context=context): + journal_ids = [j.id for j in config.journal_ids] + if self.pool['account.journal'].search(cr, uid, [ + ('id', 'in', journal_ids), + ('company_id', '!=', config.company_id.id) + ], count=True, context=context): + return False + return True + + _constraints = [ + (_check_company_location, "The company of the stock location is different than the one of point of sale", ['company_id', 'stock_location_id']), + (_check_company_journal, "The company of the sale journal is different than the one of point of sale", ['company_id', 'journal_id']), + (_check_company_payment, "The company of a payment method is different than the one of point of sale", ['company_id', 'journal_ids']), + ] + + def name_get(self, cr, uid, ids, context=None): + result = [] + states = { + 'opening_control': _('Opening Control'), + 'opened': _('In Progress'), + 'closing_control': _('Closing Control'), + 'closed': _('Closed & Posted'), + } + for record in self.browse(cr, uid, ids, context=context): + if (not record.session_ids) or (record.session_ids[0].state=='closed'): + result.append((record.id, record.name+' ('+_('not used')+')')) + continue + session = record.session_ids[0] + result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) + return result + + def _default_sale_journal(self, cr, uid, context=None): + company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id + res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1, context=context) + return res and res[0] or False + + def _default_pricelist(self, cr, uid, context=None): + res = self.pool.get('product.pricelist').search(cr, uid, [], limit=1, context=context) + return res and res[0] or False + + def _get_default_location(self, cr, uid, context=None): + wh_obj = self.pool.get('stock.warehouse') + user = self.pool.get('res.users').browse(cr, uid, uid, context) + res = wh_obj.search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context) + if res and res[0]: + return wh_obj.browse(cr, uid, res[0], context=context).lot_stock_id.id + return False + + def _get_default_company(self, cr, uid, context=None): + company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) + return company_id + + def _get_default_nomenclature(self, cr, uid, context=None): + nom_obj = self.pool.get('barcode.nomenclature') + res = nom_obj.search(cr, uid, [], limit=1, context=context) + return res and res[0] or False + + def _get_group_pos_manager(self, cr, uid, context=None): + group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_manager') + if group: + return group[1] + else: + return False + + def _get_group_pos_user(self, cr, uid, context=None): + group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_user') + if group: + return group[1] + else: + return False + + _defaults = { + 'uuid' : lambda self, cr, uid, context={}: str(uuid.uuid4()), + 'state' : POS_CONFIG_STATE[0][0], + 'journal_id': _default_sale_journal, + 'group_by' : True, + 'pricelist_id': _default_pricelist, + 'iface_invoicing': True, + 'iface_print_auto': False, + 'iface_print_skip_screen': True, + 'stock_location_id': _get_default_location, + 'company_id': _get_default_company, + 'barcode_nomenclature_id': _get_default_nomenclature, + 'group_pos_manager_id': _get_group_pos_manager, + 'group_pos_user_id': _get_group_pos_user, + } + + def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): + p_type_obj = self.pool.get("stock.picking.type") + p_type = p_type_obj.browse(cr, uid, picking_type_id, context=context) + if p_type.default_location_src_id and p_type.default_location_src_id.usage == 'internal' and p_type.default_location_dest_id and p_type.default_location_dest_id.usage == 'customer': + return {'value': {'stock_location_id': p_type.default_location_src_id.id}} + return False + + def onchange_iface_print_via_proxy(self, cr, uid, ids, print_via_proxy, context=None): + return {'value': {'iface_print_auto': print_via_proxy}} + + def set_active(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'active'}, context=context) + + def set_inactive(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) + + def set_deprecate(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) + + def create(self, cr, uid, values, context=None): + ir_sequence = self.pool.get('ir.sequence') + # force sequence_id field to new pos.order sequence + values['sequence_id'] = ir_sequence.create(cr, SUPERUSER_ID, { + 'name': 'POS Order %s' % values['name'], + 'padding': 4, + 'prefix': "%s/" % values['name'], + 'code': "pos.order", + 'company_id': values.get('company_id', False), + }, context=context) + + # TODO master: add field sequence_line_id on model + # this make sure we always have one available per company + ir_sequence.create(cr, SUPERUSER_ID, { + 'name': 'POS order line %s' % values['name'], + 'padding': 4, + 'prefix': "%s/" % values['name'], + 'code': "pos.order.line", + 'company_id': values.get('company_id', False), + }, context=context) + + return super(pos_config, self).create(cr, uid, values, context=context) + + def unlink(self, cr, uid, ids, context=None): + for obj in self.browse(cr, uid, ids, context=context): + if obj.sequence_id: + obj.sequence_id.unlink() + return super(pos_config, self).unlink(cr, uid, ids, context=context) + + # Methods to open the POS + + def open_ui(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + record = self.browse(cr, uid, ids[0], context=context) + context = dict(context or {}) + context['active_id'] = record.current_session_id.id + return { + 'type': 'ir.actions.act_url', + 'url': '/pos/web/', + 'target': 'self', + } + + def open_existing_session_cb_close(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + record = self.browse(cr, uid, ids[0], context=context) + record.current_session_id.signal_workflow('cashbox_control') + return self.open_session_cb(cr, uid, ids, context) + + def open_session_cb(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + proxy = self.pool.get('pos.session') + record = self.browse(cr, uid, ids[0], context=context) + current_session_id = record.current_session_id + if not current_session_id: + values = { + 'user_id': uid, + 'config_id': record.id, + } + session_id = proxy.create(cr, uid, values, context=context) + self.write(cr, SUPERUSER_ID, record.id, {'current_session_id': session_id}, context=context) + if record.current_session_id.state == 'opened': + return self.open_ui(cr, uid, ids, context=context) + return self._open_session(session_id) + return self._open_session(current_session_id.id) + + def open_existing_session_cb(self, cr, uid, ids, context=None): + assert len(ids) == 1, "you can open only one session at a time" + + record = self.browse(cr, uid, ids[0], context=context) + return self._open_session(record.current_session_id.id) + + def _open_session(self, session_id): + return { + 'name': _('Session'), + 'view_type': 'form', + 'view_mode': 'form,tree', + 'res_model': 'pos.session', + 'res_id': session_id, + 'view_id': False, + 'type': 'ir.actions.act_window', + } diff --git a/addons/point_of_sale/models/point_of_sale.py b/addons/point_of_sale/models/pos_order.py similarity index 53% rename from addons/point_of_sale/models/point_of_sale.py rename to addons/point_of_sale/models/pos_order.py index b09fcc8a722..65e4754908e 100644 --- a/addons/point_of_sale/models/point_of_sale.py +++ b/addons/point_of_sale/models/pos_order.py @@ -2,16 +2,15 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging + import time from datetime import datetime -import uuid -import sets from functools import partial import openerp import openerp.addons.decimal_precision as dp -from openerp import tools, models, SUPERUSER_ID +from openerp import tools, SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools import float_is_zero from openerp.tools.translate import _ @@ -20,623 +19,6 @@ from openerp.exceptions import UserError _logger = logging.getLogger(__name__) -class pos_config(osv.osv): - _name = 'pos.config' - - POS_CONFIG_STATE = [ - ('active', 'Active'), - ('inactive', 'Inactive'), - ('deprecated', 'Deprecated') - ] - - def _get_currency(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, False) - for pos_config in self.browse(cr, uid, ids, context=context): - if pos_config.journal_id: - currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id - else: - currency_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id - result[pos_config.id] = currency_id - return result - - def _get_current_session(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - session_id = record.session_ids.filtered(lambda r: r.user_id.id == uid and not r.state == 'closed') - result[record.id] = { - 'current_session_id': session_id, - 'current_session_state': session_id.state, - } - return result - - def _get_last_session(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - session_ids = self.pool['pos.session'].search_read( - cr, uid, - [('config_id', '=', record.id), ('state', '=', 'closed')], - ['cash_register_balance_end_real', 'stop_at'], - order="stop_at desc", limit=1, context=context) - if session_ids: - result[record.id] = { - 'last_session_closing_cash': session_ids[0]['cash_register_balance_end_real'], - 'last_session_closing_date': session_ids[0]['stop_at'], - } - else: - result[record.id] = { - 'last_session_closing_cash': 0, - 'last_session_closing_date': None, - } - return result - - def _get_current_session_user(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened').user_id.name - return result - - _columns = { - 'name' : fields.char('Point of Sale Name', select=1, - required=True, help="An internal identification of the point of sale"), - 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', - 'pos_config_id', 'journal_id', 'Available Payment Methods', - domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",), - 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'), - 'stock_location_id': fields.many2one('stock.location', 'Stock Location', domain=[('usage', '=', 'internal')], required=True), - 'journal_id' : fields.many2one('account.journal', 'Sale Journal', - domain=[('type', '=', 'sale')], - help="Accounting journal used to post sales entries."), - 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), - 'iface_cashdrawer' : fields.boolean('Cashdrawer', help="Automatically open the cashdrawer"), - 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), - 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), - 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard', help="Enables an integrated Virtual Keyboard"), - 'iface_print_via_proxy' : fields.boolean('Print via Proxy', help="Bypass browser printing and prints via the hardware proxy"), - 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), - 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), - 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), - # TODO master: remove the `iface_fullscreen` field. This is no longer used. - 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), - 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), - 'iface_print_skip_screen': fields.boolean('Skip Receipt Screen', help='The receipt screen will be skipped if the receipt can be printed automatically.'), - 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), - 'iface_tax_included': fields.boolean('Include Taxes in Prices', help='The displayed prices will always include all taxes, even if the taxes have been setup differently'), - 'iface_start_categ_id': fields.many2one('pos.category','Start Category', help='The point of sale will display this product category by default. If no category is specified, all available products will be shown'), - 'iface_display_categ_images': fields.boolean('Display Category Pictures', help="The product categories will be displayed with pictures."), - 'cash_control': fields.boolean('Cash Control', help="Check the amount of the cashbox at opening and closing."), - 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), - 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), - 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), - - 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), - 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), - 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, - help="This sequence is automatically created by Odoo but you can change it "\ - "to customize the reference numbers of your orders.", copy=False), - 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), - 'current_session_id': fields.function(_get_current_session, multi="session", type="many2one", relation="pos.session", string="Current Session"), - 'current_session_state': fields.function(_get_current_session, multi="session", type='char'), - 'last_session_closing_cash': fields.function(_get_last_session, multi="last_session", type='float'), - 'last_session_closing_date': fields.function(_get_last_session, multi="last_session", type='date'), - 'pos_session_username': fields.function(_get_current_session_user, type='char'), - 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), - 'pricelist_id': fields.many2one('product.pricelist','Pricelist', required=True), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcodes', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers', required=True), - 'group_pos_manager_id': fields.many2one('res.groups','Point of Sale Manager Group', help='This field is there to pass the id of the pos manager group to the point of sale client'), - 'group_pos_user_id': fields.many2one('res.groups','Point of Sale User Group', help='This field is there to pass the id of the pos user group to the point of sale client'), - 'tip_product_id': fields.many2one('product.product','Tip Product', help="The product used to encode the customer tip. Leave empty if you do not accept tips."), - 'fiscal_position_ids': fields.many2many('account.fiscal.position', string='Fiscal Positions') - } - - def _check_company_location(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - if config.stock_location_id.company_id and config.stock_location_id.company_id.id != config.company_id.id: - return False - return True - - def _check_company_journal(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - if config.journal_id and config.journal_id.company_id.id != config.company_id.id: - return False - return True - - def _check_company_payment(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - journal_ids = [j.id for j in config.journal_ids] - if self.pool['account.journal'].search(cr, uid, [ - ('id', 'in', journal_ids), - ('company_id', '!=', config.company_id.id) - ], count=True, context=context): - return False - return True - - _constraints = [ - (_check_company_location, "The company of the stock location is different than the one of point of sale", ['company_id', 'stock_location_id']), - (_check_company_journal, "The company of the sale journal is different than the one of point of sale", ['company_id', 'journal_id']), - (_check_company_payment, "The company of a payment method is different than the one of point of sale", ['company_id', 'journal_ids']), - ] - - def name_get(self, cr, uid, ids, context=None): - result = [] - states = { - 'opening_control': _('Opening Control'), - 'opened': _('In Progress'), - 'closing_control': _('Closing Control'), - 'closed': _('Closed & Posted'), - } - for record in self.browse(cr, uid, ids, context=context): - if (not record.session_ids) or (record.session_ids[0].state=='closed'): - result.append((record.id, record.name+' ('+_('not used')+')')) - continue - session = record.session_ids[0] - result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) - return result - - def _default_sale_journal(self, cr, uid, context=None): - company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1, context=context) - return res and res[0] or False - - def _default_pricelist(self, cr, uid, context=None): - res = self.pool.get('product.pricelist').search(cr, uid, [], limit=1, context=context) - return res and res[0] or False - - def _get_default_location(self, cr, uid, context=None): - wh_obj = self.pool.get('stock.warehouse') - user = self.pool.get('res.users').browse(cr, uid, uid, context) - res = wh_obj.search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context) - if res and res[0]: - return wh_obj.browse(cr, uid, res[0], context=context).lot_stock_id.id - return False - - def _get_default_company(self, cr, uid, context=None): - company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) - return company_id - - def _get_default_nomenclature(self, cr, uid, context=None): - nom_obj = self.pool.get('barcode.nomenclature') - res = nom_obj.search(cr, uid, [], limit=1, context=context) - return res and res[0] or False - - def _get_group_pos_manager(self, cr, uid, context=None): - group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_manager') - if group: - return group[1] - else: - return False - - def _get_group_pos_user(self, cr, uid, context=None): - group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_user') - if group: - return group[1] - else: - return False - - _defaults = { - 'uuid' : lambda self, cr, uid, context={}: str(uuid.uuid4()), - 'state' : POS_CONFIG_STATE[0][0], - 'journal_id': _default_sale_journal, - 'group_by' : True, - 'pricelist_id': _default_pricelist, - 'iface_invoicing': True, - 'iface_print_auto': False, - 'iface_print_skip_screen': True, - 'stock_location_id': _get_default_location, - 'company_id': _get_default_company, - 'barcode_nomenclature_id': _get_default_nomenclature, - 'group_pos_manager_id': _get_group_pos_manager, - 'group_pos_user_id': _get_group_pos_user, - } - - def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): - p_type_obj = self.pool.get("stock.picking.type") - p_type = p_type_obj.browse(cr, uid, picking_type_id, context=context) - if p_type.default_location_src_id and p_type.default_location_src_id.usage == 'internal' and p_type.default_location_dest_id and p_type.default_location_dest_id.usage == 'customer': - return {'value': {'stock_location_id': p_type.default_location_src_id.id}} - return False - - def onchange_iface_print_via_proxy(self, cr, uid, ids, print_via_proxy, context=None): - return {'value': {'iface_print_auto': print_via_proxy}} - - def set_active(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'active'}, context=context) - - def set_inactive(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) - - def set_deprecate(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) - - def create(self, cr, uid, values, context=None): - ir_sequence = self.pool.get('ir.sequence') - # force sequence_id field to new pos.order sequence - values['sequence_id'] = ir_sequence.create(cr, SUPERUSER_ID, { - 'name': 'POS Order %s' % values['name'], - 'padding': 4, - 'prefix': "%s/" % values['name'], - 'code': "pos.order", - 'company_id': values.get('company_id', False), - }, context=context) - - # TODO master: add field sequence_line_id on model - # this make sure we always have one available per company - ir_sequence.create(cr, SUPERUSER_ID, { - 'name': 'POS order line %s' % values['name'], - 'padding': 4, - 'prefix': "%s/" % values['name'], - 'code': "pos.order.line", - 'company_id': values.get('company_id', False), - }, context=context) - - return super(pos_config, self).create(cr, uid, values, context=context) - - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - if obj.sequence_id: - obj.sequence_id.unlink() - return super(pos_config, self).unlink(cr, uid, ids, context=context) - - # Methods to open the POS - - def open_ui(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - context = dict(context or {}) - context['active_id'] = record.current_session_id.id - return { - 'type': 'ir.actions.act_url', - 'url': '/pos/web/', - 'target': 'self', - } - - def open_existing_session_cb_close(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - record.current_session_id.signal_workflow('cashbox_control') - return self.open_session_cb(cr, uid, ids, context) - - def open_session_cb(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - proxy = self.pool.get('pos.session') - record = self.browse(cr, uid, ids[0], context=context) - current_session_id = record.current_session_id - if not current_session_id: - values = { - 'user_id': uid, - 'config_id': record.id, - } - session_id = proxy.create(cr, uid, values, context=context) - self.write(cr, SUPERUSER_ID, record.id, {'current_session_id': session_id}, context=context) - if record.current_session_id.state == 'opened': - return self.open_ui(cr, uid, ids, context=context) - return self._open_session(session_id) - return self._open_session(current_session_id.id) - - def open_existing_session_cb(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - return self._open_session(record.current_session_id.id) - - def _open_session(self, session_id): - return { - 'name': _('Session'), - 'view_type': 'form', - 'view_mode': 'form,tree', - 'res_model': 'pos.session', - 'res_id': session_id, - 'view_id': False, - 'type': 'ir.actions.act_window', - } - -class pos_session(osv.osv): - _name = 'pos.session' - _order = 'id desc' - - POS_SESSION_STATE = [ - ('opening_control', 'Opening Control'), # Signal open - ('opened', 'In Progress'), # Signal closing - ('closing_control', 'Closing Control'), # Signal close - ('closed', 'Closed & Posted'), - ] - - def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - result[record.id] = { - 'cash_journal_id' : False, - 'cash_register_id' : False, - 'cash_control' : False, - } - - if record.config_id.cash_control: - for st in record.statement_ids: - if st.journal_id.type == 'cash': - result[record.id]['cash_control'] = True - result[record.id]['cash_journal_id'] = st.journal_id.id - result[record.id]['cash_register_id'] = st.id - - if not result[record.id]['cash_control']: - raise UserError(_("Cash control can only be applied to cash journals.")) - - return result - - _columns = { - 'config_id' : fields.many2one('pos.config', 'Point of Sale', - help="The physical point of sale you will use.", - required=True, - select=1, - domain="[('state', '=', 'active')]", - ), - - 'name' : fields.char('Session ID', required=True, readonly=True), - 'user_id' : fields.many2one('res.users', 'Responsible', - required=True, - select=1, - readonly=True, - states={'opening_control' : [('readonly', False)]} - ), - 'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currency"), - 'start_at' : fields.datetime('Opening Date', readonly=True), - 'stop_at' : fields.datetime('Closing Date', readonly=True, copy=False), - - 'state' : fields.selection(POS_SESSION_STATE, 'Status', - required=True, readonly=True, - select=1, copy=False), - - 'sequence_number': fields.integer('Order Sequence Number', help='A sequence number that is incremented with each order'), - 'login_number': fields.integer('Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session'), - - 'cash_control' : fields.function(_compute_cash_all, - multi='cash', - type='boolean', string='Has Cash Control'), - 'cash_journal_id' : fields.function(_compute_cash_all, - multi='cash', - type='many2one', relation='account.journal', - string='Cash Journal', store=True), - 'cash_register_id' : fields.function(_compute_cash_all, - multi='cash', - type='many2one', relation='account.bank.statement', - string='Cash Register', store=True), - - 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', - type='float', - digits=0, - string="Ending Balance", - help="Total of closing cash control lines.", - readonly=True), - 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', - type='float', - digits=0, - string="Starting Balance", - help="Total of opening cash control lines.", - readonly=True), - 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', - string='Total Cash Transaction', - readonly=True, - help="Total of all paid sale orders"), - 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', - type='float', - digits=0, - string="Theoretical Closing Balance", - help="Sum of opening balance and transactions.", - readonly=True), - 'cash_register_difference' : fields.related('cash_register_id', 'difference', - type='float', - string='Difference', - help="Difference between the theoretical closing balance and the real closing balance.", - readonly=True), - - 'journal_ids' : fields.related('config_id', 'journal_ids', - type='many2many', - readonly=True, - relation='account.journal', - string='Available Payment Methods'), - 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), - - 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), - } - - _defaults = { - 'name' : '/', - 'user_id' : lambda obj, cr, uid, context: uid, - 'state' : 'opening_control', - 'sequence_number': 1, - 'login_number': 0, - } - - _sql_constraints = [ - ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), - ] - - def _check_unicity(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=None): - # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user - domain = [ - ('state', 'not in', ('closed','closing_control')), - ('user_id', '=', session.user_id.id) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - def _check_pos_config(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=None): - domain = [ - ('state', '!=', 'closed'), - ('config_id', '=', session.config_id.id) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - _constraints = [ - (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']), - (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']), - ] - - def create(self, cr, uid, values, context=None): - context = dict(context or {}) - config_id = values.get('config_id', False) or context.get('default_config_id', False) - if not config_id: - raise UserError(_("You should assign a Point of Sale to your session.")) - - # journal_id is not required on the pos_config because it does not - # exists at the installation. If nothing is configured at the - # installation we do the minimal configuration. Impossible to do in - # the .xml files as the CoA is not yet installed. - jobj = self.pool.get('pos.config') - pos_config = jobj.browse(cr, uid, config_id, context=context) - context.update({'company_id': pos_config.company_id.id}) - if not pos_config.journal_id: - jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] - if jid: - jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_id': jid}, context=context) - else: - raise UserError(_("Unable to open the session. You have to assign a sale journal to your point of sale.")) - - # define some cash journal if no payment method exists - if not pos_config.journal_ids: - journal_proxy = self.pool.get('account.journal') - cashids = journal_proxy.search(cr, uid, [('journal_user', '=', True), ('type','=','cash')], context=context) - if not cashids: - cashids = journal_proxy.search(cr, uid, [('type', '=', 'cash')], context=context) - if not cashids: - cashids = journal_proxy.search(cr, uid, [('journal_user','=',True)], context=context) - - journal_proxy.write(cr, SUPERUSER_ID, cashids, {'journal_user': True}) - jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) - - - pos_config = jobj.browse(cr, uid, config_id, context=context) - - statements = [(0, 0, { - 'journal_id': journal.id, - 'user_id': uid, - 'company_id': pos_config.company_id.id - }) for journal in pos_config.journal_ids] - - values.update({ - 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session', context=context), - 'statement_ids': statements, - 'config_id': config_id - }) - - # set the journal_id which should be used by - # account.bank.statement to set the opening balance of the - # newly created bank statement - if pos_config.cash_control: - for journal in pos_config.journal_ids: - if journal.type == 'cash': - context.update({'journal_id': journal.id}) - - return super(pos_session, self).create(cr, uid, values, context=context) - - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.pool.get('account.bank.statement').unlink(cr, uid, obj.statement_ids.ids, context=context) - return super(pos_session, self).unlink(cr, uid, ids, context=context) - - def login(self, cr, uid, ids, context=None): - this_record = self.browse(cr, uid, ids[0], context=context) - this_record.write({ - 'login_number': this_record.login_number+1, - }) - - def wkf_action_open(self, cr, uid, ids, context=None): - # second browse because we need to refetch the data from the DB for cash_register_id - for record in self.browse(cr, uid, ids, context=context): - values = {} - if not record.start_at: - values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S') - values['state'] = 'opened' - record.write(values) - for st in record.statement_ids: - st.button_open() - - return True - - def wkf_action_opening_control(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context) - - def wkf_action_closing_control(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=context): - for statement in session.statement_ids: - if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real): - self.pool.get('account.bank.statement').write(cr, uid, [statement.id], {'balance_end_real': statement.balance_end}) - return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) - - def wkf_action_close(self, cr, uid, ids, context=None): - # Close CashBox - local_context = dict(context) - for record in self.browse(cr, uid, ids, context=context): - company_id = record.config_id.company_id.id - local_context.update({'force_company': company_id, 'company_id': company_id}) - for st in record.statement_ids: - if abs(st.difference) > st.journal_id.amount_authorized_diff: - # The pos manager can close statements with maximums. - if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"): - raise UserError(_("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff)) - if (st.journal_id.type not in ['bank', 'cash']): - raise UserError(_("The type of the journal for your payment method should be bank or cash ")) - self.pool['account.bank.statement'].button_confirm_bank(cr, SUPERUSER_ID, [st.id], context=local_context) - self._confirm_orders(cr, uid, ids, context=local_context) - self.write(cr, uid, ids, {'state' : 'closed'}, context=local_context) - - obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1] - return { - 'type' : 'ir.actions.client', - 'name' : 'Point of Sale Menu', - 'tag' : 'reload', - 'params' : {'menu_id': obj}, - } - - def _confirm_orders(self, cr, uid, ids, context=None): - pos_order_obj = self.pool.get('pos.order') - for session in self.browse(cr, uid, ids, context=context): - company_id = session.config_id.journal_id.company_id.id - local_context = dict(context or {}, force_company=company_id) - order_ids = [order.id for order in session.order_ids if order.state == 'paid'] - - move_id = pos_order_obj._create_account_move(cr, uid, session.start_at, session.name, session.config_id.journal_id.id, company_id, context=context) - - pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context) - - for order in session.order_ids: - if order.state == 'done': - continue - if order.state not in ('paid', 'invoiced'): - raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) - else: - pos_order_obj.signal_workflow(cr, uid, [order.id], 'done') - - return True - - def open_frontend_cb(self, cr, uid, ids, context=None): - if not context: - context = {} - if not ids: - return {} - for session in self.browse(cr, uid, ids, context=context): - if session.user_id.id != uid: - raise UserError(_("You cannot use the session of another users. This session is owned by %s. " - "Please first close this one to use this point of sale.") % session.user_id.name) - context.update({'active_id': ids[0]}) - return { - 'type' : 'ir.actions.act_url', - 'target': 'self', - 'url': '/pos/web/', - } class pos_order(osv.osv): _name = "pos.order" @@ -1377,12 +759,6 @@ class pos_order(osv.osv): self.create_account_move(cr, uid, ids, context=context) return True -class account_bank_statement_line(osv.osv): - _inherit = 'account.bank.statement.line' - _columns= { - 'pos_statement_id': fields.many2one('pos.order', string="POS statement", ondelete='cascade'), - } - class pos_order_line(osv.osv): _name = "pos.order.line" _description = "Lines of Point of Sale" @@ -1480,125 +856,3 @@ class pos_order_line(osv.osv): 'discount': lambda *a: 0.0, 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, } - -class pos_category(osv.osv): - _name = "pos.category" - _description = "Public Category" - _order = "sequence, name" - - _constraints = [ - (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) - ] - - def name_get(self, cr, uid, ids, context=None): - res = [] - for cat in self.browse(cr, uid, ids, context=context): - names = [cat.name] - pcat = cat.parent_id - while pcat: - names.append(pcat.name) - pcat = pcat.parent_id - res.append((cat.id, ' / '.join(reversed(names)))) - return res - - def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): - res = self.name_get(cr, uid, ids, context=context) - return dict(res) - - _columns = { - 'name': fields.char('Name', required=True, translate=True), - 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'), - 'parent_id': fields.many2one('pos.category','Parent Category', select=True), - 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), - - } - - # NOTE: there is no 'default image', because by default we don't show - # thumbnails for categories. However if we have a thumbnail for at least one - # category, then we display a default image on the other, so that the - # buttons have consistent styling. - # In this case, the default image is set by the js code. - image = openerp.fields.Binary("Image", attachment=True, - help="This field holds the image used as image for the cateogry, limited to 1024x1024px.") - image_medium = openerp.fields.Binary("Medium-sized image", - compute='_compute_images', inverse='_inverse_image_medium', store=True, attachment=True, - help="Medium-sized image of the category. It is automatically "\ - "resized as a 128x128px image, with aspect ratio preserved. "\ - "Use this field in form views or some kanban views.") - image_small = openerp.fields.Binary("Small-sized image", - compute='_compute_images', inverse='_inverse_image_small', store=True, attachment=True, - help="Small-sized image of the category. It is automatically "\ - "resized as a 64x64px image, with aspect ratio preserved. "\ - "Use this field anywhere a small image is required.") - - @openerp.api.depends('image') - def _compute_images(self): - for rec in self: - rec.image_medium = tools.image_resize_image_medium(rec.image) - rec.image_small = tools.image_resize_image_small(rec.image) - - def _inverse_image_medium(self): - for rec in self: - rec.image = tools.image_resize_image_big(rec.image_medium) - - def _inverse_image_small(self): - for rec in self: - rec.image = tools.image_resize_image_big(rec.image_small) - -class product_template(osv.osv): - _inherit = 'product.template' - - _columns = { - 'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'), - 'to_weight' : fields.boolean('To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration"), - 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', help="Those categories are used to group similar products for point of sale."), - } - - _defaults = { - 'to_weight' : False, - 'available_in_pos': True, - } - - def unlink(self, cr, uid, ids, context=None): - product_ctx = dict(context or {}, active_test=False) - if self.search_count(cr, uid, [('id', 'in', ids), ('available_in_pos', '=', True)], context=product_ctx): - if self.pool['pos.session'].search_count(cr, uid, [('state', '!=', 'closed')], context=context): - raise osv.except_osv(_('Error!'), - _('You cannot delete a product saleable in point of sale while a session is still opened.')) - return super(product_template, self).unlink(cr, uid, ids, context=context) - -class res_partner(osv.osv): - _inherit = 'res.partner' - - def create_from_ui(self, cr, uid, partner, context=None): - """ create or modify a partner from the point of sale ui. - partner contains the partner's fields. """ - - #image is a dataurl, get the data after the comma - if partner.get('image',False): - img = partner['image'].split(',')[1] - partner['image'] = img - - if partner.get('id',False): # Modifying existing partner - partner_id = partner['id'] - del partner['id'] - self.write(cr, uid, [partner_id], partner, context=context) - else: - partner_id = self.create(cr, uid, partner, context=context) - - return partner_id - -class barcode_rule(models.Model): - _inherit = 'barcode.rule' - - def _get_type_selection(self): - types = sets.Set(super(barcode_rule,self)._get_type_selection()) - types.update([ - ('weight', _('Weighted Product')), - ('price', _('Priced Product')), - ('discount', _('Discounted Product')), - ('client', _('Client')), - ('cashier', _('Cashier')) - ]) - return list(types) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py new file mode 100644 index 00000000000..b1f408afb68 --- /dev/null +++ b/addons/point_of_sale/models/pos_session.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import time +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +class pos_session(osv.osv): + _name = 'pos.session' + _order = 'id desc' + + POS_SESSION_STATE = [ + ('opening_control', 'Opening Control'), # Signal open + ('opened', 'In Progress'), # Signal closing + ('closing_control', 'Closing Control'), # Signal close + ('closed', 'Closed & Posted'), + ] + + def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None): + result = dict() + + for record in self.browse(cr, uid, ids, context=context): + result[record.id] = { + 'cash_journal_id' : False, + 'cash_register_id' : False, + 'cash_control' : False, + } + + if record.config_id.cash_control: + for st in record.statement_ids: + if st.journal_id.type == 'cash': + result[record.id]['cash_control'] = True + result[record.id]['cash_journal_id'] = st.journal_id.id + result[record.id]['cash_register_id'] = st.id + + if not result[record.id]['cash_control']: + raise UserError(_("Cash control can only be applied to cash journals.")) + + return result + + _columns = { + 'config_id' : fields.many2one('pos.config', 'Point of Sale', + help="The physical point of sale you will use.", + required=True, + select=1, + domain="[('state', '=', 'active')]", + ), + + 'name' : fields.char('Session ID', required=True, readonly=True), + 'user_id' : fields.many2one('res.users', 'Responsible', + required=True, + select=1, + readonly=True, + states={'opening_control' : [('readonly', False)]} + ), + 'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currency"), + 'start_at' : fields.datetime('Opening Date', readonly=True), + 'stop_at' : fields.datetime('Closing Date', readonly=True, copy=False), + + 'state' : fields.selection(POS_SESSION_STATE, 'Status', + required=True, readonly=True, + select=1, copy=False), + + 'sequence_number': fields.integer('Order Sequence Number', help='A sequence number that is incremented with each order'), + 'login_number': fields.integer('Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session'), + + 'cash_control' : fields.function(_compute_cash_all, + multi='cash', + type='boolean', string='Has Cash Control'), + 'cash_journal_id' : fields.function(_compute_cash_all, + multi='cash', + type='many2one', relation='account.journal', + string='Cash Journal', store=True), + 'cash_register_id' : fields.function(_compute_cash_all, + multi='cash', + type='many2one', relation='account.bank.statement', + string='Cash Register', store=True), + + 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', + type='float', + digits=0, + string="Ending Balance", + help="Total of closing cash control lines.", + readonly=True), + 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', + type='float', + digits=0, + string="Starting Balance", + help="Total of opening cash control lines.", + readonly=True), + 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', + string='Total Cash Transaction', + readonly=True, + help="Total of all paid sale orders"), + 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', + type='float', + digits=0, + string="Theoretical Closing Balance", + help="Sum of opening balance and transactions.", + readonly=True), + 'cash_register_difference' : fields.related('cash_register_id', 'difference', + type='float', + string='Difference', + help="Difference between the theoretical closing balance and the real closing balance.", + readonly=True), + + 'journal_ids' : fields.related('config_id', 'journal_ids', + type='many2many', + readonly=True, + relation='account.journal', + string='Available Payment Methods'), + 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), + + 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), + } + + _defaults = { + 'name' : '/', + 'user_id' : lambda obj, cr, uid, context: uid, + 'state' : 'opening_control', + 'sequence_number': 1, + 'login_number': 0, + } + + _sql_constraints = [ + ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), + ] + + def _check_unicity(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=None): + # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user + domain = [ + ('state', 'not in', ('closed','closing_control')), + ('user_id', '=', session.user_id.id) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True + + def _check_pos_config(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=None): + domain = [ + ('state', '!=', 'closed'), + ('config_id', '=', session.config_id.id) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True + + _constraints = [ + (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']), + (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']), + ] + + def create(self, cr, uid, values, context=None): + context = dict(context or {}) + config_id = values.get('config_id', False) or context.get('default_config_id', False) + if not config_id: + raise UserError(_("You should assign a Point of Sale to your session.")) + + # journal_id is not required on the pos_config because it does not + # exists at the installation. If nothing is configured at the + # installation we do the minimal configuration. Impossible to do in + # the .xml files as the CoA is not yet installed. + jobj = self.pool.get('pos.config') + pos_config = jobj.browse(cr, uid, config_id, context=context) + context.update({'company_id': pos_config.company_id.id}) + if not pos_config.journal_id: + jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] + if jid: + jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_id': jid}, context=context) + else: + raise UserError(_("Unable to open the session. You have to assign a sale journal to your point of sale.")) + + # define some cash journal if no payment method exists + if not pos_config.journal_ids: + journal_proxy = self.pool.get('account.journal') + cashids = journal_proxy.search(cr, uid, [('journal_user', '=', True), ('type','=','cash')], context=context) + if not cashids: + cashids = journal_proxy.search(cr, uid, [('type', '=', 'cash')], context=context) + if not cashids: + cashids = journal_proxy.search(cr, uid, [('journal_user','=',True)], context=context) + + journal_proxy.write(cr, SUPERUSER_ID, cashids, {'journal_user': True}) + jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) + + + pos_config = jobj.browse(cr, uid, config_id, context=context) + + statements = [(0, 0, { + 'journal_id': journal.id, + 'user_id': uid, + 'company_id': pos_config.company_id.id + }) for journal in pos_config.journal_ids] + + values.update({ + 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session', context=context), + 'statement_ids': statements, + 'config_id': config_id + }) + + # set the journal_id which should be used by + # account.bank.statement to set the opening balance of the + # newly created bank statement + if pos_config.cash_control: + for journal in pos_config.journal_ids: + if journal.type == 'cash': + context.update({'journal_id': journal.id}) + + return super(pos_session, self).create(cr, uid, values, context=context) + + def unlink(self, cr, uid, ids, context=None): + for obj in self.browse(cr, uid, ids, context=context): + self.pool.get('account.bank.statement').unlink(cr, uid, obj.statement_ids.ids, context=context) + return super(pos_session, self).unlink(cr, uid, ids, context=context) + + def login(self, cr, uid, ids, context=None): + this_record = self.browse(cr, uid, ids[0], context=context) + this_record.write({ + 'login_number': this_record.login_number+1, + }) + + def wkf_action_open(self, cr, uid, ids, context=None): + # second browse because we need to refetch the data from the DB for cash_register_id + for record in self.browse(cr, uid, ids, context=context): + values = {} + if not record.start_at: + values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S') + values['state'] = 'opened' + record.write(values) + for st in record.statement_ids: + st.button_open() + + return True + + def wkf_action_opening_control(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context) + + def wkf_action_closing_control(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=context): + for statement in session.statement_ids: + if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real): + self.pool.get('account.bank.statement').write(cr, uid, [statement.id], {'balance_end_real': statement.balance_end}) + return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) + + def wkf_action_close(self, cr, uid, ids, context=None): + # Close CashBox + local_context = dict(context) + for record in self.browse(cr, uid, ids, context=context): + company_id = record.config_id.company_id.id + local_context.update({'force_company': company_id, 'company_id': company_id}) + for st in record.statement_ids: + if abs(st.difference) > st.journal_id.amount_authorized_diff: + # The pos manager can close statements with maximums. + if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"): + raise UserError(_("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff)) + if (st.journal_id.type not in ['bank', 'cash']): + raise UserError(_("The type of the journal for your payment method should be bank or cash ")) + self.pool['account.bank.statement'].button_confirm_bank(cr, SUPERUSER_ID, [st.id], context=local_context) + self._confirm_orders(cr, uid, ids, context=local_context) + self.write(cr, uid, ids, {'state' : 'closed'}, context=local_context) + + obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1] + return { + 'type' : 'ir.actions.client', + 'name' : 'Point of Sale Menu', + 'tag' : 'reload', + 'params' : {'menu_id': obj}, + } + + def _confirm_orders(self, cr, uid, ids, context=None): + pos_order_obj = self.pool.get('pos.order') + for session in self.browse(cr, uid, ids, context=context): + company_id = session.config_id.journal_id.company_id.id + local_context = dict(context or {}, force_company=company_id) + order_ids = [order.id for order in session.order_ids if order.state == 'paid'] + + move_id = pos_order_obj._create_account_move(cr, uid, session.start_at, session.name, session.config_id.journal_id.id, company_id, context=context) + + pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context) + + for order in session.order_ids: + if order.state == 'done': + continue + if order.state not in ('paid', 'invoiced'): + raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) + else: + pos_order_obj.signal_workflow(cr, uid, [order.id], 'done') + + return True + + def open_frontend_cb(self, cr, uid, ids, context=None): + if not context: + context = {} + if not ids: + return {} + for session in self.browse(cr, uid, ids, context=context): + if session.user_id.id != uid: + raise UserError(_("You cannot use the session of another users. This session is owned by %s. " + "Please first close this one to use this point of sale.") % session.user_id.name) + context.update({'active_id': ids[0]}) + return { + 'type' : 'ir.actions.act_url', + 'target': 'self', + 'url': '/pos/web/', + } diff --git a/addons/point_of_sale/models/product.py b/addons/point_of_sale/models/product.py new file mode 100644 index 00000000000..dd2607c4d60 --- /dev/null +++ b/addons/point_of_sale/models/product.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from openerp.osv import fields, osv +from openerp.tools.translate import _ + + +class product_template(osv.osv): + _inherit = 'product.template' + + _columns = { + 'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'), + 'to_weight' : fields.boolean('To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration"), + 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', help="Those categories are used to group similar products for point of sale."), + } + + _defaults = { + 'to_weight' : False, + 'available_in_pos': True, + } + + def unlink(self, cr, uid, ids, context=None): + product_ctx = dict(context or {}, active_test=False) + if self.search_count(cr, uid, [('id', 'in', ids), ('available_in_pos', '=', True)], context=product_ctx): + if self.pool['pos.session'].search_count(cr, uid, [('state', '!=', 'closed')], context=context): + raise osv.except_osv(_('Error!'), + _('You cannot delete a product saleable in point of sale while a session is still opened.')) + return super(product_template, self).unlink(cr, uid, ids, context=context) diff --git a/addons/point_of_sale/models/res_partner.py b/addons/point_of_sale/models/res_partner.py index 53c27c9fd96..b5167b9bea7 100644 --- a/addons/point_of_sale/models/res_partner.py +++ b/addons/point_of_sale/models/res_partner.py @@ -6,4 +6,22 @@ class res_users(osv.osv): _inherit = 'res.partner' _columns = { 'barcode' : fields.char('Barcode', help="BarCode", oldname='ean13'), - } \ No newline at end of file + } + + def create_from_ui(self, cr, uid, partner, context=None): + """ create or modify a partner from the point of sale ui. + partner contains the partner's fields. """ + + #image is a dataurl, get the data after the comma + if partner.get('image',False): + img = partner['image'].split(',')[1] + partner['image'] = img + + if partner.get('id',False): # Modifying existing partner + partner_id = partner['id'] + del partner['id'] + self.write(cr, uid, [partner_id], partner, context=context) + else: + partner_id = self.create(cr, uid, partner, context=context) + + return partner_id diff --git a/addons/point_of_sale/views/account_invoice_view.xml b/addons/point_of_sale/views/account_invoice_view.xml new file mode 100644 index 00000000000..58bfe004dc0 --- /dev/null +++ b/addons/point_of_sale/views/account_invoice_view.xml @@ -0,0 +1,13 @@ + + + + + Invoices + ir.actions.act_window + account.invoice + form + tree,form + [('origin','like','POS')] + + + \ No newline at end of file diff --git a/addons/point_of_sale/views/account_journal_view.xml b/addons/point_of_sale/views/account_journal_view.xml new file mode 100644 index 00000000000..dbd7f41299e --- /dev/null +++ b/addons/point_of_sale/views/account_journal_view.xml @@ -0,0 +1,29 @@ + + + + + Payment Methods + account.journal + form + tree,form + + [('journal_user','=', 1)] + +

+ Click to add a payment method. +

+ Payment methods are defined by accounting journals having the + field PoS Payment Method checked. In order to be useable + from the touchscreen interface, you must set the payment method + on the Point of Sale configuration. +

+
+
+ + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/views/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml index 4346c2bc20e..9eee8f2c065 100644 --- a/addons/point_of_sale/views/point_of_sale_report.xml +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -56,5 +56,223 @@ name="point_of_sale.report_sessionsummary" file="point_of_sale.report_sessionsummary" /> + + + + + report.trans.pos.user.form + report.transaction.pos + +
+ + + + + + + + + +
+
+
+ + + + Sales by user + report.transaction.pos + + + + + + + + + + + + + + + + + report.transaction.pos.calendar + report.transaction.pos + + + + + + + + + + + report.transaction.pos.graph + report.transaction.pos + + + + + + + + + + + + Sales by day + report.transaction.pos + form + tree,calendar,form,graph + [('date_create','=',time.strftime('%Y-%m-%d'))] + + + + + Sales by month + report.transaction.pos + form + tree,calendar,form,graph + [('date_create','like',time.strftime('%Y-%m'))] + + + + + Sales by user + report.transaction.pos + form + tree,calendar,form,graph + + + + + + report.sales.by.user.pos.form + report.sales.by.user.pos + +
+ + + + + + +
+
+
+ + + + report.sales.by.user.pos.tree + report.sales.by.user.pos + + + + + + + + + + + + report.sales.by.user.pos.calendar + report.sales.by.user.pos + + + + + + + + + + + report.sales.by.user.pos.graph + report.sales.by.user.pos + + + + + + + + + + + Sales by User + report.sales.by.user.pos + form + tree,calendar,form,graph + [('date_order','=',time.strftime('%Y-%m-%d'))] + + + + + + + report.sales.by.user.pos.month.form + report.sales.by.user.pos.month + +
+ + + + + + +
+
+
+ + + + report.sales.by.user.pos.month.tree + report.sales.by.user.pos.month + + + + + + + + + + + + report.sales.by.user.pos.month.calendar + report.sales.by.user.pos.month + + + + + + + + + + + report.sales.by.user.pos.month.graph + report.sales.by.user.pos.month + + + + + + + + + + + + Sales by User Monthly + report.sales.by.user.pos.month + form + tree,calendar,form,graph + [('date_order','like',time.strftime('%Y-%m'))] + + diff --git a/addons/point_of_sale/views/point_of_sale_view.xml b/addons/point_of_sale/views/point_of_sale_view.xml index a895492cc3b..547b8d64285 100644 --- a/addons/point_of_sale/views/point_of_sale_view.xml +++ b/addons/point_of_sale/views/point_of_sale_view.xml @@ -10,989 +10,5 @@ web_icon="point_of_sale,static/description/icon.png" sequence="20"/> - - pos.order - pos.order - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

- Session: - -

- - - - - - - - - - - - - -
- - - -
-
- - - - - - - - -
-
- - - - - - - - - - - - - - -
- -
-
- - - pos.session.tree.view - pos.session - - - - - - - - - - - - - - pos.session.search.view - pos.session - - - - - - - - - - - - - - - - - - - Sessions - ir.actions.act_window - pos.session - form - tree,form - - -

- Click to start a new session. -

- A session is a period of time, usually one day, during which - you sell through the point of sale. The user has to check the - currencies in your cash registers at the beginning and the end - of each session. -

- Note that you may use the menu Your Session - to quickly open a new session. -

-
-
- - - - - pos.order.list.select - pos.order - - - - - - - - - - - - - - - - - - - - - - - Point of Sale - ir.actions.act_window - pos.config - form - kanban,tree,form - - -

- Click to create a new PoS config. -

- Configure at least one Point of Sale to be able to sell through the PoS interface. -

-
-
- - - - - diff --git a/addons/point_of_sale/views/pos_category_view.xml b/addons/point_of_sale/views/pos_category_view.xml new file mode 100644 index 00000000000..9b9932df98a --- /dev/null +++ b/addons/point_of_sale/views/pos_category_view.xml @@ -0,0 +1,59 @@ + + + + + + pos.category.form + pos.category + +
+ + +
+ + + + + +
+
+
+
+
+ + pos.category.tree + pos.category + + + + + + + + + + Pos Product Categories + ir.actions.act_window + pos.category + form + tree,form + + +

+ Click to define a new category. +

+ Categories are used to browse your products through the + touchscreen interface. +

+ If you put a photo on the category and enable the option in the + point of sale configuration, the layout of the touchscreen interface + will automatically be displayed. We suggest not to put a photo + on categories for small (1024x768) screens. +

+
+
+ + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml new file mode 100644 index 00000000000..18b8d0e5c6b --- /dev/null +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -0,0 +1,147 @@ + + + + + pos.config.form.view + pos.config + +
+
+
+ + + + +
+
+
+ + + pos.config.tree.view + pos.config + + + + + + + + + + + pos.config.search.view + pos.config + + + + + + + + + + + + + + Point of Sale + ir.actions.act_window + pos.config + form + tree,form + + + + + + + Point of Sale + ir.actions.act_window + pos.config + form + kanban,tree,form + + +

+ Click to create a new PoS config. +

+ Configure at least one Point of Sale to be able to sell through the PoS interface. +

+
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/report/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml similarity index 100% rename from addons/point_of_sale/report/pos_order_report_view.xml rename to addons/point_of_sale/views/pos_order_report_view.xml diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml new file mode 100644 index 00000000000..b3fe63c1185 --- /dev/null +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -0,0 +1,279 @@ + + + + + pos.order + pos.order + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Orders + ir.actions.act_window + pos.order + form + tree,form + + [] + +

+ Click to create a new order. +

+ Use this menu to browse previous orders. To record new + orders, you may use the menu Your Session for + the touchscreen interface. +

+
+
+ + Orders + pos.order + + + + + + + + + + + + + + + + + pos.order.list.select + pos.order + + + + + + + + + + + + + + + + + + + + + + pos.order.search.view + pos.order + + + + + + + + + + + + Sale lines + pos.order.line + + + + + + + + + + + + + + + Sale line + pos.order.line + +
+ + + + + + + +
+
+
+ + + Sale line + ir.actions.act_window + pos.order.line + form + tree + + + + + Sale line + ir.actions.act_window + pos.order.line + form + form,tree + + + + + Sale line + ir.actions.act_window + pos.order.line + form + tree + + [('create_date', '>=', time.strftime('%Y-%m-%d 00:00:00')),('create_date', '<=', time.strftime('%Y-%m-%d 23:59:59'))] + + + + POS Sales Lines + pos.order.line + + + + + + + + + + + + All sales lines + ir.actions.act_window + pos.order.line + form + + + + + + + pos.order.list.select + pos.order + + + + + + + + + + + + + + + + + + + + + + + Start Point of Sale + /pos/web/ + self + + + + diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml new file mode 100644 index 00000000000..94f55caeee7 --- /dev/null +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -0,0 +1,179 @@ + + + + + + + + pos.session.form.view + pos.session + +
+
+
+ +
+ + +
+

+ Session: + +

+ + + + + + + + + + + + + +
+ + + +
+
+

+ You can define another list of available currencies on the + Cash Registers tab of the + payment method. +

+
+
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + pos.session.tree.view + pos.session + + + + + + + + + + + + + + pos.session.search.view + pos.session + + + + + + + + + + + + + + + + + + + Sessions + ir.actions.act_window + pos.session + form + tree,form + + +

+ Click to start a new session. +

+ A session is a period of time, usually one day, during which + you sell through the point of sale. The user has to check the + currencies in your cash registers at the beginning and the end + of each session. +

+ Note that you may use the menu Your Session + to quickly open a new session. +

+
+
+ + + + + diff --git a/addons/point_of_sale/views/product_view.xml b/addons/point_of_sale/views/product_view.xml new file mode 100644 index 00000000000..34623e184fe --- /dev/null +++ b/addons/point_of_sale/views/product_view.xml @@ -0,0 +1,59 @@ + + + + + Products + ir.actions.act_window + product.template + form + tree,form,kanban + + + + + +

+ Click to add a new product. +

+ You must define a product for everything you sell through + the point of sale interface. +

+ Do not forget to set the price and the point of sale category + in which it should appear. If a product has no point of sale + category, you can not sell it through the point of sale + interface. +

+
+
+ + + + product.template.form.inherit + product.template + + + + + + + + + + + + + product.template.tree.inherit + product.template + + + + + + + + +
+
\ No newline at end of file diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py index 40d8ff18a55..f609a5ffee5 100644 --- a/addons/point_of_sale/wizard/pos_payment.py +++ b/addons/point_of_sale/wizard/pos_payment.py @@ -7,27 +7,10 @@ from openerp.osv import osv, fields from openerp.tools.translate import _ -class account_journal(osv.osv): - _inherit = 'account.journal' - - def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if not context: - context = {} - session_id = context.get('pos_session_id', False) or False - - if session_id: - session = self.pool.get('pos.session').browse(cr, uid, session_id, context=context) - - if session: - journal_ids = [journal.id for journal in session.config_id.journal_ids] - args += [('id', 'in', journal_ids)] - - return super(account_journal, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) - - class pos_make_payment(osv.osv_memory): _name = 'pos.make.payment' _description = 'Point of Sale Payment' + def check(self, cr, uid, ids, context=None): """Check the order: if the order is not paid: continue payment, From 8ab5b8664a2a1791106fea0923b6d179de9c04b5 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 1 Feb 2016 14:47:50 +0100 Subject: [PATCH 3/9] [IMP] point_of_sale: remove unused action Never called --- addons/point_of_sale/__openerp__.py | 1 - addons/point_of_sale/views/account_invoice_view.xml | 13 ------------- 2 files changed, 14 deletions(-) delete mode 100644 addons/point_of_sale/views/account_invoice_view.xml diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index f3c455b9023..d49ddac2cb9 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -49,7 +49,6 @@ Main Features 'views/account_journal_view.xml', 'views/pos_config_view.xml', 'views/pos_session_view.xml', - 'views/account_invoice_view.xml', 'views/point_of_sale_sequence.xml', 'data/point_of_sale_data.xml', 'data/point_of_sale_workflow.xml', diff --git a/addons/point_of_sale/views/account_invoice_view.xml b/addons/point_of_sale/views/account_invoice_view.xml deleted file mode 100644 index 58bfe004dc0..00000000000 --- a/addons/point_of_sale/views/account_invoice_view.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Invoices - ir.actions.act_window - account.invoice - form - tree,form - [('origin','like','POS')] - - - \ No newline at end of file From 7d93f9904e01bd8df187b41e3915e935db7d78ec Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 2 Feb 2016 12:26:52 +0100 Subject: [PATCH 4/9] [IMP] point_of_sale: remove complete_name and iface_fullscreen complete_name is actually the existing orm field display_name field iface_fullscreen is never used, cf TODO --- addons/point_of_sale/models/pos_category.py | 5 ----- addons/point_of_sale/models/pos_config.py | 3 +-- addons/point_of_sale/views/pos_category_view.xml | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/addons/point_of_sale/models/pos_category.py b/addons/point_of_sale/models/pos_category.py index 8c9d2ba5253..b9d8c1b02e4 100644 --- a/addons/point_of_sale/models/pos_category.py +++ b/addons/point_of_sale/models/pos_category.py @@ -25,13 +25,8 @@ class pos_category(osv.osv): res.append((cat.id, ' / '.join(reversed(names)))) return res - def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): - res = self.name_get(cr, uid, ids, context=context) - return dict(res) - _columns = { 'name': fields.char('Name', required=True, translate=True), - 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'), 'parent_id': fields.many2one('pos.category','Parent Category', select=True), 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index dfa363d1745..db57f6007de 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -85,8 +85,7 @@ class pos_config(osv.osv): 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), - # TODO master: remove the `iface_fullscreen` field. This is no longer used. - 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), + 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), 'iface_print_skip_screen': fields.boolean('Skip Receipt Screen', help='The receipt screen will be skipped if the receipt can be printed automatically.'), 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), diff --git a/addons/point_of_sale/views/pos_category_view.xml b/addons/point_of_sale/views/pos_category_view.xml index 9b9932df98a..3b5f1106ace 100644 --- a/addons/point_of_sale/views/pos_category_view.xml +++ b/addons/point_of_sale/views/pos_category_view.xml @@ -27,7 +27,7 @@ - + From 62406fa4bf9b3cd0edf4227c137ec6295b53669d Mon Sep 17 00:00:00 2001 From: Darshan Kalola Date: Thu, 31 Dec 2015 16:07:54 +0530 Subject: [PATCH 5/9] [REM] point of sale: removed useless wizard and code Never called or not used --- addons/point_of_sale/__openerp__.py | 1 - addons/point_of_sale/models/pos_order.py | 5 +-- .../test/01_order_to_payment.yml | 2 +- addons/point_of_sale/wizard/__init__.py | 1 - addons/point_of_sale/wizard/pos_confirm.py | 32 ------------------- addons/point_of_sale/wizard/pos_confirm.xml | 20 ------------ addons/point_of_sale/wizard/pos_payment.py | 10 +----- 7 files changed, 3 insertions(+), 68 deletions(-) delete mode 100644 addons/point_of_sale/wizard/pos_confirm.py delete mode 100644 addons/point_of_sale/wizard/pos_confirm.xml diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index d49ddac2cb9..13800334325 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -34,7 +34,6 @@ Main Features 'security/ir.model.access.csv', 'data/default_barcode_patterns.xml', 'wizard/pos_box.xml', - 'wizard/pos_confirm.xml', 'wizard/pos_details.xml', 'wizard/pos_discount.xml', 'wizard/pos_open_statement.xml', diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 65e4754908e..71fb19a47e0 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -566,9 +566,6 @@ class pos_order(osv.osv): 'res_id': inv_ids and inv_ids[0] or False, } - def create_account_move(self, cr, uid, ids, context=None): - return self._create_account_move_line(cr, uid, ids, None, None, context=context) - def _prepare_analytic_account(self, cr, uid, line, context=None): '''This method is designed to be inherited in a custom module''' return False @@ -756,7 +753,7 @@ class pos_order(osv.osv): return True def action_done(self, cr, uid, ids, context=None): - self.create_account_move(cr, uid, ids, context=context) + self._create_account_move_line(cr, uid, ids, context=context) return True class pos_order_line(osv.osv): diff --git a/addons/point_of_sale/test/01_order_to_payment.yml b/addons/point_of_sale/test/01_order_to_payment.yml index 46b7b2b1444..a5f41fec71b 100644 --- a/addons/point_of_sale/test/01_order_to_payment.yml +++ b/addons/point_of_sale/test/01_order_to_payment.yml @@ -124,7 +124,7 @@ I generate the journal entries - !python {model: pos.order}: | - self.create_account_move(cr, uid, [ref('pos_order_pos0')], {}) + self._create_account_move_lines(cr, uid, [ref('pos_order_pos0')], {}) - I test that the generated journal entry is attached to the PoS order - diff --git a/addons/point_of_sale/wizard/__init__.py b/addons/point_of_sale/wizard/__init__.py index 2cd077955ca..442fc2a4133 100644 --- a/addons/point_of_sale/wizard/__init__.py +++ b/addons/point_of_sale/wizard/__init__.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import pos_box -import pos_confirm import pos_details import pos_discount import pos_open_statement diff --git a/addons/point_of_sale/wizard/pos_confirm.py b/addons/point_of_sale/wizard/pos_confirm.py deleted file mode 100644 index 78604811dad..00000000000 --- a/addons/point_of_sale/wizard/pos_confirm.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import osv - - -class pos_confirm(osv.osv_memory): - _name = 'pos.confirm' - _description = 'Post POS Journal Entries' - - def action_confirm(self, cr, uid, ids, context=None): - order_obj = self.pool.get('pos.order') - ids = order_obj.search(cr, uid, [('state','=','paid')], context=context) - for order in order_obj.browse(cr, uid, ids, context=context): - todo = True - for line in order.statement_ids: - if line.statement_id.state != 'confirm': - todo = False - break - if todo: - order.signal_workflow('done') - - # Check if there is orders to reconcile their invoices - ids = order_obj.search(cr, uid, [('state','=','invoiced'),('invoice_id.state','=','open')], context=context) - for order in order_obj.browse(cr, uid, ids, context=context): - invoice = order.invoice_id - data_lines = [x.id for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id] - for st in order.statement_ids: - for move in st.move_ids: - data_lines += [x.id for x in move.line_id if x.account_id.id == invoice.account_id.id] - self.pool.get('account.move.line').reconcile(cr, uid, data_lines, context=context) - return {} diff --git a/addons/point_of_sale/wizard/pos_confirm.xml b/addons/point_of_sale/wizard/pos_confirm.xml deleted file mode 100644 index d0a1653a348..00000000000 --- a/addons/point_of_sale/wizard/pos_confirm.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Post Sale Journal Entries - pos.confirm - -
- -
-
-
-
diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py index f609a5ffee5..ed53de42cdd 100644 --- a/addons/point_of_sale/wizard/pos_payment.py +++ b/addons/point_of_sale/wizard/pos_payment.py @@ -48,14 +48,6 @@ class pos_make_payment(osv.osv_memory): 'context': context, } - def print_report(self, cr, uid, ids, context=None): - active_id = context.get('active_id', []) - datas = {'ids' : [active_id]} - return { - 'type': 'ir.actions.report.xml', - 'report_name': 'pos.receipt', - 'datas': datas, - } def _default_journal(self, cr, uid, context=None): if not context: @@ -89,4 +81,4 @@ class pos_make_payment(osv.osv_memory): 'journal_id' : _default_journal, 'payment_date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), 'amount': _default_amount, - } + } \ No newline at end of file From e741de8b0c152279eb2d20faa77d06df8e50b91a Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 17 Feb 2016 13:27:20 +0100 Subject: [PATCH 6/9] [IMP] point_of_sale: remove some reports - report.transaction.pos - report.sales.by.user.pos - report.sales.by.user.pos.month These reports where not called anywhere and have been replaced by pivot views Remove order reports that can be easily replaced by use of the pivot view of report.pos.order Add a session_id field and a filter on report.pos.order to make it equivalent. The invoice report is only useful when called from the pos UI so keeping the report but not added in the print menu --- addons/point_of_sale/__openerp__.py | 6 - addons/point_of_sale/report/__init__.py | 8 +- addons/point_of_sale/report/pos_details.py | 181 ------------ addons/point_of_sale/report/pos_lines.py | 43 --- .../point_of_sale/report/pos_order_report.py | 16 +- .../report/pos_payment_report.py | 52 ---- addons/point_of_sale/report/pos_report.py | 121 -------- .../security/ir.model.access.csv | 3 - .../test/point_of_sale_report.yml | 43 --- .../views/point_of_sale_report.xml | 266 +----------------- .../views/pos_order_report_view.xml | 7 + addons/point_of_sale/views/pos_order_view.xml | 3 - .../views/report_detailsofsales.xml | 140 --------- addons/point_of_sale/views/report_payment.xml | 91 ------ .../point_of_sale/views/report_saleslines.xml | 98 ------- .../views/report_sessionsummary.xml | 124 -------- addons/point_of_sale/wizard/__init__.py | 1 - addons/point_of_sale/wizard/pos_details.py | 39 --- addons/point_of_sale/wizard/pos_details.xml | 36 --- 19 files changed, 28 insertions(+), 1250 deletions(-) delete mode 100644 addons/point_of_sale/report/pos_details.py delete mode 100644 addons/point_of_sale/report/pos_lines.py delete mode 100644 addons/point_of_sale/report/pos_payment_report.py delete mode 100644 addons/point_of_sale/report/pos_report.py delete mode 100644 addons/point_of_sale/test/point_of_sale_report.yml delete mode 100644 addons/point_of_sale/views/report_detailsofsales.xml delete mode 100644 addons/point_of_sale/views/report_payment.xml delete mode 100644 addons/point_of_sale/views/report_saleslines.xml delete mode 100644 addons/point_of_sale/views/report_sessionsummary.xml delete mode 100644 addons/point_of_sale/wizard/pos_details.py delete mode 100644 addons/point_of_sale/wizard/pos_details.xml diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 13800334325..9a84760bb68 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -34,7 +34,6 @@ Main Features 'security/ir.model.access.csv', 'data/default_barcode_patterns.xml', 'wizard/pos_box.xml', - 'wizard/pos_details.xml', 'wizard/pos_discount.xml', 'wizard/pos_open_statement.xml', 'wizard/pos_payment.xml', @@ -59,10 +58,6 @@ Main Features 'views/res_config_view.xml', 'views/report_statement.xml', 'views/report_usersproduct.xml', - 'views/report_saleslines.xml', - 'views/report_detailsofsales.xml', - 'views/report_payment.xml', - 'views/report_sessionsummary.xml', 'views/report_userlabel.xml', 'views/point_of_sale.xml', 'views/point_of_sale_dashboard.xml', @@ -76,7 +71,6 @@ Main Features 'test/00_register_open.yml', 'test/01_order_to_payment.yml', 'test/02_order_to_invoice.yml', - 'test/point_of_sale_report.yml', 'test/account_statement_reports.yml', ], 'installable': True, diff --git a/addons/point_of_sale/report/__init__.py b/addons/point_of_sale/report/__init__.py index 093cf63f789..3de0210b9f0 100644 --- a/addons/point_of_sale/report/__init__.py +++ b/addons/point_of_sale/report/__init__.py @@ -1,12 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import pos_users_product import account_statement -import pos_receipt import pos_invoice -import pos_lines -import pos_details -import pos_payment_report -import pos_report import pos_order_report +import pos_receipt +import pos_users_product diff --git a/addons/point_of_sale/report/pos_details.py b/addons/point_of_sale/report/pos_details.py deleted file mode 100644 index 4625ddbd766..00000000000 --- a/addons/point_of_sale/report/pos_details.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import time -from openerp.osv import osv -from openerp.report import report_sxw - - -class pos_details(report_sxw.rml_parse): - - def _get_invoice(self, inv_id): - res={} - if inv_id: - self.cr.execute("select number from account_invoice as ac where id = %s", (inv_id,)) - res = self.cr.fetchone() - return res[0] or 'Draft' - else: - return '' - - def _get_all_users(self): - user_obj = self.pool.get('res.users') - return user_obj.search(self.cr, self.uid, []) - - def _pos_sales_details(self, form): - pos_obj = self.pool.get('pos.order') - user_obj = self.pool.get('res.users') - data = [] - result = {} - user_ids = form['user_ids'] or self._get_all_users() - company_id = user_obj.browse(self.cr, self.uid, self.uid).company_id.id - pos_ids = pos_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('user_id','in',user_ids),('state','in',['done','paid','invoiced']),('company_id','=',company_id)]) - for pos in pos_obj.browse(self.cr, self.uid, pos_ids, context=self.localcontext): - for pol in pos.lines: - result = { - 'code': pol.product_id.default_code, - 'name': pol.product_id.name, - 'invoice_id': pos.invoice_id.id, - 'price_unit': pol.price_unit, - 'qty': pol.qty, - 'discount': pol.discount, - 'total': (pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0)), - 'date_order': pos.date_order, - 'pos_name': pos.name, - 'uom': pol.product_id.uom_id.name - } - data.append(result) - self.total += result['total'] - self.qty += result['qty'] - self.discount += result['discount'] - if data: - return data - else: - return {} - - def _get_qty_total_2(self): - return self.qty - - def _get_sales_total_2(self): - return self.total - - def _get_sum_invoice_2(self, form): - pos_obj = self.pool.get('pos.order') - user_obj = self.pool.get('res.users') - user_ids = form['user_ids'] or self._get_all_users() - company_id = user_obj.browse(self.cr, self.uid, self.uid).company_id.id - pos_ids = pos_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('user_id','in',user_ids),('company_id','=',company_id),('invoice_id','<>',False)]) - for pos in pos_obj.browse(self.cr, self.uid, pos_ids): - for pol in pos.lines: - self.total_invoiced += (pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0)) - return self.total_invoiced or False - - def _paid_total_2(self): - return self.total or 0.0 - - def _get_sum_dis_2(self): - return self.discount or 0.0 - - def _get_sum_discount(self, form): - #code for the sum of discount value - pos_obj = self.pool.get('pos.order') - user_obj = self.pool.get('res.users') - user_ids = form['user_ids'] or self._get_all_users() - company_id = user_obj.browse(self.cr, self.uid, self.uid).company_id.id - pos_ids = pos_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('user_id','in',user_ids),('company_id','=',company_id)]) - for pos in pos_obj.browse(self.cr, self.uid, pos_ids): - for pol in pos.lines: - self.total_discount += ((pol.price_unit * pol.qty) * (pol.discount / 100)) - return self.total_discount or False - - def _get_payments(self, form): - statement_line_obj = self.pool.get("account.bank.statement.line") - pos_order_obj = self.pool.get("pos.order") - user_ids = form['user_ids'] or self._get_all_users() - company_id = self.pool['res.users'].browse(self.cr, self.uid, self.uid).company_id.id - pos_ids = pos_order_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('state','in',['paid','invoiced','done']),('user_id','in',user_ids), ('company_id', '=', company_id)]) - data={} - if pos_ids: - st_line_ids = statement_line_obj.search(self.cr, self.uid, [('pos_statement_id', 'in', pos_ids)]) - if st_line_ids: - st_id = statement_line_obj.browse(self.cr, self.uid, st_line_ids) - a_l=[] - for r in st_id: - a_l.append(r['id']) - self.cr.execute("select aj.name,sum(amount) from account_bank_statement_line as absl,account_bank_statement as abs,account_journal as aj " \ - "where absl.statement_id = abs.id and abs.journal_id = aj.id and absl.id IN %s " \ - "group by aj.name ",(tuple(a_l),)) - - data = self.cr.dictfetchall() - return data - else: - return {} - - def _total_of_the_day(self, objects): - return self.total or 0.00 - - def _sum_invoice(self, objects): - return reduce(lambda acc, obj: - acc + obj.invoice_id.amount_total, - [o for o in objects if o.invoice_id and o.invoice_id.number], - 0.0) - - def _ellipsis(self, orig_str, maxlen=100, ellipsis='...'): - maxlen = maxlen - len(ellipsis) - if maxlen <= 0: - maxlen = 1 - new_str = orig_str[:maxlen] - return new_str - - def _strip_name(self, name, maxlen=50): - return self._ellipsis(name, maxlen, ' ...') - - def _get_tax_amount(self, form): - taxes = {} - account_tax_obj = self.pool.get('account.tax') - user_ids = form['user_ids'] or self._get_all_users() - pos_order_obj = self.pool.get('pos.order') - company_id = self.pool['res.users'].browse(self.cr, self.uid, self.uid).company_id.id - pos_ids = pos_order_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('state','in',['paid','invoiced','done']),('user_id','in',user_ids), ('company_id', '=', company_id)]) - for order in pos_order_obj.browse(self.cr, self.uid, pos_ids): - currency = order.session_id.currency_id - for line in order.lines: - if line.product_id.taxes_id: - line_taxes = line.product_id.taxes_id.compute_all(line.price_unit * (1-(line.discount or 0.0)/100.0), currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) - for tax in line_taxes['taxes']: - taxes.setdefault(tax['id'], {'name': tax['name'], 'amount':0.0}) - taxes[tax['id']]['amount'] += tax['amount'] - return taxes.values() - - def _get_user_names(self, user_ids): - user_obj = self.pool.get('res.users') - return ', '.join(map(lambda x: x.name, user_obj.browse(self.cr, self.uid, user_ids))) - - def __init__(self, cr, uid, name, context): - super(pos_details, self).__init__(cr, uid, name, context=context) - self.total = 0.0 - self.qty = 0.0 - self.total_invoiced = 0.0 - self.discount = 0.0 - self.total_discount = 0.0 - self.localcontext.update({ - 'time': time, - 'strip_name': self._strip_name, - 'getpayments': self._get_payments, - 'getsumdisc': self._get_sum_discount, - 'gettotaloftheday': self._total_of_the_day, - 'gettaxamount': self._get_tax_amount, - 'pos_sales_details':self._pos_sales_details, - 'getqtytotal2': self._get_qty_total_2, - 'getsalestotal2': self._get_sales_total_2, - 'getsuminvoice2':self._get_sum_invoice_2, - 'getpaidtotal2': self._paid_total_2, - 'getinvoice':self._get_invoice, - 'get_user_names': self._get_user_names, - }) - - -class report_pos_details(osv.AbstractModel): - _name = 'report.point_of_sale.report_detailsofsales' - _inherit = 'report.abstract_report' - _template = 'point_of_sale.report_detailsofsales' - _wrapped_report_class = pos_details diff --git a/addons/point_of_sale/report/pos_lines.py b/addons/point_of_sale/report/pos_lines.py deleted file mode 100644 index 6f74cf40ab6..00000000000 --- a/addons/point_of_sale/report/pos_lines.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import time -from openerp.osv import osv -from openerp.report import report_sxw - - -class pos_lines(report_sxw.rml_parse): - - def __init__(self, cr, uid, name, context): - super(pos_lines, self).__init__(cr, uid, name, context=context) - self.total = 0.0 - self.localcontext.update({ - 'time': time, - 'total_quantity': self.__total_quantity__, - 'taxes':self.__taxes__, - - }) - - def __total_quantity__(self, obj): - tot = 0 - for line in obj.lines: - tot += line.qty - self.total = tot - return self.total - - def __taxes__(self, obj): - self.cr.execute ( " Select acct.name from pos_order as po " \ - " LEFT JOIN pos_order_line as pol ON po.id = pol.order_id " \ - " LEFT JOIN product_product as pp ON pol.product_id = pp.id" - " LEFT JOIN product_taxes_rel as ptr ON pp.product_tmpl_id = ptr.prod_id " \ - " LEFT JOIN account_tax as acct ON acct.id = ptr.tax_id " \ - " WHERE pol.id = %s", (obj.id,)) - res=self.cr.fetchone()[0] - return res - - -class report_pos_lines(osv.AbstractModel): - _name = 'report.point_of_sale.report_saleslines' - _inherit = 'report.abstract_report' - _template = 'point_of_sale.report_saleslines' - _wrapped_report_class = pos_lines diff --git a/addons/point_of_sale/report/pos_order_report.py b/addons/point_of_sale/report/pos_order_report.py index 7adc4f8528b..7d528f8649d 100644 --- a/addons/point_of_sale/report/pos_order_report.py +++ b/addons/point_of_sale/report/pos_order_report.py @@ -33,6 +33,7 @@ class pos_order_report(osv.osv): 'pos_categ_id': fields.many2one('pos.category','Public Category', readonly=True), 'stock_location_id': fields.many2one('stock.location', 'Warehouse', readonly=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True), + 'session_id;: fields.many2one('pos.session', string='Session', readonly=True), } _order = 'date desc' @@ -63,6 +64,7 @@ class pos_order_report(osv.osv): pt.pos_categ_id, pc.stock_location_id, s.pricelist_id, + s.session_id, s.invoice_id IS NOT NULL AS invoiced from pos_order_line as l left join pos_order s on (s.id=l.order_id) @@ -73,6 +75,14 @@ class pos_order_report(osv.osv): left join pos_config pc on (ps.config_id=pc.id) group by s.date_order, s.partner_id,s.state, pt.categ_id, - s.user_id,s.location_id,s.company_id,s.sale_journal,s.pricelist_id,s.invoice_id,l.product_id,s.create_date,pt.categ_id,pt.pos_categ_id,p.product_tmpl_id,ps.config_id,pc.stock_location_id - having - sum(l.qty * u.factor) != 0)""") + s.user_id, s.location_id, s.company_id, s.sale_journal, + s.pricelist_id, s.invoice_id, s.create_date, s.session_id, + l.product_id, + pt.categ_id, pt.pos_categ_id, + p.product_tmpl_id, + ps.config_id, + pc.stock_location_id + HAVING + SUM(l.qty * u.factor) != 0 + ) + """) diff --git a/addons/point_of_sale/report/pos_payment_report.py b/addons/point_of_sale/report/pos_payment_report.py deleted file mode 100644 index e34e50e69bd..00000000000 --- a/addons/point_of_sale/report/pos_payment_report.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import time -from openerp.osv import osv -from openerp.report import report_sxw - - -class pos_payment_report(report_sxw.rml_parse): - - def __init__(self, cr, uid, name, context): - super(pos_payment_report, self).__init__(cr, uid, name, context=context) - self.total = 0.0 - self.localcontext.update({ - 'time': time, - 'pos_payment': self._pos_payment, - 'pos_payment_total':self._pos_payment_total, - }) - - def _pos_payment(self, obj): - self.total = 0 - data={} - sql = """ select id from pos_order where id = %d"""%(obj.id) - self.cr.execute(sql) - if self.cr.fetchone(): - self.cr.execute ("select pt.name,pp.default_code as code,pol.qty,pu.name as uom,pol.discount,pol.price_unit, " \ - "(pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0)) as total " \ - "from pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt, product_uom as pu " \ - "where pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id and pu.id=pt.uom_id " \ - "and po.state IN ('paid','invoiced') and to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::date = current_date and po.id=%d"%(obj.id)) - data=self.cr.dictfetchall() - else: - self.cr.execute ("select pt.name,pp.default_code as code,pol.qty,pu.name as uom,pol.discount,pol.price_unit, " \ - "(pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0)) as total " \ - "from pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt, product_uom as pu " \ - "where pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id and pu.id=pt.uom_id " \ - "and po.state IN ('paid','invoiced') and to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::date = current_date") - data=self.cr.dictfetchall() - - for d in data: - self.total += d['price_unit'] * d['qty'] - return data - - def _pos_payment_total(self, o): - return self.total - - -class report_pos_payment(osv.AbstractModel): - _name = 'report.point_of_sale.report_payment' - _inherit = 'report.abstract_report' - _template = 'point_of_sale.report_payment' - _wrapped_report_class = pos_payment_report diff --git a/addons/point_of_sale/report/pos_report.py b/addons/point_of_sale/report/pos_report.py deleted file mode 100644 index 826e6134429..00000000000 --- a/addons/point_of_sale/report/pos_report.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp import tools - -class report_transaction_pos(osv.osv): - _name = "report.transaction.pos" - _description = "transaction for the pos" - _auto = False - _columns = { - 'date_create': fields.char('Date', size=16, readonly=True), - 'journal_id': fields.many2one('account.journal', 'Sales Journal', readonly=True), - 'jl_id': fields.many2one('account.journal', 'Cash Journals', readonly=True), - 'user_id': fields.many2one('res.users', 'User', readonly=True), - 'no_trans': fields.float('Number of Transaction', readonly=True), - 'amount': fields.float('Amount', readonly=True), - 'invoice_id': fields.float('Nbr Invoice', readonly=True), - 'invoice_am': fields.float('Invoice Amount', readonly=True), - 'product_nb': fields.float('Product Nb.', readonly=True), - 'disc': fields.float('Disc.', readonly=True), - } - - def init(self, cr): - tools.drop_view_if_exists(cr, 'report_transaction_pos') - cr.execute(""" - create or replace view report_transaction_pos as ( - select - min(absl.id) as id, - count(absl.id) as no_trans, - sum(absl.amount) as amount, - sum((100.0-line.discount) * line.price_unit * line.qty / 100.0) as disc, - to_char(date_trunc('day',absl.create_date),'YYYY-MM-DD')::text as date_create, - po.user_id as user_id, - po.sale_journal as journal_id, - abs.journal_id as jl_id, - count(po.invoice_id) as invoice_id, - count(p.id) as product_nb - from - account_bank_statement_line as absl, - account_bank_statement as abs, - product_product as p, - pos_order_line as line, - pos_order as po - where - absl.pos_statement_id = po.id and - line.order_id=po.id and - line.product_id=p.id and - absl.statement_id=abs.id - - group by - po.user_id,po.sale_journal, abs.journal_id, - to_char(date_trunc('day',absl.create_date),'YYYY-MM-DD')::text - ) - """) - #to_char(date_trunc('day',absl.create_date),'YYYY-MM-DD') - #to_char(date_trunc('day',absl.create_date),'YYYY-MM-DD')::text as date_create, - -class report_sales_by_user_pos(osv.osv): - _name = "report.sales.by.user.pos" - _description = "Sales by user" - _auto = False - _columns = { - 'date_order': fields.date('Order Date',required=True, select=True), - 'amount': fields.float('Total', readonly=True, select=True), - 'qty': fields.float('Quantity', readonly=True, select=True), - 'user_id': fields.many2one('res.users', 'User', readonly=True, select=True), - } - - def init(self, cr): - tools.drop_view_if_exists(cr, 'report_sales_by_user_pos') - cr.execute(""" - create or replace view report_sales_by_user_pos as ( - select - min(po.id) as id, - to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::text as date_order, - po.user_id as user_id, - sum(pol.qty)as qty, - sum((pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0))) as amount - from - pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt - where - pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id - group by - to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::text, - po.user_id - - ) - """) - -class report_sales_by_user_pos_month(osv.osv): - _name = "report.sales.by.user.pos.month" - _description = "Sales by user monthly" - _auto = False - _columns = { - 'date_order': fields.date('Order Date',required=True, select=True), - 'amount': fields.float('Total', readonly=True, select=True), - 'qty': fields.float('Quantity', readonly=True, select=True), - 'user_id': fields.many2one('res.users', 'User', readonly=True, select=True), - } - - def init(self, cr): - tools.drop_view_if_exists(cr, 'report_sales_by_user_pos_month') - cr.execute(""" - create or replace view report_sales_by_user_pos_month as ( - select - min(po.id) as id, - to_char(date_trunc('month',po.date_order),'YYYY-MM-DD')::text as date_order, - po.user_id as user_id, - sum(pol.qty)as qty, - sum((pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0))) as amount - from - pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt - where - pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id - group by - to_char(date_trunc('month',po.date_order),'YYYY-MM-DD')::text, - po.user_id - - ) - """) diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index 2e167277dbc..17b4f584f45 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -1,13 +1,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_pos_order,pos.order,model_pos_order,group_pos_user,1,1,1,1 access_pos_order_line,pos.order.line,model_pos_order_line,group_pos_user,1,1,1,1 -access_report_transaction_pos,report.transaction.pos,model_report_transaction_pos,group_pos_manager,1,1,1,1 access_stock_picking_pos_user,stock.picking pos_user,stock.model_stock_picking,group_pos_user,1,1,1,1 access_stock_warehouse_pos_user,stock.warehouse pos_user,stock.model_stock_warehouse,group_pos_user,1,0,0,0 access_pos_order_stock_worker,pos.order stock_worker,model_pos_order,stock.group_stock_user,1,0,0,0 access_stock_move_pos_user,stock.move pos_user,stock.model_stock_move,group_pos_user,1,1,1,1 -access_report_sales_by_user_pos,report.sales.by.user.pos,model_report_sales_by_user_pos,group_pos_user,1,0,0,0 -access_report_sales_by_user_pos_month,report.sales.by.user.pos.month,model_report_sales_by_user_pos_month,group_pos_user,1,0,0,0 access_report_pos_order,report.pos.order,model_report_pos_order,group_pos_user,1,1,1,1 access_account_journal_pos_user,account.journal pos_user,account.model_account_journal,group_pos_user,1,0,0,0 access_account_payment_method_pos_user,account.payment.method pos_user,account.model_account_payment_method,group_pos_manager,1,0,0,0 diff --git a/addons/point_of_sale/test/point_of_sale_report.yml b/addons/point_of_sale/test/point_of_sale_report.yml deleted file mode 100644 index c039eab4c3d..00000000000 --- a/addons/point_of_sale/test/point_of_sale_report.yml +++ /dev/null @@ -1,43 +0,0 @@ -- - In order to test the PDF reports defined on a Point Of Sale, we will print a POS Invoice Report -- - !python {model: pos.order}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('pos_order_pos1')], 'point_of_sale.report_invoice', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-invoice_report.'+format), 'wb+').write(data) - -- - In order to test the PDF reports defined on a Point Of Sale, we will print a POS Lines Report -- - !python {model: pos.order}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('pos_order_pos1')], 'point_of_sale.report_saleslines', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-lines_report.'+format), 'wb+').write(data) - -- - Print the POS Details Report through the wizard -- - !python {model: ir.ui.menu}: | - import time - ctx={} - ctx.update({'model': 'ir.ui.menu','active_ids': []}) - data_dict = {'date_start': time.strftime('%Y-%m-%d'), 'date_end': time.strftime('%Y-%m-%d'), 'user_ids': [(6,0,[ref('base.user_root')])]} - from openerp.tools import test_reports - test_reports.try_report_action(cr, uid, 'action_report_pos_details',wiz_data=data_dict, context=ctx, our_module='point_of_sale') - -- - In order to test the PDF reports defined on a Point of Sale, we will print a POS Payment Report -- - !python {model: pos.order}: | - import os, time - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('pos_order_pos0')], 'point_of_sale.report_payment', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-payment_report.'+format), 'wb+').write(data) diff --git a/addons/point_of_sale/views/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml index 9eee8f2c065..7d68e896f41 100644 --- a/addons/point_of_sale/views/point_of_sale_report.xml +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -10,269 +10,15 @@ file="point_of_sale.report_userlabel" /> - - - - - - - - - - - - - - report.trans.pos.user.form - report.transaction.pos - -
- - - - - - - - - -
-
+ + + Invoice + pos.order + qweb-pdf + point_of_sale.report_invoice - - Sales by user - report.transaction.pos - - - - - - - - - - - - - - - - - report.transaction.pos.calendar - report.transaction.pos - - - - - - - - - - - report.transaction.pos.graph - report.transaction.pos - - - - - - - - - - - - Sales by day - report.transaction.pos - form - tree,calendar,form,graph - [('date_create','=',time.strftime('%Y-%m-%d'))] - - - - - Sales by month - report.transaction.pos - form - tree,calendar,form,graph - [('date_create','like',time.strftime('%Y-%m'))] - - - - - Sales by user - report.transaction.pos - form - tree,calendar,form,graph - - - - - - report.sales.by.user.pos.form - report.sales.by.user.pos - -
- - - - - - -
-
-
- - - - report.sales.by.user.pos.tree - report.sales.by.user.pos - - - - - - - - - - - - report.sales.by.user.pos.calendar - report.sales.by.user.pos - - - - - - - - - - - report.sales.by.user.pos.graph - report.sales.by.user.pos - - - - - - - - - - - Sales by User - report.sales.by.user.pos - form - tree,calendar,form,graph - [('date_order','=',time.strftime('%Y-%m-%d'))] - - - - - - - report.sales.by.user.pos.month.form - report.sales.by.user.pos.month - -
- - - - - - -
-
-
- - - - report.sales.by.user.pos.month.tree - report.sales.by.user.pos.month - - - - - - - - - - - - report.sales.by.user.pos.month.calendar - report.sales.by.user.pos.month - - - - - - - - - - - report.sales.by.user.pos.month.graph - report.sales.by.user.pos.month - - - - - - - - - - - - Sales by User Monthly - report.sales.by.user.pos.month - form - tree,calendar,form,graph - [('date_order','like',time.strftime('%Y-%m'))] - diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml index 56eb552b557..7351a30926b 100644 --- a/addons/point_of_sale/views/pos_order_report_view.xml +++ b/addons/point_of_sale/views/pos_order_report_view.xml @@ -53,6 +53,13 @@
+ + Per session + report.pos.order + + {'group_by': ['date', 'session_id'], 'measures': ['price_total']} + + Orders Analysis report.pos.order diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml index b3fe63c1185..79c31b00706 100644 --- a/addons/point_of_sale/views/pos_order_view.xml +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -242,9 +242,6 @@ - - pos.order.list.select pos.order diff --git a/addons/point_of_sale/views/report_detailsofsales.xml b/addons/point_of_sale/views/report_detailsofsales.xml deleted file mode 100644 index 648d40969f9..00000000000 --- a/addons/point_of_sale/views/report_detailsofsales.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - diff --git a/addons/point_of_sale/views/report_payment.xml b/addons/point_of_sale/views/report_payment.xml deleted file mode 100644 index 939391ca532..00000000000 --- a/addons/point_of_sale/views/report_payment.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - diff --git a/addons/point_of_sale/views/report_saleslines.xml b/addons/point_of_sale/views/report_saleslines.xml deleted file mode 100644 index 3a1dc1fc1c1..00000000000 --- a/addons/point_of_sale/views/report_saleslines.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - diff --git a/addons/point_of_sale/views/report_sessionsummary.xml b/addons/point_of_sale/views/report_sessionsummary.xml deleted file mode 100644 index 43c5f02b6bd..00000000000 --- a/addons/point_of_sale/views/report_sessionsummary.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - diff --git a/addons/point_of_sale/wizard/__init__.py b/addons/point_of_sale/wizard/__init__.py index 442fc2a4133..c708aa57677 100644 --- a/addons/point_of_sale/wizard/__init__.py +++ b/addons/point_of_sale/wizard/__init__.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import pos_box -import pos_details import pos_discount import pos_open_statement import pos_payment diff --git a/addons/point_of_sale/wizard/pos_details.py b/addons/point_of_sale/wizard/pos_details.py deleted file mode 100644 index afca493acca..00000000000 --- a/addons/point_of_sale/wizard/pos_details.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import time -from openerp.osv import osv, fields - - -class pos_details(osv.osv_memory): - _name = 'pos.details' - _description = 'Sales Details' - - _columns = { - 'date_start': fields.date('Date Start', required=True), - 'date_end': fields.date('Date End', required=True), - 'user_ids': fields.many2many('res.users', 'pos_details_report_user_rel', 'user_id', 'wizard_id', 'Salespeople'), - } - _defaults = { - 'date_start': fields.date.context_today, - 'date_end': fields.date.context_today, - } - - def print_report(self, cr, uid, ids, context=None): - """ - To get the date and print the report - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param context: A standard dictionary - @return : retrun report - """ - if context is None: - context = {} - datas = {'ids': context.get('active_ids', [])} - res = self.read(cr, uid, ids, ['date_start', 'date_end', 'user_ids'], context=context) - res = res and res[0] or {} - datas['form'] = res - if res.get('id',False): - datas['ids']=[res['id']] - return self.pool['report'].get_action(cr, uid, [], 'point_of_sale.report_detailsofsales', data=datas, context=context) diff --git a/addons/point_of_sale/wizard/pos_details.xml b/addons/point_of_sale/wizard/pos_details.xml deleted file mode 100644 index 87950677e5e..00000000000 --- a/addons/point_of_sale/wizard/pos_details.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - POS Details - pos.details - -
- - - - - -
-
- -
-
- - - - Sale Details - ir.actions.act_window - pos.details - form - form - new - - - -
-
From 32b5515a8ff6047db489e569935b4416f946bc26 Mon Sep 17 00:00:00 2001 From: Darshan Kalola Date: Tue, 29 Sep 2015 12:22:21 +0530 Subject: [PATCH 7/9] [REF] point_of_sale: migrate to new api --- addons/point_of_sale/__openerp__.py | 1 - addons/point_of_sale/controllers/main.py | 17 +- .../data/default_barcode_patterns.xml | 6 +- .../point_of_sale/data/point_of_sale_data.xml | 6 +- .../point_of_sale/data/point_of_sale_demo.xml | 10 +- .../data/point_of_sale_workflow.xml | 15 +- .../models/account_bank_statement.py | 20 +- .../point_of_sale/models/account_journal.py | 33 +- addons/point_of_sale/models/barcode_rule.py | 9 +- addons/point_of_sale/models/pos_category.py | 63 +- addons/point_of_sale/models/pos_config.py | 455 +++---- addons/point_of_sale/models/pos_order.py | 1201 +++++++---------- addons/point_of_sale/models/pos_session.py | 403 +++--- addons/point_of_sale/models/product.py | 35 +- addons/point_of_sale/models/res_config.py | 52 +- addons/point_of_sale/models/res_partner.py | 33 +- addons/point_of_sale/models/res_users.py | 24 +- addons/point_of_sale/report/pos_invoice.py | 35 +- .../point_of_sale/report/pos_order_report.py | 115 +- .../security/point_of_sale_security.xml | 7 +- .../views/account_journal_view.xml | 43 +- .../views/account_statement_report.xml | 8 +- .../views/account_statement_view.xml | 11 +- addons/point_of_sale/views/point_of_sale.xml | 6 +- .../views/point_of_sale_dashboard.xml | 4 +- .../views/point_of_sale_report.xml | 39 +- .../views/point_of_sale_sequence.xml | 14 +- .../views/point_of_sale_view.xml | 29 +- .../point_of_sale/views/pos_category_view.xml | 105 +- .../point_of_sale/views/pos_config_view.xml | 258 ++-- .../views/pos_order_report_view.xml | 8 +- addons/point_of_sale/views/pos_order_view.xml | 495 ++++--- .../point_of_sale/views/pos_session_view.xml | 340 ++--- addons/point_of_sale/views/pos_templates.xml | 9 +- addons/point_of_sale/views/product_view.xml | 95 +- .../point_of_sale/views/report_userlabel.xml | 6 +- .../point_of_sale/views/res_config_view.xml | 8 +- .../point_of_sale/views/res_partner_view.xml | 8 +- addons/point_of_sale/views/res_users_view.xml | 6 +- addons/point_of_sale/wizard/pos_box.py | 66 +- addons/point_of_sale/wizard/pos_box.xml | 14 +- addons/point_of_sale/wizard/pos_discount.py | 36 +- addons/point_of_sale/wizard/pos_discount.xml | 11 +- .../wizard/pos_open_statement.py | 60 +- .../wizard/pos_open_statement.xml | 18 +- addons/point_of_sale/wizard/pos_payment.py | 89 +- addons/point_of_sale/wizard/pos_payment.xml | 10 +- 47 files changed, 1908 insertions(+), 2428 deletions(-) diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 9a84760bb68..072db0b5c27 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -77,5 +77,4 @@ Main Features 'application': True, 'qweb': ['static/src/xml/pos.xml'], 'website': 'https://www.odoo.com/page/point-of-sale', - 'auto_install': False, } diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index b562b4001f8..cd4d91b56c9 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -2,8 +2,8 @@ import logging import werkzeug.utils -from openerp import http -from openerp.http import request +from odoo import http +from odoo.http import request _logger = logging.getLogger(__name__) @@ -11,15 +11,10 @@ _logger = logging.getLogger(__name__) class PosController(http.Controller): @http.route('/pos/web', type='http', auth='user') - def a(self, debug=False, **k): - cr, uid, context, session = request.cr, request.uid, request.context, request.session - + def pos_web(self, debug=False, **k): # if user not logged in, log him in - PosSession = request.registry['pos.session'] - pos_session_ids = PosSession.search(cr, uid, [('state','=','opened'),('user_id','=',session.uid)], context=context) - if not pos_session_ids: + pos_sessions = request.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', request.session.uid)]) + if not pos_sessions: return werkzeug.utils.redirect('/web#action=point_of_sale.action_client_pos_menu') - PosSession.login(cr, uid, pos_session_ids, context=context) - + pos_sessions.login() return request.render('point_of_sale.index') - diff --git a/addons/point_of_sale/data/default_barcode_patterns.xml b/addons/point_of_sale/data/default_barcode_patterns.xml index 1efcf941597..75bb18c2876 100644 --- a/addons/point_of_sale/data/default_barcode_patterns.xml +++ b/addons/point_of_sale/data/default_barcode_patterns.xml @@ -1,6 +1,5 @@ - - + Cashier Barcodes @@ -36,5 +35,4 @@ ean13 23.....{NNNDD} - - + diff --git a/addons/point_of_sale/data/point_of_sale_data.xml b/addons/point_of_sale/data/point_of_sale_data.xml index 820b599b537..940b0fdd373 100644 --- a/addons/point_of_sale/data/point_of_sale_data.xml +++ b/addons/point_of_sale/data/point_of_sale_data.xml @@ -1,5 +1,5 @@ - - + + Picking POS @@ -52,5 +52,5 @@ - + diff --git a/addons/point_of_sale/data/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml index b9fc7784dbf..86117172855 100644 --- a/addons/point_of_sale/data/point_of_sale_demo.xml +++ b/addons/point_of_sale/data/point_of_sale_demo.xml @@ -1,6 +1,5 @@ - - - + + 0420100000005 0420200000004 @@ -572,7 +571,4 @@ - - - - + diff --git a/addons/point_of_sale/data/point_of_sale_workflow.xml b/addons/point_of_sale/data/point_of_sale_workflow.xml index ad7b149f6bc..833ca713c45 100644 --- a/addons/point_of_sale/data/point_of_sale_workflow.xml +++ b/addons/point_of_sale/data/point_of_sale_workflow.xml @@ -1,6 +1,5 @@ - - - + + Pos workflow pos.order @@ -74,10 +73,6 @@ cancel - - - - POS Session Workflow pos.session @@ -168,8 +163,4 @@ cash_control == False - - - - - + diff --git a/addons/point_of_sale/models/account_bank_statement.py b/addons/point_of_sale/models/account_bank_statement.py index 098b750beba..60f7380a32e 100644 --- a/addons/point_of_sale/models/account_bank_statement.py +++ b/addons/point_of_sale/models/account_bank_statement.py @@ -1,21 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - # Copyright (C) 2004-2008 PC Solutions (). All Rights Reserved - -from openerp.osv import fields, osv +from odoo import fields, models -class account_bank_statement(osv.osv): +class AccountBankStatement(models.Model): _inherit = 'account.bank.statement' - _columns = { - 'pos_session_id' : fields.many2one('pos.session', string="Session", copy=False), - 'account_id': fields.related('journal_id', 'default_debit_account_id', type='many2one', relation='account.account', readonly=True), - } + + pos_session_id = fields.Many2one('pos.session', string="Session", copy=False) + account_id = fields.Many2one('account.account', related='journal_id.default_debit_account_id', readonly=True) -class account_bank_statement_line(osv.osv): +class AccountBankStatementLine(models.Model): _inherit = 'account.bank.statement.line' - _columns= { - 'pos_statement_id': fields.many2one('pos.order', string="POS statement", ondelete='cascade'), - } + + pos_statement_id = fields.Many2one('pos.order', string="POS statement", ondelete='cascade') diff --git a/addons/point_of_sale/models/account_journal.py b/addons/point_of_sale/models/account_journal.py index f9310410467..4785d7e02f8 100644 --- a/addons/point_of_sale/models/account_journal.py +++ b/addons/point_of_sale/models/account_journal.py @@ -1,27 +1,24 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - # Copyright (C) 2004-2008 PC Solutions (). All Rights Reserved -from openerp.osv import fields, osv +from odoo import fields, models, api -class account_journal(osv.osv): + +class AccountJournal(models.Model): _inherit = 'account.journal' - _columns = { - 'journal_user': fields.boolean('Active in Point of Sale', help="Check this box if this journal define a payment method that can be used in a point of sale."), - 'amount_authorized_diff' : fields.float('Amount Authorized Difference', help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at the closing of his session saying that he needs to contact his manager."), - } - - def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if not context: - context = {} - session_id = context.get('pos_session_id', False) or False + journal_user = fields.Boolean('Active in Point of Sale', + help="Check this box if this journal define a payment method that can be used in a point of sale.") + amount_authorized_diff = fields.Float('Amount Authorized Difference', + help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when " + "closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at " + "the closing of his session saying that he needs to contact his manager.") + @api.model + def search(self, args, offset=0, limit=None, order=None, count=False): + session_id = self.env.context.get('pos_session_id', False) if session_id: - session = self.pool.get('pos.session').browse(cr, uid, session_id, context=context) - + session = self.env['pos.session'].browse(session_id) if session: - journal_ids = [journal.id for journal in session.config_id.journal_ids] - args += [('id', 'in', journal_ids)] - - return super(account_journal, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) + args += [('id', 'in', session.config_id.journal_ids.ids)] + return super(AccountJournal, self).search(args=args, offset=offset, limit=limit, order=order, count=count) diff --git a/addons/point_of_sale/models/barcode_rule.py b/addons/point_of_sale/models/barcode_rule.py index f1f5e2abbba..ffbf0b384b8 100644 --- a/addons/point_of_sale/models/barcode_rule.py +++ b/addons/point_of_sale/models/barcode_rule.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import sets -from openerp import models -from openerp.tools.translate import _ + +from odoo import models +from odoo.tools.translate import _ -class barcode_rule(models.Model): +class BarcodeRule(models.Model): _inherit = 'barcode.rule' def _get_type_selection(self): - types = sets.Set(super(barcode_rule,self)._get_type_selection()) + types = sets.Set(super(BarcodeRule, self)._get_type_selection()) types.update([ ('weight', _('Weighted Product')), ('price', _('Priced Product')), diff --git a/addons/point_of_sale/models/pos_category.py b/addons/point_of_sale/models/pos_category.py index b9d8c1b02e4..ff68328cbaa 100644 --- a/addons/point_of_sale/models/pos_category.py +++ b/addons/point_of_sale/models/pos_category.py @@ -1,57 +1,40 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp import tools -from openerp.osv import fields, osv +from odoo import api, fields, models, tools, _ -class pos_category(osv.osv): +class PosCategory(models.Model): _name = "pos.category" _description = "Public Category" _order = "sequence, name" - _constraints = [ - (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) - ] - - def name_get(self, cr, uid, ids, context=None): - res = [] - for cat in self.browse(cr, uid, ids, context=context): - names = [cat.name] - pcat = cat.parent_id - while pcat: - names.append(pcat.name) - pcat = pcat.parent_id - res.append((cat.id, ' / '.join(reversed(names)))) - return res - - _columns = { - 'name': fields.char('Name', required=True, translate=True), - 'parent_id': fields.many2one('pos.category','Parent Category', select=True), - 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), - - } + @api.constrains('parent_id') + def _check_category_recursion(self): + if not self._check_recursion(): + raise ValueError(_('Error ! You cannot create recursive categories.')) + name = fields.Char(required=True, translate=True) + parent_id = fields.Many2one('pos.category', string='Parent Category', index=True) + child_id = fields.One2many('pos.category', 'parent_id', string='Children Categories') + sequence = fields.Integer(help="Gives the sequence order when displaying a list of product categories.") # NOTE: there is no 'default image', because by default we don't show # thumbnails for categories. However if we have a thumbnail for at least one # category, then we display a default image on the other, so that the # buttons have consistent styling. - # In this case, the default image is set by the js code. - image = openerp.fields.Binary("Image", attachment=True, + image = fields.Binary(attachment=True, help="This field holds the image used as image for the cateogry, limited to 1024x1024px.") - image_medium = openerp.fields.Binary("Medium-sized image", + image_medium = fields.Binary(string="Medium-sized image", compute='_compute_images', inverse='_inverse_image_medium', store=True, attachment=True, - help="Medium-sized image of the category. It is automatically "\ - "resized as a 128x128px image, with aspect ratio preserved. "\ + help="Medium-sized image of the category. It is automatically " + "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") - image_small = openerp.fields.Binary("Small-sized image", + image_small = fields.Binary(string="Small-sized image", compute='_compute_images', inverse='_inverse_image_small', store=True, attachment=True, - help="Small-sized image of the category. It is automatically "\ - "resized as a 64x64px image, with aspect ratio preserved. "\ + help="Small-sized image of the category. It is automatically " + "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") - @openerp.api.depends('image') + @api.depends('image') def _compute_images(self): for rec in self: rec.image_medium = tools.image_resize_image_medium(rec.image) @@ -64,3 +47,13 @@ class pos_category(osv.osv): def _inverse_image_small(self): for rec in self: rec.image = tools.image_resize_image_big(rec.image_small) + + @api.multi + def name_get(self): + def get_names(cat): + res = [] + while cat: + res.append(cat.name) + cat = cat.parent_id + return res + return [(cat.id, " / ".join(reversed(get_names(cat)))) for cat in self] diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index db57f6007de..8e2badd6c25 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - import uuid -from openerp import SUPERUSER_ID -from openerp.osv import fields, osv -from openerp.tools.translate import _ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError -class pos_config(osv.osv): +class PosConfig(models.Model): _name = 'pos.config' POS_CONFIG_STATE = [ @@ -16,300 +14,229 @@ class pos_config(osv.osv): ('deprecated', 'Deprecated') ] - def _get_currency(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, False) - for pos_config in self.browse(cr, uid, ids, context=context): + def _default_sale_journal(self): + return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.user.company_id.id)], limit=1) + + def _default_pricelist(self): + return self.env['product.pricelist'].search([], limit=1) + + def _get_default_location(self): + return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id + + def _get_default_nomenclature(self): + return self.env['barcode.nomenclature'].search([], limit=1) + + def _get_group_pos_manager(self): + return self.env.ref('point_of_sale.group_pos_manager') + + def _get_group_pos_user(self): + return self.env.ref('point_of_sale.group_pos_user') + + name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale") + journal_ids = fields.Many2many( + 'account.journal', 'pos_config_journal_rel', + 'pos_config_id', 'journal_id', string='Available Payment Methods', + domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",) + picking_type_id = fields.Many2one('stock.picking.type', string='Picking Type') + stock_location_id = fields.Many2one( + 'stock.location', string='Stock Location', + domain=[('usage', '=', 'internal')], required=True, default=_get_default_location) + journal_id = fields.Many2one( + 'account.journal', string='Sale Journal', + domain=[('type', '=', 'sale')], + help="Accounting journal used to post sales entries.", + default=_default_sale_journal) + currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency") + iface_cashdrawer = fields.Boolean(string='Cashdrawer', help="Automatically open the cashdrawer") + iface_payment_terminal = fields.Boolean(string='Payment Terminal', help="Enables Payment Terminal integration") + iface_electronic_scale = fields.Boolean(string='Electronic Scale', help="Enables Electronic Scale integration") + iface_vkeyboard = fields.Boolean(string='Virtual KeyBoard', help="Enables an integrated Virtual Keyboard") + iface_print_via_proxy = fields.Boolean(string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy") + iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner") + iface_invoicing = fields.Boolean(string='Invoicing', help='Enables invoice generation from the Point of Sale', default=True) + iface_big_scrollbars = fields.Boolean('Large Scrollbars', help='For imprecise industrial touchscreens') + iface_print_auto = fields.Boolean(string='Automatic Receipt Printing', default=False, + help='The receipt will automatically be p-rinted at the end of each order') + iface_print_skip_screen = fields.Boolean(string='Skip Receipt Screen', default=True, + help='The receipt screen will be skipped if the receipt can be printed automatically.') + iface_precompute_cash = fields.Boolean(string='Prefill Cash Payment', + help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount') + iface_tax_included = fields.Boolean(string='Include Taxes in Prices', + help='The displayed prices will always include all taxes, even if the taxes have been setup differently') + iface_start_categ_id = fields.Many2one('pos.category', string='Start Category', + help='The point of sale will display this product category by default. If no category is specified, all available products will be shown') + iface_display_categ_images = fields.Boolean(string='Display Category Pictures', + help="The product categories will be displayed with pictures.") + cash_control = fields.Boolean(string='Cash Control', help="Check the amount of the cashbox at opening and closing.") + receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt") + receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt") + proxy_ip = fields.Char(string='IP Address', size=45, + help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty') + state = fields.Selection(POS_CONFIG_STATE, string='Status', required=True, readonly=True, copy=False, default=POS_CONFIG_STATE[0][0]) + uuid = fields.Char(readonly=True, default=lambda self: str(uuid.uuid4()), + help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data') + sequence_id = fields.Many2one('ir.sequence', string='Order IDs Sequence', readonly=True, + help="This sequence is automatically created by Odoo but you can change it " + "to customize the reference numbers of your orders.", copy=False) + session_ids = fields.One2many('pos.session', 'config_id', string='Sessions') + current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session") + current_session_state = fields.Char(compute='_compute_current_session') + last_session_closing_cash = fields.Float(compute='_compute_last_session') + last_session_closing_date = fields.Date(compute='_compute_last_session') + pos_session_username = fields.Char(compute='_compute_current_session_user') + group_by = fields.Boolean(string='Group Journal Items', default=True, + help="Check this if you want to group the Journal Items by Product while closing a Session") + pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, default=_default_pricelist) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) + barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcodes', required=True, default=_get_default_nomenclature, + help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers') + group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager, + help='This field is there to pass the id of the pos manager group to the point of sale client') + group_pos_user_id = fields.Many2one('res.groups', string='Point of Sale User Group', default=_get_group_pos_user, + help='This field is there to pass the id of the pos user group to the point of sale client') + tip_product_id = fields.Many2one('product.product', string='Tip Product', + help="The product used to encode the customer tip. Leave empty if you do not accept tips.") + fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions') + + @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id') + def _compute_currency(self): + for pos_config in self: if pos_config.journal_id: - currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id + pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id else: - currency_id = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id - result[pos_config.id] = currency_id - return result + pos_config.currency_id = self.env.user.company_id.currency_id.id - def _get_current_session(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() + @api.depends('session_ids') + def _compute_current_session(self): + for pos_config in self: + session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and not r.state == 'closed') + pos_config.current_session_id = session + pos_config.current_session_state = session.state - for record in self.browse(cr, uid, ids, context=context): - session_id = record.session_ids.filtered(lambda r: r.user_id.id == uid and not r.state == 'closed') - result[record.id] = { - 'current_session_id': session_id, - 'current_session_state': session_id.state, - } - return result - - def _get_last_session(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() - - for record in self.browse(cr, uid, ids, context=context): - session_ids = self.pool['pos.session'].search_read( - cr, uid, - [('config_id', '=', record.id), ('state', '=', 'closed')], + @api.depends('session_ids') + def _compute_last_session(self): + PosSession = self.env['pos.session'] + for pos_config in self: + session = PosSession.search_read( + [('config_id', '=', pos_config.id), ('state', '=', 'closed')], ['cash_register_balance_end_real', 'stop_at'], - order="stop_at desc", limit=1, context=context) - if session_ids: - result[record.id] = { - 'last_session_closing_cash': session_ids[0]['cash_register_balance_end_real'], - 'last_session_closing_date': session_ids[0]['stop_at'], - } + order="stop_at desc", limit=1) + if session: + pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real'] + pos_config.last_session_closing_date = session[0]['stop_at'] else: - result[record.id] = { - 'last_session_closing_cash': 0, - 'last_session_closing_date': None, - } - return result + pos_config.last_session_closing_cash = 0 + pos_config.last_session_closing_date = False - def _get_current_session_user(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() + @api.depends('session_ids') + def _compute_current_session_user(self): + for pos_config in self: + pos_config.pos_session_username = pos_config.session_ids.filtered(lambda s: s.state == 'opened').user_id.name - for record in self.browse(cr, uid, ids, context=context): - result[record.id] = record.session_ids.filtered(lambda r: r.state == 'opened').user_id.name - return result + @api.constrains('company_id', 'stock_location_id') + def _check_company_location(self): + if self.stock_location_id.company_id and self.stock_location_id.company_id.id != self.company_id.id: + raise UserError(_("The company of the stock location is different than the one of point of sale")) - _columns = { - 'name' : fields.char('Point of Sale Name', select=1, - required=True, help="An internal identification of the point of sale"), - 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', - 'pos_config_id', 'journal_id', 'Available Payment Methods', - domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",), - 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'), - 'stock_location_id': fields.many2one('stock.location', 'Stock Location', domain=[('usage', '=', 'internal')], required=True), - 'journal_id' : fields.many2one('account.journal', 'Sale Journal', - domain=[('type', '=', 'sale')], - help="Accounting journal used to post sales entries."), - 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), - 'iface_cashdrawer' : fields.boolean('Cashdrawer', help="Automatically open the cashdrawer"), - 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), - 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), - 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard', help="Enables an integrated Virtual Keyboard"), - 'iface_print_via_proxy' : fields.boolean('Print via Proxy', help="Bypass browser printing and prints via the hardware proxy"), - 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), - 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), - 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), + @api.constrains('company_id', 'journal_id') + def _check_company_journal(self): + if self.journal_id and self.journal_id.company_id.id != self.company_id.id: + raise UserError(_("The company of the sale journal is different than the one of point of sale")) - 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), - 'iface_print_skip_screen': fields.boolean('Skip Receipt Screen', help='The receipt screen will be skipped if the receipt can be printed automatically.'), - 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), - 'iface_tax_included': fields.boolean('Include Taxes in Prices', help='The displayed prices will always include all taxes, even if the taxes have been setup differently'), - 'iface_start_categ_id': fields.many2one('pos.category','Start Category', help='The point of sale will display this product category by default. If no category is specified, all available products will be shown'), - 'iface_display_categ_images': fields.boolean('Display Category Pictures', help="The product categories will be displayed with pictures."), - 'cash_control': fields.boolean('Cash Control', help="Check the amount of the cashbox at opening and closing."), - 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), - 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), - 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), + @api.constrains('company_id', 'journal_ids') + def _check_company_payment(self): + if self.env['account.journal'].search_count([('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id)]): + raise UserError(_("The company of a payment method is different than the one of point of sale")) - 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), - 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), - 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, - help="This sequence is automatically created by Odoo but you can change it "\ - "to customize the reference numbers of your orders.", copy=False), - 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), - 'current_session_id': fields.function(_get_current_session, multi="session", type="many2one", relation="pos.session", string="Current Session"), - 'current_session_state': fields.function(_get_current_session, multi="session", type='char'), - 'last_session_closing_cash': fields.function(_get_last_session, multi="last_session", type='float'), - 'last_session_closing_date': fields.function(_get_last_session, multi="last_session", type='date'), - 'pos_session_username': fields.function(_get_current_session_user, type='char'), - 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), - 'pricelist_id': fields.many2one('product.pricelist','Pricelist', required=True), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcodes', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers', required=True), - 'group_pos_manager_id': fields.many2one('res.groups','Point of Sale Manager Group', help='This field is there to pass the id of the pos manager group to the point of sale client'), - 'group_pos_user_id': fields.many2one('res.groups','Point of Sale User Group', help='This field is there to pass the id of the pos user group to the point of sale client'), - 'tip_product_id': fields.many2one('product.product','Tip Product', help="The product used to encode the customer tip. Leave empty if you do not accept tips."), - 'fiscal_position_ids': fields.many2many('account.fiscal.position', string='Fiscal Positions') - } + @api.onchange('iface_print_via_proxy') + def _onchange_iface_print_via_proxy(self): + self.iface_print_auto = self.iface_print_via_proxy - def _check_company_location(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - if config.stock_location_id.company_id and config.stock_location_id.company_id.id != config.company_id.id: - return False - return True + @api.onchange('picking_type_id') + def _onchange_picking_type_id(self): + if self.picking_type_id.default_location_src_id.usage == 'internal' and self.picking_type_id.default_location_dest_id.usage == 'customer': + self.stock_location_id = self.picking_type_id.default_location_src_id.id - def _check_company_journal(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - if config.journal_id and config.journal_id.company_id.id != config.company_id.id: - return False - return True - - def _check_company_payment(self, cr, uid, ids, context=None): - for config in self.browse(cr, uid, ids, context=context): - journal_ids = [j.id for j in config.journal_ids] - if self.pool['account.journal'].search(cr, uid, [ - ('id', 'in', journal_ids), - ('company_id', '!=', config.company_id.id) - ], count=True, context=context): - return False - return True - - _constraints = [ - (_check_company_location, "The company of the stock location is different than the one of point of sale", ['company_id', 'stock_location_id']), - (_check_company_journal, "The company of the sale journal is different than the one of point of sale", ['company_id', 'journal_id']), - (_check_company_payment, "The company of a payment method is different than the one of point of sale", ['company_id', 'journal_ids']), - ] - - def name_get(self, cr, uid, ids, context=None): + @api.multi + def name_get(self): result = [] - states = { - 'opening_control': _('Opening Control'), - 'opened': _('In Progress'), - 'closing_control': _('Closing Control'), - 'closed': _('Closed & Posted'), - } - for record in self.browse(cr, uid, ids, context=context): - if (not record.session_ids) or (record.session_ids[0].state=='closed'): - result.append((record.id, record.name+' ('+_('not used')+')')) + for config in self: + if (not config.session_ids) or (config.session_ids[0].state == 'closed'): + result.append((config.id, config.name + ' (' + _('not used') + ')')) continue - session = record.session_ids[0] - result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) + result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')')) return result - def _default_sale_journal(self, cr, uid, context=None): - company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1, context=context) - return res and res[0] or False - - def _default_pricelist(self, cr, uid, context=None): - res = self.pool.get('product.pricelist').search(cr, uid, [], limit=1, context=context) - return res and res[0] or False - - def _get_default_location(self, cr, uid, context=None): - wh_obj = self.pool.get('stock.warehouse') - user = self.pool.get('res.users').browse(cr, uid, uid, context) - res = wh_obj.search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context) - if res and res[0]: - return wh_obj.browse(cr, uid, res[0], context=context).lot_stock_id.id - return False - - def _get_default_company(self, cr, uid, context=None): - company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) - return company_id - - def _get_default_nomenclature(self, cr, uid, context=None): - nom_obj = self.pool.get('barcode.nomenclature') - res = nom_obj.search(cr, uid, [], limit=1, context=context) - return res and res[0] or False - - def _get_group_pos_manager(self, cr, uid, context=None): - group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_manager') - if group: - return group[1] - else: - return False - - def _get_group_pos_user(self, cr, uid, context=None): - group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_user') - if group: - return group[1] - else: - return False - - _defaults = { - 'uuid' : lambda self, cr, uid, context={}: str(uuid.uuid4()), - 'state' : POS_CONFIG_STATE[0][0], - 'journal_id': _default_sale_journal, - 'group_by' : True, - 'pricelist_id': _default_pricelist, - 'iface_invoicing': True, - 'iface_print_auto': False, - 'iface_print_skip_screen': True, - 'stock_location_id': _get_default_location, - 'company_id': _get_default_company, - 'barcode_nomenclature_id': _get_default_nomenclature, - 'group_pos_manager_id': _get_group_pos_manager, - 'group_pos_user_id': _get_group_pos_user, - } - - def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): - p_type_obj = self.pool.get("stock.picking.type") - p_type = p_type_obj.browse(cr, uid, picking_type_id, context=context) - if p_type.default_location_src_id and p_type.default_location_src_id.usage == 'internal' and p_type.default_location_dest_id and p_type.default_location_dest_id.usage == 'customer': - return {'value': {'stock_location_id': p_type.default_location_src_id.id}} - return False - - def onchange_iface_print_via_proxy(self, cr, uid, ids, print_via_proxy, context=None): - return {'value': {'iface_print_auto': print_via_proxy}} - - def set_active(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'active'}, context=context) - - def set_inactive(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) - - def set_deprecate(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) - - def create(self, cr, uid, values, context=None): - ir_sequence = self.pool.get('ir.sequence') - # force sequence_id field to new pos.order sequence - values['sequence_id'] = ir_sequence.create(cr, SUPERUSER_ID, { - 'name': 'POS Order %s' % values['name'], + @api.model + def create(self, values): + IrSequence = self.env['ir.sequence'] + val = { + 'name': _('POS Order %s') % values['name'], 'padding': 4, - 'prefix': "%s/" % values['name'], + 'prefix': "%s/" % values['name'], 'code': "pos.order", 'company_id': values.get('company_id', False), - }, context=context) + } + # force sequence_id field to new pos.order sequence + values['sequence_id'] = IrSequence.create(val).id # TODO master: add field sequence_line_id on model # this make sure we always have one available per company - ir_sequence.create(cr, SUPERUSER_ID, { - 'name': 'POS order line %s' % values['name'], - 'padding': 4, - 'prefix': "%s/" % values['name'], - 'code': "pos.order.line", - 'company_id': values.get('company_id', False), - }, context=context) + val.update(name=_('POS order line %s') % values['name'], code='pos.order.line') + IrSequence.create(val) + return super(PosConfig, self).create(values) - return super(pos_config, self).create(cr, uid, values, context=context) + @api.multi + def unlink(self): + for pos_config in self.filtered(lambda pos_config: pos_config.sequence_id): + pos_config.sequence_id.unlink() + return super(PosConfig, self).unlink() - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - if obj.sequence_id: - obj.sequence_id.unlink() - return super(pos_config, self).unlink(cr, uid, ids, context=context) + @api.multi + def set_active(self): + self.write({'state': 'active'}) + + @api.multi + def set_inactive(self): + self.write({'state': 'inactive'}) + + @api.multi + def set_deprecate(self): + self.write({'state': 'deprecated'}) # Methods to open the POS - - def open_ui(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - context = dict(context or {}) - context['active_id'] = record.current_session_id.id + @api.multi + def open_ui(self): + assert len(self.ids) == 1, "you can open only one session at a time" return { 'type': 'ir.actions.act_url', 'url': '/pos/web/', 'target': 'self', } - def open_existing_session_cb_close(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" + @api.multi + def open_existing_session_cb_close(self): + assert len(self.ids) == 1, "you can open only one session at a time" + self.current_session_id.signal_workflow('cashbox_control') + return self.open_session_cb() - record = self.browse(cr, uid, ids[0], context=context) - record.current_session_id.signal_workflow('cashbox_control') - return self.open_session_cb(cr, uid, ids, context) + @api.multi + def open_session_cb(self): + assert len(self.ids) == 1, "you can open only one session at a time" + if not self.current_session_id: + self.current_session_id = self.env['pos.session'].create({ + 'user_id': self.env.uid, + 'config_id': self.id + }) + if self.current_session_id.state == 'opened': + return self.open_ui() + return self._open_session(self.current_session_id.id) + return self._open_session(self.current_session_id.id) - def open_session_cb(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - proxy = self.pool.get('pos.session') - record = self.browse(cr, uid, ids[0], context=context) - current_session_id = record.current_session_id - if not current_session_id: - values = { - 'user_id': uid, - 'config_id': record.id, - } - session_id = proxy.create(cr, uid, values, context=context) - self.write(cr, SUPERUSER_ID, record.id, {'current_session_id': session_id}, context=context) - if record.current_session_id.state == 'opened': - return self.open_ui(cr, uid, ids, context=context) - return self._open_session(session_id) - return self._open_session(current_session_id.id) - - def open_existing_session_cb(self, cr, uid, ids, context=None): - assert len(ids) == 1, "you can open only one session at a time" - - record = self.browse(cr, uid, ids[0], context=context) - return self._open_session(record.current_session_id.id) + @api.multi + def open_existing_session_cb(self): + assert len(self.ids) == 1, "you can open only one session at a time" + return self._open_session(self.current_session_id.id) def _open_session(self, session_id): return { diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 71fb19a47e0..d3d85cee1e9 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -1,26 +1,18 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - import logging - -import time -from datetime import datetime - from functools import partial -import openerp -import openerp.addons.decimal_precision as dp -from openerp import tools, SUPERUSER_ID -from openerp.osv import fields, osv -from openerp.tools import float_is_zero -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from odoo import api, fields, models, tools, _ +from odoo.tools import float_is_zero +from odoo.exceptions import UserError +import odoo.addons.decimal_precision as dp _logger = logging.getLogger(__name__) -class pos_order(osv.osv): +class PosOrder(models.Model): _name = "pos.order" _description = "Point of Sale" _order = "id desc" @@ -30,32 +22,29 @@ class pos_order(osv.osv): if fiscal_position_id: taxes = fiscal_position_id.map_tax(taxes) price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - cur = line.order_id.pricelist_id.currency_id - taxes = taxes.compute_all(price, cur, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes'] - val = 0.0 - for c in taxes: - val += c.get('amount', 0.0) - return val + taxes = taxes.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes'] + return sum(tax.get('amount', 0.0) for tax in taxes) - def _order_fields(self, cr, uid, ui_order, context=None): - process_line = partial(self.pool['pos.order.line']._order_line_fields, cr, uid, context=context) + @api.model + def _order_fields(self, ui_order): + process_line = partial(self.env['pos.order.line']._order_line_fields) return { 'name': ui_order['name'], 'user_id': ui_order['user_id'] or False, 'session_id': ui_order['pos_session_id'], 'lines': [process_line(l) for l in ui_order['lines']] if ui_order['lines'] else False, - 'pos_reference':ui_order['name'], + 'pos_reference': ui_order['name'], 'partner_id': ui_order['partner_id'] or False, 'date_order': ui_order['creation_date'], 'fiscal_position_id': ui_order['fiscal_position_id'] } - def _payment_fields(self, cr, uid, ui_paymentline, context=None): + def _payment_fields(self, ui_paymentline): return { 'amount': ui_paymentline['amount'] or 0.0, 'payment_date': ui_paymentline['name'], 'statement_id': ui_paymentline['statement_id'], - 'payment_name': ui_paymentline.get('note',False), + 'payment_name': ui_paymentline.get('note', False), 'journal': ui_paymentline['journal_id'], } @@ -63,570 +52,145 @@ class pos_order(osv.osv): # to recover from this we: # - assign the order to another compatible open session # - if that doesn't exist, create a new one - def _get_valid_session(self, cr, uid, order, context=None): - session = self.pool.get('pos.session') - closed_session = session.browse(cr, uid, order['pos_session_id'], context=context) - open_sessions = session.search(cr, uid, [('state', '=', 'opened'), - ('config_id', '=', closed_session.config_id.id), - ('user_id', '=', closed_session.user_id.id)], - limit=1, order="start_at DESC", context=context) + def _get_valid_session(self, order): + PosSession = self.env['pos.session'] + closed_session = PosSession.browse(order['pos_session_id']) + open_session = PosSession.search( + [('state', '=', 'opened'), + ('config_id', '=', closed_session.config_id.id), + ('user_id', '=', closed_session.user_id.id)], + limit=1, order="start_at DESC") - if open_sessions: - return open_sessions[0] + if open_session: + return open_session else: - new_session_id = session.create(cr, uid, { - 'config_id': closed_session.config_id.id, - }, context=context) - new_session = session.browse(cr, uid, new_session_id, context=context) - + new_session = PosSession.create({'config_id': closed_session.config_id.id}) # bypass opening_control (necessary when using cash control) new_session.signal_workflow('open') + return new_session - return new_session_id - - def _process_order(self, cr, uid, order, context=None): - session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) - - if session.state == 'closing_control' or session.state == 'closed': - session_id = self._get_valid_session(cr, uid, order, context=context) - session = self.pool.get('pos.session').browse(cr, uid, session_id, context=context) - order['pos_session_id'] = session_id - - order_id = self.create(cr, uid, self._order_fields(cr, uid, order, context=context),context) + @api.model + def _process_order(self, pos_order): + pos_session = self.env['pos.session'].browse(pos_order['pos_session_id']) + if pos_session.state == 'closing_control' or pos_session.state == 'closed': + pos_order['pos_session_id'] = self._get_valid_session(pos_order).id + order = self.create(self._order_fields(pos_order)) journal_ids = set() - for payments in order['statement_ids']: - self.add_payment(cr, uid, order_id, self._payment_fields(cr, uid, payments[2], context=context), context=context) + for payments in pos_order['statement_ids']: + order.add_payment(self._payment_fields(payments[2])) journal_ids.add(payments[2]['journal_id']) - if session.sequence_number <= order['sequence_number']: - session.write({'sequence_number': order['sequence_number'] + 1}) - session.refresh() + if pos_session.sequence_number <= pos_order['sequence_number']: + pos_session.write({'sequence_number': pos_order['sequence_number'] + 1}) + pos_session.refresh() - if not float_is_zero(order['amount_return'], self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')): - cash_journal = session.cash_journal_id.id - if not cash_journal: - # Select for change one of the cash journals used in this payment - cash_journal_ids = self.pool['account.journal'].search(cr, uid, [ + if not float_is_zero(pos_order['amount_return'], self.env['decimal.precision'].precision_get('Account')): + cash_journal_id = pos_session.cash_journal_id.id + if not cash_journal_id: + # Select for change one of the cash journals used in this + # payment + cash_journal = self.env['account.journal'].search([ ('type', '=', 'cash'), ('id', 'in', list(journal_ids)), - ], limit=1, context=context) - if not cash_journal_ids: + ], limit=1) + if not cash_journal: # If none, select for change one of the cash journals of the POS # This is used for example when a customer pays by credit card # an amount higher than total amount of the order and gets cash back - cash_journal_ids = [statement.journal_id.id for statement in session.statement_ids - if statement.journal_id.type == 'cash'] - if not cash_journal_ids: + cash_journal = [statement.journal_id for statement in pos_session.statement_ids if statement.journal_id.type == 'cash'] + if not cash_journal: raise UserError(_("No cash statement found for this session. Unable to record returned cash.")) - cash_journal = cash_journal_ids[0] - self.add_payment(cr, uid, order_id, { - 'amount': -order['amount_return'], - 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'), + cash_journal_id = cash_journal[0].id + order.add_payment({ + 'amount': -pos_order['amount_return'], + 'payment_date': fields.Datetime.now(), 'payment_name': _('return'), - 'journal': cash_journal, - }, context=context) - return order_id + 'journal': cash_journal_id, + }) + return order - def create_from_ui(self, cr, uid, orders, context=None): - # Keep only new orders - submitted_references = [o['data']['name'] for o in orders] - existing_order_ids = self.search(cr, uid, [('pos_reference', 'in', submitted_references)], context=context) - existing_orders = self.read(cr, uid, existing_order_ids, ['pos_reference'], context=context) - existing_references = set([o['pos_reference'] for o in existing_orders]) - orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] - - order_ids = [] - - for tmp_order in orders_to_save: - to_invoice = tmp_order['to_invoice'] - order = tmp_order['data'] - order_id = self._process_order(cr, uid, order, context=context) - order_ids.append(order_id) - - try: - self.signal_workflow(cr, uid, [order_id], 'paid') - except Exception as e: - _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) - - if to_invoice: - self.action_invoice(cr, uid, [order_id], context) - order_obj = self.browse(cr, uid, order_id, context) - self.pool['account.invoice'].signal_workflow(cr, SUPERUSER_ID, [order_obj.invoice_id.id], 'invoice_open') - - return order_ids - - def write(self, cr, uid, ids, vals, context=None): - res = super(pos_order, self).write(cr, uid, ids, vals, context=context) - #If you change the partner of the PoS order, change also the partner of the associated bank statement lines - partner_obj = self.pool.get('res.partner') - bsl_obj = self.pool.get("account.bank.statement.line") - if 'partner_id' in vals: - for posorder in self.browse(cr, uid, ids, context=context): - if posorder.invoice_id: - raise UserError(_("You cannot change the partner of a POS order for which an invoice has already been issued.")) - if vals['partner_id']: - p_id = partner_obj.browse(cr, uid, vals['partner_id'], context=context) - part_id = partner_obj._find_accounting_partner(p_id).id - else: - part_id = False - bsl_ids = [x.id for x in posorder.statement_ids] - bsl_obj.write(cr, uid, bsl_ids, {'partner_id': part_id}, context=context) - return res - - def unlink(self, cr, uid, ids, context=None): - for rec in self.browse(cr, uid, ids, context=context): - if rec.state not in ('draft','cancel'): - raise UserError(_('In order to delete a sale, it must be new or cancelled.')) - return super(pos_order, self).unlink(cr, uid, ids, context=context) - - def onchange_partner_id(self, cr, uid, ids, part=False, context=None): - if not part: - return {'value': {}} - pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id - return {'value': {'pricelist_id': pricelist}} - - def _amount_all(self, cr, uid, ids, name, args, context=None): - cur_obj = self.pool.get('res.currency') - res = {} - for order in self.browse(cr, uid, ids, context=context): - res[order.id] = { - 'amount_paid': 0.0, - 'amount_return':0.0, - 'amount_tax':0.0, - } - val1 = val2 = 0.0 - cur = order.pricelist_id.currency_id - for payment in order.statement_ids: - res[order.id]['amount_paid'] += payment.amount - res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0) - for line in order.lines: - val1 += self._amount_line_tax(cr, uid, line, order.fiscal_position_id, context=context) - val2 += line.price_subtotal - res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1) - amount_untaxed = cur_obj.round(cr, uid, cur, val2) - res[order.id]['amount_total'] = res[order.id]['amount_tax'] + amount_untaxed - return res - - _columns = { - 'name': fields.char('Order Ref', required=True, readonly=True, copy=False), - 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True), - 'date_order': fields.datetime('Order Date', readonly=True, select=True), - 'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the cash register. It can be a reliever, a student or an interim employee."), - 'amount_tax': fields.function(_amount_all, string='Taxes', digits=0, multi='all'), - 'amount_total': fields.function(_amount_all, string='Total', digits=0, multi='all'), - 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits=0, multi='all'), - 'amount_return': fields.function(_amount_all, string='Returned', digits=0, multi='all'), - 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True), - 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True), - 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True), - 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}), - 'sequence_number': fields.integer('Sequence Number', help='A session-unique sequence number for the order'), - - 'session_id' : fields.many2one('pos.session', 'Session', - required=True, - select=1, - domain="[('state', '=', 'opened')]", - states={'draft' : [('readonly', False)]}, - readonly=True), - 'config_id': fields.related('session_id', 'config_id', string="Point of Sale", type='many2one' , relation='pos.config'), - 'state': fields.selection([('draft', 'New'), - ('cancel', 'Cancelled'), - ('paid', 'Paid'), - ('done', 'Posted'), - ('invoiced', 'Invoiced')], - 'Status', readonly=True, copy=False), - - 'invoice_id': fields.many2one('account.invoice', 'Invoice', copy=False), - 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True, copy=False), - 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True, copy=False), - 'picking_type_id': fields.related('session_id', 'config_id', 'picking_type_id', string="Picking Type", type='many2one', relation='stock.picking.type'), - 'location_id': fields.related('session_id', 'config_id', 'stock_location_id', string="Location", type='many2one', store=True, relation='stock.location'), - 'note': fields.text('Internal Notes'), - 'nb_print': fields.integer('Number of Print', readonly=True, copy=False), - 'pos_reference': fields.char('Receipt Ref', readonly=True, copy=False), - 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True), - 'fiscal_position_id': fields.many2one('account.fiscal.position', 'Fiscal Position') - } - - def _default_session(self, cr, uid, context=None): - so = self.pool.get('pos.session') - session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context) - return session_ids and session_ids[0] or False - - def _default_pricelist(self, cr, uid, context=None): - session_ids = self._default_session(cr, uid, context) - if session_ids: - session_record = self.pool.get('pos.session').browse(cr, uid, session_ids, context=context) - return session_record.config_id.pricelist_id and session_record.config_id.pricelist_id.id or False - return False - - def _get_out_picking_type(self, cr, uid, context=None): - return self.pool.get('ir.model.data').xmlid_to_res_id( - cr, uid, 'point_of_sale.picking_type_posout', context=context) - - _defaults = { - 'user_id': lambda self, cr, uid, context: uid, - 'state': 'draft', - 'name': '/', - 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), - 'nb_print': 0, - 'sequence_number': 1, - 'session_id': _default_session, - 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - 'pricelist_id': _default_pricelist, - } - - def create(self, cr, uid, values, context=None): - if values.get('session_id'): - # set name based on the sequence specified on the config - session = self.pool['pos.session'].browse(cr, uid, values['session_id'], context=context) - values['name'] = session.config_id.sequence_id._next() - values.setdefault('session_id', session.config_id.pricelist_id.id) - else: - # fallback on any pos.order sequence - values['name'] = self.pool.get('ir.sequence').next_by_code(cr, uid, 'pos.order', context=context) - return super(pos_order, self).create(cr, uid, values, context=context) - - def test_paid(self, cr, uid, ids, context=None): - """A Point of Sale is paid when the sum - @return: True - """ - for order in self.browse(cr, uid, ids, context=context): - if order.lines and not order.amount_total: - return True - if (not order.lines) or (not order.statement_ids) or \ - (abs(order.amount_total-order.amount_paid) > 0.00001): - return False - return True - - def create_picking(self, cr, uid, ids, context=None): - """Create a picking for each order and validate it.""" - picking_obj = self.pool.get('stock.picking') - partner_obj = self.pool.get('res.partner') - move_obj = self.pool.get('stock.move') - - for order in self.browse(cr, uid, ids, context=context): - if all(t == 'service' for t in order.lines.mapped('product_id.type')): - continue - addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {} - picking_type = order.picking_type_id - picking_id = False - location_id = order.location_id.id - if order.partner_id: - destination_id = order.partner_id.property_stock_customer.id - else: - if (not picking_type) or (not picking_type.default_location_dest_id): - customerloc, supplierloc = self.pool['stock.warehouse']._get_partner_locations(cr, uid, [], context=context) - destination_id = customerloc.id - else: - destination_id = picking_type.default_location_dest_id.id - - #All qties negative => Create negative - if picking_type: - pos_qty = all([x.qty >= 0 for x in order.lines]) - #Check negative quantities - picking_id = picking_obj.create(cr, uid, { - 'origin': order.name, - 'partner_id': addr.get('delivery',False), - 'date_done' : order.date_order, - 'picking_type_id': picking_type.id, - 'company_id': order.company_id.id, - 'move_type': 'direct', - 'note': order.note or "", - 'location_id': location_id if pos_qty else destination_id, - 'location_dest_id': destination_id if pos_qty else location_id, - }, context=context) - self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context) - - move_list = [] - for line in order.lines: - if line.product_id and line.product_id.type not in ['product', 'consu']: - continue - - move_list.append(move_obj.create(cr, uid, { - 'name': line.name, - 'product_uom': line.product_id.uom_id.id, - 'picking_id': picking_id, - 'picking_type_id': picking_type.id, - 'product_id': line.product_id.id, - 'product_uom_qty': abs(line.qty), - 'state': 'draft', - 'location_id': location_id if line.qty >= 0 else destination_id, - 'location_dest_id': destination_id if line.qty >= 0 else location_id, - }, context=context)) - - if picking_id: - picking_obj.action_confirm(cr, uid, [picking_id], context=context) - picking_obj.force_assign(cr, uid, [picking_id], context=context) - # Mark pack operations as done - pick = picking_obj.browse(cr, uid, picking_id, context=context) - for pack in pick.pack_operation_ids: - self.pool['stock.pack.operation'].write(cr, uid, [pack.id], {'qty_done': pack.product_qty}, context=context) - picking_obj.action_done(cr, uid, [picking_id], context=context) - elif move_list: - move_obj.action_confirm(cr, uid, move_list, context=context) - move_obj.force_assign(cr, uid, move_list, context=context) - move_obj.action_done(cr, uid, move_list, context=context) - return True - - def cancel_order(self, cr, uid, ids, context=None): - """ Changes order state to cancel - @return: True - """ - stock_picking_obj = self.pool.get('stock.picking') - for order in self.browse(cr, uid, ids, context=context): - stock_picking_obj.action_cancel(cr, uid, [order.picking_id.id]) - if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel': - raise UserError(_('Unable to cancel the picking.')) - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - return True - - def add_payment(self, cr, uid, order_id, data, context=None): - """Create a new payment for the order""" - context = dict(context or {}) - statement_line_obj = self.pool.get('account.bank.statement.line') - property_obj = self.pool.get('ir.property') - order = self.browse(cr, uid, order_id, context=context) - date = data.get('payment_date', time.strftime('%Y-%m-%d')) - if len(date) > 10: - timestamp = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT) - ts = fields.datetime.context_timestamp(cr, uid, timestamp, context) - date = ts.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - args = { - 'amount': data['amount'], - 'date': date, - 'name': order.name + ': ' + (data.get('payment_name', '') or ''), - 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False, - } - - journal_id = data.get('journal', False) - statement_id = data.get('statement_id', False) - assert journal_id or statement_id, "No statement_id or journal_id passed to the method!" - - journal = self.pool['account.journal'].browse(cr, uid, journal_id, context=context) - # use the company of the journal and not of the current user - company_cxt = dict(context, force_company=journal.company_id.id) - account_def = property_obj.get(cr, uid, 'property_account_receivable_id', 'res.partner', context=company_cxt) - args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable_id \ - and order.partner_id.property_account_receivable_id.id) or (account_def and account_def.id) or False - - if not args['account_id']: - if not args['partner_id']: - msg = _('There is no receivable account defined to make payment.') - else: - msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,) - raise UserError(msg) - - context.pop('pos_session_id', False) - - for statement in order.session_id.statement_ids: - if statement.id == statement_id: - journal_id = statement.journal_id.id - break - elif statement.journal_id.id == journal_id: - statement_id = statement.id - break - - if not statement_id: - raise UserError(_('You have to open at least one cashbox.')) - - args.update({ - 'statement_id': statement_id, - 'pos_statement_id': order_id, - 'journal_id': journal_id, - 'ref': order.session_id.name, - }) - - statement_line_obj.create(cr, uid, args, context=context) - - return statement_id - - def refund(self, cr, uid, ids, context=None): - """Create a copy of order for refund order""" - clone_list = [] - line_obj = self.pool.get('pos.order.line') - - for order in self.browse(cr, uid, ids, context=context): - current_session_ids = self.pool.get('pos.session').search(cr, uid, [ - ('state', '!=', 'closed'), - ('user_id', '=', uid)], context=context) - if not current_session_ids: - raise UserError(_('To return product(s), you need to open a session that will be used to register the refund.')) - - clone_id = self.copy(cr, uid, order.id, { - 'name': order.name + ' REFUND', # not used, name forced by create - 'session_id': current_session_ids[0], - 'date_order': time.strftime('%Y-%m-%d %H:%M:%S'), - }, context=context) - clone_list.append(clone_id) - - for clone in self.browse(cr, uid, clone_list, context=context): - for order_line in clone.lines: - line_obj.write(cr, uid, [order_line.id], { - 'qty': -order_line.qty - }, context=context) - - abs = { - 'name': _('Return Products'), - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'pos.order', - 'res_id':clone_list[0], - 'view_id': False, - 'context':context, - 'type': 'ir.actions.act_window', - 'target': 'current', - } - return abs - - def action_invoice_state(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'invoiced'}, context=context) - - def action_invoice(self, cr, uid, ids, context=None): - inv_ref = self.pool.get('account.invoice') - inv_line_ref = self.pool.get('account.invoice.line') - product_obj = self.pool.get('product.product') - inv_ids = [] - - for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context): - # Force company for all SUPERUSER_ID action - company_id = order.company_id.id - local_context = dict(context or {}, force_company=company_id, company_id=company_id) - if order.invoice_id: - inv_ids.append(order.invoice_id.id) - continue - - if not order.partner_id: - raise UserError(_('Please provide a partner for the sale.')) - - acc = order.partner_id.property_account_receivable_id.id - inv = { - 'name': order.name, - 'origin': order.name, - 'account_id': acc, - 'journal_id': order.sale_journal.id or None, - 'type': 'out_invoice', - 'reference': order.name, - 'partner_id': order.partner_id.id, - 'comment': order.note or '', - 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency - 'company_id': company_id, - 'user_id': uid, - } - invoice = inv_ref.new(cr, uid, inv) - invoice._onchange_partner_id() - - inv = invoice._convert_to_write(invoice._cache) - if not inv.get('account_id', None): - inv['account_id'] = acc - inv_id = inv_ref.create(cr, SUPERUSER_ID, inv, context=local_context) - - self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=local_context) - inv_ids.append(inv_id) - for line in order.lines: - inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=local_context)[0][1] - inv_line = { - 'invoice_id': inv_id, - 'product_id': line.product_id.id, - 'quantity': line.qty, - 'account_analytic_id': self._prepare_analytic_account(cr, uid, line, context=local_context), - 'name': inv_name, - } - - #Oldlin trick - invoice_line = inv_line_ref.new(cr, SUPERUSER_ID, inv_line, context=local_context) - invoice_line._onchange_product_id() - invoice_line.invoice_line_tax_ids = [tax.id for tax in invoice_line.invoice_line_tax_ids if tax.company_id.id == company_id] - fiscal_position_id = line.order_id.fiscal_position_id - if fiscal_position_id: - invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids) - invoice_line.invoice_line_tax_ids = [tax.id for tax in invoice_line.invoice_line_tax_ids] - # We convert a new id object back to a dictionary to write to bridge between old and new api - inv_line = invoice_line._convert_to_write(invoice_line._cache) - inv_line.update(price_unit=line.price_unit, discount=line.discount) - inv_line_ref.create(cr, SUPERUSER_ID, inv_line, context=local_context) - inv_ref.compute_taxes(cr, SUPERUSER_ID, [inv_id], context=local_context) - self.signal_workflow(cr, uid, [order.id], 'invoice') - inv_ref.signal_workflow(cr, SUPERUSER_ID, [inv_id], 'validate') - - if not inv_ids: return {} - - mod_obj = self.pool.get('ir.model.data') - res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') - res_id = res and res[1] or False - return { - 'name': _('Customer Invoice'), - 'view_type': 'form', - 'view_mode': 'form', - 'view_id': [res_id], - 'res_model': 'account.invoice', - 'context': "{'type':'out_invoice'}", - 'type': 'ir.actions.act_window', - 'target': 'current', - 'res_id': inv_ids and inv_ids[0] or False, - } - - def _prepare_analytic_account(self, cr, uid, line, context=None): + def _prepare_analytic_account(self, line): '''This method is designed to be inherited in a custom module''' return False - def _create_account_move(self, cr, uid, dt, ref, journal_id, company_id, context=None): - start_at_datetime = datetime.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT) - date_tz_user = fields.datetime.context_timestamp(cr, uid, start_at_datetime, context=context) - date_tz_user = date_tz_user.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - return self.pool['account.move'].create(cr, SUPERUSER_ID, {'ref': ref, 'journal_id': journal_id, 'date': date_tz_user}, context=context) + def _create_account_move(self, dt, ref, journal_id, company_id): + date_tz_user = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(dt)) + date_tz_user = fields.Date.to_string(date_tz_user) + return self.env['account.move'].sudo().create({'ref': ref, 'journal_id': journal_id, 'date': date_tz_user}) - def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None): + def _prepare_invoice(self): + """ + Prepare the dict of values to create the new invoice for a pos order. + """ + return { + 'name': self.name, + 'origin': self.name, + 'account_id': self.partner_id.property_account_receivable_id.id, + 'journal_id': self.sale_journal.id, + 'type': 'out_invoice', + 'reference': self.name, + 'partner_id': self.partner_id.id, + 'comment': self.note or '', + # considering partner's sale pricelist's currency + 'currency_id': self.pricelist_id.currency_id.id, + 'user_id': self.env.uid, + } + + def _action_create_invoice_line(self, line=False, invoice_id=False): + InvoiceLine = self.env['account.invoice.line'] + inv_name = line.product_id.name_get()[0][1] + inv_line = { + 'invoice_id': invoice_id, + 'product_id': line.product_id.id, + 'quantity': line.qty, + 'account_analytic_id': self._prepare_analytic_account(line), + 'name': inv_name, + } + # Oldlin trick + invoice_line = InvoiceLine.sudo().new(inv_line) + invoice_line._onchange_product_id() + invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids + fiscal_position_id = line.order_id.fiscal_position_id + if fiscal_position_id: + invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids) + invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids + # We convert a new id object back to a dictionary to write to + # bridge between old and new api + inv_line = invoice_line._convert_to_write(invoice_line._cache) + inv_line.update(price_unit=line.price_unit, discount=line.discount) + return InvoiceLine.sudo().create(inv_line) + + def _create_account_move_line(self, session=None, move=None): # Tricky, via the workflow, we only have one id in the ids variable """Create a account move line of order grouped by products or not.""" - account_move_obj = self.pool.get('account.move') - account_tax_obj = self.pool.get('account.tax') - property_obj = self.pool.get('ir.property') - cur_obj = self.pool.get('res.currency') + IrProperty = self.env['ir.property'] + ResPartner = self.env['res.partner'] - #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) - - if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): + if session and not all(session.id == order.session_id.id for order in self): raise UserError(_('Selected orders do not have the same session!')) grouped_data = {} have_to_group_by = session and session.config_id.group_by or False - for order in self.browse(cr, uid, ids, context=context): - if order.account_move: - continue - if order.state != 'paid': - continue - + for order in self.filtered(lambda o: not o.account_move or order.state == 'paid'): current_company = order.sale_journal.company_id - - group_tax = {} - account_def = property_obj.get(cr, uid, 'property_account_receivable_id', 'res.partner', context=context) - - order_account = order.partner_id and \ - order.partner_id.property_account_receivable_id and \ - order.partner_id.property_account_receivable_id.id or \ - account_def and account_def.id - - if move_id is None: + account_def = IrProperty.get( + 'property_account_receivable_id', 'res.partner') + order_account = order.partner_id.property_account_receivable_id.id or account_def and account_def.id + partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False + if move is None: # Create an entry for the sale - move_id = self._create_account_move(cr, uid, order.session_id.start_at, order.name, order.sale_journal.id, order.company_id.id, context=context) - - move = account_move_obj.browse(cr, SUPERUSER_ID, move_id, context=context) + move = self._create_account_move( + order.session_id.start_at, order.name, order.sale_journal.id, order.company_id.id) def insert_data(data_type, values): # if have_to_group_by: - - sale_journal_id = order.sale_journal.id - - # 'quantity': line.qty, - # 'product_id': line.product_id.id, values.update({ 'ref': order.name, - 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False, - 'journal_id' : sale_journal_id, - 'date' : fields.date.context_today(self, cr, uid, context=context), - 'move_id' : move_id, + 'partner_id': partner_id, + 'journal_id': order.sale_journal.id, + 'date': fields.Date.context_today(self), + 'move_id': move.id, 'company_id': current_company.id, }) @@ -641,11 +205,6 @@ class pos_order(osv.osv): grouped_data.setdefault(key, []) - # if not have_to_group_by or (not grouped_data[key]): - # grouped_data[key].append(values) - # else: - # pass - if have_to_group_by: if not grouped_data[key]: grouped_data[key].append(values) @@ -653,35 +212,38 @@ class pos_order(osv.osv): for line in grouped_data[key]: if line.get('tax_code_id') == values.get('tax_code_id'): current_value = line - current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) - current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) - current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + current_value['quantity'] = current_value.get( + 'quantity', 0.0) + values.get('quantity', 0.0) + current_value['credit'] = current_value.get( + 'credit', 0.0) + values.get('credit', 0.0) + current_value['debit'] = current_value.get( + 'debit', 0.0) + values.get('debit', 0.0) break else: grouped_data[key].append(values) else: grouped_data[key].append(values) - #because of the weird way the pos order is written, we need to make sure there is at least one line, - #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that - #are set inside the for loop) - #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack + # because of the weird way the pos order is written, we need to make sure there is at least one line, + # because just after the 'for' loop there are references to 'line' and 'income_account' variables (that + # are set inside the for loop) + # TOFIX: a deep refactoring of this method (and class!) is needed + # in order to get rid of this stupid hack assert order.lines, _('The POS order must have lines when calling this method') # Create an move for each order line - cur = order.pricelist_id.currency_id for line in order.lines: amount = line.price_subtotal # Search for the income account - if line.product_id.property_account_income_id.id: + if line.product_id.property_account_income_id.id: income_account = line.product_id.property_account_income_id.id elif line.product_id.categ_id.property_account_income_categ_id.id: income_account = line.product_id.categ_id.property_account_income_categ_id.id else: - raise UserError(_('Please define income '\ - 'account for this product: "%s" (id:%d).') \ - % (line.product_id.name, line.product_id.id)) + raise UserError(_('Please define income ' + 'account for this product: "%s" (id:%d).') + % (line.product_id.name, line.product_id.id)) name = line.product_id.name if line.notice: @@ -694,162 +256,441 @@ class pos_order(osv.osv): 'quantity': line.qty, 'product_id': line.product_id.id, 'account_id': income_account, - 'analytic_account_id': self._prepare_analytic_account(cr, uid, line, context=context), - 'credit': ((amount>0) and amount) or 0.0, - 'debit': ((amount<0) and -amount) or 0.0, - 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + 'analytic_account_id': self._prepare_analytic_account(line), + 'credit': ((amount > 0) and amount) or 0.0, + 'debit': ((amount < 0) and -amount) or 0.0, + 'partner_id': partner_id }) # Create the tax lines - taxes = [] - for t in line.tax_ids_after_fiscal_position: - if t.company_id.id == current_company.id: - taxes.append(t.id) + taxes = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id) if not taxes: continue - for tax in account_tax_obj.browse(cr,uid, taxes, context=context).compute_all(line.price_unit * (100.0-line.discount) / 100.0, cur, line.qty)['taxes']: + for tax in taxes.compute_all(line.price_unit * (100.0 - line.discount) / 100.0, cur, line.qty)['taxes']: insert_data('tax', { 'name': _('Tax') + ' ' + tax['name'], 'product_id': line.product_id.id, 'quantity': line.qty, 'account_id': tax['account_id'] or income_account, - 'credit': ((tax['amount']>0) and tax['amount']) or 0.0, - 'debit': ((tax['amount']<0) and -tax['amount']) or 0.0, + 'credit': ((tax['amount'] > 0) and tax['amount']) or 0.0, + 'debit': ((tax['amount'] < 0) and -tax['amount']) or 0.0, 'tax_line_id': tax['id'], - 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + 'partner_id': partner_id }) # counterpart insert_data('counter_part', { - 'name': _("Trade Receivables"), #order.name, + 'name': _("Trade Receivables"), # order.name, 'account_id': order_account, 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, - 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + 'partner_id': partner_id }) - order.write({'state':'done', 'account_move': move_id}) + order.write({'state': 'done', 'account_move': move.id}) all_lines = [] for group_key, group_data in grouped_data.iteritems(): for value in group_data: all_lines.append((0, 0, value),) - if move_id: #In case no order was changed - self.pool.get("account.move").write(cr, SUPERUSER_ID, [move_id], {'line_ids':all_lines}, context=context) - self.pool.get("account.move").post(cr, SUPERUSER_ID, [move_id], context=context) - + if move: # In case no order was changed + move.sudo().write({'line_ids': all_lines}) + move.sudo().post() return True - def action_payment(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'payment'}, context=context) + def _default_session(self): + return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) - def action_paid(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'paid'}, context=context) - self.create_picking(cr, uid, ids, context=context) + def _default_pricelist(self): + return self._default_session().config_id.pricelist_id + + name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/') + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) + date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now) + user_id = fields.Many2one('res.users', string='Salesman', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", default=lambda self: self.env.uid) + amount_tax = fields.Float(compute='_compute_amount_all', string='Taxes', digits=0) + amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) + amount_paid = fields.Float(compute='_compute_amount_all', string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits=0) + amount_return = fields.Float(compute='_compute_amount_all', string='Returned', digits=0) + lines = fields.One2many('pos.order.line', 'order_id', string='Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True) + statement_ids = fields.One2many('account.bank.statement.line', 'pos_statement_id', string='Payments', states={'draft': [('readonly', False)]}, readonly=True) + pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, states={ + 'draft': [('readonly', False)]}, readonly=True, default=_default_pricelist) + partner_id = fields.Many2one('res.partner', string='Customer', change_default=True, index=True, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}) + sequence_number = fields.Integer(string='Sequence Number', help='A session-unique sequence number for the order', default=1) + + session_id = fields.Many2one( + 'pos.session', string='Session', required=True, index=True, + domain="[('state', '=', 'opened')]", states={'draft': [('readonly', False)]}, + readonly=True, default=_default_session) + config_id = fields.Many2one('pos.config', related='session_id.config_id', string="Point of Sale") + state = fields.Selection( + [('draft', 'New'), ('cancel', 'Cancelled'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced')], + 'Status', readonly=True, copy=False, default='draft') + + invoice_id = fields.Many2one('account.invoice', string='Invoice', copy=False) + account_move = fields.Many2one('account.move', string='Journal Entry', readonly=True, copy=False) + picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False) + picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Picking Type") + location_id = fields.Many2one('stock.location', related='session_id.config_id.stock_location_id', string="Location", store=True) + note = fields.Text(string='Internal Notes') + nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0) + pos_reference = fields.Char(string='Receipt Ref', readonly=True, copy=False) + sale_journal = fields.Many2one('account.journal', related='session_id.config_id.journal_id', string='Sale Journal', store=True, readonly=True) + fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position') + + @api.depends('statement_ids', 'lines.price_subtotal_incl', 'lines.discount') + def _compute_amount_all(self): + for order in self: + order.amount_paid = order.amount_return = order.amount_tax = 0.0 + currency = order.pricelist_id.currency_id + order.amount_paid = sum(payment.amount for payment in order.statement_ids) + order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.statement_ids) + order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines)) + amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines)) + order.amount_total = order.amount_tax + amount_untaxed + + @api.onchange('partner_id') + def _onchange_partner_id(self): + if self.partner_id: + self.pricelist = self.partner_id.property_product_pricelist.id + + @api.multi + def write(self, vals): + res = super(PosOrder, self).write(vals) + Partner = self.env['res.partner'] + # If you change the partner of the PoS order, change also the partner of the associated bank statement lines + if 'partner_id' in vals: + for order in self: + partner_id = False + if order.invoice_id: + raise UserError(_("You cannot change the partner of a POS order for which an invoice has already been issued.")) + if vals['partner_id']: + partner = Partner.browse(vals['partner_id']) + partner_id = Partner._find_accounting_partner(partner).id + order.statement_ids.write({'partner_id': partner_id}) + return res + + @api.multi + def unlink(self): + for pos_order in self.filtered(lambda pos_order: pos_order.state not in ['draft', 'cancel']): + raise UserError(_('In order to delete a sale, it must be new or cancelled.')) + return super(PosOrder, self).unlink() + + @api.model + def create(self, values): + if values.get('session_id'): + # set name based on the sequence specified on the config + session = self.env['pos.session'].browse(values['session_id']) + values['name'] = session.config_id.sequence_id._next() + values.setdefault('session_id', session.config_id.pricelist_id.id) + else: + # fallback on any pos.order sequence + values['name'] = self.env['ir.sequence'].next_by_code('pos.order') + return super(PosOrder, self).create(values) + + @api.multi + def action_invoice_state(self): + self.write({'state': 'invoiced'}) + + @api.multi + def action_invoice(self): + Invoice = self.env['account.invoice'] + + for order in self: + # Force company for all SUPERUSER_ID action + local_context = dict(self.env.context, force_company=order.company_id.id, company_id=order.company_id.id) + if order.invoice_id: + Invoice += order.invoice_id + continue + + if not order.partner_id: + raise UserError(_('Please provide a partner for the sale.')) + + invoice = Invoice.new(order._prepare_invoice()) + invoice._onchange_partner_id() + + inv = invoice._convert_to_write(invoice._cache) + new_invoice = Invoice.with_context(local_context).sudo().create(inv) + order.write({'invoice_id': new_invoice.id, 'state': 'invoiced'}) + Invoice += new_invoice + + for line in order.lines: + self.with_context(local_context)._action_create_invoice_line(line, new_invoice.id) + + new_invoice.with_context(local_context).sudo().compute_taxes() + order.sudo().signal_workflow('invoice') + new_invoice.sudo().signal_workflow('validate') + + if not Invoice: + return {} + + return { + 'name': _('Customer Invoice'), + 'view_type': 'form', + 'view_mode': 'form', + 'view_id': self.env.ref('account.invoice_form').id, + 'res_model': 'account.invoice', + 'context': "{'type':'out_invoice'}", + 'type': 'ir.actions.act_window', + 'nodestroy': True, + 'target': 'current', + 'res_id': Invoice and Invoice.ids[0] or False, + } + + @api.multi + def action_paid(self): + self.write({'state': 'paid'}) + self.create_picking() + + @api.multi + def action_cancel(self): + self.write({'state': 'cancel'}) + + @api.multi + def action_done(self): + self._create_account_move_line() + + @api.model + def create_from_ui(self, orders): + # Keep only new orders + submitted_references = [o['data']['name'] for o in orders] + pos_order = self.search([('pos_reference', 'in', submitted_references)]) + existing_orders = pos_order.read(['pos_reference']) + existing_references = set([o['pos_reference'] for o in existing_orders]) + orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] + order_ids = [] + + for tmp_order in orders_to_save: + to_invoice = tmp_order['to_invoice'] + pos_order = self._process_order(tmp_order['data']) + order_ids.append(pos_order.id) + + try: + pos_order.signal_workflow('paid') + except Exception as e: + _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) + + if to_invoice: + pos_order.action_invoice() + pos_order.invoice_id.sudo().signal_workflow('invoice_open') + return order_ids + + def test_paid(self): + """A Point of Sale is paid when the sum + @return: True + """ + for order in self: + if order.lines and not order.amount_total: + return True + if (not order.lines) or (not order.statement_ids) or (abs(order.amount_total - order.amount_paid) > 0.00001): + return False return True - def action_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) + def create_picking(self): + """Create a picking for each order and validate it.""" + Picking = self.env['stock.picking'] + Move = self.env['stock.move'] + StockWarehouse = self.env['stock.warehouse'] + for order in self: + address = order.partner_id.address_get(['delivery']) or {} + picking_type = order.picking_type_id + picking_id = False + location_id = order.location_id.id + if order.partner_id: + destination_id = order.partner_id.property_stock_customer.id + else: + if (not picking_type) or (not picking_type.default_location_dest_id): + customerloc, supplierloc = StockWarehouse._get_partner_locations([]) + destination_id = customerloc.id + else: + destination_id = picking_type.default_location_dest_id.id + if picking_type: + pos_qty = all([x.qty >= 0 for x in order.lines]) + picking_id = Picking.create({ + 'origin': order.name, + 'partner_id': address.get('delivery', False), + 'date_done': order.date_order, + 'picking_type_id': picking_type.id, + 'company_id': order.company_id.id, + 'move_type': 'direct', + 'note': order.note or "", + 'location_id': location_id if pos_qty else destination_id, + 'location_dest_id': destination_id if pos_qty else location_id, + }) + order.write({'picking_id': picking_id.id}) + + for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']): + Move += Move.create({ + 'name': line.name, + 'product_uom': line.product_id.uom_id.id, + 'picking_id': picking_id.id, + 'picking_type_id': picking_type.id, + 'product_id': line.product_id.id, + 'product_uom_qty': abs(line.qty), + 'state': 'draft', + 'location_id': location_id if line.qty >= 0 else destination_id, + 'location_dest_id': destination_id if line.qty >= 0 else location_id, + }) + if picking_id: + picking_id.action_confirm() + picking_id.force_assign() + # Mark pack operations as done + for pack in picking_id.pack_operation_ids: + pack.write({'qty_done': pack.product_qty}) + picking_id.action_done() + elif Move: + Move.action_confirm() + Move.force_assign() + Move.action_done() return True - def action_done(self, cr, uid, ids, context=None): - self._create_account_move_line(cr, uid, ids, context=context) - return True + def add_payment(self, data): + """Create a new payment for the order""" + args = { + 'amount': data['amount'], + 'date': data.get('payment_date', fields.Date.today()), + 'name': self.name + ': ' + (data.get('payment_name', '') or ''), + 'partner_id': self.env["res.partner"]._find_accounting_partner(self.partner_id).id or False, + } -class pos_order_line(osv.osv): + journal_id = data.get('journal', False) + statement_id = data.get('statement_id', False) + assert journal_id or statement_id, "No statement_id or journal_id passed to the method!" + + journal = self.env['account.journal'].browse(journal_id) + # use the company of the journal and not of the current user + company_cxt = dict(self.env.context, force_company=journal.company_id.id) + account_def = self.env['ir.property'].with_context(company_cxt).get('property_account_receivable_id', 'res.partner') + args['account_id'] = (self.partner_id.property_account_receivable_id.id) or (account_def and account_def.id) or False + + if not args['account_id']: + if not args['partner_id']: + msg = _('There is no receivable account defined to make payment.') + else: + msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % ( + self.partner_id.name, self.partner_id.id,) + raise UserError(msg) + + context = dict(self.env.context) + context.pop('pos_session_id', False) + for statement in self.session_id.statement_ids: + if statement.id == statement_id: + journal_id = statement.journal_id.id + break + elif statement.journal_id.id == journal_id: + statement_id = statement.id + break + if not statement_id: + raise UserError(_('You have to open at least one cashbox.')) + + args.update({ + 'statement_id': statement_id, + 'pos_statement_id': self.id, + 'journal_id': journal_id, + 'ref': self.session_id.name, + }) + self.env['account.bank.statement.line'].with_context(context).create(args) + return statement_id + + @api.multi + def refund(self): + """Create a copy of order for refund order""" + PosOrder = self.env['pos.order'] + current_session = self.env['pos.session'].search([('state', '!=', 'closed'), ('user_id', '=', self.env.uid)], limit=1) + if not current_session: + raise UserError(_('To return product(s), you need to open a session that will be used to register the refund.')) + for order in self: + clone = order.copy({ + # ot used, name forced by create + 'name': order.name + _(' REFUND'), + 'session_id': current_session.id, + 'date_order': fields.Datetime.now(), + }) + PosOrder += clone + + for clone in PosOrder: + for order_line in clone.lines: + order_line.write({'qty': -order_line.qty}) + return { + 'name': _('Return Products'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'pos.order', + 'res_id': PosOrder.ids[0], + 'view_id': False, + 'context': self.env.context, + 'type': 'ir.actions.act_window', + 'target': 'current', + } + + +class PosOrderLine(models.Model): _name = "pos.order.line" _description = "Lines of Point of Sale" _rec_name = "product_id" - def _order_line_fields(self, cr, uid, line, context=None): + def _order_line_fields(self, line): if line and 'tax_ids' not in line[2]: - product = self.pool['product.product'].browse(cr, uid, line[2]['product_id'], context=context) + product = self.env['product.product'].browse(line[2]['product_id']) line[2]['tax_ids'] = [(6, 0, [x.id for x in product.taxes_id])] return line - def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None): - res = dict([(i, {}) for i in ids]) - account_tax_obj = self.pool.get('account.tax') - for line in self.browse(cr, uid, ids, context=context): - cur = line.order_id.pricelist_id.currency_id - taxes = [ tax for tax in line.tax_ids if tax.company_id.id == line.order_id.company_id.id ] + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) + name = fields.Char(string='Line No', required=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('pos.order.line')) + notice = fields.Char(string='Discount Notice') + product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], required=True, change_default=True) + price_unit = fields.Float(string='Unit Price', digits=0) + qty = fields.Float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), default=1) + price_subtotal = fields.Float(compute='_compute_amount_line_all', digits=0, string='Subtotal w/o Tax') + price_subtotal_incl = fields.Float(compute='_compute_amount_line_all', digits=0, string='Subtotal') + discount = fields.Float(string='Discount (%)', digits=0, default=0.0) + order_id = fields.Many2one('pos.order', string='Order Ref', ondelete='cascade') + create_date = fields.Datetime(string='Creation Date', readonly=True) + tax_ids = fields.Many2many('account.tax', string='Taxes', readonly=True) + tax_ids_after_fiscal_position = fields.Many2many('account.tax', compute='_get_tax_ids_after_fiscal_position', string='Taxes') + + @api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'product_id') + def _compute_amount_line_all(self): + for line in self: + currency = line.order_id.pricelist_id.currency_id + taxes = line.tax_ids.filtered(lambda tax: tax.company_id.id == line.order_id.company_id.id) fiscal_position_id = line.order_id.fiscal_position_id if fiscal_position_id: taxes = fiscal_position_id.map_tax(taxes) - taxes_ids = [ tax.id for tax in taxes ] price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - res[line.id]['price_subtotal'] = res[line.id]['price_subtotal_incl'] = price * line.qty - if taxes_ids: - taxes = account_tax_obj.browse(cr, uid, taxes_ids, context).compute_all(price, cur, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) - res[line.id]['price_subtotal'] = taxes['total_excluded'] - res[line.id]['price_subtotal_incl'] = taxes['total_included'] - return res + line.price_subtotal = line.price_subtotal_incl = price * line.qty + if taxes: + taxes = taxes.compute_all(price, currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) + line.price_subtotal = taxes['total_excluded'] + line.price_subtotal_incl = taxes['total_included'] - def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None): - context = context or {} - if not product_id: - return {} - if not pricelist: - raise UserError( - _('You have to select a pricelist in the sale form !\n' \ - 'Please set one before choosing a product.')) + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + if not self.order_id.pricelist_id: + raise UserError( + _('You have to select a pricelist in the sale form !\n' + 'Please set one before choosing a product.')) + price = self.order_id.pricelist_id.price_get( + self.product_id.id, self.qty or 1.0, self.order_id.partner_id.id)[self.order_id.pricelist_id.id] + self.onchange_qty() + self.price_unit = price + self.tax_ids = self.product_id.taxes_id - price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist], - product_id, qty or 1.0, partner_id)[pricelist] + @api.onchange('qty', 'discount', 'price_unit', 'tax_ids') + def _onchange_qty(self): + if self.product_id: + if not self.order_id.pricelist_id: + raise UserError(_('You have to select a pricelist in the sale form !')) + price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) + self.price_subtotal = self.price_subtotal_incl = price * self.qty + if (self.product_id.taxes_id): + taxes = self.product_id.taxes_id.compute_all(price, self.order_id.pricelist_id.currency_id, self.qty, product=self.product_id, partner=False) + self.price_subtotal = taxes['total_excluded'] + self.price_subtotal_incl = taxes['total_included'] - result = self.onchange_qty(cr, uid, ids, pricelist, product_id, 0.0, qty, price, context=context) - result['value']['price_unit'] = price - - prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context) - result['value']['tax_ids'] = prod.taxes_id.ids - - return result - - def onchange_qty(self, cr, uid, ids, pricelist, product, discount, qty, price_unit, context=None): - result = {} - if not product: - return result - if not pricelist: - raise UserError(_('You have to select a pricelist in the sale form !')) - - account_tax_obj = self.pool.get('account.tax') - - prod = self.pool.get('product.product').browse(cr, uid, product, context=context) - - price = price_unit * (1 - (discount or 0.0) / 100.0) - result['price_subtotal'] = result['price_subtotal_incl'] = price * qty - cur = self.pool.get('product.pricelist').browse(cr, uid, [pricelist], context=context).currency_id - if (prod.taxes_id): - taxes = prod.taxes_id.compute_all(price, cur, qty, product=prod, partner=False) - result['price_subtotal'] = taxes['total_excluded'] - result['price_subtotal_incl'] = taxes['total_included'] - return {'value': result} - - def _get_tax_ids_after_fiscal_position(self, cr, uid, ids, field_name, args, context=None): - res = dict.fromkeys(ids, False) - for line in self.browse(cr, uid, ids, context=context): - res[line.id] = line.order_id.fiscal_position_id.map_tax(line.tax_ids) - return res - - _columns = { - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'name': fields.char('Line No', required=True, copy=False), - 'notice': fields.char('Discount Notice'), - 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True), - 'price_unit': fields.float(string='Unit Price', digits=0), - 'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')), - 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', digits=0, string='Subtotal w/o Tax'), - 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', digits=0, string='Subtotal'), - 'discount': fields.float('Discount (%)', digits=0), - 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'), - 'create_date': fields.datetime('Creation Date', readonly=True), - 'tax_ids': fields.many2many('account.tax', string='Taxes'), - 'tax_ids_after_fiscal_position': fields.function(_get_tax_ids_after_fiscal_position, type='many2many', relation='account.tax', string='Taxes') - } - - _defaults = { - 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'pos.order.line', context=context), - 'qty': lambda *a: 1, - 'discount': lambda *a: 0.0, - 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - } + @api.multi + def _get_tax_ids_after_fiscal_position(self): + for line in self: + line.tax_ids_after_fiscal_position = line.order_id.fiscal_position_id.map_tax(line.tax_ids) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index b1f408afb68..b29f6960882 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -1,161 +1,124 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import time -from openerp.osv import fields, osv -from openerp.tools.translate import _ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError -class pos_session(osv.osv): +class PosSession(models.Model): _name = 'pos.session' _order = 'id desc' POS_SESSION_STATE = [ ('opening_control', 'Opening Control'), # Signal open - ('opened', 'In Progress'), # Signal closing + ('opened', 'In Progress'), # Signal closing ('closing_control', 'Closing Control'), # Signal close ('closed', 'Closed & Posted'), ] - def _compute_cash_all(self, cr, uid, ids, fieldnames, args, context=None): - result = dict() + def _confirm_orders(self): + for session in self: + company_id = session.config_id.journal_id.company_id.id + orders = session.order_ids.filtered(lambda order: order.state == 'paid') + move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, session.config_id.journal_id.id, company_id) + orders.with_context(force_company=company_id)._create_account_move_line(session, move) + for order in session.order_ids.filtered(lambda o: o.state != 'done'): + if order.state not in ('paid', 'invoiced'): + raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) + order.signal_workflow('done') - for record in self.browse(cr, uid, ids, context=context): - result[record.id] = { - 'cash_journal_id' : False, - 'cash_register_id' : False, - 'cash_control' : False, - } + config_id = fields.Many2one( + 'pos.config', string='Point of Sale', + help="The physical point of sale you will use.", + required=True, + index=True, + domain="[('state', '=', 'active')]") + name = fields.Char(string='Session ID', required=True, readonly=True, default='/') + user_id = fields.Many2one( + 'res.users', string='Responsible', + required=True, + index=True, + readonly=True, + states={'opening_control': [('readonly', False)]}, + default=lambda self: self.env.uid) + currency_id = fields.Many2one('res.currency', related='config_id.currency_id', string="Currency") + start_at = fields.Datetime(string='Opening Date', readonly=True) + stop_at = fields.Datetime(string='Closing Date', readonly=True, copy=False) - if record.config_id.cash_control: - for st in record.statement_ids: - if st.journal_id.type == 'cash': - result[record.id]['cash_control'] = True - result[record.id]['cash_journal_id'] = st.journal_id.id - result[record.id]['cash_register_id'] = st.id + state = fields.Selection( + POS_SESSION_STATE, string='Status', + required=True, readonly=True, + index=True, copy=False, default='opening_control') - if not result[record.id]['cash_control']: + sequence_number = fields.Integer(string='Order Sequence Number', help='A sequence number that is incremented with each order', default=1) + login_number = fields.Integer(string='Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session', default=0) + + cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control') + cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_all', string='Cash Journal', store=True) + cash_register_id = fields.Many2one('account.bank.statement', compute='_compute_cash_all', string='Cash Register', store=True) + + cash_register_balance_end_real = fields.Monetary( + related='cash_register_id.balance_end_real', + string="Ending Balance", + help="Total of closing cash control lines.", + readonly=True) + cash_register_balance_start = fields.Monetary( + related='cash_register_id.balance_start', + string="Starting Balance", + help="Total of opening cash control lines.", + readonly=True) + cash_register_total_entry_encoding = fields.Monetary( + related='cash_register_id.total_entry_encoding', + string='Total Cash Transaction', + readonly=True, + help="Total of all paid sale orders") + cash_register_balance_end = fields.Monetary( + related='cash_register_id.balance_end', + digits=0, + string="Theoretical Closing Balance", + help="Sum of opening balance and transactions.", + readonly=True) + cash_register_difference = fields.Monetary( + related='cash_register_id.difference', + string='Difference', + help="Difference between the theoretical closing balance and the real closing balance.", + readonly=True) + + journal_ids = fields.Many2many( + 'account.journal', + related='config_id.journal_ids', + readonly=True, + string='Available Payment Methods') + order_ids = fields.One2many('pos.order', 'session_id', string='Orders') + statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Bank Statement', readonly=True) + + _sql_constraints = [('uniq_name', 'unique(name)', _("The name of this POS Session must be unique !"))] + + @api.depends('cash_control', 'cash_journal_id', 'config_id.cash_control') + def _compute_cash_all(self): + for session in self: + session.cash_journal_id = session.cash_register_id = session.cash_control = False + if session.config_id.cash_control: + for statement in session.statement_ids: + if statement.journal_id.type == 'cash': + session.cash_control = True + session.cash_journal_id = statement.journal_id.id + session.cash_register_id = statement.id + if not session.cash_control: raise UserError(_("Cash control can only be applied to cash journals.")) - return result + @api.constrains('user_id', 'state') + def _check_unicity(self): + # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user + if self.search_count([('state', 'not in', ('closed', 'closing_control')), ('user_id', '=', self.user_id.id)]) > 1: + raise UserError(_("You cannot create two active sessions with the same responsible!")) - _columns = { - 'config_id' : fields.many2one('pos.config', 'Point of Sale', - help="The physical point of sale you will use.", - required=True, - select=1, - domain="[('state', '=', 'active')]", - ), + @api.constrains('config_id') + def _check_pos_config(self): + if self.search_count([('state', '!=', 'closed'), ('config_id', '=', self.config_id.id)]) > 1: + raise UserError(_("You cannot create two active sessions related to the same point of sale!")) - 'name' : fields.char('Session ID', required=True, readonly=True), - 'user_id' : fields.many2one('res.users', 'Responsible', - required=True, - select=1, - readonly=True, - states={'opening_control' : [('readonly', False)]} - ), - 'currency_id' : fields.related('config_id', 'currency_id', type="many2one", relation='res.currency', string="Currency"), - 'start_at' : fields.datetime('Opening Date', readonly=True), - 'stop_at' : fields.datetime('Closing Date', readonly=True, copy=False), - - 'state' : fields.selection(POS_SESSION_STATE, 'Status', - required=True, readonly=True, - select=1, copy=False), - - 'sequence_number': fields.integer('Order Sequence Number', help='A sequence number that is incremented with each order'), - 'login_number': fields.integer('Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session'), - - 'cash_control' : fields.function(_compute_cash_all, - multi='cash', - type='boolean', string='Has Cash Control'), - 'cash_journal_id' : fields.function(_compute_cash_all, - multi='cash', - type='many2one', relation='account.journal', - string='Cash Journal', store=True), - 'cash_register_id' : fields.function(_compute_cash_all, - multi='cash', - type='many2one', relation='account.bank.statement', - string='Cash Register', store=True), - - 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', - type='float', - digits=0, - string="Ending Balance", - help="Total of closing cash control lines.", - readonly=True), - 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', - type='float', - digits=0, - string="Starting Balance", - help="Total of opening cash control lines.", - readonly=True), - 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', - string='Total Cash Transaction', - readonly=True, - help="Total of all paid sale orders"), - 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', - type='float', - digits=0, - string="Theoretical Closing Balance", - help="Sum of opening balance and transactions.", - readonly=True), - 'cash_register_difference' : fields.related('cash_register_id', 'difference', - type='float', - string='Difference', - help="Difference between the theoretical closing balance and the real closing balance.", - readonly=True), - - 'journal_ids' : fields.related('config_id', 'journal_ids', - type='many2many', - readonly=True, - relation='account.journal', - string='Available Payment Methods'), - 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), - - 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), - } - - _defaults = { - 'name' : '/', - 'user_id' : lambda obj, cr, uid, context: uid, - 'state' : 'opening_control', - 'sequence_number': 1, - 'login_number': 0, - } - - _sql_constraints = [ - ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), - ] - - def _check_unicity(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=None): - # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user - domain = [ - ('state', 'not in', ('closed','closing_control')), - ('user_id', '=', session.user_id.id) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - def _check_pos_config(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=None): - domain = [ - ('state', '!=', 'closed'), - ('config_id', '=', session.config_id.id) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - _constraints = [ - (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']), - (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']), - ] - - def create(self, cr, uid, values, context=None): - context = dict(context or {}) - config_id = values.get('config_id', False) or context.get('default_config_id', False) + @api.model + def create(self, values): + config_id = values.get('config_id') or self.env.context.get('default_config_id') if not config_id: raise UserError(_("You should assign a Point of Sale to your session.")) @@ -163,145 +126,105 @@ class pos_session(osv.osv): # exists at the installation. If nothing is configured at the # installation we do the minimal configuration. Impossible to do in # the .xml files as the CoA is not yet installed. - jobj = self.pool.get('pos.config') - pos_config = jobj.browse(cr, uid, config_id, context=context) - context.update({'company_id': pos_config.company_id.id}) + pos_config = self.env['pos.config'].browse(config_id) + ctx = dict(self.env.context, company_id=pos_config.company_id.id) if not pos_config.journal_id: - jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] - if jid: - jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_id': jid}, context=context) - else: + jid = pos_config.with_context(ctx).default_get(['journal_id'])['journal_id'] + if not jid: raise UserError(_("Unable to open the session. You have to assign a sale journal to your point of sale.")) - + pos_config.with_context(ctx).sudo().write({'journal_id': jid}) # define some cash journal if no payment method exists if not pos_config.journal_ids: - journal_proxy = self.pool.get('account.journal') - cashids = journal_proxy.search(cr, uid, [('journal_user', '=', True), ('type','=','cash')], context=context) - if not cashids: - cashids = journal_proxy.search(cr, uid, [('type', '=', 'cash')], context=context) - if not cashids: - cashids = journal_proxy.search(cr, uid, [('journal_user','=',True)], context=context) - - journal_proxy.write(cr, SUPERUSER_ID, cashids, {'journal_user': True}) - jobj.write(cr, SUPERUSER_ID, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) - - - pos_config = jobj.browse(cr, uid, config_id, context=context) - + Journal = self.env['account.journal'] + journals = Journal.with_context(ctx).search([('journal_user', '=', True), ('type', '=', 'cash')]) + if not journals: + journals = Journal.with_context(ctx).search([('type', '=', 'cash')]) + if not journals: + journals = Journal.with_context(ctx).search([('journal_user', '=', True)]) + journals.sudo().write({'journal_user': True}) + pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]}) statements = [(0, 0, { 'journal_id': journal.id, - 'user_id': uid, + 'user_id': self.env.uid, 'company_id': pos_config.company_id.id }) for journal in pos_config.journal_ids] - values.update({ - 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session', context=context), + 'name': self.env['ir.sequence'].with_context(ctx).next_by_code('pos.session'), 'statement_ids': statements, 'config_id': config_id }) - # set the journal_id which should be used by - # account.bank.statement to set the opening balance of the - # newly created bank statement + # account.bank.statement to set the opening balance of the newly created bank statement if pos_config.cash_control: - for journal in pos_config.journal_ids: - if journal.type == 'cash': - context.update({'journal_id': journal.id}) + for journal in pos_config.journal_ids.filtered(lambda j: j.type == 'cash'): + ctx.update({'journal_id': journal.id}) + return super(PosSession, self.with_context(ctx)).create(values) - return super(pos_session, self).create(cr, uid, values, context=context) + @api.multi + def unlink(self): + for session in self.filtered(lambda s: s.statement_ids): + session.statement_ids.unlink() + return super(PosSession, self).unlink() - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - self.pool.get('account.bank.statement').unlink(cr, uid, obj.statement_ids.ids, context=context) - return super(pos_session, self).unlink(cr, uid, ids, context=context) - - def login(self, cr, uid, ids, context=None): - this_record = self.browse(cr, uid, ids[0], context=context) - this_record.write({ - 'login_number': this_record.login_number+1, + @api.multi + def login(self): + self.ensure_one() + self.write({ + 'login_number': self.login_number + 1, }) - def wkf_action_open(self, cr, uid, ids, context=None): + def wkf_action_open(self): # second browse because we need to refetch the data from the DB for cash_register_id - for record in self.browse(cr, uid, ids, context=context): + for session in self: values = {} - if not record.start_at: - values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S') + if not session.start_at: + values['start_at'] = fields.Datetime.now() values['state'] = 'opened' - record.write(values) - for st in record.statement_ids: - st.button_open() - + session.write(values) + session.statement_ids.button_open() return True - def wkf_action_opening_control(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context) + def wkf_action_opening_control(self): + return self.write({'state': 'opening_control'}) - def wkf_action_closing_control(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=context): + def wkf_action_closing_control(self): + for session in self: for statement in session.statement_ids: if (statement != session.cash_register_id) and (statement.balance_end != statement.balance_end_real): - self.pool.get('account.bank.statement').write(cr, uid, [statement.id], {'balance_end_real': statement.balance_end}) - return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) + statement.write({'balance_end_real': statement.balance_end}) + session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()}) - def wkf_action_close(self, cr, uid, ids, context=None): + def wkf_action_close(self): # Close CashBox - local_context = dict(context) - for record in self.browse(cr, uid, ids, context=context): - company_id = record.config_id.company_id.id - local_context.update({'force_company': company_id, 'company_id': company_id}) - for st in record.statement_ids: + for session in self: + company_id = session.config_id.company_id.id + ctx = dict(self.env.context, force_company=company_id, company_id=company_id) + for st in session.statement_ids: if abs(st.difference) > st.journal_id.amount_authorized_diff: # The pos manager can close statements with maximums. - if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"): + if not self.env['ir.model.access'].check_groups("point_of_sale.group_pos_manager"): raise UserError(_("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff)) if (st.journal_id.type not in ['bank', 'cash']): raise UserError(_("The type of the journal for your payment method should be bank or cash ")) - self.pool['account.bank.statement'].button_confirm_bank(cr, SUPERUSER_ID, [st.id], context=local_context) - self._confirm_orders(cr, uid, ids, context=local_context) - self.write(cr, uid, ids, {'state' : 'closed'}, context=local_context) - - obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1] + st.with_context(ctx).sudo().button_confirm_bank() + self.with_context(ctx)._confirm_orders() + self.write({'state': 'closed'}) return { - 'type' : 'ir.actions.client', - 'name' : 'Point of Sale Menu', - 'tag' : 'reload', - 'params' : {'menu_id': obj}, + 'type': 'ir.actions.client', + 'name': 'Point of Sale Menu', + 'tag': 'reload', + 'params': {'menu_id': self.env.ref('point_of_sale.menu_point_root').id}, } - def _confirm_orders(self, cr, uid, ids, context=None): - pos_order_obj = self.pool.get('pos.order') - for session in self.browse(cr, uid, ids, context=context): - company_id = session.config_id.journal_id.company_id.id - local_context = dict(context or {}, force_company=company_id) - order_ids = [order.id for order in session.order_ids if order.state == 'paid'] - - move_id = pos_order_obj._create_account_move(cr, uid, session.start_at, session.name, session.config_id.journal_id.id, company_id, context=context) - - pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context) - - for order in session.order_ids: - if order.state == 'done': - continue - if order.state not in ('paid', 'invoiced'): - raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) - else: - pos_order_obj.signal_workflow(cr, uid, [order.id], 'done') - - return True - - def open_frontend_cb(self, cr, uid, ids, context=None): - if not context: - context = {} - if not ids: + @api.multi + def open_frontend_cb(self): + if not self.ids: return {} - for session in self.browse(cr, uid, ids, context=context): - if session.user_id.id != uid: - raise UserError(_("You cannot use the session of another users. This session is owned by %s. " - "Please first close this one to use this point of sale.") % session.user_id.name) - context.update({'active_id': ids[0]}) + for session in self.filtered(lambda s: s.user_id.id != self.env.uid): + raise UserError(_("You cannot use the session of another users. This session is owned by %s. " + "Please first close this one to use this point of sale.") % session.user_id.name) return { - 'type' : 'ir.actions.act_url', + 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/pos/web/', } diff --git a/addons/point_of_sale/models/product.py b/addons/point_of_sale/models/product.py index dd2607c4d60..4e350eed91c 100644 --- a/addons/point_of_sale/models/product.py +++ b/addons/point_of_sale/models/product.py @@ -1,27 +1,22 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools.translate import _ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError -class product_template(osv.osv): +class ProductTemplate(models.Model): _inherit = 'product.template' - _columns = { - 'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'), - 'to_weight' : fields.boolean('To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration"), - 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', help="Those categories are used to group similar products for point of sale."), - } + available_in_pos = fields.Boolean(string='Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale', default=True) + to_weight = fields.Boolean(string='To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration") + pos_categ_id = fields.Many2one( + 'pos.category', string='Point of Sale Category', + help="Those categories are used to group similar products for point of sale.") - _defaults = { - 'to_weight' : False, - 'available_in_pos': True, - } - - def unlink(self, cr, uid, ids, context=None): - product_ctx = dict(context or {}, active_test=False) - if self.search_count(cr, uid, [('id', 'in', ids), ('available_in_pos', '=', True)], context=product_ctx): - if self.pool['pos.session'].search_count(cr, uid, [('state', '!=', 'closed')], context=context): - raise osv.except_osv(_('Error!'), - _('You cannot delete a product saleable in point of sale while a session is still opened.')) - return super(product_template, self).unlink(cr, uid, ids, context=context) + @api.multi + def unlink(self): + product_ctx = dict(self.env.context or {}, active_test=False) + if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('available_in_pos', '=', True)]): + if self.env['pos.session'].search_count([('state', '!=', 'closed')]): + raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.')) + return super(ProductTemplate, self).unlink() diff --git a/addons/point_of_sale/models/res_config.py b/addons/point_of_sale/models/res_config.py index 765156bd15c..cfadb6c3699 100644 --- a/addons/point_of_sale/models/res_config.py +++ b/addons/point_of_sale/models/res_config.py @@ -1,29 +1,31 @@ -from openerp.osv import fields, osv +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields, models -class pos_configuration(osv.TransientModel): + +class PosConfiguration(models.TransientModel): _inherit = 'res.config.settings' _name = 'pos.config.settings' - _columns = { - 'module_pos_restaurant': fields.selection([ - (0, "Point of sale for shops"), - (1, "Restaurant: activate table management") - ], "Restaurant", - help='This module adds several restaurant features to the Point of Sale: \n\n- Bill Printing: Allows you to print a receipt before the order is paid \n\n- Bill Splitting: Allows you to split an order into different orders \n\n- Kitchen Order Printing: allows you to print orders updates to kitchen or bar printers'), - 'module_pos_loyalty': fields.boolean("Loyalty Program", - help='Allows you to define a loyalty program in the point of sale, where the customers earn loyalty points and get rewards'), - 'module_pos_discount': fields.selection([ - (0, "Allow discounts on order lines only"), - (1, "Allow global discounts") - ], "Discount", - help='Allows the cashier to quickly give a percentage sale discount for all the sales order to a customer'), - 'module_pos_mercury': fields.selection([ - (0, "No credit card"), - (1, "Allows customers to pay with credit cards.") - ], "Credit Cards", - help='The transactions are processed by MercuryPay'), - 'module_pos_reprint': fields.selection([ - (0, "No reprint"), - (1, "Allow cashier to reprint receipts") - ], "Reprints"), - } + module_pos_restaurant = fields.Selection([ + (0, "Point of sale for shops"), + (1, "Restaurant: activate table management") + ], string="Restaurant", + help='This module adds several restaurant features to the Point of Sale: \n\n- Bill Printing: Allows you to print a receipt before the order is paid \n\n- Bill Splitting: Allows you to split an order into different orders \n\n- Kitchen Order Printing: allows you to print orders updates to kitchen or bar printers') + module_pos_loyalty = fields.Boolean( + string="Loyalty Program", + help='Allows you to define a loyalty program in the point of sale, where the customers earn loyalty points and get rewards') + module_pos_discount = fields.Selection([ + (0, "Allow discounts on order lines only"), + (1, "Allow global discounts") + ], string="Discount", + help='Allows the cashier to quickly give a percentage sale discount for all the sales order to a customer') + module_pos_mercury = fields.Selection([ + (0, "No credit card"), + (1, "Allows customers to pay with credit cards.") + ], string="Credit Cards", + help='The transactions are processed by MercuryPay') + module_pos_reprint = fields.Selection([ + (0, "No reprint"), + (1, "Allow cashier to reprint receipts") + ], string="Reprints") diff --git a/addons/point_of_sale/models/res_partner.py b/addons/point_of_sale/models/res_partner.py index b5167b9bea7..a94f0a79b60 100644 --- a/addons/point_of_sale/models/res_partner.py +++ b/addons/point_of_sale/models/res_partner.py @@ -1,27 +1,24 @@ - -from openerp.osv import osv, fields +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import api, fields, models -class res_users(osv.osv): +class ResPartner(models.Model): _inherit = 'res.partner' - _columns = { - 'barcode' : fields.char('Barcode', help="BarCode", oldname='ean13'), - } - def create_from_ui(self, cr, uid, partner, context=None): + barcode = fields.Char(string='Barcode', help="BarCode", oldname='ean13') + + @api.model + def create_from_ui(self, partner): """ create or modify a partner from the point of sale ui. partner contains the partner's fields. """ - - #image is a dataurl, get the data after the comma - if partner.get('image',False): - img = partner['image'].split(',')[1] - partner['image'] = img - - if partner.get('id',False): # Modifying existing partner - partner_id = partner['id'] - del partner['id'] - self.write(cr, uid, [partner_id], partner, context=context) + # image is a dataurl, get the data after the comma + if partner.get('image'): + partner['image'] = partner['image'].split(',')[1] + partner_id = partner.pop('id', False) + if partner_id: # Modifying existing partner + self.browse(partner_id).write(partner) else: - partner_id = self.create(cr, uid, partner, context=context) + partner_id = self.create(partner).id return partner_id diff --git a/addons/point_of_sale/models/res_users.py b/addons/point_of_sale/models/res_users.py index e036d9062c7..6fb09e8a44e 100644 --- a/addons/point_of_sale/models/res_users.py +++ b/addons/point_of_sale/models/res_users.py @@ -1,19 +1,15 @@ - -from openerp.osv import osv, fields +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import api, fields, models, _ +from odoo.exceptions import UserError -class res_users(osv.osv): +class ResUsers(models.Model): _inherit = 'res.users' - _columns = { - 'pos_security_pin': fields.char('Security PIN',size=32, help='A Security PIN used to protect sensible functionality in the Point of Sale'), - } - def _check_pin(self, cr, uid, ids, context=None): - for user in self.browse(cr, uid, ids, context=context): - if user.pos_security_pin and not user.pos_security_pin.isdigit(): - return False - return True + pos_security_pin = fields.Char(string='Security PIN', size=32, help='A Security PIN used to protect sensible functionality in the Point of Sale') - _constraints = [ - (_check_pin, "Security PIN can only contain digits",['pos_security_pin']), - ] + @api.constrains('pos_security_pin') + def _check_pin(self): + if self.pos_security_pin and not self.pos_security_pin.isdigit(): + raise UserError(_("Security PIN can only contain digits")) diff --git a/addons/point_of_sale/report/pos_invoice.py b/addons/point_of_sale/report/pos_invoice.py index 4a7087eb66c..9ae226d4c56 100644 --- a/addons/point_of_sale/report/pos_invoice.py +++ b/addons/point_of_sale/report/pos_invoice.py @@ -1,34 +1,27 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import SUPERUSER_ID -from openerp.osv import osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from odoo import api, models, _ +from odoo.exceptions import UserError -class PosInvoiceReport(osv.AbstractModel): +class PosInvoiceReport(models.AbstractModel): _name = 'report.point_of_sale.report_invoice' - def render_html(self, cr, uid, ids, data=None, context=None): - report_obj = self.pool['report'] - posorder_obj = self.pool['pos.order'] - report = report_obj._get_report_from_name(cr, uid, 'account.report_invoice') - selected_orders = posorder_obj.browse(cr, uid, ids, context=context) + @api.multi + def render_html(self, data=None): + Report = self.env['report'] + PosOrder = self.env['pos.order'] ids_to_print = [] invoiced_posorders_ids = [] - for order in selected_orders: - if order.invoice_id: - ids_to_print.append(order.invoice_id.id) - invoiced_posorders_ids.append(order.id) - - not_invoiced_orders_ids = list(set(ids) - set(invoiced_posorders_ids)) + selected_orders = PosOrder.browse(self.ids) + for order in selected_orders.filtered(lambda o: o.invoice_id): + ids_to_print.append(order.invoice_id.id) + invoiced_posorders_ids.append(order.id) + not_invoiced_orders_ids = list(set(self.ids) - set(invoiced_posorders_ids)) if not_invoiced_orders_ids: - not_invoiced_posorders = posorder_obj.browse(cr, uid, not_invoiced_orders_ids, context=context) + not_invoiced_posorders = PosOrder.browse(not_invoiced_orders_ids) not_invoiced_orders_names = list(map(lambda a: a.name, not_invoiced_posorders)) raise UserError(_('No link to an invoice for %s.') % ', '.join(not_invoiced_orders_names)) - docargs = { - 'docs': self.pool['account.invoice'].browse(cr, uid, ids_to_print, context=context) - } - return report_obj.render(cr, SUPERUSER_ID, ids, 'account.report_invoice', docargs, context=context) + return Report.sudo().render('account.report_invoice', {'docs': self.env['account.invoice'].browse(ids_to_print)}) diff --git a/addons/point_of_sale/report/pos_order_report.py b/addons/point_of_sale/report/pos_order_report.py index 7d528f8649d..25daab1ca6a 100644 --- a/addons/point_of_sale/report/pos_order_report.py +++ b/addons/point_of_sale/report/pos_order_report.py @@ -1,64 +1,65 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import tools -from openerp.osv import fields,osv +from odoo import fields, models, tools -class pos_order_report(osv.osv): + +class PosOrderReport(models.Model): _name = "report.pos.order" _description = "Point of Sale Orders Statistics" _auto = False - - _columns = { - 'date': fields.datetime('Date Order', readonly=True), - 'partner_id':fields.many2one('res.partner', 'Partner', readonly=True), - 'product_id':fields.many2one('product.product', 'Product', readonly=True), - 'product_tmpl_id': fields.many2one('product.template', 'Product Template', readonly=True), - 'state': fields.selection([('draft', 'New'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced'), ('cancel', 'Cancelled')], - 'Status'), - 'user_id':fields.many2one('res.users', 'Salesperson', readonly=True), - 'price_total':fields.float('Total Price', readonly=True), - 'price_sub_total':fields.float('Subtotal w/o discount', readonly=True), - 'total_discount':fields.float('Total Discount', readonly=True), - 'average_price': fields.float('Average Price', readonly=True,group_operator="avg"), - 'location_id':fields.many2one('stock.location', 'Location', readonly=True), - 'company_id':fields.many2one('res.company', 'Company', readonly=True), - 'nbr':fields.integer('# of Lines', readonly=True), # TDE FIXME master: rename into nbr_lines - 'product_qty':fields.integer('Product Quantity', readonly=True), - 'journal_id': fields.many2one('account.journal', 'Journal'), - 'delay_validation': fields.integer('Delay Validation'), - 'product_categ_id': fields.many2one('product.category', 'Product Category', readonly=True), - 'invoiced': fields.boolean('Invoiced', readonly=True), - 'config_id' : fields.many2one('pos.config', 'Point of Sale', readonly=True), - 'pos_categ_id': fields.many2one('pos.category','Public Category', readonly=True), - 'stock_location_id': fields.many2one('stock.location', 'Warehouse', readonly=True), - 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True), - 'session_id;: fields.many2one('pos.session', string='Session', readonly=True), - } _order = 'date desc' + date = fields.Datetime(string='Date Order', readonly=True) + partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) + product_id = fields.Many2one('product.product', string='Product', readonly=True) + product_tmpl_id = fields.Many2one('product.template', string='Product Template', readonly=True) + state = fields.Selection( + [('draft', 'New'), ('paid', 'Paid'), ('done', 'Posted'), + ('invoiced', 'Invoiced'), ('cancel', 'Cancelled')], + string='Status') + user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) + price_total = fields.Float(string='Total Price', readonly=True) + price_sub_total = fields.Float(string='Subtotal w/o discount', readonly=True) + total_discount = fields.Float(string='Total Discount', readonly=True) + average_price = fields.Float(string='Average Price', readonly=True, group_operator="avg") + location_id = fields.Many2one('stock.location', string='Location', readonly=True) + company_id = fields.Many2one('res.company', string='Company', readonly=True) + nbr_lines = fields.Integer(string='# of Lines', readonly=True, oldname='nbr') + product_qty = fields.Integer(string='Product Quantity', readonly=True) + journal_id = fields.Many2one('account.journal', string='Journal') + delay_validation = fields.Integer(string='Delay Validation') + product_categ_id = fields.Many2one('product.category', string='Product Category', readonly=True) + invoiced = fields.Boolean(readonly=True) + config_id = fields.Many2one('pos.config', string='Point of Sale', readonly=True) + pos_categ_id = fields.Many2one('pos.category', string='Public Category', readonly=True) + stock_location_id = fields.Many2one('stock.location', string='Warehouse', readonly=True) + pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', readonly=True) + session_id = fields.Many2one('pos.session', string='Session', readonly=True) + + def init(self, cr): tools.drop_view_if_exists(cr, 'report_pos_order') cr.execute(""" - create or replace view report_pos_order as ( - select - min(l.id) as id, - count(*) as nbr, - s.date_order as date, - sum(l.qty * u.factor) as product_qty, - sum(l.qty * l.price_unit) as price_sub_total, - sum((l.qty * l.price_unit) * (100 - l.discount) / 100) as price_total, - sum((l.qty * l.price_unit) * (l.discount / 100)) as total_discount, - (sum(l.qty*l.price_unit)/sum(l.qty * u.factor))::decimal as average_price, - sum(cast(to_char(date_trunc('day',s.date_order) - date_trunc('day',s.create_date),'DD') as int)) as delay_validation, - s.partner_id as partner_id, - s.state as state, - s.user_id as user_id, - s.location_id as location_id, - s.company_id as company_id, - s.sale_journal as journal_id, - l.product_id as product_id, - pt.categ_id as product_categ_id, + CREATE OR REPLACE VIEW report_pos_order AS ( + SELECT + MIN(l.id) AS id, + COUNT(*) AS nbr_lines, + s.date_order AS date, + SUM(l.qty * u.factor) AS product_qty, + SUM(l.qty * l.price_unit) AS price_sub_total, + SUM((l.qty * l.price_unit) * (100 - l.discount) / 100) AS price_total, + SUM((l.qty * l.price_unit) * (l.discount / 100)) AS total_discount, + (SUM(l.qty*l.price_unit)/SUM(l.qty * u.factor))::decimal AS average_price, + SUM(cast(to_char(date_trunc('day',s.date_order) - date_trunc('day',s.create_date),'DD') AS INT)) AS delay_validation, + s.partner_id AS partner_id, + s.state AS state, + s.user_id AS user_id, + s.location_id AS location_id, + s.company_id AS company_id, + s.sale_journal AS journal_id, + l.product_id AS product_id, + pt.categ_id AS product_categ_id, p.product_tmpl_id, ps.config_id, pt.pos_categ_id, @@ -66,14 +67,14 @@ class pos_order_report(osv.osv): s.pricelist_id, s.session_id, s.invoice_id IS NOT NULL AS invoiced - from pos_order_line as l - left join pos_order s on (s.id=l.order_id) - left join product_product p on (l.product_id=p.id) - left join product_template pt on (p.product_tmpl_id=pt.id) - left join product_uom u on (u.id=pt.uom_id) - left join pos_session ps on (s.session_id=ps.id) - left join pos_config pc on (ps.config_id=pc.id) - group by + FROM pos_order_line AS l + LEFT JOIN pos_order s ON (s.id=l.order_id) + LEFT JOIN product_product p ON (l.product_id=p.id) + LEFT JOIN product_template pt ON (p.product_tmpl_id=pt.id) + LEFT JOIN product_uom u ON (u.id=pt.uom_id) + LEFT JOIN pos_session ps ON (s.session_id=ps.id) + LEFT JOIN pos_config pc ON (ps.config_id=pc.id) + GROUP BY s.date_order, s.partner_id,s.state, pt.categ_id, s.user_id, s.location_id, s.company_id, s.sale_journal, s.pricelist_id, s.invoice_id, s.create_date, s.session_id, diff --git a/addons/point_of_sale/security/point_of_sale_security.xml b/addons/point_of_sale/security/point_of_sale_security.xml index 1c4cf30e363..d08ee045b5b 100644 --- a/addons/point_of_sale/security/point_of_sale_security.xml +++ b/addons/point_of_sale/security/point_of_sale_security.xml @@ -1,7 +1,5 @@ - - - + User @@ -71,5 +69,4 @@ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] - - + diff --git a/addons/point_of_sale/views/account_journal_view.xml b/addons/point_of_sale/views/account_journal_view.xml index dbd7f41299e..6e62407dc61 100644 --- a/addons/point_of_sale/views/account_journal_view.xml +++ b/addons/point_of_sale/views/account_journal_view.xml @@ -1,29 +1,26 @@ - - - - - Payment Methods - account.journal - form - tree,form - - [('journal_user','=', 1)] - -

+ + + + Payment Methods + account.journal + form + tree,form + + [('journal_user','=', 1)] + +

Click to add a payment method. -

+

Payment methods are defined by accounting journals having the field PoS Payment Method checked. In order to be useable from the touchscreen interface, you must set the payment method on the Point of Sale configuration. -

-
-
+

+ +
- - - - \ No newline at end of file + + diff --git a/addons/point_of_sale/views/account_statement_report.xml b/addons/point_of_sale/views/account_statement_report.xml index 61cc1b891f4..bc1176bd314 100644 --- a/addons/point_of_sale/views/account_statement_report.xml +++ b/addons/point_of_sale/views/account_statement_report.xml @@ -1,6 +1,5 @@ - - - + + - - + diff --git a/addons/point_of_sale/views/account_statement_view.xml b/addons/point_of_sale/views/account_statement_view.xml index 7bca1e9a251..67b072b1a5d 100644 --- a/addons/point_of_sale/views/account_statement_view.xml +++ b/addons/point_of_sale/views/account_statement_view.xml @@ -1,8 +1,7 @@ - - - - POS Journal + + + account.journal.pos.user.form.inherit account.journal @@ -16,6 +15,4 @@ - - - + diff --git a/addons/point_of_sale/views/point_of_sale.xml b/addons/point_of_sale/views/point_of_sale.xml index 62d1694ded2..9856a6f58c1 100644 --- a/addons/point_of_sale/views/point_of_sale.xml +++ b/addons/point_of_sale/views/point_of_sale.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml index ddfa5f35a8e..77ea4c3b215 100644 --- a/addons/point_of_sale/views/point_of_sale_dashboard.xml +++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml @@ -1,4 +1,4 @@ - + @@ -105,7 +105,6 @@
View
- @@ -117,7 +116,6 @@
Reports
- diff --git a/addons/point_of_sale/views/point_of_sale_report.xml b/addons/point_of_sale/views/point_of_sale_report.xml index 7d68e896f41..f9787c1b4c7 100644 --- a/addons/point_of_sale/views/point_of_sale_report.xml +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -1,24 +1,21 @@ - - - - + + + - - - Invoice - pos.order - qweb-pdf - point_of_sale.report_invoice - + + + Invoice + pos.order + qweb-pdf + point_of_sale.report_invoice + - - - +
diff --git a/addons/point_of_sale/views/point_of_sale_sequence.xml b/addons/point_of_sale/views/point_of_sale_sequence.xml index c9a277cb9b5..e9a9a3872e9 100644 --- a/addons/point_of_sale/views/point_of_sale_sequence.xml +++ b/addons/point_of_sale/views/point_of_sale_sequence.xml @@ -1,18 +1,12 @@ - - + + - - # - # Sequences for pos.order - # - - + POS Session pos.session POS/%(year)s/%(month)s/%(day)s/ 2 - - + diff --git a/addons/point_of_sale/views/point_of_sale_view.xml b/addons/point_of_sale/views/point_of_sale_view.xml index 547b8d64285..7ae52d0a79e 100644 --- a/addons/point_of_sale/views/point_of_sale_view.xml +++ b/addons/point_of_sale/views/point_of_sale_view.xml @@ -1,14 +1,19 @@ - - - + + + + - - + - - + + + + + diff --git a/addons/point_of_sale/views/pos_category_view.xml b/addons/point_of_sale/views/pos_category_view.xml index 3b5f1106ace..60ea3c6358c 100644 --- a/addons/point_of_sale/views/pos_category_view.xml +++ b/addons/point_of_sale/views/pos_category_view.xml @@ -1,59 +1,54 @@ - - - - - - pos.category.form - pos.category - -
- - -
- - - - - -
-
-
-
-
- - pos.category.tree - pos.category - - - - - - - - - - Pos Product Categories - ir.actions.act_window - pos.category - form - tree,form - - -

+ + + + pos.category.form + pos.category + +

+ + +
+ + + + + +
+
+
+
+
+ + pos.category.tree + pos.category + + + + + + + + + + Pos Product Categories + ir.actions.act_window + pos.category + form + tree,form + + +

Click to define a new category. -

+

Categories are used to browse your products through the touchscreen interface. -

- If you put a photo on the category and enable the option in the - point of sale configuration, the layout of the touchscreen interface - will automatically be displayed. We suggest not to put a photo - on categories for small (1024x768) screens. -

-
-
+

+ If you put a photo on the category, the layout of the + touchscreen interface will automatically. We suggest not to put + a photo on categories for small (1024x768) screens. +

+ + - - -
-
\ No newline at end of file + + diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 18b8d0e5c6b..830ab75f697 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -1,147 +1,139 @@ - - - - - pos.config.form.view - pos.config - -
-
-
+ + + + pos.config.form.view + pos.config + + +
+
- - - -
-
+ +
+
- - pos.config.tree.view - pos.config - - - - - - - - + + pos.config.tree.view + pos.config + + + + + + + + - - pos.config.search.view - pos.config - - - - - - - - - + + pos.config.search.view + pos.config + + + + + + + + + - + - - Point of Sale - ir.actions.act_window - pos.config - form - tree,form - - + + Point of Sale + ir.actions.act_window + pos.config + form + tree,form + + - + - - Point of Sale - ir.actions.act_window - pos.config - form - kanban,tree,form - - -

+ + Point of Sale + ir.actions.act_window + pos.config + form + kanban,tree,form + + +

Click to create a new PoS config. -

+

Configure at least one Point of Sale to be able to sell through the PoS interface. -

-
-
+

+ + - - + + - - - -
-
\ No newline at end of file + + diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml index 7351a30926b..ce5aa141a4f 100644 --- a/addons/point_of_sale/views/pos_order_report_view.xml +++ b/addons/point_of_sale/views/pos_order_report_view.xml @@ -1,6 +1,5 @@ - - + report.pos.order.pivot report.pos.order @@ -69,6 +68,5 @@ {'group_by_no_leaf':1,'group_by':[]} - - - + + diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml index 79c31b00706..c9ee5c8693d 100644 --- a/addons/point_of_sale/views/pos_order_view.xml +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -1,276 +1,253 @@ - - - - - pos.order - pos.order - -
-
-
- - - - - - - - - - - - - - - + + + + pos.order.form + pos.order + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + - - -
- - - - - - - - - - - -
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - Orders - ir.actions.act_window - pos.order - form - tree,form - - [] - -

+ + - -

-

- Session: - -

+ + pos.session.form.view + pos.session + + +
+
+ + +
+ + + + +
+

+ Session: + +

+ + - - - - - - - - - - - - -
- - - -
-
-

- You can define another list of available currencies on the - Cash Registers tab of the - payment method. -

-
-
-
- - - - - - - - -
+ + + - - - - - - - - - - - - - + + + + + - - - - - - - pos.session.tree.view - pos.session - - - - - - - - - - - - - - pos.session.search.view - pos.session - - - - - - - - - - - +
+ + + +
+
+

+ You can define another list of available currencies on the + Cash Registers tab of the + payment method. +

+
+
+
+ + + + + + + + +
- - - + + + + + + + + + + + + + + + + + + + pos.session.tree.view + pos.session + + + + + + + + + + + - - Sessions - ir.actions.act_window - pos.session - form - tree,form - - -

+ + pos.session.search.view + pos.session + + + + + + + + + + + + + + + + + + Sessions + ir.actions.act_window + pos.session + form + tree,form + + +

Click to start a new session. -

+

A session is a period of time, usually one day, during which you sell through the point of sale. The user has to check the currencies in your cash registers at the beginning and the end of each session. -

+

Note that you may use the menu Your Session to quickly open a new session. -

-
-
+

+ + - - - - + + diff --git a/addons/point_of_sale/views/pos_templates.xml b/addons/point_of_sale/views/pos_templates.xml index 1c6627b631f..528db600ed5 100644 --- a/addons/point_of_sale/views/pos_templates.xml +++ b/addons/point_of_sale/views/pos_templates.xml @@ -1,8 +1,5 @@ - - - - + - - - + diff --git a/addons/point_of_sale/views/product_view.xml b/addons/point_of_sale/views/product_view.xml index 34623e184fe..8b6183acd0e 100644 --- a/addons/point_of_sale/views/product_view.xml +++ b/addons/point_of_sale/views/product_view.xml @@ -1,59 +1,56 @@ - - - - - Products - ir.actions.act_window - product.template - form - tree,form,kanban - - - - - -

+ + + + Products + ir.actions.act_window + product.template + form + tree,form,kanban + + + + + +

Click to add a new product. -

+

You must define a product for everything you sell through the point of sale interface. -

+

Do not forget to set the price and the point of sale category in which it should appear. If a product has no point of sale category, you can not sell it through the point of sale interface. -

-
-
- +

+ + + - - product.template.form.inherit - product.template - - - - - - - - + + product.template.form.inherit + product.template + + + + + + + + + + + + product.template.tree.inherit + product.template + + + + - - - product.template.tree.inherit - product.template - - - - - - - - -
-
\ No newline at end of file + + + diff --git a/addons/point_of_sale/views/report_userlabel.xml b/addons/point_of_sale/views/report_userlabel.xml index 6337e3151d3..bc02fc98db7 100644 --- a/addons/point_of_sale/views/report_userlabel.xml +++ b/addons/point_of_sale/views/report_userlabel.xml @@ -1,6 +1,5 @@ - - + - - \ No newline at end of file + diff --git a/addons/point_of_sale/views/res_config_view.xml b/addons/point_of_sale/views/res_config_view.xml index 610d96c4d46..767d83ee34a 100644 --- a/addons/point_of_sale/views/res_config_view.xml +++ b/addons/point_of_sale/views/res_config_view.xml @@ -1,6 +1,5 @@ - - + pos.config.settings.form pos.config.settings @@ -33,7 +32,6 @@ inline - - - + diff --git a/addons/point_of_sale/views/res_partner_view.xml b/addons/point_of_sale/views/res_partner_view.xml index 3320bc3a3cc..746aab53613 100644 --- a/addons/point_of_sale/views/res_partner_view.xml +++ b/addons/point_of_sale/views/res_partner_view.xml @@ -1,7 +1,5 @@ - - - + res.partner.pos.form.inherit res.partner @@ -14,6 +12,4 @@ - - - + diff --git a/addons/point_of_sale/views/res_users_view.xml b/addons/point_of_sale/views/res_users_view.xml index 47d8c7863de..84df0988d24 100644 --- a/addons/point_of_sale/views/res_users_view.xml +++ b/addons/point_of_sale/views/res_users_view.xml @@ -1,6 +1,5 @@ - - + res.users.form.view res.users @@ -16,5 +15,4 @@ - - + diff --git a/addons/point_of_sale/wizard/pos_box.py b/addons/point_of_sale/wizard/pos_box.py index e013aea3167..653a3f5b3c8 100644 --- a/addons/point_of_sale/wizard/pos_box.py +++ b/addons/point_of_sale/wizard/pos_box.py @@ -1,62 +1,48 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from odoo import api, _ +from odoo.exceptions import UserError + +from odoo.addons.account.wizard.pos_box import CashBox -from openerp.addons.account.wizard.pos_box import CashBox class PosBox(CashBox): _register = False - def run(self, cr, uid, ids, context=None): - if not context: - context = dict() - - active_model = context.get('active_model', False) or False - active_ids = context.get('active_ids', []) or [] + @api.multi + def run(self): + active_model = self.env.context.get('active_model', False) + active_ids = self.env.context.get('active_ids', []) if active_model == 'pos.session': - records = self.pool[active_model].browse(cr, uid, active_ids, context=context) - bank_statements = [record.cash_register_id for record in records if record.cash_register_id] - + bank_statements = [session.cash_register_id for session in self.env[active_model].browse(active_ids) if session.cash_register_id] if not bank_statements: raise UserError(_("There is no cash register for this PoS Session")) - - return self._run(cr, uid, ids, bank_statements, context=context) + return self._run(bank_statements) else: - return super(PosBox, self).run(cr, uid, ids, context=context) + return super(PosBox, self).run() + class PosBoxIn(PosBox): _inherit = 'cash.box.in' - def _compute_values_for_statement_line(self, cr, uid, box, record, context=None): - - if context is None: - context = {} - - values = super(PosBoxIn, self)._compute_values_for_statement_line(cr, uid, box, record, context=context) - - active_model = context.get('active_model', False) or False - active_ids = context.get('active_ids', []) or [] - - if active_model == 'pos.session': - session = self.pool[active_model].browse(cr, uid, active_ids, context=context)[0] - values['ref'] = session.name - + def _compute_values_for_statement_line(self, box, record): + values = super(PosBoxIn, self)._compute_values_for_statement_line(box=box, record=record) + active_model = self.env.context.get('active_model', False) + active_ids = self.env.context.get('active_ids', []) + if active_model == 'pos.session' and active_ids: + values['ref'] = self.env[active_model].browse(active_ids)[0].name return values class PosBoxOut(PosBox): _inherit = 'cash.box.out' - def _compute_values_for_statement_line(self, cr, uid, box, record, context=None): - values = super(PosBoxOut, self)._compute_values_for_statement_line(cr, uid, box, record, context=context) - - active_model = context.get('active_model', False) or False - active_ids = context.get('active_ids', []) or [] - - if active_model == 'pos.session': - session = self.pool[active_model].browse(cr, uid, active_ids, context=context)[0] - values['ref'] = session.name - + def _compute_values_for_statement_line(self, box, record): + values = super(PosBoxOut, self)._compute_values_for_statement_line(box=box, record=record) + active_model = self.env.context.get('active_model', False) + active_ids = self.env.context.get('active_ids', []) + if active_model == 'pos.session' and active_ids: + values['ref'] = self.env[active_model].browse(active_ids)[0].name return values diff --git a/addons/point_of_sale/wizard/pos_box.xml b/addons/point_of_sale/wizard/pos_box.xml index 1a4f7fb411e..9be27e1cde6 100644 --- a/addons/point_of_sale/wizard/pos_box.xml +++ b/addons/point_of_sale/wizard/pos_box.xml @@ -1,22 +1,20 @@ - - + + key2="client_action_multi"/> - - + key2="client_action_multi"/> + diff --git a/addons/point_of_sale/wizard/pos_discount.py b/addons/point_of_sale/wizard/pos_discount.py index 7be3692b9bb..d45f81055b8 100644 --- a/addons/point_of_sale/wizard/pos_discount.py +++ b/addons/point_of_sale/wizard/pos_discount.py @@ -1,37 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import osv, fields +from odoo import api, fields, models -class pos_discount(osv.osv_memory): +class PosDiscount(models.TransientModel): _name = 'pos.discount' _description = 'Add a Global Discount' - _columns = { - 'discount': fields.float('Discount (%)', required=True, digits=(16,2)), - } - _defaults = { - 'discount': 5, - } - def apply_discount(self, cr, uid, ids, context=None): - """ - To give the discount of product and check the. + discount = fields.Float(string='Discount (%)', required=True, digits=(16, 2), default=5) - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param context: A standard dictionary - @return : nothing - """ - order_ref = self.pool.get('pos.order') - order_line_ref = self.pool.get('pos.order.line') - if context is None: - context = {} - this = self.browse(cr, uid, ids[0], context=context) - record_id = context and context.get('active_id', False) - if isinstance(record_id, (int, long)): - record_id = [record_id] - for order in order_ref.browse(cr, uid, record_id, context=context): - order_line_ref.write(cr, uid, [x.id for x in order.lines], {'discount':this.discount}, context=context) - return {} + @api.multi + def apply_discount(self): + self.ensure_one() + for order in self.env['pos.order'].browse(self.env.context.get('active_id', False)): + order.lines.write({'discount': self.discount}) diff --git a/addons/point_of_sale/wizard/pos_discount.xml b/addons/point_of_sale/wizard/pos_discount.xml index c01be27fdcf..e060a7900ab 100644 --- a/addons/point_of_sale/wizard/pos_discount.xml +++ b/addons/point_of_sale/wizard/pos_discount.xml @@ -1,10 +1,7 @@ - - - - + - Apply Discount + pos.discount.form pos.discount
@@ -27,6 +24,4 @@ form new - - - + diff --git a/addons/point_of_sale/wizard/pos_open_statement.py b/addons/point_of_sale/wizard/pos_open_statement.py index 7737957465e..befa94b0324 100644 --- a/addons/point_of_sale/wizard/pos_open_statement.py +++ b/addons/point_of_sale/wizard/pos_open_statement.py @@ -1,58 +1,32 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from odoo import api, models, _ +from odoo.exceptions import UserError -class pos_open_statement(osv.osv_memory): + +class PosOpenStatement(models.TransientModel): _name = 'pos.open.statement' _description = 'Open Statements' - def open_statement(self, cr, uid, ids, context=None): - """ - Open the statements - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param context: A standard dictionary - @return : Blank Directory - """ - data = {} - mod_obj = self.pool.get('ir.model.data') - statement_obj = self.pool.get('account.bank.statement') - sequence_obj = self.pool.get('ir.sequence') - journal_obj = self.pool.get('account.journal') - if context is None: - context = {} - - st_ids = [] - j_ids = journal_obj.search(cr, uid, [('journal_user','=',1)], context=context) - if not j_ids: + @api.multi + def open_statement(self): + self.ensure_one() + BankStatement = self.env['account.bank.statement'] + journals = self.env['account.journal'].search([('journal_user', '=', True)]) + if not journals: raise UserError(_('You have to define which payment method must be available in the point of sale by reusing existing bank and cash through "Accounting / Configuration / Journals / Journals". Select a journal and check the field "PoS Payment Method" from the "Point of Sale" tab. You can also create new payment methods directly from menu "PoS Backend / Configuration / Payment Methods".')) - for journal in journal_obj.browse(cr, uid, j_ids, context=context): - ids = statement_obj.search(cr, uid, [('state', '!=', 'confirm'), ('user_id', '=', uid), ('journal_id', '=', journal.id)], context=context) - + for journal in journals: if journal.sequence_id: - number = sequence_obj.next_by_id(cr, uid, journal.sequence_id.id, context=context) + number = journal.sequence_id.next_by_id() else: raise UserError(_("No sequence defined on the journal")) + BankStatement += BankStatement.create({'journal_id': journal.id, 'user_id': self.env.uid, 'name': number}) - data.update({ - 'journal_id': journal.id, - 'user_id': uid, - 'name': number - }) - statement_id = statement_obj.create(cr, uid, data, context=context) - st_ids.append(int(statement_id)) - - tree_res = mod_obj.get_object_reference(cr, uid, 'account', 'view_bank_statement_tree') - tree_id = tree_res and tree_res[1] or False - form_res = mod_obj.get_object_reference(cr, uid, 'account', 'view_bank_statement_form') - form_id = form_res and form_res[1] or False - search_res = mod_obj.get_object_reference(cr, uid, 'account', 'view_bank_statement_search') - search_id = search_res and search_res[1] or False + tree_id = self.env.ref('account.view_bank_statement_tree').id + form_id = self.env.ref('account.view_bank_statement_form').id + search_id = self.env.ref('account.view_bank_statement_search').id return { 'type': 'ir.actions.act_window', @@ -60,7 +34,7 @@ class pos_open_statement(osv.osv_memory): 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.bank.statement', - 'domain': str([('id', 'in', st_ids)]), + 'domain': str([('id', 'in', BankStatement.ids)]), 'views': [(tree_id, 'tree'), (form_id, 'form')], 'search_view_id': search_id, } diff --git a/addons/point_of_sale/wizard/pos_open_statement.xml b/addons/point_of_sale/wizard/pos_open_statement.xml index cc10f44dce1..9ae3d8450a4 100644 --- a/addons/point_of_sale/wizard/pos_open_statement.xml +++ b/addons/point_of_sale/wizard/pos_open_statement.xml @@ -1,10 +1,7 @@ - - - - + - Open Statements + pos.open.statement.form pos.open.statement @@ -17,13 +14,14 @@ - + key2="client_action_multi"/> Open Cash Register @@ -33,6 +31,4 @@ form new - - -
+
diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py index ed53de42cdd..87fffb3da0a 100644 --- a/addons/point_of_sale/wizard/pos_payment.py +++ b/addons/point_of_sale/wizard/pos_payment.py @@ -1,41 +1,52 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import time - -from openerp.osv import osv, fields -from openerp.tools.translate import _ +from odoo import api, fields, models, _ -class pos_make_payment(osv.osv_memory): +class PosMakePayment(models.TransientModel): _name = 'pos.make.payment' _description = 'Point of Sale Payment' - def check(self, cr, uid, ids, context=None): + def _default_journal(self): + active_id = self.env.context.get('active_id') + if active_id: + session = self.env['pos.order'].browse(active_id).session_id + return session.config_id.journal_ids and session.config_id.journal_ids.ids[0] or False + return False + + def _default_amount(self): + active_id = self.env.context.get('active_id') + if active_id: + order = self.env['pos.order'].browse(active_id) + return (order.amount_total - order.amount_paid) + return False + + journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal) + amount = fields.Float(digits=(16, 2), required=True, default=_default_amount) + payment_name = fields.Char(string='Payment Reference') + payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Datetime.now()) + + @api.multi + def check(self): """Check the order: if the order is not paid: continue payment, if the order is paid print ticket. """ - context = context or {} - order_obj = self.pool.get('pos.order') - active_id = context and context.get('active_id', False) - - order = order_obj.browse(cr, uid, active_id, context=context) + self.ensure_one() + order = self.env['pos.order'].browse(self.env.context.get('active_id', False)) amount = order.amount_total - order.amount_paid - data = self.read(cr, uid, ids, context=context)[0] + data = self.read()[0] # this is probably a problem of osv_memory as it's not compatible with normal OSV's data['journal'] = data['journal_id'][0] - if amount != 0.0: - order_obj.add_payment(cr, uid, active_id, data, context=context) + order.add_payment(data) + if order.test_paid(): + order.signal_workflow('paid') + return {'type': 'ir.actions.act_window_close'} + return self.launch_payment() - if order_obj.test_paid(cr, uid, [active_id]): - order_obj.signal_workflow(cr, uid, [active_id], 'paid') - return {'type' : 'ir.actions.act_window_close' } - - return self.launch_payment(cr, uid, ids, context=context) - - def launch_payment(self, cr, uid, ids, context=None): + def launch_payment(self): return { 'name': _('Payment'), 'view_type': 'form', @@ -45,40 +56,6 @@ class pos_make_payment(osv.osv_memory): 'target': 'new', 'views': False, 'type': 'ir.actions.act_window', - 'context': context, + 'context': self.env.context, } - - def _default_journal(self, cr, uid, context=None): - if not context: - context = {} - session = False - order_obj = self.pool.get('pos.order') - active_id = context and context.get('active_id', False) - if active_id: - order = order_obj.browse(cr, uid, active_id, context=context) - session = order.session_id - if session: - for journal in session.config_id.journal_ids: - return journal.id - return False - - def _default_amount(self, cr, uid, context=None): - order_obj = self.pool.get('pos.order') - active_id = context and context.get('active_id', False) - if active_id: - order = order_obj.browse(cr, uid, active_id, context=context) - return order.amount_total - order.amount_paid - return False - - _columns = { - 'journal_id' : fields.many2one('account.journal', 'Payment Mode', required=True), - 'amount': fields.float('Amount', digits=(16,2), required= True), - 'payment_name': fields.char('Payment Reference'), - 'payment_date': fields.date('Payment Date', required=True), - } - _defaults = { - 'journal_id' : _default_journal, - 'payment_date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), - 'amount': _default_amount, - } \ No newline at end of file diff --git a/addons/point_of_sale/wizard/pos_payment.xml b/addons/point_of_sale/wizard/pos_payment.xml index 3b0ec0a679f..5ab532b821f 100644 --- a/addons/point_of_sale/wizard/pos_payment.xml +++ b/addons/point_of_sale/wizard/pos_payment.xml @@ -1,9 +1,7 @@ - - - + - Pay Order + pos.make.payment.form pos.make.payment
@@ -27,6 +25,4 @@ form new - - - + From 62040dea69c0b519775c459cdbabf3f10434af52 Mon Sep 17 00:00:00 2001 From: Darshan Kalola Date: Mon, 5 Oct 2015 18:17:51 +0530 Subject: [PATCH 8/9] [REF] point_of_sale: converted yml test cases to python unit test --- addons/point_of_sale/__openerp__.py | 8 - .../point_of_sale/test/00_register_open.yml | 11 - .../test/01_order_to_payment.yml | 136 ----------- .../test/02_order_to_invoice.yml | 53 ----- .../test/account_statement_reports.yml | 68 ------ addons/point_of_sale/test/tests_before.xml | 13 -- addons/point_of_sale/tests/__init__.py | 4 + addons/point_of_sale/tests/common.py | 60 +++++ .../tests/test_point_of_sale_flow.py | 217 ++++++++++++++++++ 9 files changed, 281 insertions(+), 289 deletions(-) delete mode 100644 addons/point_of_sale/test/00_register_open.yml delete mode 100644 addons/point_of_sale/test/01_order_to_payment.yml delete mode 100644 addons/point_of_sale/test/02_order_to_invoice.yml delete mode 100644 addons/point_of_sale/test/account_statement_reports.yml delete mode 100644 addons/point_of_sale/test/tests_before.xml create mode 100644 addons/point_of_sale/tests/__init__.py create mode 100644 addons/point_of_sale/tests/common.py create mode 100644 addons/point_of_sale/tests/test_point_of_sale_flow.py diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 072db0b5c27..a42c2df9106 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -65,14 +65,6 @@ Main Features 'demo': [ 'data/point_of_sale_demo.xml', ], - 'test': [ - '../account/test/account_minimal_test.xml', - 'test/tests_before.xml', - 'test/00_register_open.yml', - 'test/01_order_to_payment.yml', - 'test/02_order_to_invoice.yml', - 'test/account_statement_reports.yml', - ], 'installable': True, 'application': True, 'qweb': ['static/src/xml/pos.xml'], diff --git a/addons/point_of_sale/test/00_register_open.yml b/addons/point_of_sale/test/00_register_open.yml deleted file mode 100644 index ce13f3c0f26..00000000000 --- a/addons/point_of_sale/test/00_register_open.yml +++ /dev/null @@ -1,11 +0,0 @@ -- - In order to test the Point of Sale module, I will open all cash registers through the wizard -- - I go to the window to open the cash registers as admin -- - !record {model: pos.open.statement, id: new_statement_open}: -- - I click on the button to open all statements/cash registers -- - !python {model: pos.open.statement}: | - self.open_statement(cr, uid, [ref('new_statement_open')], context={}) diff --git a/addons/point_of_sale/test/01_order_to_payment.yml b/addons/point_of_sale/test/01_order_to_payment.yml deleted file mode 100644 index a5f41fec71b..00000000000 --- a/addons/point_of_sale/test/01_order_to_payment.yml +++ /dev/null @@ -1,136 +0,0 @@ -- | - In order to test the Point of Sale in module, I will do a full flow from the sale to the payment and invoicing. - I will use two products, one with price including a 10% tax, the other one with 5% tax excluded from the price. -- | - I create a VAT tax of 10%, included in the public price -- - !record {model: account.tax, id: account_tax_10_incl}: - name: VAT 10 perc Incl - amount_type: percent - amount: 10.0 - price_include: 1 -- - I assign this 10 percent tax on the [PCSC234] PC Assemble SC234 product as a sale tax -- - !record {model: product.product, id: product.product_product_3, view: False}: - taxes_id: [account_tax_10_incl] -- | - I create a VAT tax of 5%, which is added to the public price -- - !record {model: account.tax, id: account_tax_05_incl}: - name: VAT 05 perc Excl - amount_type: percent - amount: 5.0 - price_include: 0 -- - I will create a second VAT tax of 5% but this time for a child company, to - ensure that only product taxes of the current session's company are considered - (this tax should be ignore when computing order's taxes in following tests) -- - !record {model: account.tax, id: account_tax_05_incl_chicago}: - name: VAT 05 perc Excl (US) - amount_type: percent - amount: 5.0 - price_include: 0 - company_id: stock.res_company_1 -- - I assign those 5 percent taxes on the PCSC349 product as a sale taxes -- - !record {model: product.product, id: product.product_product_4, view: False}: - taxes_id: [account_tax_05_incl, account_tax_05_incl_chicago] -- - I click on create a new session button -- - !python {model: pos.config}: | - self.open_session_cb(cr, uid, [ref('pos_config_main')]) -- - I create a PoS order with 2 units of PCSC234 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) -- - !record {model: pos.order, id: pos_order_pos0}: - company_id: base.main_company - lines: - - name: OL/0001 - product_id: product.product_product_3 - price_unit: 450 - discount: 0.0 - qty: 2.0 - - name: OL/0002 - product_id: product.product_product_4 - price_unit: 300 - discount: 0.0 - qty: 3.0 -- - I check that the total of the order is equal to 450*2 + 300*3*1.05 and the tax of the order is equal to 900 -(450 * 2 / 1.1) + 300*0.05*3 -- - !python {model: pos.order}: | - order = self.browse(cr, uid, ref('pos_order_pos0')) - assert(abs(order.amount_total - (450*2 + 300*3*1.05)) < 0.01), "The order has a wrong amount, tax included" - assert(abs(order.amount_tax - (900-(450*2/1.1) + 300*0.05*3)) < 0.01), "The order has a wrong tax amount" -- - I want to add a global discount of 5 percent using the wizard -- - !record {model: pos.discount, id: pos_discount_0}: - discount: 5.0 -- - I click the apply button to set the discount on all lines -- - !python {model: pos.discount}: | - self.apply_discount(cr, uid, [ref("pos_discount_0")], {"active_model": "pos.order", - "active_ids": [ref("pos_order_pos0")], "active_id": ref("pos_order_pos0"), }) -- - I check that the total of the order is now equal to (450*2 + 300*3*1.05)*0.95 -- - !python {model: pos.order}: | - order = self.browse(cr, uid, ref('pos_order_pos0')) - assert(abs(order.amount_total - (450*2 + 300*3*1.05)*0.95) < 0.01), "The order has a wrong total including tax and discounts" -- - I click on the "Make Payment" wizard to pay the PoS order with a partial amount of 100.0 EUR -- - !record {model: pos.make.payment, id: pos_make_payment_0, context: '{"active_id": ref("pos_order_pos0"), "active_ids": [ref("pos_order_pos0")]}' }: - amount: 100.0 -- - I click on the validate button to register the payment. -- - !python {model: pos.make.payment}: | - self.check(cr, uid, [ref('pos_make_payment_0')], context={'active_id': ref('pos_order_pos0'), 'lang':'en_US', 'active_model': 'pos.order', 'tz': False, 'active_ids': [ref('pos_order_pos0')]} ) -- | - I check that the order is not marked as paid yet -- - !assert {model: pos.order, id: pos_order_pos0}: - - state == 'draft' -- | - On the second payment proposition, I check that it proposes me the remaining balance which is 1790.0 EUR -- - !python {model: pos.make.payment}: | - defs = self.default_get(cr, uid, ['amount'], {'active_id': ref('pos_order_pos0')}) - assert abs(defs['amount'] - ((450*2 + 300*3*1.05)*0.95-100.0)) < 0.01, "The remaining balance is incorrect" -- - I pay the remaining balance. -- - !record {model: pos.make.payment, id: pos_make_payment_1, context: '{"active_id": ref("pos_order_pos0"), "active_ids": [ref("pos_order_pos0")]}' }: - amount: !eval > - (450*2 + 300*3*1.05)*0.95-100.0 -- - I click on the validate button to register the payment. -- - !python {model: pos.make.payment}: | - self.check(cr, uid, [ref('pos_make_payment_1')], context={'active_id': ref('pos_order_pos0')} ) -- | - I check that the order is marked as paid -- - !assert {model: pos.order, id: pos_order_pos0}: - - state == 'paid' -- - I generate the journal entries -- - !python {model: pos.order}: | - self._create_account_move_lines(cr, uid, [ref('pos_order_pos0')], {}) -- - I test that the generated journal entry is attached to the PoS order -- - !assert {model: pos.order, id: pos_order_pos0}: - - bool(account_move) - - - - diff --git a/addons/point_of_sale/test/02_order_to_invoice.yml b/addons/point_of_sale/test/02_order_to_invoice.yml deleted file mode 100644 index 90442a9a2e1..00000000000 --- a/addons/point_of_sale/test/02_order_to_invoice.yml +++ /dev/null @@ -1,53 +0,0 @@ -- - I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) -- - !record {model: pos.order, id: pos_order_pos1}: - company_id: base.main_company - partner_id: base.res_partner_1 - lines: - - name: OL/0001 - product_id: product.product_product_3 - price_unit: 450 - discount: 5.0 - qty: 2.0 - - name: OL/0002 - product_id: product.product_product_4 - price_unit: 300 - discount: 5.0 - qty: 3.0 -- - I click on the "Make Payment" wizard to pay the PoS order -- - !record {model: pos.make.payment, id: pos_make_payment_2, context: '{"active_id": ref("pos_order_pos1"), "active_ids": [ref("pos_order_pos1")]}' }: - amount: !eval > - (450*2 + 300*3*1.05)*0.95 -- - I click on the validate button to register the payment. -- - !python {model: pos.make.payment}: | - self.check(cr, uid, [ref('pos_make_payment_2')], context={'active_id': ref('pos_order_pos1')} ) -- | - I check that the order is marked as paid and there is no invoice attached to it -- - !python {model: pos.order}: | - order = self.browse(cr,uid,ref('pos_order_pos1')) -- - !assert {model: pos.order, id: pos_order_pos1, string: State not correct}: - - state == 'paid' - - not invoice_id -- - I set the order as invoiced -- - !workflow {model: pos.order, action: invoice, ref: pos_order_pos1} -- - I generate an invoice from the order -- - !python {model: pos.order}: | - invoice = self.action_invoice(cr, uid, [ref('pos_order_pos1')]) -- - I test that the total of the attached invoice is correct -- - !python {model: pos.order}: | - from openerp.tools import float_compare - amount_total = self.browse(cr, uid, ref('pos_order_pos1')).amount_total - assert float_compare(amount_total, 1752.75, precision_digits=2) == 0, "Invoice not correct" diff --git a/addons/point_of_sale/test/account_statement_reports.yml b/addons/point_of_sale/test/account_statement_reports.yml deleted file mode 100644 index b03cf7e3f29..00000000000 --- a/addons/point_of_sale/test/account_statement_reports.yml +++ /dev/null @@ -1,68 +0,0 @@ -- - In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports -- - I select the period and journal for the bank statement -- - !python {model: account.bank.statement}: | - import time - journal = self._default_journal(cr, uid, {'journal_type': 'bank'}) - assert journal, 'Journal has not been selected' -- - I create a bank statement with Opening and Closing balance 0. -- - !record {model: account.bank.statement, id: account_bank_statement_0}: - balance_end_real: 0.0 - balance_start: 0.0 - date: !eval time.strftime('%Y-%m-%d') - journal_id: bank_journal -- - I create bank statement line -- - !record {model: account.bank.statement.line, id: account_bank_statement_line_0}: - amount: 1000.0 - partner_id: base.res_partner_4 - statement_id: account_bank_statement_0 - name: 'EXT001' -- - I modify the bank statement and set the Closing Balance. -- - !record {model: account.bank.statement, id: account_bank_statement_0}: - balance_end_real: 1000.0 -- - I reconcile the bank statement. -- - !python {model: account.bank.statement.line}: | - new_aml_dicts = [{ - 'account_id': self.pool.get('res.partner').browse(cr, uid, ref('base.res_partner_4')).property_account_receivable_id.id, - 'name': "EXT001", - 'credit': 1000.0, - 'debit': 0.0, - }] - self.process_reconciliations(cr, uid, [ref("account_bank_statement_line_0")], [{'new_aml_dicts': new_aml_dicts}]) -- - I confirm the bank statement using Confirm button -- - !python {model: account.bank.statement}: | - self.button_confirm_bank(cr, uid, [ref("account_bank_statement_0")]) -- - Print the account statement report -- - !python {model: account.bank.statement}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('account_bank_statement_0')], 'point_of_sale.report_statement', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-statement.'+format), 'wb+').write(data) - -- - Printing the user s product report -- - !python {model: account.bank.statement}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('account_bank_statement_0')], 'point_of_sale.report_usersproduct', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'point_of_sale-usersproduct.'+format), 'wb+').write(data) - diff --git a/addons/point_of_sale/test/tests_before.xml b/addons/point_of_sale/test/tests_before.xml deleted file mode 100644 index 24276f9cd1b..00000000000 --- a/addons/point_of_sale/test/tests_before.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/addons/point_of_sale/tests/__init__.py b/addons/point_of_sale/tests/__init__.py new file mode 100644 index 00000000000..676a99e4a6a --- /dev/null +++ b/addons/point_of_sale/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_point_of_sale_flow diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py new file mode 100644 index 00000000000..ebf971d2ec3 --- /dev/null +++ b/addons/point_of_sale/tests/common.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from odoo.tests import common + + +class TestPointOfSaleCommon(common.TransactionCase): + + def setUp(self): + super(TestPointOfSaleCommon, self).setUp() + self.AccountBankStatement = self.env['account.bank.statement'] + self.AccountBankStatementLine = self.env['account.bank.statement.line'] + self.PosMakePayment = self.env['pos.make.payment'] + self.PosOrder = self.env['pos.order'] + self.company_id = self.ref('base.main_company') + self.product3 = self.env.ref('product.product_product_3') + self.product4 = self.env.ref('product.product_product_4') + self.partner1 = self.env.ref('base.res_partner_1') + self.partner4 = self.env.ref('base.res_partner_4') + self.pos_config = self.env.ref('point_of_sale.pos_config_main') + + # create a new session + self.pos_order_session0 = self.env['pos.session'].create({ + 'user_id': 1, + 'config_id': self.pos_config.id + }) + + # create a VAT tax of 10%, included in the public price + Tax = self.env['account.tax'] + account_tax_10_incl = Tax.create({ + 'name': 'VAT 10 perc Incl', + 'amount_type': 'percent', + 'amount': 10.0, + 'price_include': 1 + }) + + # assign this 10 percent tax on the [PCSC234] PC Assemble SC234 product + # as a sale tax + self.product3.taxes_id = [(6, 0, [account_tax_10_incl.id])] + + # create a VAT tax of 5%, which is added to the public price + account_tax_05_incl = Tax.create({ + 'name': 'VAT 5 perc Incl', + 'amount_type': 'percent', + 'amount': 5.0, + 'price_include': 0 + }) + + # create a second VAT tax of 5% but this time for a child company, to + # ensure that only product taxes of the current session's company are considered + #(this tax should be ignore when computing order's taxes in following tests) + account_tax_05_incl_chicago = Tax.create({ + 'name': 'VAT 05 perc Excl (US)', + 'amount_type': 'percent', + 'amount': 5.0, + 'price_include': 0, + 'company_id': self.ref('stock.res_company_1') + }) + + # I assign those 5 percent taxes on the PCSC349 product as a sale taxes + self.product4.write( + {'taxes_id': [(6, 0, [account_tax_05_incl.id, account_tax_05_incl_chicago.id])]}) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py new file mode 100644 index 00000000000..023eff033a0 --- /dev/null +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +import datetime +import time +import os + +import odoo +from odoo import tools, report as odoo_report +from odoo.tools import float_compare, test_reports + +from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon + + +@odoo.tests.common.at_install(False) +@odoo.tests.common.post_install(True) +class TestPointOfSaleFlow(TestPointOfSaleCommon): + + def test_register_open(self): + """ + In order to test the Point of Sale module, I will open all cash registers through the wizard + """ + # open all statements/cash registers + self.env['pos.open.statement'].create({}).open_statement() + + def test_order_to_payment(self): + """ + In order to test the Point of Sale in module, I will do a full flow from the sale to the payment and invoicing. + I will use two products, one with price including a 10% tax, the other one with 5% tax excluded from the price. + """ + # I click on create a new session button + self.pos_config.open_session_cb() + + # I create a PoS order with 2 units of PCSC234 at 450 EUR (Tax Incl) + # and 3 units of PCSC349 at 300 EUR. (Tax Excl) + self.pos_order_pos0 = self.PosOrder.create({ + 'company_id': self.company_id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 2.0, + 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], + })] + }) + + # I check that the total of the order is equal to 450*2 + 300*3*1.05 + # and the tax of the order is equal to 900 -(450 * 2 / 1.1) + + # 300*0.05*3 + self.assertLess( + abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05)), + 0.01, 'The order has a wrong amount, tax included.') + + self.assertLess( + abs(self.pos_order_pos0.amount_tax - (900 - (450 * 2 / 1.1) + 300 * 0.05 * 3)), + 0.01, 'The order has a wrong tax amount.') + + # I want to add a global discount of 5 percent using the wizard + + self.pos_discount_0 = self.env['pos.discount'].create({'discount': 5.0}) + + context = {"active_model": "pos.order", "active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + + # I click the apply button to set the discount on all lines + self.pos_discount_0.with_context(context).apply_discount() + + # I check that the total of the order is now equal to (450*2 + + # 300*3*1.05)*0.95 + self.assertLess( + abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05) * 0.95), + 0.01, 'The order has a wrong total including tax and discounts') + + # I click on the "Make Payment" wizard to pay the PoS order with a + # partial amount of 100.0 EUR + context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 100.0 + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # I check that the order is not marked as paid yet + self.assertEqual(self.pos_order_pos0.state, 'draft', 'Order should be in draft state.') + + # On the second payment proposition, I check that it proposes me the + # remaining balance which is 1790.0 EUR + defs = self.pos_make_payment_0.with_context({'active_id': self.pos_order_pos0.id}).default_get(['amount']) + + self.assertLess( + abs(defs['amount'] - ((450 * 2 + 300 * 3 * 1.05) * 0.95 - 100.0)), 0.01, "The remaining balance is incorrect.") + + #'I pay the remaining balance. + context_make_payment = { + "active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + + self.pos_make_payment_1 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': (450 * 2 + 300 * 3 * 1.05) * 0.95 - 100.0 + }) + + # I click on the validate button to register the payment. + self.pos_make_payment_1.with_context(context_make_payment).check() + + # I check that the order is marked as paid + self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.') + + # I generate the journal entries + self.pos_order_pos0._create_account_move_line() + + # I test that the generated journal entry is attached to the PoS order + self.assertTrue(self.pos_order_pos0.account_move, "Journal entry has not been attached to Pos order.") + + def test_order_to_invoice(self): + + #I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) + self.pos_order_pos1 = self.PosOrder.create({ + 'company_id': self.company_id, + 'partner_id': self.partner1.id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 5.0, + 'qty': 2.0, + 'tax_ids': [(6, 0, self.product3.taxes_id.ids)], + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 5.0, + 'qty': 3.0, + 'tax_ids': [(6, 0, self.product4.taxes_id.ids)], + })] + }) + + # I click on the "Make Payment" wizard to pay the PoS order + context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id} + self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': (450 * 2 + 300 * 3 * 1.05) * 0.95, + }) + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos1.id} + self.pos_make_payment.with_context(context_payment).check() + + # I check that the order is marked as paid and there is no invoice + # attached to it + self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.") + self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.') + + # I set the order as invoiced + self.pos_order_pos1.signal_workflow('invoice') + + # I generate an invoice from the order + self.invoice = self.pos_order_pos1.action_invoice() + + # I test that the total of the attached invoice is correct + self.amount_total = self.pos_order_pos1.amount_total + self.assertEqual( + float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct") + + """In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports""" + + # I select the period and journal for the bank statement + + context_journal = {'journal_type': 'bank'} + self.assertTrue(self.AccountBankStatement.with_context( + context_journal)._default_journal(), 'Journal has not been selected') + journal = self.env['account.journal'].create({ + 'name': 'Bank Test', + 'code': 'BNKT', + 'type': 'bank', + 'company_id': self.company_id, + }) + # I create a bank statement with Opening and Closing balance 0. + account_statement = self.AccountBankStatement.create({ + 'balance_start': 0.0, + 'balance_end_real': 0.0, + 'date': time.strftime('%Y-%m-%d'), + 'journal_id': journal.id, + 'company_id': self.company_id, + }) + # I create bank statement line + account_statement_line = self.AccountBankStatementLine.create({ + 'amount': 1000, + 'partner_id': self.partner4.id, + 'statement_id': account_statement.id, + 'name': 'EXT001' + }) + # I modify the bank statement and set the Closing Balance. + account_statement.write({ + 'balance_end_real': 1000.0, + }) + + # I reconcile the bank statement. + new_aml_dicts = [{ + 'account_id': self.partner4.property_account_receivable_id.id, + 'name': "EXT001", + 'credit': 1000.0, + 'debit': 0.0, + }] + + self.AccountBankStatementLine._model.process_reconciliations( + self.env.cr, self.env.uid, [account_statement_line.id], [{'new_aml_dicts': new_aml_dicts}]) + + # I confirm the bank statement using Confirm button + + self.AccountBankStatement.button_confirm_bank() From 90d406b567fd1319ec0a5d7285aa52521b3c90ab Mon Sep 17 00:00:00 2001 From: Darshan Kalola Date: Wed, 14 Oct 2015 17:04:16 +0530 Subject: [PATCH 9/9] [FIX] pos_mercury: adapt to new signature method add_payment is no longer an api.model so self represent the current order --- addons/pos_mercury/models/pos_mercury.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/pos_mercury/models/pos_mercury.py b/addons/pos_mercury/models/pos_mercury.py index 0abf956d726..1a9e36e3cfc 100644 --- a/addons/pos_mercury/models/pos_mercury.py +++ b/addons/pos_mercury/models/pos_mercury.py @@ -68,11 +68,10 @@ class pos_order_card(models.Model): return fields - @api.model - def add_payment(self, order_id, data): - statement_id = super(pos_order_card, self).add_payment(order_id, data) + def add_payment(self, data): + statement_id = super(pos_order_card, self).add_payment(data) statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id), - ('pos_statement_id', '=', order_id), + ('pos_statement_id', '=', self.id), ('journal_id', '=', data['journal']), ('amount', '=', data['amount'])])