From ae28f029123565c15f87b038d02fbf1b80d1ef9a Mon Sep 17 00:00:00 2001 From: pedrambiria Date: Tue, 10 May 2022 12:14:08 +0000 Subject: [PATCH] [FIX] point_of_sale: wrong fixed taxes sign for zero amount products Before this commit: If a fixed tax is used for a product with the price of zero, the tax line sign would be positive and lead to an unbalanced move. The solution is to use its sign in its context for correct calculation. Also, in the PoS front-end, when you want to return a zero amount product with a fixed tax, its sign would be positive, which is incorrect. Steps to reproduce the first issue: - Install 'Point of Sale' module - Create a new 'Tax' X with a fixed price (e.g.: 1 $) - Create a new 'Product' with zero Sales Price - Create a PoS session - Add the product to the order - Close the session It creates a line for the "Difference at closing PoS session," and doesn't prevent closing. But the problem is that the tax capture as debit is incorrect. The tax for the invoice must be credit. Steps to reproduce the second issue: - Install 'Point of Sale' module - Create a new 'Tax' X with a fixed price (e.g.: 1 $) - Create a new 'Product' with zero Sales Price - Create a PoS session - Add the product to the order - Change the quantity to -1 (refund) The final amount is positive, while it should be negative. Solution The sign must be sent in the context, and for the PoS front-end, it should consider zero amount cases. opw-2833590 closes odoo/odoo#95067 X-original-commit: c5fb4f778db95fb33845b9a5e0c2190d4d9037ee Signed-off-by: Masereel Pierre Signed-off-by: Joseph Caburnay (jcb) --- addons/point_of_sale/models/pos_session.py | 2 +- .../static/tests/tours/ProductScreen.tour.js | 28 +++++++++ addons/point_of_sale/tests/test_frontend.py | 57 +++++++++++++++++++ .../tests/test_pos_products_with_tax.py | 42 ++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 0107e1a8446..7f31f23ba54 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -1155,7 +1155,7 @@ class PosSession(models.Model): # may arise in 'Round Globally'. check_refund = lambda x: x.qty * x.price_unit < 0 is_refund = check_refund(order_line) - tax_data = tax_ids.compute_all(price_unit=price, quantity=abs(order_line.qty), currency=self.currency_id, is_refund=is_refund) + tax_data = tax_ids.with_context(force_sign=sign).compute_all(price_unit=price, quantity=abs(order_line.qty), currency=self.currency_id, is_refund=is_refund) taxes = tax_data['taxes'] # For Cash based taxes, use the account from the repartition line immediately as it has been paid already for tax in taxes: diff --git a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js index 281a59cbe2a..f532bb59f1f 100644 --- a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js @@ -113,3 +113,31 @@ odoo.define('point_of_sale.tour.ProductScreen', function (require) { Tour.register('ProductScreenTour', { test: true, url: '/pos/ui' }, getSteps()); }); + +odoo.define('point_of_sale.tour.FixedPriceNegativeQty', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + ProductScreen.do.clickHomeCategory(); + + ProductScreen.do.clickDisplayedProduct('Zero Amount Product'); + ProductScreen.check.selectedOrderlineHas('Zero Amount Product', '1.0', '1.0'); + ProductScreen.do.pressNumpad('+/- 1'); + ProductScreen.check.selectedOrderlineHas('Zero Amount Product', '-1.0', '-1.0'); + + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.check.remainingIs('0.00'); + PaymentScreen.do.clickValidate(); + + ReceiptScreen.check.receiptIsThere(); + + Tour.register('FixedTaxNegativeQty', { test: true, url: '/pos/ui' }, getSteps()); +}); diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index 3facada5bd8..fbd2ffd7779 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -524,3 +524,60 @@ class TestUi(TestPointOfSaleHttpCommon): def test_05_ticket_screen(self): self.main_pos_config.open_session_cb() self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'TicketScreenTour', login="accountman") + + def test_fixed_tax_negative_qty(self): + """ Assert the negative amount of a negative-quantity orderline + with zero-amount product with fixed tax. + """ + + # setup the zero-amount product + tax_received_account = self.env['account.account'].create({ + 'name': 'TAX_BASE', + 'code': 'TBASE', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + 'company_id': self.env.company.id, + }) + fixed_tax = self.env['account.tax'].create({ + 'name': 'fixed amount tax', + 'amount_type': 'fixed', + 'amount': 1, + 'invoice_repartition_line_ids': [ + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': tax_received_account.id, + }), + ], + }) + zero_amount_product = self.env['product.product'].create({ + 'name': 'Zero Amount Product', + 'available_in_pos': True, + 'list_price': 0, + 'taxes_id': [(6, 0, [fixed_tax.id])], + }) + + # Make an order with the zero-amount product from the frontend. + # We need to do this because of the fix in the "compute_all" port. + self.main_pos_config.write({'iface_tax_included': 'total'}) + self.main_pos_config.open_session_cb() + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'FixedTaxNegativeQty', login="accountman") + pos_session = self.main_pos_config.current_session_id + + # Close the session and check the session journal entry. + pos_session.action_pos_session_validate() + + lines = pos_session.move_id.line_ids.sorted('balance') + + # order in the tour is paid using the bank payment method. + bank_pm = self.main_pos_config.payment_method_ids.filtered(lambda pm: pm.name == 'Bank') + + self.assertEqual(lines[0].account_id, bank_pm.receivable_account_id or self.env.company.account_default_pos_receivable_account_id) + self.assertAlmostEqual(lines[0].balance, -1) + self.assertEqual(lines[1].account_id, zero_amount_product.categ_id.property_account_income_categ_id) + self.assertAlmostEqual(lines[1].balance, 0) + self.assertEqual(lines[2].account_id, tax_received_account) + self.assertAlmostEqual(lines[2].balance, 1) diff --git a/addons/point_of_sale/tests/test_pos_products_with_tax.py b/addons/point_of_sale/tests/test_pos_products_with_tax.py index 9c4873d51ed..45539fd63b7 100644 --- a/addons/point_of_sale/tests/test_pos_products_with_tax.py +++ b/addons/point_of_sale/tests/test_pos_products_with_tax.py @@ -501,3 +501,45 @@ class TestPoSProductsWithTax(TestPoSCommon): {'account_id': self.tax_received_account.id, 'balance': 15.14, 'tax_ids': [], 'tax_tag_ids': self.tax_tag_refund_tax.ids}, {'account_id': self.sale_account.id, 'balance': 72.07, 'tax_ids': tax_21_incl.ids, 'tax_tag_ids': self.tax_tag_refund_base.ids}, ]) + + def test_fixed_tax_positive_qty(self): + + fixed_tax = self.env['account.tax'].create({ + 'name': 'fixed amount tax', + 'amount_type': 'fixed', + 'amount': 1, + 'invoice_repartition_line_ids': [ + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [(6, 0, self.tax_tag_invoice_base.ids)], + }), + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': self.tax_received_account.id, + 'tag_ids': [(6, 0, self.tax_tag_invoice_tax.ids)], + }), + ], + }) + + zero_amount_product = self.env['product.product'].create({ + 'name': 'Zero Amount Product', + 'available_in_pos': True, + 'list_price': 0, + 'taxes_id': [(6, 0, [fixed_tax.id])], + }) + + self.open_new_session() + self.env['pos.order'].create_from_ui([self.create_ui_order_data([ + (zero_amount_product, 1), + ])]) + self.pos_session.action_pos_session_validate() + + lines = self.pos_session.move_id.line_ids.sorted('balance') + + self.assertRecordValues(lines, [ + {'account_id': self.tax_received_account.id, 'balance': -1}, + {'account_id': self.sale_account.id, 'balance': 0}, + {'account_id': self.cash_pm1.receivable_account_id.id, 'balance': 1}, + ])