mirror of
https://github.com/bringout/oca-ocb-pos.git
synced 2026-04-23 18:42:02 +02:00
19.0 vanilla
This commit is contained in:
parent
6e54c1af6c
commit
3ca647e428
1087 changed files with 132065 additions and 108499 deletions
|
|
@ -0,0 +1,26 @@
|
|||
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(PosOrderline.prototype, {
|
||||
/**
|
||||
* Checks if the current line applies for a global discount from `pos_discount.DiscountButton`.
|
||||
* @returns Boolean
|
||||
*/
|
||||
isGlobalDiscountApplicable() {
|
||||
return !(
|
||||
// Ignore existing discount line as not removing it before adding new discount line successfully
|
||||
(
|
||||
(this.config.tip_product_id &&
|
||||
this.product_id.id === this.config.tip_product_id?.id) ||
|
||||
(this.config.discount_product_id &&
|
||||
this.product_id.id === this.config.discount_product_id?.id)
|
||||
)
|
||||
);
|
||||
},
|
||||
get isDiscountLine() {
|
||||
return (
|
||||
this.config.module_pos_discount &&
|
||||
this.product_id.id === this.config.discount_product_id?.id
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { NumberPopup } from "@point_of_sale/app/components/popups/number_popup/number_popup";
|
||||
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ControlButtons.prototype, {
|
||||
async clickDiscount() {
|
||||
this.dialog.add(NumberPopup, {
|
||||
title: _t("Discount Percentage"),
|
||||
startingValue: this.pos.config.discount_pc,
|
||||
getPayload: (num) => {
|
||||
const percent = Math.max(
|
||||
0,
|
||||
Math.min(100, this.env.utils.parseValidFloat(num.toString()))
|
||||
);
|
||||
this.applyDiscount(percent);
|
||||
},
|
||||
});
|
||||
},
|
||||
// FIXME business method in a compoenent, maybe to move in pos_store
|
||||
async applyDiscount(percent) {
|
||||
return this.pos.applyDiscount(percent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_discount.ControlButtons" t-inherit="point_of_sale.ControlButtons" t-inherit-mode="extension">
|
||||
<xpath
|
||||
expr="//t[@t-if='props.showRemainingButtons']/div/button[hasclass('o_pricelist_button')]"
|
||||
position="before">
|
||||
<button t-if="pos.config.module_pos_discount and pos.config.discount_product_id and pos.cashier._role !== 'minimal'"
|
||||
class="js_discount"
|
||||
t-att-class="buttonClass"
|
||||
t-on-click="() => this.clickDiscount()">
|
||||
<i class="fa fa-tag me-1"/>Discount
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ProductScreen.prototype, {
|
||||
getNumpadButtons() {
|
||||
const buttons = super.getNumpadButtons();
|
||||
if (!this.currentOrder?.getSelectedOrderline()?.isDiscountLine) {
|
||||
return buttons;
|
||||
}
|
||||
const toDisable = new Set(["quantity", "discount"]);
|
||||
return buttons.map((button) => {
|
||||
if (toDisable.has(button.value)) {
|
||||
return { ...button, disabled: true };
|
||||
}
|
||||
return button;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
|
||||
|
||||
patch(TicketScreen.prototype, {
|
||||
async onDoRefund() {
|
||||
await super.onDoRefund(...arguments);
|
||||
const order = this.getSelectedOrder();
|
||||
const discountLines = order.discountLines;
|
||||
const destinationOrder = this.pos.getOrder();
|
||||
|
||||
if (discountLines?.length && destinationOrder) {
|
||||
const percentage = order.globalDiscountPc;
|
||||
this.pos.applyDiscount(percentage, destinationOrder);
|
||||
}
|
||||
},
|
||||
|
||||
_onUpdateSelectedOrderline() {
|
||||
const selectedOrderlineId = this.getSelectedOrderlineId();
|
||||
const orderline = this.getSelectedOrder().lines.find(
|
||||
(line) => line.id == selectedOrderlineId
|
||||
);
|
||||
if (orderline && orderline.product_id.id === this.pos.config.discount_product_id?.id) {
|
||||
return this.dialog.add(AlertDialog, {
|
||||
title: _t("Error"),
|
||||
body: _t("You cannot edit a discount line."),
|
||||
});
|
||||
}
|
||||
return super._onUpdateSelectedOrderline(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { accountTaxHelpers } from "@account/helpers/account_tax";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { PosOrderAccounting } from "@point_of_sale/app/models/accounting/pos_order_accounting";
|
||||
|
||||
patch(PosStore.prototype, {
|
||||
async setup() {
|
||||
await super.setup(...arguments);
|
||||
this.debouncedDiscount = debounce(this.applyDiscount.bind(this));
|
||||
|
||||
const updateOrderDiscount = (order) => {
|
||||
if (!order || order.state !== "draft") {
|
||||
return;
|
||||
}
|
||||
if (!order.globalDiscountPc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const percentage = order.globalDiscountPc;
|
||||
this.debouncedDiscount(percentage, order); // Wait an animation frame before applying the discount
|
||||
};
|
||||
|
||||
this.models["pos.order.line"].addEventListener("update", (data) => {
|
||||
const line = this.models["pos.order.line"].get(data.id);
|
||||
const order = line.order_id;
|
||||
|
||||
if (!line.isDiscountLine) {
|
||||
updateOrderDiscount(order);
|
||||
}
|
||||
});
|
||||
|
||||
this.models["pos.order"].addEventListener("update", ({ id, fields }) => {
|
||||
const areAccountingFields = fields?.some((field) =>
|
||||
PosOrderAccounting.accountingFields.has(field)
|
||||
);
|
||||
|
||||
if (areAccountingFields) {
|
||||
updateOrderDiscount(this.models["pos.order"].get(id));
|
||||
}
|
||||
});
|
||||
},
|
||||
selectOrderLine(order, line) {
|
||||
super.selectOrderLine(order, line);
|
||||
// Ensure the numpadMode should be `price` when the discount line is selected
|
||||
if (line?.isDiscountLine) {
|
||||
this.numpadMode = "price";
|
||||
}
|
||||
},
|
||||
async applyDiscount(percent, order = this.getOrder()) {
|
||||
const taxKey = (taxIds) =>
|
||||
taxIds
|
||||
.map((tax) => tax.id)
|
||||
.sort((a, b) => a - b)
|
||||
.join("_");
|
||||
|
||||
const product = this.config.discount_product_id;
|
||||
if (product === undefined) {
|
||||
this.dialog.add(AlertDialog, {
|
||||
title: _t("No discount product found"),
|
||||
body: _t(
|
||||
"The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'."
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const discountLinesMap = {};
|
||||
(order.discountLines || []).forEach((line) => {
|
||||
const key = taxKey(line.tax_ids);
|
||||
discountLinesMap[key] = line;
|
||||
});
|
||||
const isGlobalDiscountBtnClicked = Object.keys(discountLinesMap).length === 0;
|
||||
|
||||
const lines = order.getOrderlines();
|
||||
const discountableLines = lines.filter((line) => line.isGlobalDiscountApplicable());
|
||||
const baseLines = discountableLines.map((line) =>
|
||||
accountTaxHelpers.prepare_base_line_for_taxes_computation(
|
||||
line,
|
||||
line.prepareBaseLineForTaxesComputationExtraValues()
|
||||
)
|
||||
);
|
||||
accountTaxHelpers.add_tax_details_in_base_lines(baseLines, order.company_id);
|
||||
accountTaxHelpers.round_base_lines_tax_details(baseLines, order.company_id);
|
||||
|
||||
const groupingFunction = (base_line) => ({
|
||||
grouping_key: { product_id: product },
|
||||
raw_grouping_key: { product_id: product.id },
|
||||
});
|
||||
|
||||
const globalDiscountBaseLines = accountTaxHelpers.prepare_global_discount_lines(
|
||||
baseLines,
|
||||
order.company_id,
|
||||
"percent",
|
||||
percent,
|
||||
{
|
||||
computation_key: "global_discount",
|
||||
grouping_function: groupingFunction,
|
||||
}
|
||||
);
|
||||
let lastDiscountLine = null;
|
||||
for (const baseLine of globalDiscountBaseLines) {
|
||||
const extra_tax_data = accountTaxHelpers.export_base_line_extra_tax_data(baseLine);
|
||||
extra_tax_data.discount_percentage = percent;
|
||||
|
||||
const key = taxKey(baseLine.tax_ids);
|
||||
const existingLine = discountLinesMap[key];
|
||||
|
||||
if (existingLine) {
|
||||
existingLine.extra_tax_data = extra_tax_data;
|
||||
existingLine.price_unit = baseLine.price_unit;
|
||||
delete discountLinesMap[key];
|
||||
} else {
|
||||
lastDiscountLine = await this.addLineToOrder(
|
||||
{
|
||||
product_id: baseLine.product_id,
|
||||
price_unit: baseLine.price_unit,
|
||||
qty: baseLine.quantity,
|
||||
tax_ids: [["link", ...baseLine.tax_ids]],
|
||||
product_tmpl_id: baseLine.product_id.product_tmpl_id,
|
||||
extra_tax_data: extra_tax_data,
|
||||
},
|
||||
order,
|
||||
{ force: true },
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(discountLinesMap).forEach((line) => {
|
||||
line.delete();
|
||||
});
|
||||
|
||||
if (lastDiscountLine && isGlobalDiscountBtnClicked) {
|
||||
order.selectOrderline(lastDiscountLine);
|
||||
this.numpadMode = "price";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
odoo.define('pos_discount.DiscountButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class DiscountButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
async onClick() {
|
||||
var self = this;
|
||||
const { confirmed, payload } = await this.showPopup('NumberPopup',{
|
||||
title: this.env._t('Discount Percentage'),
|
||||
startingValue: this.env.pos.config.discount_pc,
|
||||
isInputSelected: true
|
||||
});
|
||||
if (confirmed) {
|
||||
const val = Math.max(0,Math.min(100,parseFloat(payload)));
|
||||
await self.apply_discount(val);
|
||||
}
|
||||
}
|
||||
|
||||
async apply_discount(pc) {
|
||||
var order = this.env.pos.get_order();
|
||||
var lines = order.get_orderlines();
|
||||
var product = this.env.pos.db.get_product_by_id(this.env.pos.config.discount_product_id[0]);
|
||||
if (product === undefined) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title : this.env._t("No discount product found"),
|
||||
body : this.env._t("The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing discounts
|
||||
lines.filter(line => line.get_product() === product)
|
||||
.forEach(line => order.remove_orderline(line));
|
||||
|
||||
// Add one discount line per tax group
|
||||
let linesByTax = order.get_orderlines_grouped_by_tax_ids();
|
||||
for (let [tax_ids, lines] of Object.entries(linesByTax)) {
|
||||
|
||||
// Note that tax_ids_array is an Array of tax_ids that apply to these lines
|
||||
// That is, the use case of products with more than one tax is supported.
|
||||
let tax_ids_array = tax_ids.split(',').filter(id => id !== '').map(id => Number(id));
|
||||
|
||||
let baseToDiscount = order.calculate_base_amount(
|
||||
tax_ids_array, lines.filter(ll => ll.isGlobalDiscountApplicable())
|
||||
);
|
||||
|
||||
// We add the price as manually set to avoid recomputation when changing customer.
|
||||
let discount = - pc / 100.0 * baseToDiscount;
|
||||
if (discount < 0) {
|
||||
order.add_product(product, {
|
||||
price: discount,
|
||||
lst_price: discount,
|
||||
tax_ids: tax_ids_array,
|
||||
merge: false,
|
||||
description:
|
||||
`${pc}%, ` +
|
||||
(tax_ids_array.length ?
|
||||
_.str.sprintf(
|
||||
this.env._t('Tax: %s'),
|
||||
tax_ids_array.map(taxId => this.env.pos.taxes_by_id[taxId].amount + '%').join(', ')
|
||||
) :
|
||||
this.env._t('No tax')),
|
||||
extras: {
|
||||
price_automatically_set: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DiscountButton.template = 'DiscountButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: DiscountButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.module_pos_discount && this.env.pos.config.discount_product_id;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(DiscountButton);
|
||||
|
||||
return DiscountButton;
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
odoo.define('pos_discount.models', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { Orderline } = require('point_of_sale.models');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosDiscountOrderline = (Orderline) => class PosDiscountOrderline extends Orderline {
|
||||
/**
|
||||
* Checks if the current line applies for a global discount from `pos_discount.DiscountButton`.
|
||||
* @returns Boolean
|
||||
*/
|
||||
isGlobalDiscountApplicable() {
|
||||
return !(this.pos.config.tip_product_id && this.product.id === this.pos.config.tip_product_id[0]);
|
||||
}
|
||||
}
|
||||
Registries.Model.extend(Orderline, PosDiscountOrderline);
|
||||
|
||||
});
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="DiscountButton" owl="1">
|
||||
<span class="control-button js_discount">
|
||||
<i class="fa fa-tag"></i>
|
||||
<span> </span>
|
||||
<span>Discount</span>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
|
||||
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
|
||||
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
|
||||
import * as TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
|
||||
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
|
||||
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
|
||||
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("test_pos_global_discount_sell_and_refund", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
ProductScreen.addOrderline("Desk Pad", "1", "3"),
|
||||
Chrome.clickOrders(),
|
||||
Order.hasLine({
|
||||
withoutClass: ".selected",
|
||||
run: "click",
|
||||
productName: "Desk Pad",
|
||||
quantity: "1",
|
||||
}),
|
||||
// Check that the draft order's order line is not selected and not causing issues while
|
||||
// comparing the line to the discount line
|
||||
{
|
||||
content: "Manually trigger keyup event",
|
||||
trigger: ".ticket-screen",
|
||||
run: function () {
|
||||
window.dispatchEvent(new KeyboardEvent("keyup", { key: "9" }));
|
||||
},
|
||||
},
|
||||
TicketScreen.loadSelectedOrder(),
|
||||
ProductScreen.clickControlButton("Discount"),
|
||||
{
|
||||
content: `click discount numpad button: 5`,
|
||||
trigger: `.o_dialog div.numpad button:contains(5)`,
|
||||
run: "click",
|
||||
},
|
||||
Dialog.confirm(),
|
||||
ProductScreen.selectedOrderlineHas("discount", 1, "-0.15"),
|
||||
ProductScreen.totalAmountIs("2.85"),
|
||||
ProductScreen.clickPayButton(),
|
||||
PaymentScreen.clickPaymentMethod("Bank"),
|
||||
PaymentScreen.clickValidate(),
|
||||
ReceiptScreen.isShown(),
|
||||
ReceiptScreen.clickNextOrder(),
|
||||
...ProductScreen.clickRefund(),
|
||||
TicketScreen.selectOrder("001"),
|
||||
ProductScreen.clickNumpad("1"),
|
||||
TicketScreen.toRefundTextContains("To Refund: 1"),
|
||||
ProductScreen.clickLine("discount"),
|
||||
ProductScreen.clickNumpad("1"),
|
||||
Dialog.confirm(),
|
||||
TicketScreen.confirmRefund(),
|
||||
PaymentScreen.isShown(),
|
||||
PaymentScreen.clickBack(),
|
||||
ProductScreen.clickLine("discount"),
|
||||
ProductScreen.clickNumpad("1"),
|
||||
Dialog.is({ title: "price update not allowed" }),
|
||||
Dialog.confirm(),
|
||||
ProductScreen.clickPayButton(),
|
||||
PaymentScreen.clickPaymentMethod("Bank"),
|
||||
PaymentScreen.clickValidate(),
|
||||
ReceiptScreen.isShown(),
|
||||
].flat(),
|
||||
});
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
|
||||
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
|
||||
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
|
||||
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
|
||||
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
|
||||
import * as TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
|
||||
import { escapeRegExp } from "@web/core/utils/strings";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export function addDocument(documentParams) {
|
||||
const steps = [];
|
||||
for (const values of documentParams) {
|
||||
steps.push(...ProductScreen.addOrderline(values.product, values.quantity));
|
||||
}
|
||||
steps.push(...[ProductScreen.clickPartnerButton(), ProductScreen.clickCustomer("AAAAAA")]);
|
||||
return steps;
|
||||
}
|
||||
|
||||
export function clickDiscountNumpad(num) {
|
||||
return {
|
||||
content: `click discount numpad button: ${num}`,
|
||||
trigger: `.o_dialog div.numpad button:contains(/^${escapeRegExp(num)}$/)`,
|
||||
run: "click",
|
||||
};
|
||||
}
|
||||
|
||||
export function addDiscount(percentage) {
|
||||
const steps = [ProductScreen.clickControlButton("Discount")];
|
||||
for (const num of percentage.split("")) {
|
||||
steps.push(clickDiscountNumpad(num));
|
||||
}
|
||||
steps.push({
|
||||
trigger: `.popup-input:contains(/^${escapeRegExp(percentage)}$/)`,
|
||||
run: "click",
|
||||
});
|
||||
steps.push(Dialog.confirm());
|
||||
return steps;
|
||||
}
|
||||
|
||||
export function payAndInvoice(totalAmount) {
|
||||
return [
|
||||
ProductScreen.clickPayButton(),
|
||||
|
||||
PaymentScreen.totalIs(totalAmount),
|
||||
PaymentScreen.clickPaymentMethod("Bank"),
|
||||
PaymentScreen.remainingIs("0.0"),
|
||||
|
||||
PaymentScreen.clickInvoiceButton(),
|
||||
PaymentScreen.clickValidate(),
|
||||
|
||||
ReceiptScreen.receiptAmountTotalIs(totalAmount),
|
||||
ReceiptScreen.clickNextOrder(),
|
||||
];
|
||||
}
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_in_pos_global_discount_round_per_line_price_excluded", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_1_1", quantity: "1" },
|
||||
{ product: "product_1_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("35.91"),
|
||||
ProductScreen.checkTaxAmount("4.76"),
|
||||
...payAndInvoice("35.91"),
|
||||
...addDocument([
|
||||
{ product: "product_1_1", quantity: "1" },
|
||||
{ product: "product_1_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("7"),
|
||||
ProductScreen.checkTotalAmount("34.08"),
|
||||
ProductScreen.checkTaxAmount("4.53"),
|
||||
...payAndInvoice("34.08"),
|
||||
...addDocument([
|
||||
{ product: "product_1_1", quantity: "1" },
|
||||
{ product: "product_1_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("18"),
|
||||
ProductScreen.checkTotalAmount("30.04"),
|
||||
ProductScreen.checkTaxAmount("3.99"),
|
||||
...payAndInvoice("30.04"),
|
||||
// On refund, check if the global discount line is correctly prorated in the refund order
|
||||
...ProductScreen.clickRefund(),
|
||||
TicketScreen.filterIs("Paid"),
|
||||
TicketScreen.selectOrder("001"),
|
||||
ProductScreen.clickNumpad("1"),
|
||||
TicketScreen.confirmRefund(),
|
||||
PaymentScreen.totalIs("-17.95"), // -18.32 (product_1_1) + 0.37 (discount)
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_in_pos_global_discount_round_globally_price_excluded", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_2_1", quantity: "1" },
|
||||
{ product: "product_2_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("35.94"),
|
||||
ProductScreen.checkTaxAmount("4.79"),
|
||||
...payAndInvoice("35.94"),
|
||||
...addDocument([
|
||||
{ product: "product_2_1", quantity: "1" },
|
||||
{ product: "product_2_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("7"),
|
||||
ProductScreen.checkTotalAmount("34.10"),
|
||||
ProductScreen.checkTaxAmount("4.56"),
|
||||
...payAndInvoice("34.10"),
|
||||
...addDocument([
|
||||
{ product: "product_2_1", quantity: "1" },
|
||||
{ product: "product_2_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("18"),
|
||||
ProductScreen.checkTotalAmount("30.07"),
|
||||
ProductScreen.checkTaxAmount("4.02"),
|
||||
...payAndInvoice("30.07"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_in_pos_global_discount_round_per_line_price_included", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_3_1", quantity: "1" },
|
||||
{ product: "product_3_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("35.91"),
|
||||
ProductScreen.checkTaxAmount("4.76"),
|
||||
...payAndInvoice("35.91"),
|
||||
...addDocument([
|
||||
{ product: "product_3_1", quantity: "1" },
|
||||
{ product: "product_3_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("7"),
|
||||
ProductScreen.checkTotalAmount("34.08"),
|
||||
ProductScreen.checkTaxAmount("4.53"),
|
||||
...payAndInvoice("34.08"),
|
||||
...addDocument([
|
||||
{ product: "product_3_1", quantity: "1" },
|
||||
{ product: "product_3_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("18"),
|
||||
ProductScreen.checkTotalAmount("30.04"),
|
||||
ProductScreen.checkTaxAmount("3.99"),
|
||||
...payAndInvoice("30.04"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_in_pos_global_discount_round_globally_price_included", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_4_1", quantity: "1" },
|
||||
{ product: "product_4_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("35.93"),
|
||||
ProductScreen.checkTaxAmount("4.79"),
|
||||
...payAndInvoice("35.93"),
|
||||
...addDocument([
|
||||
{ product: "product_4_1", quantity: "1" },
|
||||
{ product: "product_4_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("7"),
|
||||
ProductScreen.checkTotalAmount("34.09"),
|
||||
ProductScreen.checkTaxAmount("4.56"),
|
||||
...payAndInvoice("34.09"),
|
||||
...addDocument([
|
||||
{ product: "product_4_1", quantity: "1" },
|
||||
{ product: "product_4_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("18"),
|
||||
ProductScreen.checkTotalAmount("30.06"),
|
||||
ProductScreen.checkTaxAmount("4.02"),
|
||||
...payAndInvoice("30.06"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_br_pos_global_discount_round_per_line_price_excluded", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_1_1", quantity: "1" },
|
||||
{ product: "product_1_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("94.08"),
|
||||
ProductScreen.checkTaxAmount("30.7"),
|
||||
...payAndInvoice("94.08"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_br_pos_global_discount_round_globally_price_excluded", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_2_1", quantity: "1" },
|
||||
{ product: "product_2_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("94.08"),
|
||||
ProductScreen.checkTaxAmount("30.71"),
|
||||
...payAndInvoice("94.08"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_br_pos_global_discount_round_per_line_price_included", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_3_1", quantity: "1" },
|
||||
{ product: "product_3_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("94.08"),
|
||||
ProductScreen.checkTaxAmount("30.7"),
|
||||
...payAndInvoice("94.08"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_br_pos_global_discount_round_globally_price_included", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_4_1", quantity: "1" },
|
||||
{ product: "product_4_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("94.08"),
|
||||
ProductScreen.checkTaxAmount("30.71"),
|
||||
...payAndInvoice("94.08"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_be_pos_global_discount_round_per_line_price_excluded", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_1_1", quantity: "1" },
|
||||
{ product: "product_1_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("42.25"),
|
||||
ProductScreen.checkTaxAmount("9.34"),
|
||||
...payAndInvoice("42.25"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_be_pos_global_discount_round_globally_price_excluded", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_2_1", quantity: "1" },
|
||||
{ product: "product_2_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("42.24"),
|
||||
ProductScreen.checkTaxAmount("9.33"),
|
||||
...payAndInvoice("42.24"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_be_pos_global_discount_round_per_line_price_included", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_3_1", quantity: "1" },
|
||||
{ product: "product_3_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("42.25"),
|
||||
ProductScreen.checkTaxAmount("9.34"),
|
||||
...payAndInvoice("42.25"),
|
||||
].flat(),
|
||||
});
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("test_taxes_l10n_be_pos_global_discount_round_globally_price_included", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
|
||||
...addDocument([
|
||||
{ product: "product_4_1", quantity: "1" },
|
||||
{ product: "product_4_2", quantity: "1" },
|
||||
]),
|
||||
...addDiscount("2"),
|
||||
ProductScreen.checkTotalAmount("42.25"),
|
||||
ProductScreen.checkTaxAmount("9.33"),
|
||||
...payAndInvoice("42.25"),
|
||||
].flat(),
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { animationFrame, expect, test } from "@odoo/hoot";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { setupPosEnv, expectFormattedPrice } from "@point_of_sale/../tests/unit/utils";
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("getNumpadButtons", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const product1 = store.models["product.template"].get(5);
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product1,
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
const productScreen = await mountWithCleanup(ProductScreen, {
|
||||
props: { orderUuid: order.uuid },
|
||||
});
|
||||
await store.applyDiscount(10);
|
||||
await animationFrame();
|
||||
const receivedButtonsDisableStatue = productScreen
|
||||
.getNumpadButtons()
|
||||
.filter((button) => ["quantity", "discount"].includes(button.value))
|
||||
.map((button) => button.disabled);
|
||||
expect(Math.abs(order.discountLines[0].priceIncl).toString()).toBe(
|
||||
(order.lines[0].priceIncl * 0.1).toPrecision(2)
|
||||
);
|
||||
|
||||
expect(receivedButtonsDisableStatue).toEqual([true, true]);
|
||||
|
||||
await productScreen.addProductToOrder(product1);
|
||||
// Animation frame doesn't work here since the debounced function used to recompute
|
||||
// discount is using eventListener, so we use setTimeout instead.
|
||||
setTimeout(() => {
|
||||
expect(Math.abs(order.discountLines[0].priceIncl).toString()).toBe(
|
||||
(order.lines[0].priceIncl * 0.1).toPrecision(2)
|
||||
);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
test("addProductToOrder reapplies the global discount", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const product = store.models["product.template"].get(5);
|
||||
const productScreen = await mountWithCleanup(ProductScreen, {
|
||||
props: { orderUuid: order.uuid },
|
||||
});
|
||||
|
||||
await productScreen.addProductToOrder(product);
|
||||
expectFormattedPrice(productScreen.total, "$ 3.45");
|
||||
expect(order.priceIncl).toBe(3.45);
|
||||
expect(order.priceExcl).toBe(3);
|
||||
expect(order.amountTaxes).toBe(0.45);
|
||||
|
||||
await store.applyDiscount(10);
|
||||
expectFormattedPrice(productScreen.total, "$ 3.10");
|
||||
expect(order.priceIncl).toBe(3.1);
|
||||
expect(order.priceExcl).toBe(2.7);
|
||||
expect(order.amountTaxes).toBe(0.4);
|
||||
|
||||
await productScreen.addProductToOrder(product);
|
||||
await animationFrame();
|
||||
expectFormattedPrice(productScreen.total, "$ 6.21");
|
||||
expect(order.priceIncl).toBeCloseTo(6.21, { margin: 1e-12 });
|
||||
expect(order.priceExcl).toBe(5.4);
|
||||
expect(order.amountTaxes).toBe(0.81);
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { PosConfig } from "@point_of_sale/../tests/unit/data/pos_config.data";
|
||||
|
||||
PosConfig._records = PosConfig._records.map((record) => ({
|
||||
...record,
|
||||
module_pos_discount: true,
|
||||
discount_product_id: 151,
|
||||
}));
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { ProductProduct } from "@point_of_sale/../tests/unit/data/product_product.data";
|
||||
|
||||
ProductProduct._records = [
|
||||
...ProductProduct._records,
|
||||
{
|
||||
id: 151,
|
||||
product_tmpl_id: 151,
|
||||
lst_price: 1,
|
||||
standard_price: 0,
|
||||
display_name: "Discount",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { ProductTemplate } from "@point_of_sale/../tests/unit/data/product_template.data";
|
||||
|
||||
ProductTemplate._records = [
|
||||
...ProductTemplate._records,
|
||||
{
|
||||
id: 151,
|
||||
name: "Discount",
|
||||
display_name: "Discount",
|
||||
list_price: 0,
|
||||
standard_price: 0,
|
||||
type: "consu",
|
||||
service_tracking: "none",
|
||||
pos_categ_ids: [1],
|
||||
categ_id: false,
|
||||
uom_id: 1,
|
||||
available_in_pos: true,
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { animationFrame, expect, test } from "@odoo/hoot";
|
||||
import { setupPosEnv, getFilledOrder } from "@point_of_sale/../tests/unit/utils";
|
||||
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("isDiscountLine", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const product1 = store.models["product.template"].get(5);
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product1,
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
await store.applyDiscount(10);
|
||||
await animationFrame();
|
||||
const orderline = order.getSelectedOrderline();
|
||||
expect(Math.abs(orderline.price_subtotal_incl).toString()).toBe(
|
||||
((order.amount_total + order.amount_tax) * 0.1).toPrecision(2)
|
||||
);
|
||||
expect(orderline.isDiscountLine).toBe(true);
|
||||
});
|
||||
|
||||
test("Test taxes after fiscal position with discount product (should not change)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
order.fiscal_position_id = store.models["account.fiscal.position"].get(1);
|
||||
await store.applyDiscount(20);
|
||||
await animationFrame();
|
||||
const discountLine = order.discountLines[0];
|
||||
const lineValues = discountLine.prepareBaseLineForTaxesComputationExtraValues();
|
||||
const recomputedTaxes = order.fiscal_position_id.getTaxesAfterFiscalPosition(
|
||||
discountLine.product_id.taxes_id
|
||||
);
|
||||
expect(recomputedTaxes).not.toBe(lineValues.tax_ids);
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { test, describe, expect } from "@odoo/hoot";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
|
||||
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
|
||||
definePosModels();
|
||||
|
||||
describe("PoS Discount", () => {
|
||||
test("changing fiscal positions reapplies the global discount", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
|
||||
const product = store.models["product.template"].get(5);
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 10 }, order);
|
||||
expect(order.priceIncl).toBe(34.5);
|
||||
expect(order.priceExcl).toBe(30);
|
||||
expect(order.amountTaxes).toBe(4.5);
|
||||
|
||||
await store.applyDiscount(10);
|
||||
expect(order.priceIncl).toBe(31.05);
|
||||
expect(order.priceExcl).toBe(27);
|
||||
expect(order.amountTaxes).toBe(4.05);
|
||||
|
||||
let [productLine, discountLine] = order.lines;
|
||||
expect(productLine.priceIncl).toBe(34.5);
|
||||
expect(discountLine.priceIncl).toBe(-3.45);
|
||||
|
||||
let resolveReapplyDiscount = null;
|
||||
const reapplyDiscountPromise = new Promise((resolve) => {
|
||||
resolveReapplyDiscount = resolve;
|
||||
});
|
||||
|
||||
patchWithCleanup(store, {
|
||||
async debouncedDiscount() {
|
||||
await super.applyDiscount(...arguments);
|
||||
resolveReapplyDiscount();
|
||||
},
|
||||
});
|
||||
|
||||
const nonTaxFP = store.models["account.fiscal.position"].get(2);
|
||||
order.fiscal_position_id = nonTaxFP;
|
||||
|
||||
await reapplyDiscountPromise;
|
||||
expect(order.priceIncl).toBe(27);
|
||||
expect(order.priceExcl).toBe(27);
|
||||
expect(order.amountTaxes).toBe(0);
|
||||
|
||||
[productLine, discountLine] = order.lines;
|
||||
expect(productLine.priceIncl).toBe(30);
|
||||
expect(discountLine.priceIncl).toBe(-3);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue