[FIX] point_of_sale: ensure accurate price display with tax exclusion

Before this commit, when a product's tax was included in its price and
the Point of Sale setting was set to "Tax-Excluded Price",
the following issues were encountered:

 1. The product screen displayed the orderline's price tax included.
 2. If the pricelist's discount policy was set to without_discount,
    and a discount was applied to a line, the old/new price would
    not be displayed.
 3. The receipt showed the discount based on the price inclusive of
    tax, rather than the tax-excluded price.

Additionally, when the product's tax wasn't included in its price and
the Point of Sale setting was set to "Tax-Included Price", the
discount displayed on the receipt was incorrect.

This commit resolves these issues, ensuring the correct tax-exclusive
and tax-inclusive prices are displayed in all scenarios, providing
accurate discount calculations on the receipt.

opw-3324090

closes odoo/odoo#130820

X-original-commit: f2569a6583ac799b1b01c023cb8ccb65d294f867
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
Signed-off-by: Pedram Bi Ria (pebr) <pebr@odoo.com>
This commit is contained in:
pedrambiria
2023-08-04 12:22:02 +02:00
committed by Pedram (PEBR)
parent 8a787f1866
commit c97cc9498e
4 changed files with 146 additions and 89 deletions
@@ -309,21 +309,26 @@ export class Product extends PosModel {
return price;
}
get_display_price(pricelist, quantity) {
if (this.pos.config.iface_tax_included === "total") {
const order = this.pos.get_order();
const taxes = this.pos.get_taxes_after_fp(
this.taxes_id,
order && order.fiscal_position
);
const allPrices = this.pos.compute_all(
taxes,
this.get_price(pricelist, quantity),
1,
this.pos.currency.rounding
);
const order = this.pos.get_order();
const taxes = this.pos.get_taxes_after_fp(
this.taxes_id,
order && order.fiscal_position
);
const currentTaxes = this.pos.getTaxesByIds(this.taxes_id);
const priceAfterFp = this.pos.computePriceAfterFp(
this.get_price(pricelist, quantity),
currentTaxes
);
const allPrices = this.pos.compute_all(
taxes,
priceAfterFp,
1,
this.pos.currency.rounding
);
if (this.pos.config.iface_tax_included === 'total') {
return allPrices.total_included;
} else {
return this.get_price(pricelist, quantity);
return allPrices.total_excluded;
}
}
}
@@ -835,7 +840,14 @@ export class Orderline extends PosModel {
if (this.pos.config.iface_tax_included === "total") {
return this.get_all_prices(1).priceWithTax;
} else {
return this.get_unit_price();
return this.get_all_prices(1).priceWithoutTax;
}
}
getUnitDisplayPriceBeforeDiscount(){
if (this.pos.config.iface_tax_included === 'total') {
return this.get_all_prices(1).priceWithTaxBeforeDiscount;
} else {
return this.get_all_prices(1).priceWithoutTaxBeforeDiscount;
}
}
get_base_price() {
@@ -868,20 +880,20 @@ export class Orderline extends PosModel {
if (this.pos.config.iface_tax_included === "total") {
return this.get_price_with_tax();
} else {
return this.get_base_price();
return this.get_price_without_tax();
}
}
get_taxed_lst_unit_price() {
var lst_price = this.compute_fixed_price(this.get_lst_price());
if (this.pos.config.iface_tax_included === "total") {
var product = this.get_product();
var taxes_ids = product.taxes_id;
var product_taxes = this.pos.get_taxes_after_fp(taxes_ids);
return this.compute_all(product_taxes, lst_price, 1, this.pos.currency.rounding)
.total_included;
get_taxed_lst_unit_price(){
const lstPrice = this.compute_fixed_price(this.get_lst_price());
const product = this.get_product();
const taxesIds = product.taxes_id;
const productTaxes = this.pos.get_taxes_after_fp(taxesIds, this.order.fiscal_position);
const unitPrices = this.compute_all(productTaxes, lstPrice, 1, this.pos.currency.rounding);
if (this.pos.config.iface_tax_included === 'total') {
return unitPrices.total_included;
} else {
return unitPrices.total_excluded;
}
var digits = this.pos.dp["Product Price"];
return lst_price.toFixed(digits);
}
get_price_without_tax() {
return this.get_all_prices().priceWithoutTax;
@@ -917,13 +929,7 @@ export class Orderline extends PosModel {
}
get_taxes() {
var taxes_ids = this.tax_ids || this.get_product().taxes_id;
var taxes = [];
for (var i = 0; i < taxes_ids.length; i++) {
if (this.pos.taxes_by_id[taxes_ids[i]]) {
taxes.push(this.pos.taxes_by_id[taxes_ids[i]]);
}
}
return taxes;
return this.pos.getTaxesByIds(taxes_ids);
}
/**
* Calculate the amount of taxes of a specific Orderline, that are included in the price.
@@ -1006,6 +1012,7 @@ export class Orderline extends PosModel {
priceWithTax: all_taxes.total_included,
priceWithoutTax: all_taxes.total_excluded,
priceWithTaxBeforeDiscount: all_taxes_before_discount.total_included,
priceWithoutTaxBeforeDiscount: all_taxes_before_discount.total_excluded,
tax: taxtotal,
taxDetails: taxdetail,
};
@@ -1013,51 +1020,8 @@ export class Orderline extends PosModel {
display_discount_policy() {
return this.order.pricelist ? this.order.pricelist.discount_policy : "with_discount";
}
compute_fixed_price(price) {
var order = this.order;
if (order.fiscal_position) {
var taxes = this.get_taxes();
var mapped_included_taxes = [];
var new_included_taxes = [];
var self = this;
taxes.forEach((tax) => {
var line_taxes = self.pos.get_taxes_after_fp([tax.id], order.fiscal_position);
if (line_taxes.length && line_taxes[0].price_include) {
new_included_taxes = new_included_taxes.concat(line_taxes);
}
if (tax.price_include && !line_taxes.includes(tax)) {
mapped_included_taxes.push(tax);
}
});
if (mapped_included_taxes.length > 0) {
if (new_included_taxes.length > 0) {
var price_without_taxes = this.compute_all(
mapped_included_taxes,
price,
1,
order.pos.currency.rounding,
true
).total_excluded;
return this.compute_all(
new_included_taxes,
price_without_taxes,
1,
order.pos.currency.rounding,
false
).total_included;
} else {
return this.compute_all(
mapped_included_taxes,
price,
1,
order.pos.currency.rounding,
true
).total_excluded;
}
}
}
return price;
compute_fixed_price (price) {
return this.pos.computePriceAfterFp(price, this.get_taxes());
}
get_fixed_lst_price() {
return this.compute_fixed_price(this.get_lst_price());
@@ -2221,17 +2185,12 @@ export class Order extends PosModel {
_get_ignored_product_ids_total_discount() {
return [];
}
_reduce_total_discount_callback(sum, orderLine) {
sum +=
orderLine.get_unit_price() *
(orderLine.get_discount() / 100) *
orderLine.get_quantity();
if (orderLine.display_discount_policy() === "without_discount") {
sum +=
(orderLine.get_taxed_lst_unit_price() - orderLine.get_unit_price()) *
orderLine.get_quantity();
_reduce_total_discount_callback(sum, orderLine){
let discountUnitPrice = orderLine.getUnitDisplayPriceBeforeDiscount() * (orderLine.get_discount()/100);
if (orderLine.display_discount_policy() === 'without_discount'){
discountUnitPrice += orderLine.get_taxed_lst_unit_price() - orderLine.getUnitDisplayPriceBeforeDiscount();
}
return sum;
return sum + discountUnitPrice * orderLine.get_quantity();
}
get_total_discount() {
const ignored_product_ids = this._get_ignored_product_ids_total_discount();
@@ -2239,12 +2198,12 @@ export class Order extends PosModel {
this.orderlines.reduce((sum, orderLine) => {
if (!ignored_product_ids.includes(orderLine.product.id)) {
sum +=
orderLine.get_unit_price() *
orderLine.getUnitDisplayPriceBeforeDiscount() *
(orderLine.get_discount() / 100) *
orderLine.get_quantity();
if (orderLine.display_discount_policy() === "without_discount") {
sum +=
(orderLine.get_lst_price() - orderLine.get_unit_price()) *
(orderLine.get_taxed_lst_unit_price() - orderLine.getUnitDisplayPriceBeforeDiscount()) *
orderLine.get_quantity();
}
}
@@ -957,6 +957,62 @@ export class PosStore extends Reactive {
return this.orders;
}
computePriceAfterFp(price, taxes){
const order = this.get_order();
if(order && order.fiscal_position) {
let mapped_included_taxes = [];
let new_included_taxes = [];
taxes.forEach(tax => {
const line_taxes = this.get_taxes_after_fp([tax.id], order.fiscal_position);
if (line_taxes.length && line_taxes[0].price_include){
new_included_taxes = new_included_taxes.concat(line_taxes);
}
if(tax.price_include && !line_taxes.includes(tax)){
mapped_included_taxes.push(tax);
}
});
if (mapped_included_taxes.length > 0) {
if (new_included_taxes.length > 0) {
const price_without_taxes = this.compute_all(
mapped_included_taxes,
price,
1,
this.currency.rounding,
true
).total_excluded
return this.compute_all(
new_included_taxes,
price_without_taxes,
1,
this.currency.rounding,
false
).total_included
}
else{
return this.compute_all(
mapped_included_taxes,
price,
1,
this.currency.rounding,
true
).total_excluded;
}
}
}
return price;
}
getTaxesByIds(taxIds) {
let taxes = [];
for (let i = 0; i < taxIds.length; i++) {
if (this.taxes_by_id[taxIds[i]]) {
taxes.push(this.taxes_by_id[taxIds[i]]);
}
}
return taxes;
}
/**
* Renders the HTML for the customer display and returns it as a string.
*
@@ -175,3 +175,20 @@ registry.category("web_tour.tours").add("CashClosingDetails", {
return getSteps()
}
});
registry.category("web_tour.tours").add("ShowTaxExcludedTour", {
test: true,
url: "/pos/ui",
steps: () => {
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product');
ProductScreen.check.selectedOrderlineHas('Test Product', '1.0', '100.0');
ProductScreen.check.totalAmountIs('110.0');
return getSteps()
}
});
@@ -856,6 +856,31 @@ class TestUi(TestPointOfSaleHttpCommon):
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptScreenDiscountWithPricelistTour', login="pos_user")
def test_08_show_tax_excluded(self):
# define a tax included tax record
tax = self.env['account.tax'].create({
'name': 'Tax 10% Included',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
})
# define a product record with the tax
self.env['product.product'].create({
'name': 'Test Product',
'list_price': 110,
'taxes_id': [(6, 0, [tax.id])],
'available_in_pos': True,
})
# set Tax-Excluded Price
self.main_pos_config.write({
'iface_tax_included': 'subtotal'
})
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ShowTaxExcludedTour', login="pos_user")
# This class just runs the same tests as above but with mobile emulation
class MobileTestUi(TestUi):
browser_size = '375x667'