From 21f93b80359c344c2df9332e4a50405e1c322964 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 14 Mar 2017 17:02:39 +0100 Subject: [PATCH] [FIX] point_of_sale: backport #15862 to saas-11 --- addons/point_of_sale/controllers/main.py | 5 +- addons/point_of_sale/models/pos_config.py | 12 +- addons/point_of_sale/models/pos_order.py | 31 ++-- addons/point_of_sale/models/pos_session.py | 12 +- addons/point_of_sale/tests/common.py | 4 + .../tests/test_point_of_sale_flow.py | 136 +++++++++++++++++- 6 files changed, 174 insertions(+), 26 deletions(-) diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 0161c2afcdd..849a48909de 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -14,7 +14,10 @@ class PosController(http.Controller): @http.route('/pos/web', type='http', auth='user') def pos_web(self, debug=False, **k): # if user not logged in, log him in - pos_sessions = request.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', request.session.uid)]) + pos_sessions = request.env['pos.session'].search([ + ('state', '=', 'opened'), + ('user_id', '=', request.session.uid), + ('name', 'not like', '(RESCUE FOR')]) if not pos_sessions: return werkzeug.utils.redirect('/web#action=point_of_sale.action_client_pos_menu') pos_sessions.login() diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 22ea92762f9..599c0712257 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -104,9 +104,12 @@ class PosConfig(models.Model): @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 + session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \ + not r.state == 'closed' and \ + '(RESCUE FOR' not in r.name) + # sessions ordered by id desc + pos_config.current_session_id = session and session[0].id or False + pos_config.current_session_state = session and session[0].state or False @api.depends('session_ids') def _compute_last_session(self): @@ -126,7 +129,8 @@ class PosConfig(models.Model): @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 + session = pos_config.session_ids.filtered(lambda s: s.state == 'opened' and '(RESCUE FOR' not in s.name) + pos_config.pos_session_username = session and session[0].user_id.name or False @api.constrains('company_id', 'stock_location_id') def _check_company_location(self): diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 03e1a082a3f..d0b750dbdec 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -66,24 +66,21 @@ class PosOrder(models.Model): order['name'], order['amount_total']) - open_sessions = PosSession.search([('state', '=', 'opened'), - ('rescue', '=', True), - ('config_id', '=', closed_session.config_id.id), - ('name', 'ilike', closed_session.name)], - limit=1, order="start_at DESC") - if open_sessions: - new_session = open_sessions[0] - _logger.warning('using existing recovery session %s for saving order %s', new_session, order['name']) - else: - _logger.warning('attempting to create recovery session for saving order %s', order['name']) - new_session = PosSession.create({ - 'config_id': closed_session.config_id.id, - 'name': _('(RESCUE FOR %(session)s)') % {'session': closed_session.name}, - 'rescue': True, # avoid conflict with live sessions - }) - # bypass opening_control (necessary when using cash control) - new_session.signal_workflow('open') + rescue_session = PosSession.search([ + ('name', 'like', '(RESCUE FOR %(session)s)' % {'session': closed_session.name}), + ('state', 'not in', ('closed', 'closing_control')), + ], limit=1) + if rescue_session: + _logger.warning('reusing recovery session %s for saving order %s', rescue_session.name, order['name']) + return rescue_session + _logger.warning('attempting to create recovery session for saving order %s', order['name']) + new_session = PosSession.create({ + 'config_id': closed_session.config_id.id, + 'name': _('(RESCUE FOR %(session)s)') % {'session': closed_session.name}, + }) + # bypass opening_control (necessary when using cash control) + new_session.signal_workflow('open') return new_session def _match_payment_to_invoice(self, order): diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 966743da560..2755b51c648 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -109,12 +109,20 @@ class PosSession(models.Model): @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: + if self.search_count([ + ('state', 'not in', ('closed', 'closing_control')), + ('user_id', '=', self.user_id.id), + ('name', 'not like', 'RESCUE FOR'), + ]) > 1: raise ValidationError(_("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: + if self.search_count([ + ('state', '!=', 'closed'), + ('config_id', '=', self.config_id.id), + ('name', 'not like', 'RESCUE FOR'), + ]) > 1: raise ValidationError(_("You cannot create two active sessions related to the same point of sale!")) @api.model diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index ebf971d2ec3..4c5597979c6 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -10,12 +10,16 @@ class TestPointOfSaleCommon(common.TransactionCase): self.AccountBankStatementLine = self.env['account.bank.statement.line'] self.PosMakePayment = self.env['pos.make.payment'] self.PosOrder = self.env['pos.order'] + self.PosSession = self.env['pos.session'] 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') + self.carotte = self.env.ref('point_of_sale.carotte') + self.courgette = self.env.ref('point_of_sale.courgette') + self.onions = self.env.ref('point_of_sale.Onions') # create a new session self.pos_order_session0 = self.env['pos.session'].create({ 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 index 4f029df9439..7ddf15de08a 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -4,8 +4,8 @@ import time import os import odoo -from odoo import tools, report as odoo_report -from odoo.tools import float_compare, test_reports +from odoo import fields, tools, report as odoo_report +from odoo.tools import float_compare, mute_logger, test_reports from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon @@ -414,3 +414,135 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I confirm the bank statement using Confirm button self.AccountBankStatement.button_confirm_bank() + + def test_create_from_ui(self): + """ + Simulation of sales coming from the interface, even after closing the session + """ + # I click on create a new session button + self.pos_config.open_session_cb() + + current_session = self.pos_config.current_session_id + num_starting_orders = len(current_session.order_ids) + + carrot_order = {'data': + {'amount_paid': 0.9, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 0.9, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 42, + 'price_unit': 0.9, + 'product_id': self.carotte.id, + 'qty': 1, + 'tax_ids': []}]], + 'name': 'Order 00042-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': 2, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 0.9, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00042-003-0014', + 'user_id': self.env.uid}, + 'id': '00042-003-0014', + 'to_invoice': False} + + zucchini_order = {'data': + {'amount_paid': 1.2, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 1.1, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'price_unit': 1.2, + 'product_id': self.courgette.id, + 'qty': 1, + 'tax_ids': []}]], + 'name': 'Order 00043-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 1.2, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00043-003-0014', + 'user_id': self.env.uid}, + 'id': '00043-003-0014', + 'to_invoice': False} + + onions_order = {'data': + {'amount_paid': 1.28, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 1.28, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'price_unit': 1.28, + 'product_id': self.onions.id, + 'qty': 1, + 'tax_ids': [[6, False, self.onions.taxes_id.ids]]}]], + 'name': 'Order 00044-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 1.28, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00044-003-0014', + 'user_id': self.env.uid}, + 'id': '00044-003-0014', + 'to_invoice': False} + + + # I create an order on an open session + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded") + + # I resubmit the same order + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Resubmitted order was not skipped") + + # I close the session + current_session.signal_workflow('close') + self.assertEqual(current_session.state, 'closed', "Session was not properly closed") + self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed") + + # I keep selling after the session is closed + with mute_logger('openerp.addons.point_of_sale.models.pos_order'): + self.PosOrder.create_from_ui([zucchini_order, onions_order]) + rescue_session = self.PosSession.search([ + ('config_id', '=', self.pos_config.id), + ('state', '=', 'opened'), + ]) + self.assertEqual(len(rescue_session), 1, "One (and only one) rescue session should be created for orphan orders") + self.assertIn("(RESCUE FOR %s)" % current_session.name, rescue_session.name, "Rescue session is not linked to the previous one") + self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders") + + # I close the rescue session + rescue_session.signal_workflow('close') + self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed")