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/__openerp__.py b/addons/point_of_sale/__openerp__.py index 6d34f8a5cc1..a42c2df9106 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -30,53 +30,43 @@ 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/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', - 'views/report_detailsofsales.xml', - 'views/report_payment.xml', - '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', - ], - '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/point_of_sale_report.yml', - 'test/account_statement_reports.yml', + 'data/point_of_sale_demo.xml', ], 'installable': True, '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/account_bank_statement.py b/addons/point_of_sale/account_bank_statement.py deleted file mode 100644 index 5faa31b3007..00000000000 --- a/addons/point_of_sale/account_bank_statement.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- 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."), - } - - -class account_bank_statement(osv.osv): - _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), - } 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/point_of_sale_data.xml b/addons/point_of_sale/data/point_of_sale_data.xml similarity index 98% rename from addons/point_of_sale/point_of_sale_data.xml rename to addons/point_of_sale/data/point_of_sale_data.xml index 820b599b537..940b0fdd373 100644 --- a/addons/point_of_sale/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/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml similarity index 99% rename from addons/point_of_sale/point_of_sale_demo.xml rename to addons/point_of_sale/data/point_of_sale_demo.xml index b9fc7784dbf..86117172855 100644 --- a/addons/point_of_sale/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/point_of_sale_workflow.xml b/addons/point_of_sale/data/point_of_sale_workflow.xml similarity index 98% rename from addons/point_of_sale/point_of_sale_workflow.xml rename to addons/point_of_sale/data/point_of_sale_workflow.xml index ad7b149f6bc..833ca713c45 100644 --- a/addons/point_of_sale/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/__init__.py b/addons/point_of_sale/models/__init__.py new file mode 100644 index 00000000000..7170dbcf350 --- /dev/null +++ b/addons/point_of_sale/models/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import account_bank_statement +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 new file mode 100644 index 00000000000..60f7380a32e --- /dev/null +++ b/addons/point_of_sale/models/account_bank_statement.py @@ -0,0 +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 odoo import fields, models + + +class AccountBankStatement(models.Model): + _inherit = 'account.bank.statement' + + 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 AccountBankStatementLine(models.Model): + _inherit = 'account.bank.statement.line' + + 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..4785d7e02f8 --- /dev/null +++ b/addons/point_of_sale/models/account_journal.py @@ -0,0 +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 odoo import fields, models, api + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + 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.env['pos.session'].browse(session_id) + if session: + 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 new file mode 100644 index 00000000000..ffbf0b384b8 --- /dev/null +++ b/addons/point_of_sale/models/barcode_rule.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import sets + +from odoo import models +from odoo.tools.translate import _ + + +class BarcodeRule(models.Model): + _inherit = 'barcode.rule' + + def _get_type_selection(self): + types = sets.Set(super(BarcodeRule, 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..ff68328cbaa --- /dev/null +++ b/addons/point_of_sale/models/pos_category.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import api, fields, models, tools, _ + + +class PosCategory(models.Model): + _name = "pos.category" + _description = "Public Category" + _order = "sequence, name" + + @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. + image = fields.Binary(attachment=True, + help="This field holds the image used as image for the cateogry, limited to 1024x1024px.") + 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. " + "Use this field in form views or some kanban views.") + 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. " + "Use this field anywhere a small image is required.") + + @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) + + @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 new file mode 100644 index 00000000000..8e2badd6c25 --- /dev/null +++ b/addons/point_of_sale/models/pos_config.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import uuid + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + +class PosConfig(models.Model): + _name = 'pos.config' + + POS_CONFIG_STATE = [ + ('active', 'Active'), + ('inactive', 'Inactive'), + ('deprecated', 'Deprecated') + ] + + 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: + pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id + else: + pos_config.currency_id = self.env.user.company_id.currency_id.id + + @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 + + @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) + 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: + pos_config.last_session_closing_cash = 0 + pos_config.last_session_closing_date = False + + @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 + + @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")) + + @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")) + + @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")) + + @api.onchange('iface_print_via_proxy') + def _onchange_iface_print_via_proxy(self): + self.iface_print_auto = self.iface_print_via_proxy + + @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 + + @api.multi + def name_get(self): + result = [] + 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 + result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')')) + return result + + @api.model + def create(self, values): + IrSequence = self.env['ir.sequence'] + val = { + 'name': _('POS Order %s') % values['name'], + 'padding': 4, + 'prefix': "%s/" % values['name'], + 'code': "pos.order", + 'company_id': values.get('company_id', False), + } + # 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 + val.update(name=_('POS order line %s') % values['name'], code='pos.order.line') + IrSequence.create(val) + return super(PosConfig, self).create(values) + + @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() + + @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 + @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', + } + + @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() + + @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) + + @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 { + '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/pos_order.py b/addons/point_of_sale/models/pos_order.py new file mode 100644 index 00000000000..d3d85cee1e9 --- /dev/null +++ b/addons/point_of_sale/models/pos_order.py @@ -0,0 +1,696 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +from functools import partial + +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 PosOrder(models.Model): + _name = "pos.order" + _description = "Point of Sale" + _order = "id desc" + + def _amount_line_tax(self, cr, uid, line, fiscal_position_id, context=None): + taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) + if fiscal_position_id: + taxes = fiscal_position_id.map_tax(taxes) + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + 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) + + @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'], + '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, 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), + 'journal': ui_paymentline['journal_id'], + } + + # This deals with orders that belong to a closed session. In order + # 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, 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_session: + return open_session + else: + 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 + + @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 pos_order['statement_ids']: + order.add_payment(self._payment_fields(payments[2])) + journal_ids.add(payments[2]['journal_id']) + + 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(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) + 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 = [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_id = cash_journal[0].id + order.add_payment({ + 'amount': -pos_order['amount_return'], + 'payment_date': fields.Datetime.now(), + 'payment_name': _('return'), + 'journal': cash_journal_id, + }) + return order + + def _prepare_analytic_account(self, line): + '''This method is designed to be inherited in a custom module''' + return False + + 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 _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.""" + IrProperty = self.env['ir.property'] + ResPartner = self.env['res.partner'] + + 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.filtered(lambda o: not o.account_move or order.state == 'paid'): + current_company = order.sale_journal.company_id + 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 = 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: + values.update({ + 'ref': order.name, + '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, + }) + + if data_type == 'product': + key = ('product', values['partner_id'], (values['product_id'], values['name']), values['analytic_account_id'], values['debit'] > 0) + elif data_type == 'tax': + key = ('tax', values['partner_id'], values['tax_line_id'], values['debit'] > 0) + elif data_type == 'counter_part': + key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0) + else: + return + + grouped_data.setdefault(key, []) + + if have_to_group_by: + if not grouped_data[key]: + grouped_data[key].append(values) + else: + 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) + 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 + 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: + 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)) + + name = line.product_id.name + if line.notice: + # add discount reason in move + name = name + ' (' + line.notice + ')' + + # Create a move for the line for the order line + insert_data('product', { + 'name': name, + 'quantity': line.qty, + 'product_id': line.product_id.id, + 'account_id': income_account, + '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 = line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == current_company.id) + if not taxes: + continue + 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, + 'tax_line_id': tax['id'], + 'partner_id': partner_id + }) + + # counterpart + insert_data('counter_part', { + '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': partner_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: # In case no order was changed + move.sudo().write({'line_ids': all_lines}) + move.sudo().post() + return True + + def _default_session(self): + return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) + + 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 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 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, + } + + 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, line): + if line and 'tax_ids' not in line[2]: + 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 + + 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) + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + 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'] + + @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 + + @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'] + + @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 new file mode 100644 index 00000000000..b29f6960882 --- /dev/null +++ b/addons/point_of_sale/models/pos_session.py @@ -0,0 +1,230 @@ +# -*- 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 PosSession(models.Model): + _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 _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') + + 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) + + state = fields.Selection( + POS_SESSION_STATE, string='Status', + required=True, readonly=True, + index=True, copy=False, default='opening_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.")) + + @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!")) + + @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!")) + + @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.")) + + # 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. + 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 = 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 = 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': self.env.uid, + 'company_id': pos_config.company_id.id + }) for journal in pos_config.journal_ids] + values.update({ + '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 + if pos_config.cash_control: + 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) + + @api.multi + def unlink(self): + for session in self.filtered(lambda s: s.statement_ids): + session.statement_ids.unlink() + return super(PosSession, self).unlink() + + @api.multi + def login(self): + self.ensure_one() + self.write({ + 'login_number': self.login_number + 1, + }) + + def wkf_action_open(self): + # second browse because we need to refetch the data from the DB for cash_register_id + for session in self: + values = {} + if not session.start_at: + values['start_at'] = fields.Datetime.now() + values['state'] = 'opened' + session.write(values) + session.statement_ids.button_open() + return True + + def wkf_action_opening_control(self): + return self.write({'state': 'opening_control'}) + + 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): + statement.write({'balance_end_real': statement.balance_end}) + session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()}) + + def wkf_action_close(self): + # Close CashBox + 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.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 ")) + 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': self.env.ref('point_of_sale.menu_point_root').id}, + } + + @api.multi + def open_frontend_cb(self): + if not self.ids: + return {} + 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', + '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..4e350eed91c --- /dev/null +++ b/addons/point_of_sale/models/product.py @@ -0,0 +1,22 @@ +# -*- 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 ProductTemplate(models.Model): + _inherit = 'product.template' + + 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.") + + @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 new file mode 100644 index 00000000000..cfadb6c3699 --- /dev/null +++ b/addons/point_of_sale/models/res_config.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import fields, models + + +class PosConfiguration(models.TransientModel): + _inherit = 'res.config.settings' + _name = 'pos.config.settings' + + 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 new file mode 100644 index 00000000000..a94f0a79b60 --- /dev/null +++ b/addons/point_of_sale/models/res_partner.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + 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'): + 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(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 new file mode 100644 index 00000000000..6fb09e8a44e --- /dev/null +++ b/addons/point_of_sale/models/res_users.py @@ -0,0 +1,15 @@ +# -*- 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 ResUsers(models.Model): + _inherit = 'res.users' + + pos_security_pin = fields.Char(string='Security PIN', size=32, help='A Security PIN used to protect sensible functionality in the Point of Sale') + + @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/point_of_sale.py b/addons/point_of_sale/point_of_sale.py deleted file mode 100644 index b09fcc8a722..00000000000 --- a/addons/point_of_sale/point_of_sale.py +++ /dev/null @@ -1,1604 +0,0 @@ -# -*- coding: utf-8 -*- -# 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.osv import fields, osv -from openerp.tools import float_is_zero -from openerp.tools.translate import _ -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" - _description = "Point of Sale" - _order = "id desc" - - def _amount_line_tax(self, cr, uid, line, fiscal_position_id, context=None): - taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) - 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 - - 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) - 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'], - '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): - 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), - 'journal': ui_paymentline['journal_id'], - } - - # This deals with orders that belong to a closed session. In order - # 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) - - if open_sessions: - return open_sessions[0] - 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) - - # bypass opening_control (necessary when using cash control) - new_session.signal_workflow('open') - - 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) - 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) - 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 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, [ - ('type', '=', 'cash'), - ('id', 'in', list(journal_ids)), - ], limit=1, context=context) - if not cash_journal_ids: - # 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: - 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'), - 'payment_name': _('return'), - 'journal': cash_journal, - }, context=context) - return order_id - - 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 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 - - 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_line(self, cr, uid, ids, session=None, move_id=None, context=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') - - #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)): - 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 - - 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: - # 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) - - 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, - 'company_id': current_company.id, - }) - - if data_type == 'product': - key = ('product', values['partner_id'], (values['product_id'], values['name']), values['analytic_account_id'], values['debit'] > 0) - elif data_type == 'tax': - key = ('tax', values['partner_id'], values['tax_line_id'], values['debit'] > 0) - elif data_type == 'counter_part': - key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0) - else: - return - - 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) - else: - 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) - 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 - 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: - 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)) - - name = line.product_id.name - if line.notice: - # add discount reason in move - name = name + ' (' + line.notice + ')' - - # Create a move for the line for the order line - insert_data('product', { - 'name': name, - '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 - }) - - # 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) - 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']: - 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, - '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 - }) - - # counterpart - insert_data('counter_part', { - '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 - }) - - 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) - - return True - - def action_payment(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'payment'}, context=context) - - 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) - return True - - def action_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - return True - - def action_done(self, cr, uid, ids, context=None): - 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" - _rec_name = "product_id" - - def _order_line_fields(self, cr, uid, line, context=None): - if line and 'tax_ids' not in line[2]: - product = self.pool['product.product'].browse(cr, uid, line[2]['product_id'], context=context) - 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 ] - 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 - - 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.')) - - price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist], - product_id, qty or 1.0, partner_id)[pricelist] - - 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, - } - -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/point_of_sale_report.xml b/addons/point_of_sale/point_of_sale_report.xml deleted file mode 100644 index 4346c2bc20e..00000000000 --- a/addons/point_of_sale/point_of_sale_report.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml deleted file mode 100644 index a895492cc3b..00000000000 --- a/addons/point_of_sale/point_of_sale_view.xml +++ /dev/null @@ -1,998 +0,0 @@ - - - - - - - - - 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/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_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_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..25daab1ca6a 100644 --- a/addons/point_of_sale/report/pos_order_report.py +++ b/addons/point_of_sale/report/pos_order_report.py @@ -1,78 +1,89 @@ # -*- 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), - } _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, 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) - 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,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/res_config.py b/addons/point_of_sale/res_config.py deleted file mode 100644 index 765156bd15c..00000000000 --- a/addons/point_of_sale/res_config.py +++ /dev/null @@ -1,29 +0,0 @@ -from openerp.osv import fields, osv - -class pos_configuration(osv.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"), - } diff --git a/addons/point_of_sale/res_partner.py b/addons/point_of_sale/res_partner.py deleted file mode 100644 index 53c27c9fd96..00000000000 --- a/addons/point_of_sale/res_partner.py +++ /dev/null @@ -1,9 +0,0 @@ - -from openerp.osv import osv, fields - - -class res_users(osv.osv): - _inherit = 'res.partner' - _columns = { - 'barcode' : fields.char('Barcode', help="BarCode", oldname='ean13'), - } \ No newline at end of file diff --git a/addons/point_of_sale/res_users.py b/addons/point_of_sale/res_users.py deleted file mode 100644 index e036d9062c7..00000000000 --- a/addons/point_of_sale/res_users.py +++ /dev/null @@ -1,19 +0,0 @@ - -from openerp.osv import osv, fields - - -class res_users(osv.osv): - _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 - - _constraints = [ - (_check_pin, "Security PIN can only contain digits",['pos_security_pin']), - ] 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/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/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 46b7b2b1444..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(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/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/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() 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..6e62407dc61 --- /dev/null +++ b/addons/point_of_sale/views/account_journal_view.xml @@ -0,0 +1,26 @@ + + + + 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. +

+
+
+ + +
diff --git a/addons/point_of_sale/account_statement_report.xml b/addons/point_of_sale/views/account_statement_report.xml similarity index 86% rename from addons/point_of_sale/account_statement_report.xml rename to addons/point_of_sale/views/account_statement_report.xml index 61cc1b891f4..bc1176bd314 100644 --- a/addons/point_of_sale/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/account_statement_view.xml b/addons/point_of_sale/views/account_statement_view.xml similarity index 80% rename from addons/point_of_sale/account_statement_view.xml rename to addons/point_of_sale/views/account_statement_view.xml index 7bca1e9a251..67b072b1a5d 100644 --- a/addons/point_of_sale/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/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml similarity index 99% rename from addons/point_of_sale/point_of_sale_dashboard.xml rename to addons/point_of_sale/views/point_of_sale_dashboard.xml index ddfa5f35a8e..77ea4c3b215 100644 --- a/addons/point_of_sale/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 new file mode 100644 index 00000000000..f9787c1b4c7 --- /dev/null +++ b/addons/point_of_sale/views/point_of_sale_report.xml @@ -0,0 +1,21 @@ + + + + + + + Invoice + pos.order + qweb-pdf + point_of_sale.report_invoice + + + + 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 67% rename from addons/point_of_sale/point_of_sale_sequence.xml rename to addons/point_of_sale/views/point_of_sale_sequence.xml index c9a277cb9b5..e9a9a3872e9 100644 --- a/addons/point_of_sale/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/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/views/point_of_sale_view.xml b/addons/point_of_sale/views/point_of_sale_view.xml new file mode 100644 index 00000000000..7ae52d0a79e --- /dev/null +++ b/addons/point_of_sale/views/point_of_sale_view.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + 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..60ea3c6358c --- /dev/null +++ b/addons/point_of_sale/views/pos_category_view.xml @@ -0,0 +1,54 @@ + + + + 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, the layout of the + touchscreen interface will automatically. We suggest not to put + a photo on categories for small (1024x768) screens. +

+
+
+ + +
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..830ab75f697 --- /dev/null +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -0,0 +1,139 @@ + + + + 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. +

+
+
+ + + + + +
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 86% rename from addons/point_of_sale/report/pos_order_report_view.xml rename to addons/point_of_sale/views/pos_order_report_view.xml index 56eb552b557..ce5aa141a4f 100644 --- a/addons/point_of_sale/report/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 @@ -53,6 +52,13 @@ + + Per session + report.pos.order + + {'group_by': ['date', 'session_id'], 'measures': ['price_total']} + + Orders Analysis report.pos.order @@ -62,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 new file mode 100644 index 00000000000..c9ee5c8693d --- /dev/null +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -0,0 +1,253 @@ + + + + pos.order.form + pos.order + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ 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/templates.xml b/addons/point_of_sale/views/pos_templates.xml similarity index 98% rename from addons/point_of_sale/views/templates.xml rename to addons/point_of_sale/views/pos_templates.xml index 1c6627b631f..528db600ed5 100644 --- a/addons/point_of_sale/views/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 new file mode 100644 index 00000000000..8b6183acd0e --- /dev/null +++ b/addons/point_of_sale/views/product_view.xml @@ -0,0 +1,56 @@ + + + + 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 + + + + + + + +
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/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/res_config_view.xml b/addons/point_of_sale/views/res_config_view.xml similarity index 94% rename from addons/point_of_sale/res_config_view.xml rename to addons/point_of_sale/views/res_config_view.xml index 610d96c4d46..767d83ee34a 100644 --- a/addons/point_of_sale/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/res_partner_view.xml b/addons/point_of_sale/views/res_partner_view.xml similarity index 93% rename from addons/point_of_sale/res_partner_view.xml rename to addons/point_of_sale/views/res_partner_view.xml index 3320bc3a3cc..746aab53613 100644 --- a/addons/point_of_sale/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/res_users_view.xml b/addons/point_of_sale/views/res_users_view.xml similarity index 93% rename from addons/point_of_sale/res_users_view.xml rename to addons/point_of_sale/views/res_users_view.xml index 47d8c7863de..84df0988d24 100644 --- a/addons/point_of_sale/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/__init__.py b/addons/point_of_sale/wizard/__init__.py index 2cd077955ca..c708aa57677 100644 --- a/addons/point_of_sale/wizard/__init__.py +++ b/addons/point_of_sale/wizard/__init__.py @@ -2,8 +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 import pos_payment 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_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_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 - - - -
-
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 40d8ff18a55..87fffb3da0a 100644 --- a/addons/point_of_sale/wizard/pos_payment.py +++ b/addons/point_of_sale/wizard/pos_payment.py @@ -1,58 +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 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): +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', @@ -62,48 +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 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: - 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, - } 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 - - - + 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'])])