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}, + ])