Skip to content

Commit 9538698

Browse files
committed
[REF] point_of_sale, pos_*: improve tax computation in PoS
*: l10n_es_pos, l10n_sa_pos, point_of_sale, pos_discount, pos_loyalty, pos_razorpay, pos_restaurant, pos_self_order Before this commit taxes computation was a mess in PoS, with differents implementations in different places, and some of them not taking into account all the complexity of the tax system (taxes included in price, taxes on taxes, etc.). This commit aims to unify all the tax computation logic in a single place, and to make it more robust and easier to understand. --- What's changes: Rounding methods available in PoS are now: 1) Rounding applied only on cash payments In this case the remaining due is rounded only if there is at least one cash payment line and the remaining due is less than the rounding tolerance. 2) Rounding applied on all payment methods In this case the remaining due is always rounded even if a card payment method is used. The remaining due is rounded if it is less than the rounding tolerance. No payment method is rounded in this case, the whole order is rounded instead. Taxes computation is now done globally on the order, and not on each line. This way we ensure that the total tax amount is always correct, even if there is some rounding issues on the lines. --- Changes in tests: `test_cash_rounding_down_add_invoice_line_not_only_round_cash_method_with_residual_rounding` `test_cash_rounding_up_add_invoice_line_not_only_round_cash_method` Are removed because the tested behavior is not longer present. Now when rounding is enabled with only_round_cash_method=False, the whole order is always rounded. Accounting tests are now principally tested with Hoot instead of tours. --- Developer note: No prices should be calculated manually in the code base. Getters have been created for this purpose in the following files: - `product_template_accounting.js` - `pos_order_accounting.js` - `pos_order_line_accounting.js` In the future, all PRs containing tax calculations must justify them. closes odoo#229683 Taskid: 5143758 Related: odoo/enterprise#96123 Signed-off-by: David Monnom (moda) <[email protected]>
1 parent 7f20097 commit 9538698

File tree

97 files changed

+2149
-2277
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+2149
-2277
lines changed

addons/l10n_es_pos/static/src/app/models/pos_order.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ patch(PosOrder.prototype, {
66
canBeSimplifiedInvoiced() {
77
return (
88
this.config.is_spanish &&
9-
roundCurrency(this.getTotalWithTax(), this.currency) <
9+
roundCurrency(this.priceIncl, this.currency) <
1010
this.company.l10n_es_simplified_invoice_limit
1111
);
1212
},

addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
Old unit price:
1616
<span class="oldPrice">
1717
<s>
18-
<t t-out="formatCurrency(line.getTaxedlstUnitPrice())" /> / Units
18+
<t t-out="line.currencyDisplayPrice" /> / Units
1919
</s>
2020
</span>
2121
</div>

addons/l10n_fr_pos_cert/static/src/xml/OrderSummary.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Old unit price:
88
<span class="oldPrice">
99
<s>
10-
<t t-esc="env.utils.formatCurrency(line.getTaxedlstUnitPrice())" /> / Units
10+
<t t-esc="line.currencyDisplayPrice" /> / Units
1111
</s>
1212
</span>
1313
</li>

addons/l10n_fr_pos_cert/tests/test_hash.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def test_hashes_should_be_equal_if_no_alteration(self):
4444
paid_order = {
4545
'access_token': False,
4646
'amount_paid': 20,
47-
'amount_return': 5.0,
47+
'amount_return': -5.0,
4848
'amount_tax': 0,
4949
'amount_total': 15.0,
5050
'date_order': fields.Datetime.to_string(fields.Datetime.now()),

addons/l10n_my_edi_pos/tests/test_myinvois_pos.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ def test_refund_constrains_consolidated_invoice(self):
469469
# Fails, the order should be invoiced in such a case
470470
with self.assertRaises(UserError):
471471
self._create_order({
472+
'pos_order_ui_args': {
473+
'is_refund': True,
474+
},
472475
'pos_order_lines_ui_args': [
473476
{
474477
'product': self.product_one,
@@ -480,6 +483,9 @@ def test_refund_constrains_consolidated_invoice(self):
480483
# If it is, it will work
481484
self.invoicing_customer.vat = 'EI00000000010'
482485
self._create_order({
486+
'pos_order_ui_args': {
487+
'is_refund': True,
488+
},
483489
'pos_order_lines_ui_args': [
484490
{
485491
'product': self.product_one,
@@ -500,6 +506,9 @@ def test_refund_constrains_regular_invoice(self):
500506
# Fails, the order should be invoiced in such a case
501507
with self.assertRaises(UserError):
502508
self._create_order({
509+
'pos_order_ui_args': {
510+
'is_refund': True,
511+
},
503512
'pos_order_lines_ui_args': [
504513
{
505514
'product': self.product_one,
@@ -510,6 +519,9 @@ def test_refund_constrains_regular_invoice(self):
510519
})
511520
# If invoicing is checked, it will work.
512521
self._create_order({
522+
'pos_order_ui_args': {
523+
'is_refund': True,
524+
},
513525
'pos_order_lines_ui_args': [
514526
{
515527
'product': self.product_one,
@@ -571,6 +583,9 @@ def test_consolidate_invoices_refund_with_customer(self):
571583
# We then create the refund for the order
572584
with self.with_pos_session(), patch(CONTACT_PROXY_METHOD, new=self._mock_successful_submission):
573585
self._create_order({
586+
'pos_order_ui_args': {
587+
'is_refund': True,
588+
},
574589
'pos_order_lines_ui_args': [
575590
{
576591
'product': self.product_one,
@@ -690,6 +705,9 @@ def test_consolidate_invoices_refund_export_xml(self):
690705
# We then create the refund for the first_order
691706
with self.with_pos_session(), patch(CONTACT_PROXY_METHOD, new=self._mock_successful_submission):
692707
self._create_order({
708+
'pos_order_ui_args': {
709+
'is_refund': True,
710+
},
693711
'pos_order_lines_ui_args': [
694712
{
695713
'product': product_1,

addons/l10n_sa_pos/static/src/overrides/models/pos_order.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ patch(PosOrder.prototype, {
1616
company.name,
1717
company.vat,
1818
this.date_order,
19-
this.getTotalWithTax(),
20-
this.getTotalTax()
19+
this.priceIncl,
20+
this.amountTaxes
2121
);
2222
const qr_code_svg = new XMLSerializer().serializeToString(
2323
codeWriter.write(qr_values, 200, 200)

addons/point_of_sale/models/pos_order.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def _process_payment_lines(self, pos_order, order, pos_session, draft):
188188
return_payment_vals = {
189189
'name': _('return'),
190190
'pos_order_id': order.id,
191-
'amount': -pos_order['amount_return'],
191+
'amount': pos_order['amount_return'],
192192
'payment_date': fields.Datetime.now(),
193193
'payment_method_id': cash_payment_method.id,
194194
'is_change': True,
@@ -209,7 +209,7 @@ def _prepare_tax_base_line_values(self):
209209
@api.model
210210
def _get_invoice_lines_values(self, line_values, pos_line, move_type):
211211
# correct quantity sign based on move type and if line is refund.
212-
is_refund_order = pos_line.order_id.amount_total < 0.0
212+
is_refund_order = pos_line.order_id.is_refund
213213
qty_sign = -1 if (
214214
(move_type == 'out_invoice' and is_refund_order)
215215
or (move_type == 'out_refund' and not is_refund_order)
@@ -847,8 +847,7 @@ def _prepare_invoice_vals(self):
847847
fiscal_position = self.fiscal_position_id
848848
pos_config = self.config_id
849849
rounding_method = pos_config.rounding_method
850-
amount_total = sum(order.amount_total for order in self)
851-
move_type = 'out_invoice' if amount_total >= 0 else 'out_refund'
850+
move_type = 'out_invoice' if not any(order.is_refund for order in self) else 'out_refund'
852851
invoice_payment_term_id = (
853852
self.partner_id.property_payment_term_id.id
854853
if self.partner_id.property_payment_term_id and any(p.payment_method_id.type == 'pay_later' for p in self.payment_ids)

addons/point_of_sale/static/src/app/components/order_display/order_display.xml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,15 @@
2727
<TagsList tags="getInternalNotes()"/>
2828
</div>
2929
</div>
30-
<t t-set="taxTotals" t-value="order.taxTotals" />
31-
<div t-if="taxTotals and props.mode !== 'receipt'" class="order-summary d-flex flex-column gap-1 p-2 border-bottom fw-bolder lh-sm">
32-
<div t-if="taxTotals.has_tax_groups" class="tax-info subentry d-flex justify-content-between w-100 fs-6 text-muted ">
30+
<div t-if="props.mode !== 'receipt'" class="order-summary d-flex flex-column gap-1 p-2 border-bottom fw-bolder lh-sm">
31+
<div t-if="order.prices.taxDetails.has_tax_groups" class="tax-info subentry d-flex justify-content-between w-100 fs-6 text-muted ">
3332
Taxes
3433
<div id="order-widget-taxes">
35-
<span t-esc="formatCurrency(taxTotals.order_sign * taxTotals.tax_amount_currency)" class="tax"/>
34+
<span t-esc="order.currencyAmountTaxes" class="tax"/>
3635
</div>
3736
</div>
3837
<div class="d-flex justify-content-between w-100 fs-3">
39-
Total
40-
<span t-esc="formatCurrency(taxTotals.order_sign * taxTotals.order_total)"
41-
class="total"/>
38+
Total <span t-esc="order.currencyDisplayPrice" class="total"/>
4239
</div>
4340
</div>
4441
</div>

addons/point_of_sale/static/src/app/components/orderline/orderline.js

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,6 @@ export class Orderline extends Component {
2727
onLongPress: () => {},
2828
};
2929

30-
formatCurrency(amount) {
31-
return formatCurrency(amount, this.line.currency.id);
32-
}
33-
34-
get line() {
35-
return this.props.line;
36-
}
37-
38-
get taxGroup() {
39-
return [
40-
...new Set(
41-
this.line.product_id.taxes_id
42-
?.map((tax) => tax.tax_group_id.pos_receipt_label)
43-
.filter((label) => label)
44-
),
45-
].join(" ");
46-
}
47-
getInternalNotes() {
48-
return JSON.parse(this.line.note || "[]");
49-
}
50-
5130
setup() {
5231
this.root = useRef("root");
5332
if (this.props.mode === "display") {
@@ -69,4 +48,98 @@ export class Orderline extends Component {
6948
]);
7049
}
7150
}
51+
52+
get line() {
53+
return this.props.line;
54+
}
55+
56+
get lineContainerClasses() {
57+
return {
58+
selected: this.line.isSelected() && this.props.mode === "display",
59+
...this.line.getDisplayClasses(),
60+
...(this.props.class || []),
61+
"border-start": this.props.mode != "receipt" && this.line.combo_parent_id,
62+
"orderline-combo fst-italic ms-4": this.line.combo_parent_id,
63+
"position-relative d-flex align-items-center lh-sm cursor-pointer": true, // Keep all classes here
64+
};
65+
}
66+
67+
get lineClasses() {
68+
const line = this.line;
69+
const props = this.props;
70+
if (line.combo_parent_id) {
71+
return props.mode === "receipt" ? "px-2" : "p-2";
72+
} else {
73+
if (props.mode === "receipt") {
74+
return line.combo_line_ids.length > 0 ? "" : "py-1";
75+
} else {
76+
return "p-2";
77+
}
78+
}
79+
}
80+
81+
get infoListClasses() {
82+
const line = this.line;
83+
const props = this.props;
84+
if (props.mode === "receipt") {
85+
return "";
86+
}
87+
if (line.customer_note || line.note || line.discount || line.packLotLines?.length) {
88+
return "gap-2 mt-1";
89+
}
90+
return "";
91+
}
92+
93+
/**
94+
* To avoid to much logic in the template, we compute all values here
95+
* and use them in the template.
96+
*/
97+
get lineScreenValues() {
98+
const line = this.line;
99+
100+
// Prevent rendering if the line is not yet linked to an order
101+
// this can happen during related models connections
102+
if (!line.order_id) {
103+
return {};
104+
}
105+
106+
const imageUrl = line.product_id?.getImageUrl();
107+
const basic = this.props.basic_receipt;
108+
const unitPart = line.getQuantityStr().unitPart;
109+
const decimalPart = line.getQuantityStr().decimalPart;
110+
const decimalPoint = line.getQuantityStr().decimalPoint;
111+
const discount = line.getDiscountStr();
112+
const mode = this.props.mode;
113+
const attributeStr = line.orderDisplayProductName.attributeString;
114+
const taxGroup = [
115+
...new Set(
116+
this.line.product_id.taxes_id
117+
?.map((tax) => tax.tax_group_id.pos_receipt_label)
118+
.filter((label) => label)
119+
),
120+
].join(" ");
121+
const showPrice =
122+
!basic &&
123+
line.getQuantityStr() != 1 &&
124+
(mode === "receipt" || (line.price_type !== "original" && !line.combo_parent_id));
125+
const priceUnit = `${line.currencyDisplayPriceUnit} / ${
126+
line.product_id?.uom_id?.name || ""
127+
}`;
128+
return {
129+
name: mode === "receipt" ? line.full_product_name : line.orderDisplayProductName.name,
130+
attributeString: mode === "display" && attributeStr && `- ${attributeStr}`,
131+
internalNote: mode === "display" && line.note && JSON.parse(this.line.note || "[]"),
132+
isReceipt: mode === "receipt",
133+
isDisplay: mode === "display",
134+
discount: !basic && discount && discount !== "0" && !line.combo_parent_id && discount,
135+
noDiscountPrice: formatCurrency(line.displayPriceNoDiscount, line.currency.id),
136+
displayPriceUnit: showPrice && line.price !== 0 && priceUnit,
137+
unitPart: unitPart,
138+
decimalPart: decimalPart && `${decimalPoint}${decimalPart}`,
139+
productImage: this.props.showImage && imageUrl,
140+
taxGroup: this.props.showTaxGroup && taxGroup,
141+
price: !basic && !line.combo_parent_id && this.line.currencyDisplayPrice,
142+
lotLines: line.product_id.tracking !== "none" && (line.packLotLines || []),
143+
};
144+
}
72145
}

0 commit comments

Comments
 (0)