From 07aa3a77abcf75a6da84cb60dc3a9c2d61c53c0f Mon Sep 17 00:00:00 2001 From: Joseph Caburnay Date: Tue, 24 Nov 2020 07:26:50 +0000 Subject: [PATCH] [FIX] point_of_sale: balance pos session move when closing Instances like deleting a tax used in a pos.order before closing a session is allowed but it renders error when closing the session because of imbalance move lines. In this commit, we are adding a 'balancing' line in the move before it is created. This ensures the closing of pos session. The introduced move line is named 'Difference at closing PoS session' and is a receivable making it easy to find and can be reconciled. closes odoo/odoo#62308 X-original-commit: 7b9545387962981b5729318e2bbc5714dcbac68c Signed-off-by: pimodoo Signed-off-by: Joseph Caburnay (jcb) --- addons/point_of_sale/models/pos_session.py | 33 +++++ .../tests/test_point_of_sale_flow.py | 131 ++++++++++++------ 2 files changed, 119 insertions(+), 45 deletions(-) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 70b8d662075..5b410e6048c 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -352,6 +352,38 @@ class PosSession(models.Model): pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(location_dest_id, lines, picking_type) pickings.write({'pos_session_id': self.id, 'origin': self.name}) + def _create_balancing_line(self, data): + imbalance_amount = 0 + for line in self.move_id.line_ids: + # it is an excess debit so it should be credited + imbalance_amount += line.debit - line.credit + + if (not float_is_zero(imbalance_amount, precision_rounding=self.currency_id.rounding)): + balancing_vals = self._prepare_balancing_line_vals(imbalance_amount, self.move_id) + MoveLine = data.get('MoveLine') + MoveLine.create(balancing_vals) + + return data + + def _prepare_balancing_line_vals(self, imbalance_amount, move): + account = self._get_balancing_account() + partial_vals = { + 'name': _('Difference at closing PoS session'), + 'account_id': account.id, + 'move_id': move.id, + 'partner_id': False, + } + # `imbalance_amount` is already in terms of company currency so it is the amount_converted + # param when calling `_credit_amounts`. amount param will be the converted value of + # `imbalance_amount` from company currency to the session currency. + imbalance_amount_session = 0 + if (not self.is_in_company_currency): + imbalance_amount_session = self.company_id.currency_id._convert(imbalance_amount, self.currency_id, self.company_id, fields.Date.context_today(self)) + return self._credit_amounts(partial_vals, imbalance_amount_session, imbalance_amount) + + def _get_balancing_account(self): + return self.company_id.account_default_pos_receivable_account_id or self.env['ir.property'].get('property_account_receivable_id', 'res.partner') + def _create_account_move(self): """ Create account.move and account.move.line records for this session. @@ -376,6 +408,7 @@ class PosSession(models.Model): data = self._create_cash_statement_lines_and_cash_move_lines(data) data = self._create_invoice_receivable_lines(data) data = self._create_stock_output_lines(data) + data = self._create_balancing_line(data) if account_move.line_ids: account_move._post() 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 c2b1696b39e..04b9b952725 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 @@ -13,6 +13,14 @@ from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon @odoo.tests.tagged('post_install', '-at_install') class TestPointOfSaleFlow(TestPointOfSaleCommon): + def compute_tax(self, product, price, qty=1, taxes=None): + if not taxes: + taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) + currency = self.pos_config.pricelist_id.currency_id + res = taxes.compute_all(price, currency, qty, product=product) + untax = res['total_excluded'] + return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) + def test_order_refund(self): self.pos_config.open_session_cb(check_coa=False) current_session = self.pos_config.current_session_id @@ -196,22 +204,14 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): check the quantity, the locations and return picking logic """ - def compute_tax(product, price, qty=1, taxes=None): - if taxes is None: - taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) - currency = self.pos_config.pricelist_id.currency_id - res = taxes.compute_all(price, currency, qty, product=product) - untax = res['total_excluded'] - return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) - # I click on create a new session button self.pos_config.open_session_cb(check_coa=False) current_session = self.pos_config.current_session_id # I create a PoS order with 2 units of PCSC234 at 450 EUR # and 3 units of PCSC349 at 300 EUR. - untax1, atax1 = compute_tax(self.product3, 450, 2) - untax2, atax2 = compute_tax(self.product4, 300, 3) + untax1, atax1 = self.compute_tax(self.product3, 450, 2) + untax2, atax2 = self.compute_tax(self.product4, 300, 3) self.pos_order_pos1 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, @@ -275,8 +275,8 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): ) # I create a second order - untax1, atax1 = compute_tax(self.product3, 450, -2) - untax2, atax2 = compute_tax(self.product4, 300, -3) + untax1, atax1 = self.compute_tax(self.product3, 450, -2) + untax2, atax2 = self.compute_tax(self.product4, 300, -3) self.pos_order_pos2 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, @@ -339,8 +339,8 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'Move Lines should be in done state.' ) - untax1, atax1 = compute_tax(self.product3, 450, -2) - untax2, atax2 = compute_tax(self.product4, 300, 3) + untax1, atax1 = self.compute_tax(self.product3, 450, -2) + untax2, atax2 = self.compute_tax(self.product4, 300, 3) self.pos_order_pos3 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, @@ -407,19 +407,11 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): def test_order_to_invoice(self): - def compute_tax(product, price, qty=1, taxes=None): - if taxes is None: - taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) - currency = self.pos_config.pricelist_id.currency_id - res = taxes.compute_all(price, currency, qty, product=product) - untax = res['total_excluded'] - return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) - self.pos_config.open_session_cb(check_coa=False) current_session = self.pos_config.current_session_id - untax1, atax1 = compute_tax(self.product3, 450*0.95, 2) - untax2, atax2 = compute_tax(self.product4, 300*0.95, 3) + untax1, atax1 = self.compute_tax(self.product3, 450*0.95, 2) + untax2, atax2 = self.compute_tax(self.product4, 300*0.95, 3) # 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.env.company.id, @@ -529,21 +521,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): Simulation of sales coming from the interface, even after closing the session """ - def compute_tax(product, price, qty=1, taxes=None): - if not taxes: - taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) - currency = self.pos_config.pricelist_id.currency_id - res = taxes.compute_all(price, currency, qty, product=product) - untax = res['total_excluded'] - return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) - # I click on create a new session button self.pos_config.open_session_cb(check_coa=False) current_session = self.pos_config.current_session_id num_starting_orders = len(current_session.order_ids) - untax, atax = compute_tax(self.led_lamp, 0.9) + untax, atax = self.compute_tax(self.led_lamp, 0.9) carrot_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, @@ -577,7 +561,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'id': '00042-003-0014', 'to_invoice': False} - untax, atax = compute_tax(self.whiteboard_pen, 1.2) + untax, atax = self.compute_tax(self.whiteboard_pen, 1.2) zucchini_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, @@ -611,7 +595,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'id': '00043-003-0014', 'to_invoice': False} - untax, atax = compute_tax(self.newspaper_rack, 1.28) + untax, atax = self.compute_tax(self.newspaper_rack, 1.28) newspaper_rack_order = {'data': {'amount_paid': untax + atax, 'amount_return': 0, @@ -689,14 +673,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'currency_id': self.env.ref('base.EUR').id, }) - def compute_tax(product, price, qty=1, taxes=None): - if not taxes: - taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) - currency = self.pos_config.pricelist_id.currency_id - res = taxes.compute_all(price, currency, qty, product=product) - untax = res['total_excluded'] - return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) - # make a config that has currency different from the company eur_pricelist = self.partner1.property_product_pricelist.copy(default={'currency_id': self.env.ref('base.EUR').id}) sale_journal = self.env['account.journal'].create({ @@ -724,8 +700,8 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # 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) - untax1, atax1 = compute_tax(self.product3, 450, 2) - untax2, atax2 = compute_tax(self.product4, 300, 3) + untax1, atax1 = self.compute_tax(self.product3, 450, 2) + untax2, atax2 = self.compute_tax(self.product4, 300, 3) self.pos_order_pos0 = self.PosOrder.create({ 'company_id': self.env.company.id, 'session_id': current_session.id, @@ -877,3 +853,68 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): self.assertFalse(iline.tax_ids) self.pos_config.current_session_id.action_pos_session_closing_control() + + def test_order_with_deleted_tax(self): + # create tax + dummy_50_perc_tax = self.env['account.tax'].create({ + 'name': 'Tax 50%', + 'amount_type': 'percent', + 'amount': 50.0, + 'price_include': 0 + }) + + # set tax to product + product5 = self.env['product.product'].create({ + 'name': 'product5', + 'type': 'product', + 'categ_id': self.env.ref('product.product_category_all').id, + 'taxes_id': dummy_50_perc_tax.ids + }) + + # sell product thru pos + self.pos_config.open_session_cb(check_coa=False) + pos_session = self.pos_config.current_session_id + untax, atax = self.compute_tax(product5, 10.0) + product5_order = {'data': + {'amount_paid': untax + atax, + 'amount_return': 0, + 'amount_tax': atax, + 'amount_total': untax + atax, + 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), + 'fiscal_position_id': False, + 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 42, + 'pack_lot_ids': [], + 'price_unit': 10.0, + 'product_id': product5.id, + 'price_subtotal': 10.0, + 'price_subtotal_incl': 15.0, + 'qty': 1, + 'tax_ids': [(6, 0, product5.taxes_id.ids)]}]], + 'name': 'Order 12345-123-1234', + 'partner_id': False, + 'pos_session_id': pos_session.id, + 'sequence_number': 2, + 'statement_ids': [[0, + 0, + {'amount': untax + atax, + 'name': fields.Datetime.now(), + 'payment_method_id': self.cash_payment_method.id}]], + 'uid': '12345-123-1234', + 'user_id': self.env.uid}, + 'id': '12345-123-1234', + 'to_invoice': False} + self.PosOrder.create_from_ui([product5_order]) + + # delete tax + dummy_50_perc_tax.unlink() + + # close session (should not fail here) + pos_session.action_pos_session_closing_control() + + # check the difference line + diff_line = pos_session.move_id.line_ids.filtered(lambda line: line.name == 'Difference at closing PoS session') + self.assertAlmostEqual(diff_line.credit, 5.0, msg="Missing amount of 5.0")