19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -0,0 +1,110 @@
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import * as CustomerDisplay from "@point_of_sale/../tests/customer_display/customer_display_utils";
import { registry } from "@web/core/registry";
import { isVisible } from "@web/core/utils/ui";
registry.category("web_tour.tours").add("CustomerDisplayTour", {
steps: () =>
[
CustomerDisplay.addProduct(CustomerDisplay.ADD_PRODUCT, "add product"),
Order.hasLine({ productName: "Letter Tray", price: "2,972.75" }),
{
content: "An order line with `isSelected: false` should not have 'selected' class",
trigger: ".order-container .orderline:last-child:not(.selected)",
},
CustomerDisplay.amountIs("Total", "2,972.75"),
CustomerDisplay.postMessage(CustomerDisplay.PAY_WITH_CASH, "pay with cash"),
CustomerDisplay.amountIs("Cash", "2,972.75"),
CustomerDisplay.postMessage(CustomerDisplay.ORDER_IS_FINALIZED, "order is finalized"),
{
content: "Check that we are now on the 'Thank you' screen",
trigger: "div:contains('Thank you.')",
},
CustomerDisplay.postMessage(CustomerDisplay.NEW_ORDER, "new order"),
{
trigger: " div:contains('Welcome.')",
},
Order.doesNotHaveLine({}),
CustomerDisplay.amountIs("Total", "0.00"),
{
trigger: "body",
run: () =>
CustomerDisplay.postMessage(
CustomerDisplay.ADD_PRODUCT_SELECTED,
"add products"
).run(),
},
{
content: "An order line with `isSelected: true` should have 'selected' class",
trigger: ".order-container .orderline:last-child.selected",
},
].flat(),
});
registry.category("web_tour.tours").add("CustomerDisplayTourScroll", {
steps: () =>
[
CustomerDisplay.addProduct(CustomerDisplay.ADD_MULTI_PRODUCTS, "add 20 products"),
{
content: "An order line with `isSelected: true` should have 'selected' class",
trigger: ".order-container .orderline:last-child.selected",
run: async () =>
await new Promise((resolve) => {
const orderLine = document.querySelector(
".order-container .orderline:last-child.selected"
);
const animationDuration = parseFloat(
getComputedStyle(orderLine).animationDuration
);
if (animationDuration === 0) {
return resolve();
}
orderLine.onanimationend = function (event) {
if (event.target === orderLine && event.animationName === "item_in") {
resolve(event);
}
};
}),
},
{
content: "The order container should have scrolled to show the selected order line",
trigger: ".order-container",
run: async () => {
const orderContainer = document.querySelector(".order-container");
const orderLine = document.querySelector(
".order-container .orderline:last-child.selected"
);
await new Promise((resolve) => {
const checkScroll = () => {
requestAnimationFrame(() => {
if (orderContainer.scrollTop > 0 && isVisible(orderLine)) {
resolve();
} else {
setTimeout(checkScroll, 1000);
}
});
};
checkScroll();
});
},
},
].flat(),
});
registry.category("web_tour.tours").add("CustomerDisplayTourWithQr", {
steps: () =>
[
CustomerDisplay.addProduct(CustomerDisplay.ADD_PRODUCT, "add product"),
Order.hasLine({ productName: "Letter Tray", price: "2,972.75" }),
CustomerDisplay.amountIs("Total", "2,972.75"),
CustomerDisplay.postMessage(CustomerDisplay.PAY_WITH_CARD, "pay with card"),
CustomerDisplay.postMessage(CustomerDisplay.SEND_QR, "send qr code"),
{ trigger: "img[alt='QR Code']" },
CustomerDisplay.postMessage(CustomerDisplay.PAY_WITH_CARD, "confirm payment"),
CustomerDisplay.postMessage(CustomerDisplay.ORDER_IS_FINALIZED, "order is finalized"),
{
content: "Check that we are now on the 'Thank you' screen",
trigger: "div:contains('Thank you.')",
},
].flat(),
});

View file

@ -0,0 +1,186 @@
import { run } from "@point_of_sale/../tests/generic_helpers/utils";
export function postMessage(message, description = "") {
return run(() => {
window.customerDisplayChannel.postMessage(
typeof message === "string" ? JSON.parse(message) : message
);
}, `send message to customer display: ${description}, with value: ${message}`);
}
export function amountIs(method, amount) {
return {
content: `Check that the ${method} amount is ${amount}`,
trigger: `div.row:has(div:contains('${method}')):has(div:contains('${amount}'))`,
};
}
export function addProduct(product, description = "") {
return {
trigger: "div:contains('Welcome.')",
run: async () => {
window.customerDisplayChannel = new BroadcastChannel("UPDATE_CUSTOMER_DISPLAY");
postMessage(product, description).run();
},
};
}
export const ADD_PRODUCT =
'{"lines":[{"productName":"Letter Tray","price":"$ 2,972.75","qty":"1.00","unit":"Units","unitPrice":"$ 2,972.75","customerNote":"","internalNote":"[]","comboParent":"","packLotLines":[],"price_without_discount":"$ 2,972.75","isSelected":false,"imageSrc":"/web/image/product.product/855/image_128"}],"finalized":false,"amount":"2,972.75","paymentLines":[],"change":0,"onlinePaymentData":{}}';
export const ADD_PRODUCT_SELECTED =
'{"lines":[{"productName":"Letter Tray","price":"$ 2,972.75","qty":"1.00","unit":"Units","unitPrice":"$ 2,972.75","customerNote":"","internalNote":"[]","comboParent":"","packLotLines":[],"price_without_discount":"$ 2,972.75","isSelected":true,"imageSrc":"/web/image/product.product/855/image_128"}],"finalized":false,"amount":"2,972.75","paymentLines":[],"change":0,"onlinePaymentData":{}}';
export const ADD_MULTI_PRODUCTS = (() => {
const count = 20;
const lines = Array.from({ length: count }, (_, i) => {
const price = (Math.random() * 100 + 1).toFixed(2);
return {
productName: `Product ${i + 1}`,
price: `$${price}`,
qty: "1.00",
unit: "Units",
unitPrice: `$${price}`,
customerNote: "",
internalNote: "[]",
comboParent: "",
packLotLines: [],
price_without_discount: `$${price}`,
isSelected: i === count - 1,
imageSrc: "/web/image/product.product/855/image_128",
};
});
const amount = lines
.reduce((sum, line) => sum + parseFloat(line.price.replace("$", "")), 0)
.toFixed(2);
return JSON.stringify({
lines,
finalized: false,
amount,
paymentLines: [],
change: 0,
onlinePaymentData: {},
});
})();
export const PAY_WITH_CASH =
'{"lines":[{"productName":"Letter Tray","price":"$ 2,972.75","qty":"1.00","unit":"Units","unitPrice":"$ 2,972.75","customerNote":"","internalNote":"[]","comboParent":"","packLotLines":[],"price_without_discount":"$ 2,972.75","isSelected":true,"imageSrc":"/web/image/product.product/855/image_128"}],"finalized":false,"amount":"2,972.75","paymentLines":[{"name":"Cash","amount":"2,972.75"}],"change":0,"onlinePaymentData":{}}';
export const ORDER_IS_FINALIZED =
'{"lines":[{"productName":"Letter Tray","price":"$ 2,972.75","qty":"1.00","unit":"Units","unitPrice":"$ 2,972.75","customerNote":"","internalNote":"[]","comboParent":"","packLotLines":[],"price_without_discount":"$ 2,972.75","isSelected":false,"imageSrc":"/web/image/product.product/855/image_128"}],"finalized":true,"amount":"2,972.75","paymentLines":[{"name":"Cash","amount":"2,972.75"}],"change":0,"onlinePaymentData":{}}';
export const NEW_ORDER =
'{"lines":[],"finalized":false,"amount":"0.00","paymentLines":[],"change":0,"onlinePaymentData":{}}';
export const QR_URL =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
export const PAY_WITH_CARD = {
lines: [
{
productName: "Letter Tray",
price: "$ 2,972.75",
qty: "1.00",
unit: "Units",
unitPrice: "$ 2,972.75",
oldUnitPrice: "",
customerNote: "",
internalNote: "",
comboParent: "",
packLotLines: [],
price_without_discount: "$ 2,972.75",
isSelected: true,
imageSrc: "/web/image/product.product/855/image_128",
},
],
finalized: false,
amount: "2,972.75",
paymentLines: [{ name: "CARD", amount: "2,972.75" }],
change: 0,
onlinePaymentData: {},
qrPaymentData: null,
};
export const SEND_QR = {
lines: [
{
productName: "Letter Tray",
price: "$ 2,972.75",
qty: "1.00",
unit: "Units",
unitPrice: "$ 2,972.75",
oldUnitPrice: "",
customerNote: "",
internalNote: "",
comboParent: "",
packLotLines: [],
price_without_discount: "$ 2,972.75",
isSelected: true,
imageSrc: "/web/image/product.product/855/image_128",
},
],
finalized: false,
amount: "2,972.75",
paymentLines: [{ name: "CARD", amount: "2,972.75" }],
change: 0,
onlinePaymentData: {},
qrPaymentData: {
amount: "$ 2,972.75",
name: "CARD",
qrCode: QR_URL,
},
};
export const PAY_ONLINE = {
lines: [
{
productName: "Letter Tray",
price: "$ 2,972.75",
qty: "1.00",
unit: "Units",
unitPrice: "$ 2,972.75",
oldUnitPrice: "",
customerNote: "",
internalNote: "",
comboParent: "",
packLotLines: [],
price_without_discount: "$ 2,972.75",
isSelected: true,
imageSrc: "/web/image/product.product/855/image_128",
},
],
finalized: false,
amount: "2,972.75",
paymentLines: [{ name: "ONLINE", amount: "2,972.75" }],
change: 0,
onlinePaymentData: {
formattedAmount: "$ 2,972.75",
orderName: "/",
qrCode: QR_URL,
},
};
export const PAID = {
lines: [
{
productName: "Letter Tray",
price: "$ 2,972.75",
qty: "1.00",
unit: "Units",
unitPrice: "$ 2,972.75",
oldUnitPrice: "",
customerNote: "",
internalNote: "",
comboParent: "",
packLotLines: [],
price_without_discount: "$ 2,972.75",
isSelected: true,
imageSrc: "/web/image/product.product/855/image_128",
},
],
finalized: false,
amount: "2,972.75",
paymentLines: [{ name: "ONLINE", amount: "2,972.75" }],
change: 0,
onlinePaymentData: {},
};

View file

@ -0,0 +1,40 @@
import { expect, test } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { Component, useState, xml } from "@odoo/owl";
import { OdooLogo } from "@point_of_sale/app/components/odoo_logo/odoo_logo";
import { CenteredIcon } from "@point_of_sale/app/components/centered_icon/centered_icon";
import { Input } from "@point_of_sale/app/components/inputs/input/input";
import { NumericInput } from "@point_of_sale/app/components/inputs/numeric_input/numeric_input";
import { registry } from "@web/core/registry";
import { waitFor } from "@odoo/hoot-dom";
test("test that generic components can be mounted; the goal is to ensure that they don't have any unmet dependencies", async () => {
class TestComponent extends Component {
static props = [];
static components = {
OdooLogo,
CenteredIcon,
Input,
NumericInput,
};
static template = xml`
<div class="test-container">
<OdooLogo />
<CenteredIcon icon="'fa-smile'"/>
<Input tModel="[state, 'number']"/>
<NumericInput tModel="[state, 'number']" />
</div>
`;
setup() {
this.state = useState({ number: 1 });
}
}
registry.category("services").content = {};
await mountWithCleanup(TestComponent, {
noMainContainer: true,
});
await waitFor("div.test-container");
expect(true).toBe(true);
});

View file

@ -0,0 +1,60 @@
import { negate } from "@point_of_sale/../tests/generic_helpers/utils";
export function confirm(confirmationText, button = ".btn-primary") {
let trigger = `.modal:not(.o_inactive_modal) .modal-footer ${button}`;
if (confirmationText) {
trigger += `:contains("${confirmationText}")`;
}
return {
content: "confirm dialog",
trigger,
run: "click",
};
}
export function cancel({ title } = {}) {
return {
content: "cancel dialog",
trigger: `.modal .modal-header${
title ? `:contains(${title})` : ""
} button[aria-label="Close"]`,
run: "click",
};
}
export function discard() {
return {
content: "discard dialog",
trigger: `.modal .modal-footer button:contains("Discard")`,
run: "click",
};
}
export function is({ title } = {}) {
let trigger = ".modal .modal-content";
if (title) {
trigger += ` .modal-header:contains("${title}")`;
}
return {
content: "dialog is open",
trigger,
};
}
export function isNot(...args) {
const { trigger } = is(...args);
return {
content: "no dialog is open",
trigger: negate(trigger),
};
}
export function bodyIs(body) {
return {
content: "dialog is open",
trigger: `.modal-body:contains(${body})`,
};
}
export function footerBtnIsDisabled(buttonText) {
return {
content: `footer btn ${buttonText} should be disabled`,
trigger: `.modal .modal-footer button:contains(${buttonText})[disabled]`,
};
}

View file

@ -0,0 +1,10 @@
export function has(text, type) {
let trigger = `.o_notification:contains("${text}")`;
if (type) {
trigger += `:has(.o_notification_bar.bg-${type})`;
}
return {
content: `Check if there is a notification with text "${text}"`,
trigger,
};
}

View file

@ -0,0 +1,16 @@
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
export function enterValue(keys) {
return Numpad.enterValue(keys).map((step) => ({
...step,
trigger: `.modal ${step.trigger}`,
}));
}
export function isShown(val = "") {
return [
{
content: `input shown is '${val}'`,
trigger: `.modal .value:contains("${val}")`,
},
];
}

View file

@ -0,0 +1,20 @@
import { escapeRegExp } from "@web/core/utils/strings";
export const buttonTriger = (buttonValue) =>
`div.numpad button:contains(/^${escapeRegExp(buttonValue)}$/)`; // regex to match the exact button value ( for ex: avoids matching "+10" instead of "1")
export const click = (buttonValue) => ({
content: `click numpad button: ${buttonValue}`,
trigger: buttonTriger(buttonValue),
run: "click",
});
export const enterValue = (keys) => keys.split("").map((key) => click(key));
export const isActive = (buttonValue) => ({
content: `check if --${buttonValue}-- mode is activated`,
trigger: `${buttonTriger(buttonValue)}.active`,
});
export const isVisible = () => ({
content: "check if numpad is visible",
trigger: "div.numpad:visible",
});

View file

@ -0,0 +1,33 @@
import { run } from "@point_of_sale/../tests/generic_helpers/utils";
import { ConnectionLostError } from "@web/core/network/rpc";
const originalFetch = window.fetch;
const originalSend = XMLHttpRequest.prototype.send;
const originalConsoleError = console.error;
export function setOfflineMode() {
return run(() => {
window.fetch = () => {
throw new ConnectionLostError();
};
XMLHttpRequest.prototype.send = () => {
throw new ConnectionLostError();
};
console.error = (...args) => {
const message = args[0] instanceof Error ? args[0].message : args[0];
if (typeof message === "string" && message.includes("ConnectionLostError")) {
console.info("Connection lost error handled in offline mode:", ...args);
} else {
originalConsoleError.apply(console, args);
}
};
}, "Offline mode is now enabled");
}
export function setOnlineMode() {
return run(() => {
window.fetch = originalFetch;
XMLHttpRequest.prototype.send = originalSend;
console.error = originalConsoleError;
}, "Offline mode is now disabled");
}

View file

@ -0,0 +1,146 @@
import { negate } from "@point_of_sale/../tests/generic_helpers/utils";
/**
* @typedef {{
* withClass?: string, // ex: withClass: ".selected.blue"
* withoutClass?: string,
* run?: function | string,
* productName?: string,
* quantity?: string,
* price?: string,
* customerNote?: string,
* comboParent?: string,
* }} LineOptions
*/
/**
* @param {LineOptions} options
* @returns {import("@web_tour/js/tour_service").TourStep[]}
*/
export function hasLine({
withClass = "",
withoutClass = "",
run = () => {},
productName,
quantity,
price,
priceUnit,
customerNote,
internalNote,
comboParent,
discount,
oldPrice,
priceNoDiscount,
attributeLine,
} = {}) {
let trigger = `.order-container .orderline${withClass}`;
if (withoutClass) {
trigger += `:not(${withoutClass})`;
}
if (productName) {
trigger += `:has(.product-name:contains("${productName}"))`;
}
if (quantity) {
quantity = parseFloat(quantity) % 1 === 0 ? parseInt(quantity).toString() : quantity;
trigger += `:has(.qty:contains("${quantity}"))`;
}
if (price) {
trigger += `:has(.price:contains("${price}"))`;
}
if (priceUnit) {
trigger += `:has(.price-per-unit:contains("${priceUnit}"))`;
}
if (customerNote) {
trigger += `:has(.info-list .customer-note:contains("${customerNote}"))`;
}
if (internalNote) {
trigger += `:has(.info-list .o_tag_badge_text:contains("${internalNote}"))`;
}
if (comboParent) {
trigger += `:has(.info-list .combo-parent-name:contains("${comboParent}"))`;
}
if (discount || discount === "") {
trigger += `:has(.info-list .discount.em:contains("${discount}"))`;
}
if (priceNoDiscount) {
trigger += `:has(.info-list:contains("${priceNoDiscount}"))`;
}
if (attributeLine) {
trigger += `:has(.attribute-line:contains("${attributeLine}"))`;
}
const args = JSON.stringify(arguments[0]);
return [
{
content: `Check orderline with attributes: ${args}`,
trigger,
run: typeof run === "string" ? run : () => run(trigger),
},
];
}
/**
* @param {LineOptions} options
* @returns {import("@web_tour/tour_service").TourStep}
*/
export function doesNotHaveLine(options = {}) {
const step = hasLine(options)[0];
return [{ ...step, trigger: negate(step.trigger) }];
}
// TODO: there are instances where we have no selected orderline. Fix those instances
export function hasTotal(amount) {
return [
{
isActive: ["desktop"],
content: `order total amount is '${amount}'`,
trigger: `.product-screen .order-summary .total:contains("${amount}")`,
},
{
isActive: ["mobile"],
content: `order total amount is '${amount}'`,
trigger: `.product-screen .order-summary .total:contains("${amount}"):not(:visible)`,
},
];
}
export function hasSubtotal(amount) {
return [
{
isActive: ["desktop"],
content: `order total amount is '${amount}'`,
trigger: `.product-screen .order-summary .subtotal:contains("${amount}")`,
},
{
isActive: ["mobile"],
content: `order total amount is '${amount}'`,
trigger: `.product-screen .order-summary .subtotal:contains("${amount}"):not(:visible)`,
},
];
}
export function hasTax(amount) {
return {
content: `order total tax is '${amount}'`,
trigger: `.order-summary .tax:contains("${amount}")`,
};
}
export function hasInternalNote(note) {
return [
{
content: `Order internal note is '${note}'`,
trigger: `.order-container .internal-note-container span div:contains("${note}")`,
},
];
}
export function hasCustomerNote(note) {
return [
{
content: `Order customer note is '${note}'`,
trigger: `.order-container .customer-note div:contains("${note}")`,
},
];
}
export function hasNoTax() {
return {
content: "order has not tax",
trigger: negate(".tax-info"),
};
}

View file

@ -0,0 +1,9 @@
export function has(item, { run = () => {} } = {}) {
return [
{
content: `selection popup has '${item}'`,
trigger: `.selection-item:contains("${item}")`,
run,
},
];
}

View file

@ -0,0 +1,7 @@
export function inputText(val) {
return {
content: `input text '${val}'`,
trigger: `.modal:not(.o_inactive_modal) textarea`,
run: `edit ${val}`,
};
}

View file

@ -0,0 +1,78 @@
/* global posmodel */
import { simulateBarCode } from "@barcodes/../tests/legacy/helpers";
export function negate(selector, parent = "body") {
return `${parent}:not(:has(${selector}))`;
}
export function run(run, content = "run function", expectUnloadPage = false) {
return { content, trigger: "body", run, expectUnloadPage };
}
export function scan_barcode(barcode) {
return [
{
content: `PoS model scan barcode '${barcode}'`,
trigger: "body", // The element here does not really matter as long as it is present
run: () => {
simulateBarCode([...barcode, "Enter"]);
},
},
];
}
export function negateStep(step) {
return {
...step,
content: `Check that: ---${step.content}--- is not true`,
trigger: negate(step.trigger),
};
}
export function refresh() {
return run(
async () => {
await new Promise((resolve) => {
const checkTransaction = () => {
const activeTransactions = posmodel.data.indexedDB.activeTransactions;
if (activeTransactions.size === 0) {
window.location.reload();
resolve();
} else {
setTimeout(checkTransaction, 100);
}
};
setTimeout(() => {
checkTransaction();
}, 305);
setTimeout(() => {
const activeTx = posmodel.data.indexedDB.activeTransactions;
const storeNames = Array.from(activeTx).flatMap((tx) =>
Array.from(tx.objectStoreName)
);
const uniqueStores = [...new Set(storeNames)].join(", ");
throw new Error(
`Timeout waiting indexedDB for transactions to finish. Stores open: [${uniqueStores}]`
);
}, 2000);
});
},
"refresh page",
true
);
}
export function elementDoesNotExist(selector) {
return {
content: `Check that element "${selector}" don't exist.`,
trigger: negate(selector),
};
}
export function assertCurrentOrderDirty(dirty = true) {
return {
trigger: "body",
run() {
if (posmodel.getOrder().isDirty() !== dirty) {
throw new Error("Order should be " + (dirty ? "dirty" : "not dirty"));
}
},
};
}

View file

@ -0,0 +1,51 @@
/** @odoo-module */
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_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 * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_03_pos_with_lots", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Monitor Stand"),
ProductScreen.enterLotNumber("1", "lot"),
ProductScreen.selectedOrderlineHas("Monitor Stand", "1"),
ProductScreen.clickReview(),
{ ...ProductScreen.clickLine("Monitor Stand")[0], isActive: ["mobile"] },
Numpad.click("2"),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.totalAmountIs("6.38"),
ProductScreen.clickDisplayedProduct("Monitor Stand"),
ProductScreen.enterLotNumber("2", "lot"),
ProductScreen.clickReview(),
{ ...ProductScreen.clickLine("Monitor Stand")[0], isActive: ["mobile"] },
Numpad.click("3"),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.totalAmountIs("15.95"),
ProductScreen.clickPriceList("min_quantity ordering"),
ProductScreen.totalAmountIs("5.00"),
ProductScreen.clickReview(),
{ ...ProductScreen.clickLine("Monitor Stand")[0], isActive: ["mobile"] },
Numpad.click("⌫"),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.totalAmountIs("6.38"),
ProductScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_lot_tracking_without_lot_creation", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Monitor Stand"),
ProductScreen.totalAmountIs("3.19"),
ProductScreen.clickDisplayedProduct("Monitor Stand"),
ProductScreen.totalAmountIs("6.38"),
].flat(),
});

View file

@ -0,0 +1,44 @@
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import { registry } from "@web/core/registry";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import { inLeftSide, waitForLoading } from "@point_of_sale/../tests/pos/tours/utils/common";
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 Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
registry.category("web_tour.tours").add("pos_basic_order_02_decimal_order_quantity", {
steps: () =>
[
waitForLoading(),
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "1"),
inLeftSide([
{ ...ProductScreen.clickLine("Desk Organizer")[0], isActive: ["mobile"] },
Numpad.click("."),
...ProductScreen.selectedOrderlineHasDirect("Desk Organizer", "0"),
Numpad.click("9"),
...ProductScreen.selectedOrderlineHasDirect("Desk Organizer", "0.9"),
Numpad.click("9"),
...ProductScreen.selectedOrderlineHasDirect("Desk Organizer", "0.99"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash", true, { amount: "5.05" }),
ProductScreen.finishOrder(),
].flat(),
});
registry.category("web_tour.tours").add("pos_basic_order_03_tax_position", {
steps: () =>
[
waitForLoading(),
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Letter Tray", true, "1"),
inLeftSide(...Order.hasTotal("5.28")),
ProductScreen.clickFiscalPosition("FP-POS-2M", true),
inLeftSide(...Order.hasTotal("5.52")),
ProductScreen.closePos(),
].flat(),
});

View file

@ -0,0 +1,114 @@
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_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";
import { scan_barcode } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("BarcodeScanningTour", {
steps: () =>
[
// The following step is to make sure that the Chrome widget initialization ends
// If we try to use the barcode parser before its initiation, we will have
// some inconsistent JS errors:
// TypeError: Cannot read properties of undefined (reading 'parse_barcode')
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Add a product with its barcode
scan_barcode("0123456789"),
ProductScreen.selectedOrderlineHas("Monitor Stand"),
scan_barcode("0123456789"),
ProductScreen.selectedOrderlineHas("Monitor Stand", 2),
// Test "Prices product" EAN-13 `23.....{NNNDD}` barcode pattern
scan_barcode("2305000000004"),
ProductScreen.selectedOrderlineHas("Magnetic Board", 1, "0.00"),
scan_barcode("2305000123451"),
ProductScreen.selectedOrderlineHas("Magnetic Board", 1, "123.45"),
// Test "Weighted product" EAN-13 `21.....{NNDDD}` barcode pattern
scan_barcode("2100005000000"),
ProductScreen.selectedOrderlineHas("Wall Shelf Unit", 0, "0.00"),
scan_barcode("2100005080002"),
ProductScreen.selectedOrderlineHas("Wall Shelf Unit", 8),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("BarcodeScanningProductPackagingTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Add the product with its barcode
scan_barcode("12345601"),
ProductScreen.selectedOrderlineHas("Packaging Product", 1),
scan_barcode("12345601"),
ProductScreen.selectedOrderlineHas("Packaging Product", 2),
// Add the product packaging with its barcode
scan_barcode("12345610"),
ProductScreen.selectedOrderlineHas("Packaging Product", 12),
scan_barcode("12345610"),
ProductScreen.selectedOrderlineHas("Packaging Product", 22),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("GS1BarcodeScanningTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Add the Product 1 with GS1 barcode
scan_barcode("0108431673020125100000001"),
ProductScreen.selectedOrderlineHas("Product 1"),
scan_barcode("0108431673020125100000001"),
ProductScreen.selectedOrderlineHas("Product 1", 2),
// Add the product 1 with GS1 barcode and quantity
scan_barcode("0108431673020125305"),
ProductScreen.selectedOrderlineHas("Product 1", 7),
scan_barcode("01084316730201253010"),
ProductScreen.selectedOrderlineHas("Product 1", 17),
// Add the Product 2 with normal barcode
scan_barcode("08431673020126"),
ProductScreen.selectedOrderlineHas("Product 2"),
scan_barcode("08431673020126"),
ProductScreen.selectedOrderlineHas("Product 2", 2),
// Add the Product 3 with normal barcode
scan_barcode("3760171283370"),
ProductScreen.selectedOrderlineHas("Product 3"),
scan_barcode("3760171283370"),
ProductScreen.selectedOrderlineHas("Product 3", 2),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("BarcodeScanPartnerTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// scan the customer barcode
scan_barcode("0421234567890"),
ProductScreen.customerIsSelected("John Doe"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_quantity_package_of_non_basic_unit", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
scan_barcode("555555"),
ProductScreen.selectedOrderlineHas("Cord", 12),
Chrome.endTour(),
].flat(),
});

View file

@ -0,0 +1,330 @@
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 Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as CashMoveList from "@point_of_sale/../tests/pos/tours/utils/cash_move_list_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 Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as Utils from "@point_of_sale/../tests/pos/tours/utils/common";
import { refresh } from "@point_of_sale/../tests/generic_helpers/utils";
import { registry } from "@web/core/registry";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util";
registry.category("web_tour.tours").add("ChromeTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickMenuButton(),
Chrome.clickMenuDropdownOption("Cash In/Out"),
Chrome.fillTextArea(".cash-reason", "MOBT"),
Dialog.confirm(),
Chrome.clickMenuButton(),
// Order 1 is at Product Screen
ProductScreen.addOrderline("Desk Pad", "1", "2", "2.0"),
Chrome.clickOrders(),
TicketScreen.checkStatus("001", "Ongoing"),
// Order 2 is at Payment Screen
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Monitor Stand", "3", "4", "12.0"),
ProductScreen.clickPayButton(),
PaymentScreen.isShown(),
Chrome.clickOrders(),
TicketScreen.checkStatus("002", "Payment"),
// Order 3 is at Receipt Screen
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Whiteboard Pen", "5", "6", "30.0"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.0" }),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
Chrome.clickOrders(),
TicketScreen.checkStatus("003", "Receipt"),
// Select order 1, should be at Product Screen
TicketScreen.selectOrder("001"),
TicketScreen.loadSelectedOrder(),
ProductScreen.productIsDisplayed("Desk Pad"),
inLeftSide([
...ProductScreen.clickLine("Desk Pad"),
...ProductScreen.selectedOrderlineHasDirect("Desk Pad", "1", "2.0"),
]),
// Select order 2, should be at Payment Screen
Chrome.clickOrders(),
TicketScreen.selectOrder("002"),
TicketScreen.loadSelectedOrder(),
PaymentScreen.emptyPaymentlines("12.0"),
PaymentScreen.validateButtonIsHighlighted(false),
// Select order 3, should be at Receipt Screen
Chrome.clickOrders(),
TicketScreen.selectOrder("003"),
TicketScreen.loadSelectedOrder(),
ReceiptScreen.totalAmountContains("30.0"),
// Pay order 1, with change
Chrome.clickOrders(),
TicketScreen.selectOrder("001"),
TicketScreen.loadSelectedOrder(),
ProductScreen.isShown(),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.enterPaymentLineAmount("Cash", "20", true, { change: "18.0" }),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickValidate(),
ReceiptScreen.totalAmountContains("2.0"),
// Order 1 now should have Receipt status
Chrome.clickOrders(),
TicketScreen.checkStatus("001", "Receipt"),
// Select order 3, should still be at Receipt Screen
// and the total amount doesn't change.
TicketScreen.selectOrder("003"),
TicketScreen.loadSelectedOrder(),
ReceiptScreen.totalAmountContains("30.0"),
// click next screen on order 3
// then delete the new empty order
ReceiptScreen.clickNextOrder(),
ProductScreen.orderIsEmpty(),
Chrome.clickOrders(),
TicketScreen.deleteOrder("004"),
// After deleting order 1 above, order 2 became
// the 1st-row order and it has payment status
TicketScreen.nthRowContains(1, "Payment"),
TicketScreen.deleteOrder("002"),
Dialog.confirm(),
Chrome.clickRegister(),
// Invoice an order
ProductScreen.addOrderline("Whiteboard Pen", "5", "6"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
{ trigger: ".receipt-screen .pos-config-name:contains(Shop)" },
// Cancelling a floating order should remove it from the floating orders list.
ReceiptScreen.clickNextOrder(),
Chrome.hasFloatingOrder("004"),
].flat(),
});
registry.category("web_tour.tours").add("OrderModificationAfterValidationError", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product", true, "1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.0" }),
PaymentScreen.clickValidate(),
// Dialog showing the error
Dialog.confirm(),
PaymentScreen.clickBack(),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.isShown(),
// Allow order changes after the error
ProductScreen.clickDisplayedProduct("Test Product", true, "2"),
].flat(),
});
registry.category("web_tour.tours").add("test_tracking_number_closing_session", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "1.0"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
Chrome.clickMenuOption("Close Register"),
{
content: `Select button close register`,
trigger: `button:contains(close register)`,
run: "click",
expectUnloadPage: true,
},
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Pad", true, "1.0"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.enterPaymentLineAmount("Bank", "20"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry.category("web_tour.tours").add("test_reload_page_before_payment_with_customer_account", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "1.0"),
refresh(),
ProductScreen.productIsDisplayed("Desk Organizer"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Customer Account"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "1.0"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Customer Account"),
PaymentScreen.clickValidate(),
Dialog.cancel(),
PaymentScreen.clickValidate(),
Dialog.confirm("Ok"),
PaymentScreen.clickCustomer("Partner Test 1"),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("test_cash_in_out", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.freezeDateTime(1749965940000),
Chrome.doCashMove("10", "MOBT in"),
Chrome.doCashMove("5", "MOBT out"),
Chrome.clickMenuOption("Close Register"),
Utils.selectButton("Cash In/Out"),
Utils.selectButton("Details"),
CashMoveList.checkNumberOfRows(2),
CashMoveList.checkCashMoveShown("10"),
CashMoveList.checkCashMoveShown("5"),
CashMoveList.checkCashMoveDateTime(),
CashMoveList.deleteCashMove("10"),
CashMoveList.checkNumberOfRows(1),
CashMoveList.checkCashMoveShown("5"),
].flat(),
});
registry.category("web_tour.tours").add("test_zero_decimal_places_currency", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product", true, "1.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.totalAmountContains("100"),
].flat(),
});
registry.category("web_tour.tours").add("SessionStatisticsDisplay", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.enterOpeningAmount("100.00"),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "5", "5"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
ProductScreen.addOrderline("Monitor Stand", "2", "10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
Chrome.clickMenuOption("Backend", { expectUnloadPage: true }),
{
trigger: `[name=opening_cash]:contains(100.00)`,
},
{
trigger: `[name=paid_orders]:contains(45.00 (2 orders))`,
},
].flat(),
});
registry.category("web_tour.tours").add("test_click_all_orders_keep_customer", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickPartnerButton(),
PartnerList.clickPartnerOptions("Partner Test 1"),
{
isActive: ["auto"],
trigger: "body .dropdown-item:contains('All Orders')",
content: "Check the popover opened",
run: "click",
},
Chrome.clickRegister(),
ProductScreen.isShown(),
{
content: "customer is selected",
trigger: ".product-screen .set-partner:contains('Partner Test 1')",
},
].flat(),
});
registry.category("web_tour.tours").add("test_ctrl_number_ignored", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "1", "6", "6.0"),
{
trigger: "body",
run: () => {
window.dispatchEvent(new KeyboardEvent("keyup", { key: "5", ctrlKey: true }));
},
},
{
trigger: "body",
run: () =>
new Promise((resolve) => {
setTimeout(resolve, 300); // wait 300ms so NumberBuffer timeout runs
}),
},
inLeftSide([
{ ...ProductScreen.clickLine("Whiteboard Pen")[0], isActive: ["mobile"] },
...ProductScreen.selectedOrderlineHasDirect("Whiteboard Pen", "1", "6.0"),
]),
].flat(),
});
registry.category("web_tour.tours").add("test_set_opening_note_without_cash_method", {
steps: () =>
[
Chrome.startPoS(),
{
content: "Add Opening Notes",
trigger: ".opening-notes",
run: "edit Opening Notes",
},
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "1", "6", "6.0"),
].flat(),
});

View file

@ -0,0 +1,13 @@
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("chrome_without_cash_move_permission", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickMenuButton(),
Chrome.isCashMoveButtonHidden(),
].flat(),
});

View file

@ -0,0 +1,42 @@
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("customer_display_shows_qr_popup", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.waitForMenuButtons(),
Chrome.clickMenuButton(),
Chrome.waitForMenuOptionsToOpen(),
Chrome.ClickOnCustomerDisplayButton(),
Chrome.CustomerDisplayHasThisDeviceButton(),
Chrome.CustomerDisplayHasQRButton(),
Chrome.ClickCustomerDisplayQRButton(),
Chrome.CustomerDisplayQRIsDisplayed(),
{
isActive: ["mobile"],
content: "Check that the Customer display url is valid",
trigger: ".o-overlay-item .modal .modal-body .small a",
run: function (el) {
const url = el.anchor.href;
if (!url || url.includes("undefined")) {
throw new Error(
`Invalid customer display URL (contains undefined): ${url}`
);
}
try {
new URL(url);
} catch {
throw new Error(`Invalid customer display URL: ${url}`);
}
},
},
{
isActive: ["mobile"],
content: "Check that the Qr popup has close button",
trigger: ".o-overlay-item .modal .modal-body button.button.btn-secondary",
},
].flat(),
});

View file

@ -0,0 +1,15 @@
import { registry } from "@web/core/registry";
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";
registry.category("web_tour.tours").add("PoSFakeTourSimpleOrder", {
steps: () =>
[
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
].flat(),
});

View file

@ -0,0 +1,21 @@
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_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 Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as FeedbackScreen from "@point_of_sale/../tests/pos/tours/utils/feedback_screen_util";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_automatic_receipt_printing", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
FeedbackScreen.isShown(),
FeedbackScreen.clickScreen(),
ProductScreen.isShown(),
].flat(),
});

View file

@ -0,0 +1,30 @@
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 Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
import { registry } from "@web/core/registry";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
registry.category("web_tour.tours").add("FixedTaxNegativeQty", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Zero Amount Product", true, "1", "1.0"),
inLeftSide([
{
...ProductScreen.clickLine("Zero Amount Product", "1")[0],
isActive: ["mobile"],
},
...["+/-"].map(Numpad.click),
...ProductScreen.selectedOrderlineHasDirect("Zero Amount Product", "-1", "-1.0"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.00" }),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
].flat(),
});

View file

@ -0,0 +1,32 @@
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 { GenericHooks } from "@point_of_sale/../tests/pos/tours/utils/generic_hooks";
import { registry } from "@web/core/registry";
//This tour is meant to be run on all localizations
registry.category("web_tour.tours").add("generic_localization_tour", {
steps: () =>
[
Chrome.startPoS().map((step) => ({ ...step, timeout: 20000 })),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA Generic Partner"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Wall Shelf Unit"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
GenericHooks.afterValidateHook(),
{
timeout: 20000,
content: "receipt screen is shown",
trigger: ".pos .receipt-screen",
},
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
Chrome.endTour(),
].flat(),
});

View file

@ -0,0 +1,65 @@
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as OptionalProduct from "@point_of_sale/../tests/pos/tours/utils/optional_product_util";
import { registry } from "@web/core/registry";
import { scan_barcode } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("test_optional_product", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Select a product without configurable options
ProductScreen.clickDisplayedProduct("Desk Pad", false),
Dialog.is({ title: "Optional Products" }),
// Cancel the popup; no optional product should be added to the cart
Dialog.cancel(),
ProductScreen.selectedOrderlineHas("Desk Pad", "1.0", "1.98"),
// Add a product with optional products
ProductScreen.clickDisplayedProduct("Desk Pad", false),
Dialog.is({ title: "Optional Products" }),
// Check image of optional product
OptionalProduct.checkImage("Small Shelf", true),
// Add a specific optional product
OptionalProduct.addOptionalProduct("Small Shelf", 5),
ProductScreen.selectedOrderlineHas("Small Shelf", "5.0"),
ProductScreen.clickDisplayedProduct("Letter Tray"),
// Add an optional product with configurations
OptionalProduct.addOptionalProduct("Configurable Chair", 5, true),
// Verify the configurable product is added with correct attributes and quantity
ProductScreen.selectedOrderlineHas(
"Configurable Chair",
"5.0",
"50.0",
"Blue, Metal, wool"
),
// Scan a product with optional products
scan_barcode("lettertray"),
Dialog.is({ title: "Optional Products" }),
// Add an optional product
OptionalProduct.addOptionalProduct("Configurable Chair", 2, true),
// Verify the configurable product is added with correct attributes and quantity
ProductScreen.selectedOrderlineHas(
"Configurable Chair",
"7.0",
"70.0",
"Blue, Metal, wool"
),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_optional_product_image_not_display", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickDisplayedProduct("Desk Pad"),
OptionalProduct.checkImage("Small Shelf", false),
].flat(),
});

View file

@ -0,0 +1,272 @@
/* global posmodel */
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 { registry } from "@web/core/registry";
import * as OfflineUtil from "@point_of_sale/../tests/generic_helpers/offline_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 Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import * as NumberPopup from "@point_of_sale/../tests/generic_helpers/number_popup_util";
import { inLeftSide } from "./utils/common";
registry.category("web_tour.tours").add("PaymentScreenTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
OfflineUtil.setOfflineMode(),
ProductScreen.addOrderline("Letter Tray", "10"),
ProductScreen.clickPayButton(),
PaymentScreen.emptyPaymentlines("52.8"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.enterPaymentLineAmount("Cash", "11", true, {
amount: "11.00",
remaining: "41.8",
}),
PaymentScreen.validateButtonIsHighlighted(false),
// remove the selected paymentline with multiple backspace presses
PaymentScreen.clickNumpad("⌫ ⌫"),
PaymentScreen.fillPaymentLineAmountMobile("Cash", "0"),
PaymentScreen.selectedPaymentlineHas("Cash", "0.00"),
PaymentScreen.clickPaymentlineDelButton("Cash", "0", true),
PaymentScreen.emptyPaymentlines("52.8"),
// Pay with bank, the selected line should have full amount
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.0" }),
PaymentScreen.validateButtonIsHighlighted(true),
// remove the line using the delete button
PaymentScreen.clickPaymentlineDelButton("Bank", "52.8"),
// Use +10 and +50 to increment the amount of the paymentline
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickNumpad("⌫"),
PaymentScreen.clickNumpad("+10"),
PaymentScreen.fillPaymentLineAmountMobile("Cash", "10"),
PaymentScreen.remainingIs("42.8"),
PaymentScreen.validateButtonIsHighlighted(false),
PaymentScreen.clickNumpad("5"),
PaymentScreen.fillPaymentLineAmountMobile("Cash", "105"),
PaymentScreen.changeIs("52.2"),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickNumpad("+50"),
PaymentScreen.fillPaymentLineAmountMobile("Cash", "155"),
PaymentScreen.changeIs("102.2"),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickPaymentlineDelButton("Cash", "155.0"),
// Multiple paymentlines
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickNumpad("1"),
PaymentScreen.fillPaymentLineAmountMobile("Cash", "1"),
PaymentScreen.remainingIs("51.8"),
PaymentScreen.validateButtonIsHighlighted(false),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.fillPaymentLineAmountMobile("Bank", "5"),
PaymentScreen.clickNumpad("5"),
PaymentScreen.remainingIs("46.8"),
PaymentScreen.validateButtonIsHighlighted(false),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.0" }),
PaymentScreen.validateButtonIsHighlighted(true),
OfflineUtil.setOnlineMode(),
].flat(),
});
registry.category("web_tour.tours").add("PaymentScreenTour2", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Letter Tray", "1", "10"),
ProductScreen.clickPayButton(),
// check that ship later button is present
{ trigger: ".payment-buttons button:contains('Ship Later')" },
PaymentScreen.enterPaymentLineAmount("Bank", "99"),
// trying to put 99 as an amount should throw an error. We thus confirm the dialog.
Dialog.confirm(),
PaymentScreen.remainingIs("0.0"),
].flat(),
});
registry.category("web_tour.tours").add("PaymentScreenRoundingUp", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product Test", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("1.96"),
PaymentScreen.clickPaymentMethod("Cash", true, { remaining: "0.0", amount: "2.00" }),
PaymentScreen.clickValidate(),
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("001"),
inLeftSide([
...Order.hasLine({ productName: "Product Test", withClass: ".selected" }),
Numpad.click("1"),
]),
TicketScreen.confirmRefund(),
// To get negative of existing quantity just send -
PaymentScreen.isShown(),
PaymentScreen.totalIs("-1.96"),
PaymentScreen.clickPaymentMethod("Cash", true, { remaining: "0.0", amount: "-2.00" }),
].flat(),
});
registry.category("web_tour.tours").add("PaymentScreenRoundingDown", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product Test", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("1.98"),
PaymentScreen.clickPaymentMethod("Cash", true, { remaining: "0.0", amount: "1.95" }),
PaymentScreen.clickValidate(),
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("001"),
inLeftSide([
...Order.hasLine({ productName: "Product Test", withClass: ".selected" }),
Numpad.click("1"),
]),
TicketScreen.confirmRefund(),
// To get negative of existing quantity just send -
PaymentScreen.isShown(),
PaymentScreen.totalIs("-1.98"),
PaymentScreen.clickPaymentMethod("Cash", true, { remaining: "0.0", amount: "-1.95" }),
].flat(),
});
registry.category("web_tour.tours").add("PaymentScreenTotalDueWithOverPayment", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.addOrderline("Product Test", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("1.98"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.enterPaymentLineAmount("Cash", "5", true, {
change: "3",
}),
].flat(),
});
registry.category("web_tour.tours").add("InvoiceShipLaterAccessRight", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.confirmOpeningPopup(),
ProductScreen.clickHomeCategory(),
ProductScreen.addOrderline("Whiteboard Pen", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Acme Corporation"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickShipLaterButton(),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PaymentScreenInvoiceOrder", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product Test", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("test_pos_large_amount_confirmation_dialog", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Overpay Test Product"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.enterPaymentLineAmount("Cash", "1500"),
PaymentScreen.clickValidate(),
{
trigger: ".modal .modal-footer .btn-primary",
run: "click",
},
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_add_money_button_with_different_decimal_separator", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickNumpad("+50"),
PaymentScreen.fillPaymentLineAmountMobile("Bank", "53,20"),
PaymentScreen.changeIs("50"),
].flat(),
});
registry.category("web_tour.tours").add("test_payment_screen_tip_scenario", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Letter Tray", "1", "10"),
ProductScreen.clickPayButton(),
{
content: "Switch localization to comma",
trigger: "body",
run: () => {
posmodel.numberBuffer.localization.decimalPoint = ",";
posmodel.numberBuffer.localization.thousandsSep = ".";
posmodel.numberBuffer._setUp();
},
},
PaymentScreen.clickTipButton(),
NumberPopup.enterValue("1,50"),
NumberPopup.isShown("1,50"),
Dialog.confirm(),
PaymentScreen.totalIs("12,50"),
{
content: "Switch localization back to dot",
trigger: "body",
run: () => {
posmodel.numberBuffer.localization.decimalPoint = ".";
posmodel.numberBuffer.localization.thousandsSep = ",";
posmodel.numberBuffer._setUp();
},
},
PaymentScreen.clickTipButton(),
NumberPopup.enterValue("2.5"),
NumberPopup.isShown("2.5"),
Dialog.confirm(),
PaymentScreen.totalIs("13.50"),
].flat(),
});

View file

@ -0,0 +1,249 @@
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 { registry } from "@web/core/registry";
registry
.category("web_tour.tours")
.add("test_cash_rounding_halfup_biggest_tax_not_only_round_cash_method", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Order.
ProductScreen.addOrderline("random_product", "1"),
ProductScreen.checkTaxAmount("2.03"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("15.70"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAA"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("15.70"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("15.70"),
ReceiptScreen.receiptToPayAmountIsNotThere(),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
// Refund.
Chrome.clickOrders(),
TicketScreen.selectFilter("Active"),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("0001"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.checkTaxAmount("-2.03"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("-15.70"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("-15.70"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("-15.70"),
ReceiptScreen.receiptToPayAmountIsNotThere(),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_cash_rounding_halfup_biggest_tax_not_only_round_cash_method_pay_by_bank_and_cash", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Order.
ProductScreen.addOrderline("random_product", "1"),
ProductScreen.checkTaxAmount("2.03"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("15.70"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAA"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("15.70"),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickNumpad("0 . 6 7"),
PaymentScreen.fillPaymentLineAmountMobile("Bank", "0.67"),
PaymentScreen.remainingIs("15.03"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("15.70"),
ReceiptScreen.receiptRoundingAmountIs("0.02"),
ReceiptScreen.receiptToPayAmountIs("15.72"),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
// Refund.
Chrome.clickOrders(),
TicketScreen.selectFilter("Active"),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("0001"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.checkTaxAmount("-2.03"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("-15.70"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("-15.70"),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickNumpad("0 . 6 7 +/-"),
PaymentScreen.fillPaymentLineAmountMobile("Bank", "-0.67"),
PaymentScreen.remainingIs("-15.03"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("-15.70"),
ReceiptScreen.receiptRoundingAmountIs("-0.02"),
ReceiptScreen.receiptToPayAmountIs("-15.72"),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_cash_rounding_halfup_biggest_tax_only_round_cash_method", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Order.
ProductScreen.addOrderline("random_product", "1"),
ProductScreen.checkTaxAmount("2.05"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("15.72"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAA"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("15.72"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("15.72"),
ReceiptScreen.receiptRoundingAmountIs("-0.02"),
ReceiptScreen.receiptToPayAmountIs("15.70"),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
// Refund.
Chrome.clickOrders(),
TicketScreen.selectFilter("Active"),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("0001"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.checkTaxAmount("-2.05"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("-15.72"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("-15.72"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("-15.72"),
ReceiptScreen.receiptRoundingAmountIs("0.02"),
ReceiptScreen.receiptToPayAmountIs("-15.70"),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_cash_rounding_halfup_biggest_tax_only_round_cash_method_pay_by_bank_and_cash", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Order.
ProductScreen.addOrderline("random_product", "1"),
ProductScreen.checkTaxAmount("2.05"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("15.72"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAA"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("15.72"),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickNumpad("0 . 6 8"),
PaymentScreen.fillPaymentLineAmountMobile("Bank", "0.68"),
PaymentScreen.remainingIs("15.04"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("15.72"),
ReceiptScreen.receiptRoundingAmountIs("0.01"),
ReceiptScreen.receiptToPayAmountIs("15.73"),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
// Refund.
Chrome.clickOrders(),
TicketScreen.selectFilter("Active"),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("0001"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.checkTaxAmount("-2.05"),
ProductScreen.checkRoundingAmountIsNotThere(),
ProductScreen.checkTotalAmount("-15.72"),
ProductScreen.clickPayButton(),
PaymentScreen.totalIs("-15.72"),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickNumpad("0 . 6 8 +/-"),
PaymentScreen.fillPaymentLineAmountMobile("Bank", "-0.68"),
PaymentScreen.remainingIs("-15.04"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs("-15.72"),
ReceiptScreen.receiptRoundingAmountIs("-0.01"),
ReceiptScreen.receiptToPayAmountIs("-15.73"),
ReceiptScreen.receiptChangeAmountIsNotThere(),
ReceiptScreen.clickNextOrder(),
].flat(),
});

View file

@ -0,0 +1,345 @@
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 TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
import * as combo from "@point_of_sale/../tests/pos/tours/utils/combo_popup_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import { scan_barcode } from "@point_of_sale/../tests/generic_helpers/utils";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import { registry } from "@web/core/registry";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import * as Utils from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("ProductComboPriceTaxIncludedTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...ProductScreen.clickDisplayedProduct("Sofa Combo"),
combo.select("Combo Product Sofa (L, red)"),
Dialog.confirm(),
inLeftSide([
...Order.hasLine({
productName: "Combo product Sofa",
run: "click",
quantity: "1",
attributeLine: "L, red",
}),
Numpad.click("⌫"),
Numpad.click("⌫"),
...Order.doesNotHaveLine(),
]),
scan_barcode("SuperCombo"),
combo.select("Combo Product 3"),
combo.isConfirmationButtonDisabled(),
combo.select("Combo Product 9"),
// Check Product Configurator is open
Dialog.is("Attribute selection"),
{
content: "dialog discard",
trigger: ".modal-footer .btn:text(Add) + .btn:text(Discard)",
run: "click",
},
combo.select("Combo Product 5"),
combo.select("Combo Product 7"),
combo.isSelected("Combo Product 7"),
combo.select("Combo Product 8"),
combo.isSelected("Combo Product 8"),
combo.isNotSelected("Combo Product 7"),
Dialog.confirm(),
inLeftSide([
...ProductScreen.selectedOrderlineHasDirect("Office Combo", "1", "62.1"),
...ProductScreen.clickLine("Combo Product 3"),
...ProductScreen.selectedOrderlineHasDirect("Combo Product 3", "1"),
...ProductScreen.clickLine("Combo Product 5"),
...ProductScreen.selectedOrderlineHasDirect("Combo Product 5", "1"),
...ProductScreen.clickLine("Combo Product 8"),
...ProductScreen.selectedOrderlineHasDirect("Combo Product 8", "1"),
]),
// check that you can select a customer which triggers a recomputation of the price
...ProductScreen.clickPartnerButton(),
...ProductScreen.clickCustomer("Partner Test 1"),
// check that you can change the quantity of a combo product
inLeftSide([
...ProductScreen.clickLine("Combo Product 3"),
Numpad.click("2"),
...ProductScreen.selectedOrderlineHasDirect("Combo Product 3", "2"),
...ProductScreen.orderLineHas("Combo Product 5", "2"),
...ProductScreen.orderLineHas("Combo Product 8", "2"),
...ProductScreen.orderLineHas("Office Combo", "2", "124.2"),
]),
// check that removing a combo product removes all the combo products
inLeftSide([
{
...ProductScreen.clickLine("Combo Product 3", "2")[0],
isActive: ["mobile"],
},
Numpad.click("⌫"),
Numpad.click("⌫"),
...Order.doesNotHaveLine(),
]),
...ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 3"),
combo.select("Combo Product 5"),
combo.select("Combo Product 8"),
Dialog.confirm(),
...ProductScreen.totalAmountIs("62.10"),
...ProductScreen.clickPayButton(),
...PaymentScreen.clickPaymentMethod("Bank"),
...PaymentScreen.clickValidate(),
...ReceiptScreen.isShown(),
...ReceiptScreen.clickNextOrder(),
// another order but won't be sent to the backend
...ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 2"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
{
content: "The 'Combo Product 6' card should not display a quantity.",
trigger:
"article.product .product-content:has(.product-name:contains('Combo Product 6')):not(:has(.product-cart-qty))",
},
...ProductScreen.totalAmountIs("59.17"),
...inLeftSide(Order.hasTax("10.56")),
// the split screen is tested in `pos_restaurant`
].flat(),
});
registry.category("web_tour.tours").add("ProductComboPriceCheckTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Combo"),
inLeftSide([
...ProductScreen.selectedOrderlineHasDirect("Desk Combo", "1", "7.00"),
...ProductScreen.orderLineHas("Desk Organizer", "1"),
...ProductScreen.orderLineHas("Desk Pad", "1"),
...ProductScreen.orderLineHas("Whiteboard Pen", "1"),
]),
ProductScreen.totalAmountIs("7.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("ProductComboChangeFP", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Office Combo"),
ProductScreen.checkProductExtraPrice("Combo Product 3", "2"),
combo.select("Combo Product 2"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
inLeftSide([...ProductScreen.orderLineHas("Office Combo", "1", "50.00")]),
ProductScreen.totalAmountIs("50.00"),
inLeftSide(Order.hasTax("4.55")),
// Test than changing the fp, doesn't change the price of the combo
ProductScreen.clickFiscalPosition("test fp"),
inLeftSide([...ProductScreen.orderLineHas("Office Combo", "1", "50.00")]),
ProductScreen.totalAmountIs("50.00"),
inLeftSide(Order.hasTax("2.38")),
ProductScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_combo_refund_different_qty", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Desk accessories combo
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 3"),
combo.select("Combo Product 4"),
combo.select("Combo Product 4"),
combo.checkProductQty("Combo Product 4", "2"),
combo.select("Combo Product 6"),
Dialog.confirm(),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
// First refund order
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundLineContains("Office Combo", "To Refund: 1.00"),
TicketScreen.toRefundLineContains("Combo Product 4", "To Refund: 2.00"),
TicketScreen.toRefundLineContains("Combo Product 3", "To Refund: 1.00"),
TicketScreen.toRefundLineContains("Combo Product 6", "To Refund: 1.00"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("ProductComboMaxFreeQtyTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Desk accessories combo
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.checkTotal("40.00"),
combo.select("Combo Product 3"),
combo.checkTotal("42.00"),
// Desks combo
combo.select("Combo Product 5"),
combo.checkProductQty("Combo Product 5", "1"),
combo.select("Combo Product 5"),
combo.select("Combo Product 5"),
// Check that we cannot exceed the combo 'max_qty' which is 2
combo.checkProductQty("Combo Product 5", "2"),
combo.checkTotal("46.00"),
combo.clickQtyBtnMinus("Combo Product 5"),
combo.checkProductQty("Combo Product 5", "1"),
combo.select("Combo Product 4"),
combo.checkProductQty("Combo Product 4", "1"),
combo.checkTotal("44.00"),
combo.isConfirmationButtonDisabled(),
// Chairs combo
combo.select("Combo Product 6"),
combo.clickQtyBtnAdd("Combo Product 6"),
combo.checkProductQty("Combo Product 6", "2"),
// Confirmation should be enabled as we have selected the "min" qty for each combo
Utils.negateStep(combo.isConfirmationButtonDisabled()),
// As for chairs combo : 'qty_max' > 'qty_free', we can still select the product, but we'll pay them as extra (combo 'base_price')
combo.checkTotal("44.00"),
combo.select("Combo Product 7"),
combo.clickQtyBtnAdd("Combo Product 7"),
combo.clickQtyBtnAdd("Combo Product 7"),
combo.checkProductQty("Combo Product 7", "3"),
combo.checkTotal("134.00"),
Dialog.confirm(),
inLeftSide([
...ProductScreen.selectedOrderlineHasDirect("Office Combo", "1", "151.97"),
]),
ProductScreen.totalAmountIs("151.98"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("ProductComboChangePricelist", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 2"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
inLeftSide([
...ProductScreen.orderComboLineHas("Combo Product 2", "1.0"),
...ProductScreen.orderComboLineHas("Combo Product 4", "1.0"),
...ProductScreen.orderComboLineHas("Combo Product 6", "1.0"),
]),
ProductScreen.totalAmountIs("47.33"),
ProductScreen.clickPriceList("sale 10%"),
inLeftSide([
...ProductScreen.orderComboLineHas("Combo Product 2", "1.0"),
...ProductScreen.orderComboLineHas("Combo Product 4", "1.0"),
...ProductScreen.orderComboLineHas("Combo Product 6", "1.0"),
]),
ProductScreen.totalAmountIs("42.60"),
ProductScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("ProductComboDiscountTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Office Combo"),
ProductScreen.checkProductExtraPrice("Combo Product 3", "2"),
combo.select("Combo Product 2"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
inLeftSide([Numpad.click("%"), Numpad.click("2"), Numpad.click("0")]),
ProductScreen.totalAmountIs("80.00"),
ProductScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_combo_item_image_display", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.checkImgAndSelect("Combo Product 2", true),
combo.checkImgAndSelect("Combo Product 4", true),
combo.checkImgAndSelect("Combo Product 6", true),
Dialog.confirm(),
].flat(),
});
registry.category("web_tour.tours").add("test_combo_item_image_not_display", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.checkImgAndSelect("Combo Product 2", false),
combo.checkImgAndSelect("Combo Product 4", false),
combo.checkImgAndSelect("Combo Product 6", false),
Dialog.confirm(),
].flat(),
});
registry.category("web_tour.tours").add("test_combo_no_free_item", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Desk accessories combo
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 1"),
combo.select("Combo Product 2"),
combo.select("Combo Product 3"),
combo.checkTotal(`${10 * 3 + 2 + 40}.00`),
combo.select("Combo Product 4"),
combo.select("Combo Product 5"),
combo.checkTotal(`${72 + 20 * 2 + 2}.00`),
combo.select("Combo Product 6"),
combo.select("Combo Product 7"),
combo.select("Combo Product 8"),
combo.checkTotal(`${114 + 30 * 3 + 5}.00`),
Dialog.confirm(),
inLeftSide([
...ProductScreen.selectedOrderlineHasDirect("Office Combo", "1", "232.10"),
]),
ProductScreen.totalAmountIs("232.10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});

View file

@ -0,0 +1,133 @@
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as combo from "@point_of_sale/../tests/pos/tours/utils/combo_popup_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import { refresh } from "@point_of_sale/../tests/generic_helpers/utils";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import { registry } from "@web/core/registry";
import * as ProductConfigurator from "@point_of_sale/../tests/pos/tours/utils/product_configurator_util";
const setupProductConfigurator = [
ProductConfigurator.pickColor("Blue"),
ProductConfigurator.pickSelect("Wood"),
ProductConfigurator.pickRadio("Other"),
ProductConfigurator.fillCustomAttribute("Azerty"),
ProductConfigurator.pickMulti("Cushion"),
ProductConfigurator.pickMulti("Headrest"),
].flat();
const checkProductConfigurator = [
ProductConfigurator.selectedColor("Blue"),
ProductConfigurator.selectedSelect("Wood"),
ProductConfigurator.selectedRadio("Other"),
ProductConfigurator.selectedCustomAttribute("Azerty"),
ProductConfigurator.selectedMulti("Cushion"),
ProductConfigurator.selectedMulti("Headrest"),
].flat();
const checkConfiguredLine = (isCombo = false) => {
const method = isCombo ? ProductScreen.orderComboLineHas : ProductScreen.orderLineHas;
return [
method(
"Configurable Chair",
"1.0",
"",
"Blue, Wood, Fabrics: Other: Azerty, Cushion, Headrest"
),
].flat();
};
registry.category("web_tour.tours").add("test_line_configurators_product", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Configurable Chair"),
...setupProductConfigurator,
Dialog.confirm(),
inLeftSide([
...ProductScreen.longPressOrderline("Configurable Chair"),
Dialog.discard(),
...checkConfiguredLine(false),
...ProductScreen.longPressOrderline("Configurable Chair"),
...checkProductConfigurator,
Dialog.confirm(),
...checkConfiguredLine(false),
]),
refresh(),
inLeftSide([
...checkConfiguredLine(false),
...ProductScreen.longPressOrderline("Configurable Chair"),
...checkProductConfigurator,
Dialog.discard(),
...checkConfiguredLine(false),
]),
].flat(),
});
registry.category("web_tour.tours").add("test_line_configurators_combo", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickDisplayedProduct("Office Combo"),
// Select first combo (combo 1)
combo.select("Combo Product 2"),
combo.isSelected("Combo Product 2"),
// Open Product Configurator + Configure + Confirm (combo 2)
combo.select("Configurable Chair"),
...setupProductConfigurator,
Dialog.confirm(),
// Select it again
combo.select("Configurable Chair"),
...setupProductConfigurator,
Dialog.confirm(),
// Select last combo (combo 3)
combo.select("Combo Product 6"),
combo.isSelected("Combo Product 6"),
Dialog.confirm(),
inLeftSide([
...ProductScreen.orderComboLineHas("Combo Product 2", "1.0"),
...checkConfiguredLine(true),
...ProductScreen.orderComboLineHas("Combo Product 6", "1.0"),
// Edit combo
...ProductScreen.longPressOrderline("Office Combo"),
combo.isSelected("Combo Product 2"),
combo.isSelected("Configurable Chair"),
combo.isSelected("Combo Product 6"),
Dialog.confirm("Add to Order"),
...ProductScreen.orderComboLineHas("Combo Product 2", "1.0"),
...checkConfiguredLine(true),
...ProductScreen.orderComboLineHas("Combo Product 6", "1.0"),
]),
refresh(),
inLeftSide([
...ProductScreen.longPressOrderline("Office Combo"),
combo.isSelected("Combo Product 2"),
combo.isSelected("Configurable Chair"),
combo.isSelected("Combo Product 6"),
combo.select("Configurable Chair"),
...checkProductConfigurator,
Dialog.confirm(),
Dialog.confirm("Add to Order"),
...ProductScreen.orderComboLineHas("Combo Product 2", "1.0"),
...checkConfiguredLine(true),
...ProductScreen.orderComboLineHas("Combo Product 6", "1.0"),
...ProductScreen.longPressOrderline("Office Combo"),
Dialog.cancel(),
...ProductScreen.longPressOrderline("Office Combo"),
combo.isSelected("Configurable Chair"),
Dialog.confirm("Add to Order"),
]),
].flat(),
});

View file

@ -0,0 +1,28 @@
import { registry } from "@web/core/registry";
function logText(displayText) {
console.log(
"\n\n┏" + "━".repeat(displayText.length) + "┓",
`\n${displayText}`,
"\n┗" + "━".repeat(displayText.length) + "┛\n"
);
}
registry.category("web_tour.tours").add("tourSessionOpenProductPerformance", {
steps: () =>
[
{
trigger: "body",
timeout: 25000,
async run({ waitFor }) {
await waitFor("body:not(:has(.pos-loader))", { timeout: 20000 });
const startTime = performance.timeOrigin;
const endTime = Date.now();
const loadingTimeSec = (endTime - startTime) / 1000;
logText(
` POS loading time: ${loadingTimeSec.toFixed(2)} seconds for 20000 products`
);
},
},
].flat(),
});

View file

@ -0,0 +1,103 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("invoicePoSOrderWithSelfInvocing", {
steps: () => [
{
trigger: "input[name='pos_reference']",
run: "edit 2500-002-00002",
},
{
trigger: ".o_portal_wrap input[name='date_order']",
run: function () {
const date_order = luxon.DateTime.now();
document.querySelector(".o_portal_wrap input[name='date_order']").value =
date_order.toFormat("yyyy-MM-dd");
},
},
{
trigger: ".o_portal_wrap input[name='ticket_code']",
run: "edit inPoS",
},
{
trigger: ".o_portal_wrap button:contains('Request Invoice')",
run: "click",
expectUnloadPage: true,
},
{
trigger: ".o_portal_wrap input[name='name']",
run: "edit Anant Parmar",
},
{
trigger: ".o_portal_wrap input[name='phone']",
run: "edit +911234567890",
},
{
trigger: ".o_portal_wrap input[name='email']",
run: "edit test@test.com",
},
{
trigger: ".o_portal_wrap input[name='company_name']",
run: function () {
const companyNameInput = document.querySelector("input[name='company_name']");
if (companyNameInput.hasAttribute("readonly")) {
throw new Error("The company name input must not be readonly.");
}
companyNameInput.value = "TEST COMPANY NAME";
},
},
{
trigger: ".o_portal_wrap input[name='vat']",
run: function () {
const vatInput = document.querySelector("input[name='vat']");
if (vatInput.hasAttribute("readonly")) {
throw new Error("The vat input must not be readonly.");
}
vatInput.value = "1234567890";
},
},
{
trigger: ".o_portal_wrap input[name='street']",
run: "edit 131, Satyamcity society",
},
{
trigger: ".o_portal_wrap input[name='street2']",
run: "edit opposite new rto office",
},
{
trigger: ".o_portal_wrap input[name='city']",
run: "edit palanpur",
},
{
trigger: ".o_portal_wrap input[name='zip']",
run: "edit 385001",
},
{
trigger: ".o_portal_wrap select[name='country_id']",
run: function () {
const countrySelect = document.querySelector("select[name='country_id']");
if (Array.from(countrySelect.classList).includes("d-none")) {
throw new Error("The language selector must not be hidden.");
}
countrySelect.value = "233";
},
},
{
trigger: ".o_portal_wrap select[name='state_id']",
run: function () {
const stateSelect = document.querySelector("select[name='state_id']");
if (Array.from(stateSelect.classList).includes("d-none")) {
throw new Error("The language selector must not be hidden.");
}
stateSelect.value = "19";
},
},
{
trigger: ".o_portal_wrap button:contains('Get my invoice')",
run: "click",
expectUnloadPage: true,
},
{
trigger: ".rounded.text-bg-success.fw-normal.badge",
},
],
});

View file

@ -0,0 +1,160 @@
/* global posmodel */
import { registry } from "@web/core/registry";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Pricelist from "@point_of_sale/../tests/pos/tours/utils/pricelist_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as OfflineUtil from "@point_of_sale/../tests/generic_helpers/offline_util";
import * as ProductConfigurator from "@point_of_sale/../tests/pos/tours/utils/product_configurator_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 { refresh, scan_barcode } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("pos_pricelist", {
steps: () =>
[
Chrome.startPoS(),
Pricelist.setUp(),
Pricelist.waitForUnitTest(),
Dialog.confirm("Open Register"),
OfflineUtil.setOfflineMode(),
ProductScreen.clickPriceList("Fixed", true, "Public Pricelist"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Acme Corporation"),
ProductScreen.clickPriceList("Public Pricelist", true),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Lumber Inc"),
ProductScreen.clickPriceList("Public Pricelist", true),
ProductScreen.clickDisplayedProduct("Wall Shelf", true, "1"),
ProductScreen.clickPriceList("min_quantity ordering"),
ProductScreen.clickReview(),
{ ...ProductScreen.clickLine("Wall Shelf")[0], isActive: ["mobile"] },
Numpad.click("2"),
...ProductScreen.selectedOrderlineHasDirect("Wall Shelf", "2"),
{ ...ProductScreen.back(), isActive: ["mobile"] },
...Order.hasTotal(`$ 2.00`),
ProductScreen.clickDisplayedProduct("Small Shelf", true, "1"),
ProductScreen.clickReview(),
{ ...ProductScreen.clickLine("Small Shelf")[0], isActive: ["mobile"] },
Numpad.click("Price"),
Numpad.isActive("Price"),
Numpad.click("5"),
...Order.hasLine({ productName: "Small Shelf", price: "5.0", withClass: ".selected" }),
Numpad.click("Qty"),
Numpad.isActive("Qty"),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.clickPriceList("Public Pricelist"),
...Order.hasTotal(`$ 8.96`),
ProductScreen.clickPriceList("min_quantity ordering"),
OfflineUtil.setOnlineMode(),
ProductScreen.closePos(),
].flat(),
});
// # With test_pricelist set on the order:
// # - First banana variant will be priced at 100 via product variant
// # - Second banana variant will be priced at 150 via product variant
// # - Third banana variant will be priced at 20 via product template
// # - First apple variant will be priced at 100 via product variant
// # - All product without rules and with product_category will be priced at 500
const test_pricelists_in_pos_steps = [
ProductScreen.clickPriceList("Test Pricelist"),
scan_barcode("banana_0"),
ProductScreen.selectedOrderlineHas("Banana", "1", "100.0", "BIG"),
scan_barcode("banana_1"),
ProductScreen.selectedOrderlineHas("Banana", "1", "150.0", "MEDIUM"),
scan_barcode("banana_2"),
ProductScreen.selectedOrderlineHas("Banana", "1", "20.0", "SMALL"),
scan_barcode("apple_0"),
ProductScreen.selectedOrderlineHas("Apple", "1", "100.0", "BIG"),
scan_barcode("apple_1"),
ProductScreen.selectedOrderlineHas("Apple", "1", "500.0", "MEDIUM"),
scan_barcode("apple_2"),
ProductScreen.selectedOrderlineHas("Apple", "1", "500.0", "SMALL"),
ProductScreen.clickPriceList("Percentage Pricelist"),
scan_barcode("banana_0"),
ProductScreen.selectedOrderlineHas("Banana", "2", "100.0", "BIG"),
scan_barcode("banana_1"),
ProductScreen.selectedOrderlineHas("Banana", "2", "150.0", "MEDIUM"),
scan_barcode("banana_2"),
ProductScreen.selectedOrderlineHas("Banana", "2", "20.0", "SMALL"),
scan_barcode("apple_0"),
ProductScreen.selectedOrderlineHas("Apple", "2", "100.0", "BIG"),
scan_barcode("apple_1"),
ProductScreen.selectedOrderlineHas("Apple", "2", "500.0", "MEDIUM"),
scan_barcode("apple_2"),
ProductScreen.selectedOrderlineHas("Apple", "2", "500.0", "SMALL"),
// Try scan a product with nested pricelist on variant
scan_barcode("pear_0"),
ProductScreen.selectedOrderlineHas("Pear", "1", "10.0", "BIG"),
scan_barcode("pear_1"),
ProductScreen.selectedOrderlineHas("Pear", "1", "20.0", "MEDIUM"),
scan_barcode("pear_2"),
ProductScreen.selectedOrderlineHas("Pear", "1", "30.0", "SMALL"),
// Try scan a product with nested pricelist on template
scan_barcode("lime_0"),
ProductScreen.selectedOrderlineHas("Lime", "1", "50.0", "BIG"),
scan_barcode("lime_1"),
ProductScreen.selectedOrderlineHas("Lime", "1", "100.0", "MEDIUM"),
scan_barcode("lime_2"),
ProductScreen.selectedOrderlineHas("Lime", "1", "200.0", "SMALL"),
// Try scan a product with nested pricelist on category
scan_barcode("orange_0"),
ProductScreen.selectedOrderlineHas("Orange", "1", "500.0", "BIG"),
scan_barcode("orange_1"),
ProductScreen.selectedOrderlineHas("Orange", "1", "300.0", "MEDIUM"),
scan_barcode("orange_2"),
ProductScreen.selectedOrderlineHas("Orange", "1", "250.0", "SMALL"),
// Try scan a product with no pricelist rules
scan_barcode("kiwi_0"),
ProductScreen.selectedOrderlineHas("Kiwi", "1", "10.0", "BIG"),
scan_barcode("kiwi_1"),
ProductScreen.selectedOrderlineHas("Kiwi", "1", "5.0", "MEDIUM"),
scan_barcode("kiwi_2"),
ProductScreen.selectedOrderlineHas("Kiwi", "1", "5.0", "SMALL"),
// Test if post-loaded product with attribute open the configrator
scan_barcode("cherry_3"),
Chrome.waitRequest(),
{
content: "Click hided product with attribute",
trigger: "body",
run: () => {
const productTemplate = posmodel.models["product.template"].find(
(p) => p.name === "Cherry"
);
posmodel.addLineToCurrentOrder({
product_tmpl_id: productTemplate,
});
},
},
ProductConfigurator.pickRadio("BIG"),
ProductConfigurator.pickRadio("GREEN"),
ProductConfigurator.isUnavailable("RED"),
Dialog.confirm(),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.00" }),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
];
registry.category("web_tour.tours").add("test_pricelists_in_pos", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...test_pricelists_in_pos_steps,
refresh(), // Check pricelist sorting after a refresh
...test_pricelists_in_pos_steps,
].flat(),
});

View file

@ -0,0 +1,265 @@
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as ProductConfigurator from "@point_of_sale/../tests/pos/tours/utils/product_configurator_util";
import * as combo from "@point_of_sale/../tests/pos/tours/utils/combo_popup_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import { registry } from "@web/core/registry";
import { negateStep } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("ProductConfiguratorTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Click on Configurable Chair product
ProductScreen.clickDisplayedProduct("Configurable Chair"),
ProductConfigurator.selectedColor("Red"),
ProductConfigurator.selectedSelect("Metal"),
ProductConfigurator.selectedRadio("Leather"),
// Cancel configuration, not product should be in order
Dialog.cancel(),
ProductScreen.orderIsEmpty(),
// Click on Configurable Chair product
ProductScreen.clickDisplayedProduct("Configurable Chair"),
// Select attributes
ProductConfigurator.pickRadio("Other"),
ProductConfigurator.fillCustomAttribute("Custom Fabric"),
ProductConfigurator.pickMulti("Cushion"),
ProductConfigurator.pickMulti("Headrest"),
ProductConfigurator.selectedColor("Red"),
ProductConfigurator.selectedSelect("Metal"),
ProductConfigurator.selectedRadio("Other"),
ProductConfigurator.selectedCustomAttribute("Custom Fabric"),
ProductConfigurator.selectedMulti("Cushion"),
ProductConfigurator.selectedMulti("Headrest"),
// Check that the product has been added to the order with correct attributes and price
Dialog.confirm(),
ProductScreen.selectedOrderlineHas(
"Configurable Chair",
"1",
"11.0",
"Red, Metal, Fabrics: Other: Custom Fabric, Cushion, Headrest"
),
// Orderlines with the same attributes should be merged
ProductScreen.clickDisplayedProduct("Configurable Chair"),
ProductConfigurator.pickRadio("Other"),
ProductConfigurator.fillCustomAttribute("Custom Fabric"),
ProductConfigurator.pickMulti("Cushion"),
ProductConfigurator.pickMulti("Headrest"),
Dialog.confirm(),
ProductScreen.selectedOrderlineHas(
"Configurable Chair",
"2",
"22.0",
"Red, Metal, Fabrics: Other: Custom Fabric, Cushion, Headrest"
),
// Orderlines with different attributes shouldn't be merged
ProductScreen.clickDisplayedProduct("Configurable Chair"),
ProductConfigurator.pickColor("Blue"),
Dialog.confirm(),
ProductScreen.selectedOrderlineHas(
"Configurable Chair",
"1",
"10.0",
"Blue, Metal, Leather"
),
// Inactive variant attributes should not be displayed
ProductScreen.clickDisplayedProduct("Configurable Chair"),
// Active: Other and Leather, Inactive: Wool
ProductConfigurator.numberRadioOptions(2),
Dialog.cancel(),
// Reopen configuration and discard changes --> Come back to previous attributes
ProductScreen.openCartMobile(),
ProductScreen.longPressOrderline("Configurable Chair"),
ProductConfigurator.selectedColor("Red"),
ProductConfigurator.selectedSelect("Metal"),
ProductConfigurator.selectedRadio("Other"),
ProductConfigurator.selectedCustomAttribute("Custom Fabric"),
ProductConfigurator.selectedMulti("Cushion"),
ProductConfigurator.selectedMulti("Headrest"),
ProductConfigurator.pickColor("Blue"),
ProductConfigurator.fillCustomAttribute("Azerty"),
Dialog.cancel(),
ProductScreen.clickLine("Configurable Chair", 2),
ProductScreen.selectedOrderlineHasDirect(
"Configurable Chair",
"2",
"22.0",
"Red, Metal, Fabrics: Other: Custom Fabric, Cushion, Headrest"
),
].flat(),
});
registry.category("web_tour.tours").add("PosProductWithDynamicAttributes", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.searchProduct("Non Existing Product"),
ProductScreen.productIsDisplayed("Dynamic Product").map(negateStep),
ProductScreen.searchProduct("Dynamic Product"),
ProductScreen.productIsDisplayed("Dynamic Product"),
ProductScreen.clickDisplayedProduct("Dynamic Product"),
ProductConfigurator.pickRadio("Test 1"),
Dialog.confirm(),
ProductScreen.selectedOrderlineHas("Dynamic Product", "1", "1.15", "Test 1"),
ProductScreen.clickDisplayedProduct("Dynamic Product"),
ProductConfigurator.pickRadio("Test 2"),
Dialog.confirm(),
ProductScreen.selectedOrderlineHas("Dynamic Product", "1", "12.65", "Test 2"),
].flat(),
});
registry.category("web_tour.tours").add("test_attribute_order", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product Test"),
ProductConfigurator.pickRadio("Value 1"),
ProductConfigurator.pickRadio("Value 2"),
ProductConfigurator.pickRadio("Value 3"),
Dialog.confirm(),
ProductScreen.selectedOrderlineHas(
"Product Test",
"1",
"10",
"Value 1, Value 2, Value 3"
),
].flat(),
});
registry.category("web_tour.tours").add("test_combo_variant_mix", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Click on Configurable Chair product
ProductScreen.clickDisplayedProduct("Test Product Combo"),
combo.select("Test Product (Large)"),
Dialog.is("Attribute selection"),
ProductConfigurator.pickRadio("Blue"),
Dialog.confirm("Add"),
Dialog.confirm(),
inLeftSide(
[
Order.hasLine({
product: "Test Product",
quantity: 1,
price: 20.0,
attributes: "Blue, Large",
}),
].flat()
),
].flat(),
});
registry.category("web_tour.tours").add("test_cross_exclusion_attribute_values", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product 1"),
ProductConfigurator.pickRadio("attribute_1_value_1"),
[
{
content: `check radio attribute with name attribute_2_value_1 is muted`,
trigger: `.modal .attribute-name-cell:contains('attribute_2_value_1') span.text-muted`,
},
],
ProductConfigurator.pickRadio("attribute_2_value_1"),
ProductConfigurator.isAddDisabled(),
ProductConfigurator.pickRadio("attribute_2_value_2"),
[
{
content: `check radio attribute with name attribute_1_value_2 is muted`,
trigger: `.modal .attribute-name-cell:contains('attribute_1_value_2') span.text-muted`,
},
],
ProductConfigurator.pickRadio("attribute_1_value_2"),
ProductConfigurator.isAddDisabled(),
ProductConfigurator.pickRadio("attribute_1_value_1"),
ProductConfigurator.pickRadio("attribute_2_value_2"),
ProductConfigurator.isAddEnabled(),
ProductConfigurator.pickRadio("attribute_1_value_2"),
ProductConfigurator.pickRadio("attribute_2_value_1"),
ProductConfigurator.isAddEnabled(),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_exclusion_attribute_values", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Configurable Chair"),
ProductConfigurator.pickColor("Red"),
ProductConfigurator.pickSelect("Metal"),
ProductConfigurator.isUnavailable("Other"),
ProductConfigurator.isUnavailable("Wool"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_custom_attribute_alone_displayed", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Only Custom"),
ProductConfigurator.fillCustomAttribute("Filling"),
ProductConfigurator.selectedCustomAttribute("Filling"),
Dialog.confirm(),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_product_configurator_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Configurable Product"),
ProductConfigurator.priceIs("13.20"), // 10 (Small) + 2 (Red) + 1.2 (10% tax)
ProductConfigurator.pickRadio("Large"),
ProductConfigurator.priceIs("14.30"), // 10 + 1 (Large) + 2 (Red) + 1.3 (10% tax)
ProductConfigurator.pickRadio("Blue"),
ProductConfigurator.priceIs("15.40"), // 10 + 1 (Large) + 3 (Blue) + 1.4 (10% tax)
Dialog.confirm(),
ProductScreen.totalAmountIs("15.40"),
ProductScreen.clickPriceList("Pricelist 2"),
ProductScreen.totalAmountIs("22.00"),
ProductScreen.clickDisplayedProduct("Configurable Product"),
ProductConfigurator.priceIs("22.00"), // 20 (pricelist 2) + 2 (10% tax)
ProductConfigurator.pickRadio("Blue"),
ProductConfigurator.priceIs("22.00"), // 20 (pricelist 2) + 2 (10% tax)
Dialog.confirm(),
ProductScreen.totalAmountIs("44.00"),
Chrome.createFloatingOrder(),
ProductScreen.clickFiscalPosition("Include to Exclude"),
ProductScreen.clickDisplayedProduct("Configurable Product"),
ProductConfigurator.priceIs("12.00"), // 10 (Small) + 2 (Red)
ProductConfigurator.pickRadio("Large"),
ProductConfigurator.priceIs("13.00"), // 10 + 1 (Large) + 2 (Red)
ProductConfigurator.pickRadio("Blue"),
ProductConfigurator.priceIs("14.00"), // 10 + 1 (Large) + 3 (Blue)
Dialog.confirm(),
ProductScreen.totalAmountIs("14.00"),
Chrome.endTour(),
].flat(),
});

View file

@ -0,0 +1,128 @@
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 Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import { registry } from "@web/core/registry";
import * as ProductConfiguratorPopup from "@point_of_sale/../tests/pos/tours/utils/product_configurator_util";
function check_variant_price(product, choices, price) {
const steps = [...ProductScreen.clickDisplayedProduct(product)];
for (const choice of choices) {
steps.push(...ProductConfiguratorPopup.pickRadio(choice));
}
steps.push(
Dialog.confirm(),
...ProductScreen.totalAmountIs(price),
...ProductScreen.clickNumpad("⌫"),
...ProductScreen.clickNumpad("⌫")
);
return steps.flat();
}
registry.category("web_tour.tours").add("test_integration_dynamic_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A dynamic product", ["dyn1"], "1.00"),
check_variant_price("A dynamic product", ["dyn2"], "6.00"),
check_variant_price("A dynamic product", ["dyn3"], "11.00"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_integration_always_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A always product", ["S"], "1.00"),
check_variant_price("A always product", ["M"], "6.00"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_integration_never_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A never product", ["extra"], "1.00"),
check_variant_price("A never product", ["second"], "6.00"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_integration_dynamic_always_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A dyn/alw product", ["dyn1", "S"], "1.00"),
check_variant_price("A dyn/alw product", ["dyn1", "M"], "6.00"),
check_variant_price("A dyn/alw product", ["dyn2", "S"], "11.00"),
check_variant_price("A dyn/alw product", ["dyn2", "M"], "16.00"),
check_variant_price("A dyn/alw product", ["dyn3", "S"], "21.00"),
check_variant_price("A dyn/alw product", ["dyn3", "M"], "26.00"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_integration_dynamic_never_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A dyn/nev product", ["dyn1", "extra"], "1.00"),
check_variant_price("A dyn/nev product", ["dyn1", "second"], "6.00"),
check_variant_price("A dyn/nev product", ["dyn2", "extra"], "11.00"),
check_variant_price("A dyn/nev product", ["dyn2", "second"], "16.00"),
check_variant_price("A dyn/nev product", ["dyn3", "extra"], "21.00"),
check_variant_price("A dyn/nev product", ["dyn3", "second"], "26.00"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_integration_always_never_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A alw/nev product", ["S", "extra"], "1.00"),
check_variant_price("A alw/nev product", ["S", "second"], "6.00"),
check_variant_price("A alw/nev product", ["M", "extra"], "11.00"),
check_variant_price("A alw/nev product", ["M", "second"], "16.00"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_integration_dynamic_always_never_variant_price", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
check_variant_price("A dyn/alw/nev product", ["dyn1", "S", "extra"], "1.00"),
check_variant_price("A dyn/alw/nev product", ["dyn1", "S", "second"], "1.50"),
check_variant_price("A dyn/alw/nev product", ["dyn1", "M", "extra"], "6.00"),
check_variant_price("A dyn/alw/nev product", ["dyn1", "M", "second"], "6.50"),
check_variant_price("A dyn/alw/nev product", ["dyn2", "S", "extra"], "11.00"),
check_variant_price("A dyn/alw/nev product", ["dyn2", "S", "second"], "11.50"),
check_variant_price("A dyn/alw/nev product", ["dyn2", "M", "extra"], "16.00"),
check_variant_price("A dyn/alw/nev product", ["dyn2", "M", "second"], "16.50"),
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("test_image_variants_displayed", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Image Product"),
ProductConfiguratorPopup.checkImageVariantVisible(),
ProductConfiguratorPopup.checkImageVariantTextVisible("First Image"),
ProductConfiguratorPopup.checkImageVariantTextVisible("Second Image"),
ProductConfiguratorPopup.checkImagePriceExtraVisible("$ 20"),
].flat(),
});

View file

@ -0,0 +1,255 @@
/* global posmodel */
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 Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as NumberPopup from "@point_of_sale/../tests/generic_helpers/number_popup_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import { registry } from "@web/core/registry";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import * as OfflineUtil from "@point_of_sale/../tests/generic_helpers/offline_util";
import { run, negateStep } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("ReceiptScreenTour", {
steps: () =>
[
// press close button in receipt screen
Chrome.startPoS(),
Dialog.confirm("Open Register"),
OfflineUtil.setOfflineMode(),
ProductScreen.addOrderline("Letter Tray", "10", "5"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Full"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickShipLaterButton(),
PaymentScreen.shippingLaterHighlighted(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.cashierNameExists("A"), // A simple PoS man! (Take the first word)
Dialog.confirm("Continue with limited functionality"),
//receipt had expected delivery printed
ReceiptScreen.shippingDateExists(),
ReceiptScreen.shippingDateIsToday(),
// letter tray has 10% tax (search SRC)
ReceiptScreen.totalAmountContains("55.0"),
ReceiptScreen.clickNextOrder(),
// send email in receipt screen
ProductScreen.addOrderline("Desk Pad", "6", "5", "30.0"),
ProductScreen.addOrderline("Whiteboard Pen", "6", "6", "36.0"),
ProductScreen.addOrderline("Monitor Stand", "6", "1", "6.0"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.enterPaymentLineAmount("Cash", "70", true, { remaining: "2.0" }),
PaymentScreen.clickNumpad("0"),
PaymentScreen.fillPaymentLineAmountMobile("Cash", "700"),
PaymentScreen.changeIs("628.0"),
OfflineUtil.setOnlineMode(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.totalAmountContains("72.0"),
ReceiptScreen.setEmail("test@receiptscreen.com"),
ReceiptScreen.clickSend(),
ReceiptScreen.emailIsSuccessful(),
OfflineUtil.setOfflineMode(),
ReceiptScreen.clickNextOrder(),
// order with tip
// check if tip amount is displayed
ProductScreen.addOrderline("Desk Pad", "6", "5"),
ProductScreen.clickPayButton(),
PaymentScreen.clickTipButton(),
{
content: "click numpad button: 1",
trigger: ".modal div.numpad button:contains(/^1/)",
run: "click",
},
NumberPopup.isShown("1"),
Dialog.confirm(),
PaymentScreen.emptyPaymentlines("31.0"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.totalAmountContains(`$ 30.00 + $ 1.00 tip`),
ReceiptScreen.clickNextOrder(),
// Test customer note in receipt
ProductScreen.addOrderline("Desk Pad", "1", "5"),
inLeftSide([
{ ...ProductScreen.clickLine("Desk Pad")[0], isActive: ["mobile"] },
...ProductScreen.addCustomerNote("Test customer note"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
Order.hasLine({ customerNote: "Test customer note" }),
ReceiptScreen.clickNextOrder(),
// Test that Internal notes are not available on receipt
ProductScreen.addOrderline("Desk Pad", "1", "5"),
inLeftSide([
{ ...ProductScreen.clickLine("Desk Pad")[0], isActive: ["mobile"] },
...ProductScreen.addInternalNote("Test internal note"),
...ProductScreen.clickSelectedLine("Desk Pad"),
...ProductScreen.addInternalNote("Test internal note on order"),
...Order.hasInternalNote("Test internal note on order"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
negateStep(...Order.hasLine({ internalNote: "Test internal note" })),
negateStep(...Order.hasInternalNote("Test internal note on order")),
ReceiptScreen.clickNextOrder(),
// Test discount and original price
ProductScreen.addOrderline("Desk Pad", "1", "20"),
inLeftSide([
{ ...ProductScreen.clickLine("Desk Pad")[0], isActive: ["mobile"] },
Numpad.click("%"),
...ProductScreen.selectedOrderlineHasDirect("Desk Pad", "1", "20"),
Numpad.click("5"),
...ProductScreen.selectedOrderlineHasDirect("Desk Pad", "1", "19.0"),
Numpad.click("."),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
Order.hasLine({ productName: "Desk Pad", priceNoDiscount: "20" }),
ReceiptScreen.totalAmountContains("19.00"),
ReceiptScreen.clickNextOrder(),
OfflineUtil.setOnlineMode(),
].flat(),
});
registry.category("web_tour.tours").add("ReceiptScreenDiscountWithPricelistTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Test Product", "1"),
ProductScreen.clickPriceList("special_pricelist"),
inLeftSide(Order.hasLine({ productName: "Test Product", price: "6.30" })),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
Order.hasLine({ price: "6.30" }),
ReceiptScreen.clickNextOrder(),
ProductScreen.addOrderline("Test Product", "1"),
inLeftSide([
{ ...ProductScreen.clickLine("Test Product")[0], isActive: ["mobile"] },
Numpad.click("Price"),
Numpad.isActive("Price"),
Numpad.click("9"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.noDiscountAmount(),
].flat(),
});
registry.category("web_tour.tours").add("OrderPaidInCash", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "5", "5"),
inLeftSide(ProductScreen.orderLineHas("Desk Pad", "5")),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.validateButtonIsHighlighted(true),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
// Close the session
Chrome.clickMenuOption("Close Register"),
ProductScreen.closeWithCashAmount("25"),
ProductScreen.cashDifferenceIs("0.00"),
{
trigger: ".modal .modal-footer .btn:contains(close register)",
run: "click",
expectUnloadPage: true,
},
{
trigger: "button:contains(backend)",
run: "click",
expectUnloadPage: true,
},
{
trigger: "body",
expectUnloadPage: true,
},
].flat(),
});
registry.category("web_tour.tours").add("ReceiptTrackingMethodTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.enterLotNumber("123456789", "lot"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.trackingMethodIsLot("123456789"),
].flat(),
});
registry.category("web_tour.tours").add("point_of_sale.test_printed_receipt_tour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "1", "5"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
run(async () => {
window.print = (e) => {
const rendered = e.innerHTML;
if (!rendered.includes("Desk Pad")) {
throw new Error("Desk Pad is not present on the ticket");
}
if (rendered.includes("5.00 / Units")) {
throw new Error("The price should not be included on a basic receipt");
}
};
await posmodel.printReceipt({ basic: true });
}, "Basic receipt doesn't have price"),
].flat(),
});
registry.category("web_tour.tours").add("test_auto_validate_force_done", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
{
trigger: "body",
run: () => {
posmodel.getOrder().payment_ids[0].setPaymentStatus("force_done");
},
},
{
trigger: ".send_force_done",
run: "click",
},
ReceiptScreen.receiptIsThere(),
].flat(),
});

View file

@ -0,0 +1,67 @@
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 { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_pos_order_shipping_date", {
steps: () =>
[
ProductScreen.setTimeZone("America/New_York"),
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test with Address"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
{
content: "click ship later button",
trigger: ".button:contains('Ship Later')",
run: "click",
},
{
content: "pick a date",
trigger: '.modal-body input[type="date"]',
run: () => {
const input = document.querySelector('.modal-body input[type="date"]');
const nextYear = new Date().getFullYear() + 1;
input.value = `${nextYear}-05-30`;
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
},
},
{
content: "click confirm button",
trigger: ".btn:contains('Confirm')",
run: "click",
},
{
content: "Assert shipping date was set",
trigger: ".payment-buttons .d-flex .btn span",
run: () => {
const spans = [
...document.querySelectorAll(".payment-buttons .d-flex .btn span"),
];
const nextYear = new Date().getFullYear() + 1;
const expectedDate = `05/30/${nextYear}`;
if (!spans.some((span) => span.innerText === expectedDate)) {
throw new Error("Expected shipping date is not set");
}
},
},
PaymentScreen.clickValidate(),
{
content: "Assert shipping date in receipt",
trigger: ".pos-receipt-order-data",
run: () => {
const dateDiv = document.querySelector(".pos-receipt-order-data div");
const nextYear = new Date().getFullYear() + 1;
const expectedDate = `5/30/${nextYear}`;
if (dateDiv.innerText !== expectedDate) {
throw new Error("Expected shipping date is not set in receipt");
}
},
},
].flat(),
});

View file

@ -0,0 +1,37 @@
/* global posmodel */
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 ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_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 { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_sync_from_ui_one_by_one", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
{
trigger: "body",
content: "Create fake orders",
run: async () => {
// Create 5 orders that will be synced one by one
for (let i = 0; i < 5; i++) {
const product = posmodel.models["product.template"].find(
(p) => p.name === "Desk Pad"
);
const order = posmodel.createNewOrder();
await posmodel.addLineToOrder({ product_tmpl_id: product }, order);
posmodel.addPendingOrder([order.id]);
}
},
},
// Create one more order to be able to trigger the sync from the UI
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});

View file

@ -0,0 +1,651 @@
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_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 PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_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 { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import { registry } from "@web/core/registry";
import * as OfflineUtil from "@point_of_sale/../tests/generic_helpers/offline_util";
import * as ProductConfiguratorPopup from "@point_of_sale/../tests/pos/tours/utils/product_configurator_util";
registry.category("web_tour.tours").add("TicketScreenTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
OfflineUtil.setOfflineMode(),
Chrome.clickOrders(),
Dialog.confirm("Continue with limited functionality"),
OfflineUtil.setOnlineMode(),
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Desk Pad", "1", "3"),
Chrome.clickOrders(),
TicketScreen.deleteOrder("002"),
Dialog.confirm(),
TicketScreen.nthRowContains(1, "001"),
TicketScreen.nthRowIsHighlighted(1),
Chrome.clickRegister(),
ProductScreen.orderIsEmpty(),
ProductScreen.addOrderline("Desk Pad", "1", "2"),
Chrome.clickOrders(),
TicketScreen.deleteOrder("001"),
Dialog.confirm(),
TicketScreen.nthRowContains(1, "001"),
TicketScreen.nthRowIsHighlighted(1),
Chrome.clickRegister(),
ProductScreen.addOrderline("Desk Pad", "1", "2"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
Chrome.clickOrders(),
TicketScreen.nthRowContains(1, "Partner Test 1", false),
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Desk Pad", "1", "3"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 2"),
ProductScreen.clickPayButton(),
PaymentScreen.isShown(),
Chrome.clickOrders(),
TicketScreen.nthRowContains(1, "Partner Test 2", false),
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Desk Pad", "2", "4"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
Chrome.clickOrders(),
TicketScreen.nthRowContains(3, "Receipt"),
TicketScreen.selectFilter("Receipt"),
TicketScreen.nthRowContains(1, "Receipt"),
TicketScreen.selectFilter("Payment"),
TicketScreen.nthRowContains(1, "Payment"),
TicketScreen.selectFilter("Ongoing"),
TicketScreen.nthRowContains(1, "Ongoing"),
TicketScreen.selectFilter("Active"),
TicketScreen.nthRowContains(3, "Receipt"),
TicketScreen.search("Receipt Number", "-00003"),
TicketScreen.nthRowContains(1, "Receipt"),
TicketScreen.search("Customer", "Partner Test 1"),
TicketScreen.nthRowContains(1, "Partner Test 1", false),
TicketScreen.search("Customer", "Partner Test 2"),
TicketScreen.nthRowContains(1, "Partner Test 2", false),
// Close the TicketScreen to see the current order which is in ReceiptScreen.
// This is just to remove the search string in the search bar.
Chrome.clickRegister(),
ReceiptScreen.isShown(),
// Open again the TicketScreen to check the Paid filter.
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
TicketScreen.nthRowContains(1, "003"),
TicketScreen.selectOrder("003"),
// Pay the order that was in PaymentScreen.
TicketScreen.selectFilter("Payment"),
TicketScreen.selectOrder("002"),
TicketScreen.loadSelectedOrder(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
ProductScreen.isShown(),
// Check that the Paid filter will show the 2 synced orders.
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
TicketScreen.nthRowContains(1, "Partner Test 2", false),
TicketScreen.nthRowContains(2, "003"),
// Invoice order
TicketScreen.selectOrder("003"),
inLeftSide(Order.hasLine()),
TicketScreen.clickControlButton("Invoice"),
Dialog.confirm(),
PartnerList.clickPartner("Partner Test 3"),
TicketScreen.invoicePrinted(),
TicketScreen.back(),
// When going back, the ticket screen should be in its previous state.
TicketScreen.filterIs("Paid"),
// Test refund //
Chrome.clickRegister(),
ProductScreen.isShown(),
ProductScreen.orderIsEmpty(),
...ProductScreen.clickRefund(),
//Filter should be automatically 'Paid'.
TicketScreen.filterIs("Paid"),
TicketScreen.selectOrder("003"),
inLeftSide([
...Order.hasLine({ productName: "Desk Pad", withClass: ".selected" }),
Numpad.click("3"),
Dialog.confirm(),
]),
Chrome.clickRegister(),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.isShown(),
ProductScreen.orderIsEmpty(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("003"),
inLeftSide(Order.hasLine({ productName: "Desk Pad", withClass: ".selected" })),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundTextContains("To Refund: 1"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.isShown(),
inLeftSide([
...ProductScreen.clickLine("Desk Pad"),
...ProductScreen.selectedOrderlineHasDirect("Desk Pad", "-1"),
// Try changing the refund line's qty, price, discount but altering of refund line not allowed.
// Error popup should show.
Numpad.click("2"),
Dialog.confirm(),
...["Price", "2"].map(Numpad.click),
Dialog.confirm(),
...["%", "5"].map(Numpad.click),
Dialog.confirm(),
]),
// Check if the amount being refunded changed to 2.
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("003"),
TicketScreen.toRefundTextContains("Refunding 1.00"),
Chrome.clickRegister(),
{ ...ProductScreen.back(), isActive: ["mobile"] },
// Pay the refund order.
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
// Check refunded quantity.
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("003"),
TicketScreen.refundedNoteContains("1.00 Refunded"),
].flat(),
});
registry.category("web_tour.tours").add("FiscalPositionNoTaxRefund", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product Test"),
ProductScreen.totalAmountIs("100.00"),
ProductScreen.clickFiscalPosition("No Tax"),
ProductScreen.totalAmountIs("100.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.00" }),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
{ ...ProductScreen.back(), isActive: ["mobile"] },
ProductScreen.totalAmountIs("100.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("LotRefundTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickOrders(),
Chrome.clickOnScanButton(),
TicketScreen.checkCameraIsOpen(),
Chrome.clickOnScanButton(),
Chrome.clickRegister(),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.enterLotNumber("123456789"),
ProductScreen.selectedOrderlineHas("Product A", "1"),
Chrome.clickOrders(),
TicketScreen.selectOrder("001"),
Chrome.clickOnScanButton(),
TicketScreen.checkCameraIsOpen(),
Chrome.clickOnScanButton(),
Chrome.clickRegister(),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundTextContains("To Refund: 1"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
ProductScreen.clickLotIcon(),
ProductScreen.checkFirstLotNumber("123456789"),
].flat(),
});
registry.category("web_tour.tours").add("RefundFewQuantities", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Sugar"),
inLeftSide([
...["0", "."].map(Numpad.click),
...ProductScreen.selectedOrderlineHasDirect("Sugar", "0", "0.00"),
...["0", "2"].map(Numpad.click),
...ProductScreen.selectedOrderlineHasDirect("Sugar", "0.02", "0.06"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("0", "."),
ProductScreen.clickNumpad("0", "2"),
TicketScreen.toRefundTextContains("To Refund: 0.02"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
Order.hasLine("Sugar", "-0.02", "-0.06"),
].flat(),
});
registry.category("web_tour.tours").add("test_order_refund_flow", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "2", "3"),
ProductScreen.addOrderline("Letter Tray", "3", "2"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
// First refund order
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundTextContains("To Refund: 1.00"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
Order.hasLine("Desk Pad", "-1"),
// Second refund order
Chrome.createFloatingOrder(),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
TicketScreen.toRefundTextContains("Refunding"),
inLeftSide([...ProductScreen.clickLine("Letter Tray", "3.0")]),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundTextContains("To Refund: 1.00"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
// Verify refund order has only one line
Order.hasLine("Letter Tray", "-1"),
// Delete both refunding orders
Chrome.clickOrders(),
TicketScreen.deleteOrder("002"),
Dialog.confirm(),
TicketScreen.deleteOrder("003"),
Dialog.confirm(),
TicketScreen.selectFilter("Paid"),
TicketScreen.selectOrder("001"),
TicketScreen.noLinesToRefund(),
].flat(),
});
registry.category("web_tour.tours").add("test_pay_unpaid_order_from_kiosk", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickOrders(),
TicketScreen.selectOrder(2.53),
TicketScreen.loadSelectedOrder(),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("refund_multiple_products_amounts_compliance", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product"),
inLeftSide([
...["2"].map(Numpad.click),
...ProductScreen.selectedOrderlineHasDirect("Test Product", "2", "20"),
]),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("2"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("LotTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.enterLotNumber("1"),
ProductScreen.selectedOrderlineHas("Product A", "1"),
inLeftSide(
[
ProductScreen.clickLotIcon(),
ProductScreen.deleteNthLotNumber(1),
ProductScreen.enterLotNumber("2", "serial", true),
Order.hasLine({
productName: "Product A",
quantity: 1,
}),
ProductScreen.clickLotIcon(),
ProductScreen.enterLotNumber("1"),
Order.hasLine({
productName: "Product A",
quantity: 2.0,
}),
].flat()
),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.enterLotNumber("3"),
ProductScreen.selectedOrderlineHas("Product A", "3"),
inLeftSide({
trigger: ".info-list:contains('SN 3')",
}),
// Verify if the serial number can be reused for the current order
Chrome.createFloatingOrder(),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.enterLotNumber("5"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.enterLotNumber("3"),
inLeftSide({
trigger: ".info-list:not(:contains('SN 3'))",
}),
// Check auto assign lot number if there is only one available option
ProductScreen.clickDisplayedProduct("Product B"),
inLeftSide({
trigger: ".info-list:contains('Lot Number 1001')",
}),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("002"),
inLeftSide(
[Numpad.click("1"), ProductScreen.clickLine("Product B"), Numpad.click("1")].flat()
),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry.category("web_tour.tours").add("OrderTimeTour", {
steps: () => {
const validateDateStep = {
content: "Validate order date is Today",
trigger: ".orders .order-row:first .fw-bolder",
run: function ({ anchor: displayedDateElement }) {
if (displayedDateElement.textContent.trim() !== "Today") {
throw new Error("Order date does not match local timezone");
}
},
};
const validateTimeStep = {
content: "Validate order time matches local timezone",
trigger: ".orders .order-row:first .small.text-muted",
run: function ({ anchor: displayedTimeElement }) {
const orderDateUTC = window.posmodel.getOrder().date_order;
const orderDateTime = luxon.DateTime.fromSQL(orderDateUTC, {
zone: "UTC",
}).toLocal();
if (orderDateTime.toFormat("HH:mm") !== displayedTimeElement.textContent.trim()) {
throw new Error("Order time does not match local timezone");
}
},
};
return [
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.setTimeZone("Pacific/Honolulu"),
Chrome.clickOrders(),
validateDateStep,
validateTimeStep,
ProductScreen.setTimeZone("Europe/Brussels"),
Chrome.clickRegister(),
Chrome.clickOrders(),
validateDateStep,
validateTimeStep,
].flat();
},
});
registry
.category("web_tour.tours")
.add("test_consistent_refund_process_between_frontend_and_backend", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Pad", "2", "4"),
ProductScreen.clickPriceList("Percentage Pricelist"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
inLeftSide(Order.hasLine({ productName: "Desk Pad", withClass: ".selected" })),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundTextContains("To Refund: 1"),
TicketScreen.confirmRefund(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_paid_order_with_archived_product_loads", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
TicketScreen.nthRowContains(1, "0002"),
TicketScreen.selectOrder("0002"),
inLeftSide([
...Order.hasLine({ productName: "Archived Product", withClass: ".selected" }),
]),
].flat(),
});
registry.category("web_tour.tours").add("test_order_invoice_search", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
TicketScreen.search("Invoice Number", "00001"),
TicketScreen.nthRowContains(1, "001", false),
Chrome.clickMenuOption("Close Register"),
{
content: `Select button close register`,
trigger: `button:contains(close register)`,
run: "click",
expectUnloadPage: true,
},
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
{
content:
"Verify that the order is paid; this ensures that the RPC process is complete.",
trigger: ".orders .order-row:eq(0):has(.badge.rounded:contains(Paid))",
},
].flat(),
});
registry.category("web_tour.tours").add("test_order_with_existing_serial", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Serial Product"),
ProductScreen.enterExistingLotNumber("SN1"),
ProductScreen.selectedOrderlineHas("Serial Product", "1.00"),
inLeftSide({
trigger: ".info-list:contains('SN SN1')",
}),
ProductScreen.clickDisplayedProduct("Serial Product"),
ProductScreen.enterExistingLotNumber("SN2"),
ProductScreen.selectedOrderlineHas("Serial Product", "2.00"),
inLeftSide({
trigger: ".info-list:contains('SN SN2')",
}),
].flat(),
});
registry.category("web_tour.tours").add("test_lot_refund_lower_qty", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Serial Product"),
ProductScreen.enterExistingLotNumbers(["SN1", "SN2"]),
ProductScreen.selectedOrderlineHas("Serial Product", "2.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.toRefundTextContains("To Refund: 1"),
TicketScreen.confirmRefund(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
{
trigger: ".info-list:contains('SN SN1')",
},
ProductScreen.clickLotIcon(),
{
trigger: ".o-autocomplete--dropdown-item:contains('SN2')",
},
Dialog.confirm(),
{
content: "go back to the products",
trigger: ".actionpad .back-button",
run: "click",
isActive: ["mobile"],
},
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
{
trigger: ".info-list:contains('SN SN2')",
},
].flat(),
});
registry.category("web_tour.tours").add("test_refund_line_keep_attributes", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Donut"),
ProductConfiguratorPopup.pickRadio("Sugar"),
Dialog.confirm(),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.clickBack(),
Order.hasLine({
productName: "Donut",
attributeLine: "Sugar",
}),
].flat(),
});
registry.category("web_tour.tours").add("test_not_available_pricelist_not_set_on_order", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
Chrome.clickOrders(),
TicketScreen.selectFilter("Paid"),
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Desk Pad", "2", "3"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AA Customer"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});

View file

@ -0,0 +1,63 @@
export function editShopConfiguration(shop) {
return [
{
trigger: "body",
expectUnloadPage: true,
},
{
trigger: ".o_main_navbar span:contains('Configuration')",
run: "click",
},
{
trigger: ".dropdown-item:contains('Point of Sales')",
run: "click",
},
{
trigger: `.o_data_cell[data-tooltip=${shop}]`,
run: "click",
},
];
}
export function openShopSession(shop) {
return [
{
trigger: ".o_main_navbar .o-dropdown-item:contains('Dashboard')",
run: "click",
},
{
trigger: `.o_kanban_record:contains(${shop}) .btn-primary`,
run: "click",
expectUnloadPage: true,
},
];
}
export function saveConfiguration() {
return [
{
trigger: ".o_form_button_save",
run: "click",
},
];
}
export function openProductForm(name) {
return [
{
trigger: ".o_main_navbar span:contains('Products')",
run: "click",
},
{
trigger: ".dropdown-item:contains('Products')",
run: "click",
},
{
trigger: `.o_kanban_record:contains("${name}")`,
run: "click",
},
{
trigger: `.o_form_renderer`,
},
];
}

View file

@ -0,0 +1,46 @@
import { negateStep } from "@point_of_sale/../tests/generic_helpers/utils";
export function checkCashMoveShown(amount) {
return {
content: `Check has cash move with amount ${amount}`,
trigger: `.cash-move-list .cash-move-row .cash-move-amount:contains(${amount})`,
};
}
export function noCashMoveDeleteButton() {
return [
negateStep({
content: `Check that the delete button is not present`,
trigger: `.cash-move-list .cash-move-row .delete-row`,
}),
];
}
export function deleteCashMove(amount) {
return [
{
content: `Delete cash move with amount ${amount}`,
trigger: `.cash-move-list .cash-move-row:contains(${amount}) .delete-row .btn`,
run: "click",
},
negateStep(checkCashMoveShown(amount)),
];
}
export function checkNumberOfRows(number) {
return {
content: "check number of cash moves",
trigger: ".cash-move-list .cash-move-row",
run: () => {
const cashMoveRows = document.querySelectorAll(".cash-move-list .cash-move-row").length;
if (cashMoveRows !== number) {
throw new Error(`Expected ${number} cash moves, found ${cashMoveRows}`);
}
},
};
}
export function checkCashMoveDateTime() {
const date = "Today";
const time = "11:09";
return {
content: `Check has cash move with Date: ${date} and Time: ${time}`,
trigger: `.cash-move-list .cash-move-row:has(.cash-move-date:contains(${date})):has(.cash-move-time:contains(${time}))`,
};
}

View file

@ -0,0 +1,373 @@
/* global posmodel */
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import { negate } from "@point_of_sale/../tests/generic_helpers/utils";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
const { DateTime } = luxon;
export function confirmPopup() {
return [Dialog.confirm()];
}
export function clickMenuButton() {
return {
content: "Click on the menu button",
trigger: ".pos-rightheader button:has(.fa-bars)",
run: "click",
};
}
export function clickMenuOption(name, options) {
return [
waitForMenuButtons(),
clickMenuButton(),
waitForMenuOptionsToOpen(),
clickMenuDropdownOption(name, options),
];
}
export function waitForMenuButtons() {
return {
content: "Wait for the menu buttons to be available",
trigger: ".pos-rightheader button:has(.fa-bars)",
};
}
export function waitForMenuOptionsToOpen() {
return {
content: `Wait for the menu options to be available`,
trigger: `span.dropdown-item`,
};
}
export function clickMenuDropdownOption(name, { expectUnloadPage = false } = {}) {
return {
content: `click on something in the burger menu`,
trigger: `span.dropdown-item:contains(${name})`,
run: "click",
expectUnloadPage,
};
}
export function existMenuOption(name) {
return [
clickMenuButton(),
{
content: `check that ${name} exists in the burger menu`,
trigger: `span.dropdown-item:contains(${name})`,
},
clickMenuButton(),
];
}
export function notExistMenuOption(name) {
return [
clickMenuButton(),
{
content: `check that ${name} doesn't exist in the burger menu`,
trigger: negate(`span.dropdown-item:contains(${name})`),
},
];
}
export function isCashMoveButtonHidden() {
return [
{
trigger: ".pos-topheader:not(:contains(Cash In/Out))",
},
];
}
export function doCashMove(amount, reason) {
const numpadWrite = (val) => val.split("").flatMap((key) => Numpad.click(key));
return [
...clickMenuOption("Cash In/Out"),
fillTextArea(".cash-reason", reason),
{
isActive: ["desktop"],
content: "Enter the amount to cash in/out",
trigger: ".modal input.o_input",
run: "edit " + amount,
},
{
isActive: ["mobile"],
content: "Enter the amount to cash in/out",
trigger: ".modal input.o_input",
run: "click",
},
...numpadWrite(amount).map((step) => ({
isActive: ["mobile"],
...step,
})),
{
isActive: ["mobile"],
trigger: ".o-overlay-item:nth-child(2) .modal-footer button:contains('Confirm')",
run: "click",
},
Dialog.confirm(),
];
}
export function endTour() {
return {
content: "Last tour step that avoids error mentioned in commit 443c209",
trigger: "body",
};
}
export function isSyncStatusConnected() {
return {
trigger: negate(".oe_status", ".pos-rightheader .status-buttons"),
};
}
export function clickPlanButton() {
return [
{
content: "go back to the floor screen",
trigger: ".pos-leftheader .table-button",
run: "click",
},
...waitRequest(),
];
}
export function startPoS() {
return [
{
content: "Start PoS",
trigger: ".screen-login .btn.open-register-btn",
run: "click",
},
];
}
export function clickBtn(name, { expectUnloadPage = false } = {}) {
return {
content: `Click on ${name}`,
trigger: `body button:contains(${name})`,
run: "click",
expectUnloadPage,
};
}
export function hasBtn(name) {
return {
content: `Check button ${name} exist`,
trigger: `body button:contains(${name})`,
};
}
export function fillTextArea(target, value) {
return {
content: `Fill text area with ${value}`,
trigger: `textarea${target}`,
run: `edit ${value}`,
};
}
export function createFloatingOrder() {
return { trigger: ".pos-leftheader .list-plus-btn", run: "click" };
}
function _hasFloatingOrder(name, yes) {
const negateIfNecessary = (trigger) => (yes ? trigger : negate(trigger));
return [
{
isActive: ["desktop"],
trigger: negateIfNecessary(
`.pos-topheader .floating-order-container:contains('${name}')`
),
},
{
isActive: ["mobile"],
trigger: ".pos-leftheader button.fa-caret-down",
run: "click",
},
{
isActive: ["mobile"],
trigger: negateIfNecessary(
`.modal-header:contains(Choose an order) ~ .modal-body .floating-order-container:contains('${name}')`
),
},
{
isActive: ["mobile"],
trigger: ".oi-arrow-left",
run: "click",
},
];
}
export function hasFloatingOrder(name) {
return _hasFloatingOrder(name, true);
}
export function noFloatingOrder(name) {
return _hasFloatingOrder(name, false);
}
export function clickOrders() {
return { trigger: ".pos-leftheader .orders-button", run: "click" };
}
export function selectPresetTimingSlotHour(hour) {
return [
{
content: `Click on the slot hour ${hour} in the modal`,
trigger: `.modal:has(.modal-header:contains(select a preset)) button:contains('${hour}')`,
run: "click",
},
{
content: `Wait the slot hour ${hour} is set and loading is done (to avoid currency error)`,
trigger: `body:not(:has(.modal)):not(:has(.oe_status .fa-spin)) .pos-leftheader .preset-time-btn:contains(${hour})`,
},
];
}
export function presetTimingSlotIs(hour) {
return { trigger: `.pos-leftheader .preset-time-btn:contains('${hour}')` };
}
export function selectPresetTimingSlot(slot) {
return { trigger: `.modal button:contains('${slot}')`, run: "click" };
}
export function presetTimingSlotHourNotExists(hour) {
return { trigger: negate(`.modal button:visible:contains('${hour}')`) };
}
export function presetTimingSlotHourExists(hour) {
return { trigger: `.modal button:contains('${hour}')` };
}
export function selectSlotDays(d) {
return {
trigger: `.modal .d-flex.w-100.flex-wrap.gap-2.mt-2 button:nth-of-type(${d})`,
run: "click",
};
}
export function selectPresetTimingSlotIndex(index) {
return {
trigger: `.modal .row div:not(.d-none) .d-flex.flex-wrap.gap-1 button:nth-of-type(${index})`,
run: "click",
};
}
export function clickRegister() {
return { trigger: ".pos-leftheader .register-label", run: "click" };
}
export function waitRequest() {
return [
{
trigger: "body",
content: "Wait loading is finished if it is shown",
timeout: 15000,
async run({ waitFor }) {
let isLoading = false;
try {
isLoading = await waitFor("body:has(.fa-circle-o-notch)", { timeout: 2000 });
} catch {
/* fa-circle-o-notch will certainly never appears :'( */
}
if (isLoading) {
await waitFor("body:not(:has(.fa-circle-o-notch))", { timeout: 10000 });
}
},
},
];
}
export function storedOrderCount(expectedCount) {
return {
content: `Stored order count should be ${expectedCount}`,
trigger: "body",
run: () => {
const actualCount = posmodel.data.models["pos.order"].length;
if (actualCount !== expectedCount) {
throw new Error(
`Expected stored order count to be ${expectedCount}, but got ${actualCount}`
);
}
},
};
}
export function isSynced() {
return {
content: "Check if the request is proceeded",
trigger: negate(".fa-spin", ".status-buttons"),
};
}
export function clickOnScanButton() {
return {
content: "Click the Scan button located in the top header.",
trigger: ".pos-topheader .status-buttons .fa-barcode",
run: "click",
};
}
export function ClickOnCustomerDisplayButton() {
return {
content: "Click on the customer display button inside the burger menu",
trigger: "span i.fa-desktop",
run: "click",
};
}
export function CustomerDisplayHasThisDeviceButton() {
return {
isActive: ["desktop"],
content: "Check that the customer display popup has a 'This device' button",
trigger: ".o_dialog .modal-body .container .btn-primary:contains('This device')",
};
}
export function CustomerDisplayHasQRButton() {
return {
isActive: ["desktop"],
content: "Check that the customer display popup has a 'Display QR' button",
trigger: ".o_dialog .modal-body .container .btn-secondary:contains('Display QR')",
};
}
export function ClickCustomerDisplayThisDeviceButton() {
return {
isActive: ["desktop"],
content: "Check that the customer display popup has a 'This device' button",
trigger: ".btn-primary:contains('This device')",
run: "click",
};
}
export function ClickCustomerDisplayQRButton() {
return {
isActive: ["desktop"],
content: "Check that the customer display popup has a 'Display QR' button",
trigger: ".btn-secondary:contains('Display QR')",
run: "click",
};
}
export function CustomerDisplayQRIsDisplayed() {
return {
isActive: ["desktop"],
content: "Check that the QR code is displayed on screen",
trigger: ".o-overlay-item .modal .modal-body img.square",
};
}
export function freezeDateTime(millis) {
return [
{
trigger: "body",
run: () => {
DateTime.now = () => DateTime.fromMillis(millis);
},
},
];
}
const originalNow = DateTime.now;
export function withTimeFreeze(millis, steps) {
return [
{
content: `Freeze time to ${millis}`,
trigger: "body",
run: () => {
sessionStorage.setItem("pos_test_frozen_time", millis);
DateTime.now = () => DateTime.fromMillis(millis);
},
},
...steps,
{
content: "Unfreeze time",
trigger: "body",
run: () => {
sessionStorage.removeItem("pos_test_frozen_time");
DateTime.now = originalNow;
},
},
].flat();
}
if (sessionStorage.getItem("pos_test_frozen_time")) {
const millis = parseInt(sessionStorage.getItem("pos_test_frozen_time"));
DateTime.now = () => DateTime.fromMillis(millis);
}
export function selectPresetDateButton(formattedDate) {
return {
trigger: `.modal-body button:contains("${formattedDate}")`,
run: "click",
};
}

View file

@ -0,0 +1,81 @@
import { negate } from "@point_of_sale/../tests/generic_helpers/utils";
const productTrigger = (productName) =>
`article.product:has(.product-name:contains("${productName}"))`;
const isComboSelectedTrigger = (productName) =>
`label.combo-item.selected ${productTrigger(productName)}`;
const confirmationButtonTrigger = `footer button.confirm`;
export function select(productName) {
return {
content: `Select combo item ${productName}`,
trigger: `.modal label.combo-item ${productTrigger(productName)}`,
run: "click",
};
}
export function isSelected(productName) {
return {
content: `Check that ${productName} is selected`,
trigger: `.modal ${isComboSelectedTrigger(productName)}`,
};
}
export function isNotSelected(productName) {
return {
content: `Check that ${productName} is not selected`,
trigger: `.modal ${negate(isComboSelectedTrigger(productName), ".modal-body")}`,
};
}
export function isConfirmationButtonDisabled() {
return {
content: "try to click `confirm` without having made all the selections",
trigger: `.modal ${confirmationButtonTrigger}[disabled]`,
};
}
export function checkTotal(expectedAmount) {
return {
content: `Check that combo total amount is $${expectedAmount}`,
trigger: `.modal div.h3:contains("Total: $ ${expectedAmount}")`,
};
}
export function clickQtyBtnAdd(productName) {
return {
content: `Click the add quantity button for ${productName}`,
trigger: `.modal article:has(.product-name:contains("${productName}")) button[name="pos_quantity_button_plus"]`,
run: "click",
};
}
export function clickQtyBtnMinus(productName) {
return {
content: `Click the minus quantity button for ${productName}`,
trigger: `.modal article:has(.product-name:contains("${productName}")) button[name="pos_quantity_button_minus"]`,
run: "click",
};
}
export function checkProductQty(productName, expectedQty) {
return {
content: `Check that product ${productName} has quantity ${expectedQty}`,
trigger: `.modal article:has(.product-name:contains("${productName}")):has(input[name="pos_quantity"])`,
run: () => {
const article = [...document.querySelectorAll(".modal article")].find((el) =>
el.textContent.includes(productName)
);
const input = article.querySelector('input[name="pos_quantity"]');
if (input.value != expectedQty) {
throw new Error(
`Expected ${expectedQty}, but got ${input.value} for "${productName}".`
);
}
},
};
}
export function checkImgAndSelect(productName, checkImg = false) {
const productArticleSelector = productTrigger(productName);
const withImg = `${productArticleSelector}:has(.product-img)`;
const withoutImg = `${productArticleSelector}:not(:has(.product-img))`;
const trigger = `.modal ${checkImg ? withImg : withoutImg}`;
return {
content: `Check image & select combo item ${productName}`,
trigger: trigger,
run: "click",
};
}

View file

@ -0,0 +1,37 @@
export function back() {
return {
content: "go back to the products",
trigger: ".actionpad .back-button",
run: "click",
};
}
export function inLeftSide(steps) {
return [
{
isActive: ["mobile"],
content: "click review button",
trigger: ".btn-switchpane.review-button",
run: "click",
},
...[steps].flat(),
{ ...back(), isActive: ["mobile"] },
];
}
export function waitForLoading() {
return [
{
content: "waiting for loading to finish",
trigger: "body:not(:has(.loader))",
},
];
}
export function selectButton(name) {
return {
content: `Select button ${name}`,
trigger: `button:contains("${name}")`,
run: "click",
};
}

View file

@ -0,0 +1,14 @@
export function isShown() {
return {
content: "feedback screen is shown",
trigger: ".feedback-screen",
};
}
export function clickScreen() {
return {
content: "click on feedback screen",
trigger: ".feedback-screen",
run: "click",
};
}

View file

@ -0,0 +1,6 @@
export class GenericHooks {
static afterValidateHook() {
// This function can be overridden in the localization to add steps after payment validation
return [];
}
}

View file

@ -0,0 +1,69 @@
import * as ProductConfigurator from "@point_of_sale/../tests/pos/tours/utils/product_configurator_util";
export function addOptionalProduct(productName, quantity, configurable) {
const step = [
// Verify that the optional product is visible in the list
{
content: `Verify that the optional product "${productName}" is available in the list.`,
trigger: `.optional-product-line .product-name:contains("${productName}")`,
},
{
content: `Click the "+ Add" button to add the optional product "${productName}" to the cart.`,
trigger: `.optional-product-line .cart-buttons button:contains("+ Add")`,
run: "click",
},
];
// Handle configuration steps for configurable optional products
if (configurable) {
step.push(
// Choose the color attribute for the configurable product
...ProductConfigurator.pickColor("Blue"),
// Select the material type from dropdown options
...ProductConfigurator.pickSelect("Metal"),
// Choose the texture or fabric type via radio buttons
...ProductConfigurator.pickRadio("wool"),
// confirm Attribute Selection dialogue
{
trigger: ".o-overlay-item:nth-child(2) .modal-footer button:contains('Add')",
run: "click",
}
);
}
if (quantity > 1) {
for (let i = 1; i < quantity; i++) {
// Increment the product quantity by clicking the "+" button
step.push(
{
content: `Verify the quantity of "${productName}" is updated to ${i}.`,
trigger: `.optional-product-line .cart-buttons input:value("${i}")`,
},
{
content: `Increase the quantity of "${productName}" by clicking the "+" button.`,
trigger: `.optional-product-line .cart-buttons button:eq(1)`,
run: "click",
}
);
}
step.push({
content: `Click the "Add" button to confirm adding "${productName}" to the order.`,
trigger: `.modal-footer button:contains("Add")`,
run: "click",
});
return step;
}
}
export function checkImage(productName, shouldHaveImage = false) {
const baseSelector = `.modal .optional-product-line:has(.product-name:contains("${productName}"))`;
const trigger = shouldHaveImage
? `${baseSelector}:has(img.product-img)`
: `${baseSelector}:not(:has(img.product-img))`;
return {
content: `Check image visibility for optional product "${productName}"`,
trigger,
};
}

View file

@ -0,0 +1,176 @@
import { negateStep } from "@point_of_sale/../tests/generic_helpers/utils";
export function clickPartner(name = "", { expectUnloadPage = false } = {}) {
return {
content: `click partner '${name}' from partner list screen`,
trigger: `.modal .partner-list b:contains(${name})`,
run: "click",
expectUnloadPage,
};
}
export function clickPartnerOptions(name) {
return {
content: `click partner from partner list screen`,
trigger: `.partner-info:contains("${name}") button.dropdown`,
run: "click",
};
}
export function checkDropDownItemText(text) {
return {
content: `check for dropdown item containing text`,
trigger: `.o-dropdown-item:contains("${text}")`,
};
}
export function clickDropDownItemText(text) {
return {
content: `click for dropdown item containing text`,
trigger: `.o-dropdown-item:contains("${text}")`,
run: "click",
};
}
export function clickSettleOrderName(
prefix,
suffix = "",
checkCurrentYear = false,
availability = true
) {
let trigger = `tr.o_data_row td[name='name']:contains("${prefix}")`;
if (checkCurrentYear) {
trigger += `:contains("${new Date().getFullYear()}")`;
}
if (suffix) {
trigger += `:contains("${suffix}")`;
}
const step = {
content: "Check the settle due account line is present",
trigger,
run: "click",
};
if (!availability) {
return negateStep(step);
}
return step;
}
export function settleCustomerAccount(
partner,
dueAmount,
orderPrefix,
orderSuffix = "",
checkYear = false,
orderSettlement = false,
availability = true
) {
const steps = [
{
trigger: `tr:contains(${partner}) .partner-due:contains(${dueAmount})`,
},
clickPartnerOptions(`${partner}`),
];
const buttonText = orderSettlement ? "Settle orders" : "Settle invoices";
steps.push(
...[
clickDropDownItemText(buttonText),
clickSettleOrderName(orderPrefix, orderSuffix, checkYear, availability),
]
);
return steps;
}
export function checkContactValues(name, address = "", phone = "", email = "") {
const steps = [
{
content: `Check partner "${name}" from partner list screen`,
trigger: `.partner-list .partner-info:contains("${name}")`,
},
{
content: `Check address "${address}" for partner "${name}"`,
trigger: `.partner-list .partner-info:contains("${name}") .partner-line-adress:contains("${address}")`,
},
];
if (phone) {
steps.push({
content: `Check phone number "${phone}" for partner "${name}"`,
trigger: `.partner-list .partner-info:contains("${name}") .partner-line-email:contains("${phone}")`,
});
}
if (email) {
steps.push({
content: `Check email address "${email}" for partner "${name}"`,
trigger: `.partner-list .partner-info:contains("${name}") .partner-line-email .email-field:contains("${email}")`,
});
}
return steps;
}
export function checkCustomerShown(val) {
return {
content: `Check "${val}" is shown`,
trigger: `.partner-list .partner-info:nth-child(1):contains("${val}")`,
};
}
export function searchCustomerValue(val, pressEnter = false) {
const steps = [
{
isActive: ["mobile"],
content: `Click search field`,
trigger: `.modal-dialog .fa-search.undefined`,
run: `click`,
},
{
content: `Search customer with "${val}"`,
trigger: `.modal-dialog .input-group input`,
run: `edit ${val}`,
},
];
if (pressEnter) {
steps.push({
content: `Manually trigger keyup event`,
trigger: ".modal-header .input-group input",
run: function () {
document
.querySelector(".modal-header .input-group input")
.dispatchEvent(new KeyboardEvent("keyup", { key: "" }));
},
});
steps.push({
content: `Press Enter to trigger "search more"`,
trigger: `.modal-dialog .input-group input`,
run: function () {
document
.querySelector(".modal-dialog .input-group input")
.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
},
});
}
steps.push(checkCustomerShown(val));
return steps;
}
export function scrollBottom() {
return {
content: `Scroll to the bottom of the partner list`,
trigger: `.modal-body.partner-list`,
run: () => {
const partnerList = document.querySelector(".modal-body.partner-list");
partnerList.scrollTop = partnerList.scrollHeight;
},
};
}
export function isShown() {
return [
{
content: "partner list screen is shown",
trigger: ".modal .partner-list",
},
];
}

View file

@ -0,0 +1,418 @@
/* global posmodel */
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util";
import * as NumberPopup from "@point_of_sale/../tests/generic_helpers/number_popup_util";
/**
* Clicks on the payment method and then performs checks if necessary.
*
* @param {string} name - The name of the payment method to click on. This name is used to identify the corresponding element in the user interface.
* @param {boolean} [isCheckNeeded=false] - Indicates whether additional checks are necessary after clicking on the payment method. If `true`, additional verification steps will be added to ensure that the expected changes (such as the remaining amount, change, or selected amount) are correctly applied.
* @param {Object} [options={}] - An object containing additional options for the checks. The options include:
* @param {string|null} [options.remaining=null] - The expected remaining amount after selecting the payment method. If provided and `isCheckNeeded` is `true`, a check will be performed to ensure this remaining amount is correct.
* @param {string|null} [options.change=null] - The expected change amount after selecting the payment method. If provided and `isCheckNeeded` is `true`, a check will be performed to confirm this change amount.
* @param {string|null} [options.amount=null] - The specific amount associated with the selected payment method. If provided and `isCheckNeeded` is `true`, a check will ensure that the selected amount is correctly displayed.
*
*
* @example
* // Clicks on the "Cash" payment method without additional checks
* clickPaymentMethod("Cash");
*
* // Clicks on the "Bank" payment method and checks the remaining amount and change
* clickPaymentMethod("Cash", true, { remaining: "50.20", change: "10.50" });
*
* // Clicks on the "Cash" payment method and checks the amount to be paid
* clickPaymentMethod("Cash", true, { amount: "10.20" });
*/
export function clickPaymentMethod(name, isCheckNeeded = false, options = {}) {
const { remaining = null, change = null, amount = null } = options;
const step = [
{
content: `click '${name}' payment method`,
trigger: `.paymentmethods .button.paymentmethod .payment-name:contains("${name}")`,
run: "click",
},
];
if (isCheckNeeded) {
if (remaining) {
step.push(...remainingIs(remaining));
}
if (change) {
step.push(...changeIs(change));
}
if (amount) {
step.push(...selectedPaymentlineHas(name, amount));
}
}
return step;
}
/**
* Delete the paymentline having the given payment method name and amount.
* @param {String} name payment method
* @param {String} amount
*/
export function clickPaymentlineDelButton(name, amount, mobile = false) {
return [
{
content: `delete ${name} paymentline with ${amount} amount`,
trigger: `.paymentlines .paymentline .payment-infos:contains("${name}"):has(.payment-amount:contains("${amount}")) ~ .delete-button`,
run: "click",
},
];
}
export function clickCancelButton() {
return [
{
content: "Cancel the ongoing payment request currently being processed.",
trigger: ".paymentlines .paymentline .send_payment_cancel",
run: "click",
},
];
}
export function clickRetryButton() {
return [
{
content: "Retry sending the payment request using the payment terminal.",
trigger: ".paymentlines .paymentline .send_payment_request:contains('Retry')",
run: "click",
},
];
}
export function clickRefundButton() {
return [
{
content: "Initiate a refund request for the selected order.",
trigger: ".paymentlines .send_refund_request:contains('Refund')",
run: "click",
},
];
}
/**
* Click the paymentline having the given payment method name and amount.
* @param {String} name payment method
* @param {String} amount
*/
export function clickPaymentline(name, amount) {
return [
{
content: `click ${name} paymentline with ${amount} amount`,
trigger: `.paymentlines .paymentline .payment-infos:contains("${name}"):has(.payment-amount:contains("${amount}"))`,
run: "click",
},
];
}
export function clickInvoiceButton() {
return [
{
content: "click invoice button",
trigger: ".payment-buttons .js_invoice",
run: "click",
},
];
}
export function clickValidate() {
return [
{
content: "validate payment",
trigger: `.payment-screen button.validation-button.next`,
run: "click",
},
];
}
/**
* Press the numpad in sequence based on the given space-separated keys.
* Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours. This method is only for the
* desktop environment. The mobile environment doesn't work exactly the same way
* so we have to call fillPaymentLineAmountMobile to have the same behaviour.
*
* e.g. :
* PaymentScreen.enterPaymentLineAmount("Cash", "70"),
* PaymentScreen.remainingIs("2.0"),
* PaymentScreen.clickNumpad("0"), <- desktop: add a 0
* PaymentScreen.fillPaymentLineAmountMobile("Cash", "700"), <- mobile: rewrite the amount
* PaymentScreen.remainingIs("0.00"),
* PaymentScreen.changeIs("628.0"),
*
* @param {String} keys space-separated numpad keys
*/
export function clickNumpad(keys) {
return keys.split(" ").map((key) => ({ ...Numpad.click(key), isActive: ["desktop"] }));
}
export function clickBack() {
return [
{
content: "click back button",
trigger: ".back-button",
run: "click",
},
];
}
export function clickBackToProductScreen() {
return [
{
content: "click back to product screen",
trigger: ".payment-screen .back-button",
run: "click",
},
];
}
export function clickTipButton() {
return [
{
trigger: ".payment-screen .button:contains('Tip')",
run: "click",
},
];
}
/**
* Enter an amount for a specified payment line and then perform checks if necessary.
*
* This function performs the entry of an amount on a payment line in the user interface. It can also check for expected conditions such as the remaining amount, change, or the selected amount after the entry.
*
* @param {string} lineName - The name of the payment line where the amount needs to be entered. This name helps to identify the target payment line in the user interface.
* @param {string} keys - The sequence of keys to simulate for the amount entry, in the form of a string where each character represents a key to press.
* @param {boolean} [isCheckNeeded=false] - Indicates whether additional checks need to be performed after the amount entry.
* @param {Object} [options={}] - An object containing additional options for checks. The options include:
* @param {string|null} [options.remaining=null] - The expected remaining amount after the amount is entered on the payment line. If provided and `isCheckNeeded` is `true`, a check will be performed to ensure this remaining amount is correct.
* @param {string|null} [options.change=null] - The expected change amount after the amount is entered on the payment line. If provided and `isCheckNeeded` is `true`, a check will be performed to confirm this change amount.
* @param {string|null} [options.amount=null] - The specific amount expected on the payment for this line after the entry. If provided and `isCheckNeeded` is `true`, a check will ensure that the selected amount is correctly displayed.
*
* @example
* // Enter the amount "50" on the "Cash" payment line without additional checks
* enterPaymentLineAmount("Cash", "50");
*
* @example
* // Enter the amount "100" on the "Bank" payment line and check that the remaining amount is 50 and the change is 20
* enterPaymentLineAmount("Bank", "100", true, { remaining: "50.0", change: "20.0" });
*/
export function enterPaymentLineAmount(lineName, keys, isCheckNeeded = false, options = {}) {
const { remaining = null, change = null, amount = null } = options;
const step = [
...clickNumpad(keys.split("").join(" ")),
...fillPaymentLineAmountMobile(lineName, keys),
];
if (isCheckNeeded) {
if (remaining) {
step.push(...remainingIs(remaining));
}
if (change) {
step.push(...changeIs(change));
}
if (amount) {
step.push(...selectedPaymentlineHas(lineName, amount));
}
}
return step;
}
export function fillPaymentLineAmountMobile(lineName, keys) {
return [
{
isActive: ["mobile"],
content: "click payment line",
trigger: `.paymentlines .paymentline .payment-infos:contains("${lineName}")`,
run: "click",
},
...NumberPopup.enterValue(keys).map((step) => ({
...step,
isActive: ["mobile"],
run: "click",
})),
{
...Dialog.confirm(),
isActive: ["mobile"],
run: "click",
},
];
}
export function isShown() {
return [
{
content: "payment screen is shown",
trigger: ".pos .payment-screen",
},
];
}
/**
* Check if change is the provided amount.
* @param {String} amount
*/
export function changeIs(amount) {
return [
{
content: `change is ${amount}`,
trigger: `.payment-status-amount .amount:contains("${amount}")`,
},
];
}
export function isInvoiceOptionSelected() {
return [
{
content: "Invoice option is selected",
trigger: ".payment-buttons .js_invoice.highlight",
},
];
}
/**
* Check if the remaining is the provided amount.
* @param {String} amount
*/
export function remainingIs(amount) {
return [
{
content: `remaining amount is ${amount}`,
trigger: `.payment-status-amount .amount:contains("${amount}")`,
},
];
}
/**
* Check if validate button is highlighted.
* @param {Boolean} isHighlighted
*/
export function validateButtonIsHighlighted(isHighlighted = true) {
return [
{
isActive: ["desktop"],
content: `validate button is ${isHighlighted ? "highlighted" : "not highlighted"}`,
trigger: isHighlighted
? `.payment-screen button.validation-button.next.highlight`
: `.payment-screen button.validation-button.next:not(:has(.highlight))`,
},
];
}
/**
* Check if the paymentlines are empty. Also provide the amount to pay.
* @param {String} amountToPay
*/
export function emptyPaymentlines(amountToPay) {
return [
{
content: `there are no paymentlines`,
trigger: `.paymentlines-empty`,
},
{
content: `amount to pay is '${amountToPay}'`,
trigger: `.paymentlines-empty .total:contains("${amountToPay}")`,
},
];
}
/**
* Check if the selected paymentline has the given payment method and amount.
* @param {String} paymentMethodName
* @param {String} amount
*/
export function selectedPaymentlineHas(paymentMethodName, amount) {
return [
{
content: `line paid via '${paymentMethodName}' is selected`,
trigger: `.paymentlines .paymentline.selected .payment-name:contains("${paymentMethodName}")`,
},
{
content: `amount tendered in the line is '${amount}'`,
trigger: `.paymentlines .paymentline.selected .payment-amount:contains("${amount}")`,
},
];
}
export function totalIs(amount) {
return [
{
content: `total is ${amount}`,
trigger: `.total:contains("${amount}")`,
},
];
}
export function pay(method, amount) {
const steps = [];
steps.push(...clickPaymentMethod(method));
for (const char of amount.split("")) {
steps.push(...clickNumpad(char));
}
steps.push(...validateButtonIsHighlighted());
steps.push(...clickValidate());
return steps;
}
export function isInvoiceButtonChecked() {
return [
{
content: "check invoice button is checked",
trigger: ".js_invoice.highlight",
},
];
}
export function clickShipLaterButton() {
return [
{
content: "click ship later button",
trigger: ".button:contains('Ship Later')",
run: "click",
},
{
content: "click confirm button",
trigger: ".btn:contains('Confirm')",
run: "click",
},
];
}
export function clickPartnerButton() {
return [
{
content: "click customer button",
trigger: "button.partner-button",
run: "click",
},
{
content: "partner screen is shown",
trigger: `${PartnerList.clickPartner().trigger}`,
},
];
}
export function clickCustomer(name, pressEnter = false) {
return [...PartnerList.searchCustomerValue(name, pressEnter), PartnerList.clickPartner(name)];
}
export function shippingLaterHighlighted() {
return {
content: "Shipping later button is highlighted",
trigger: ".button:contains('Ship Later').highlight",
};
}
// This method is used to simulate payment with a payment terminal, before using terminal the order
// is synced to ensure that the order is up-to-date and ready for payment.
export function syncCurrentOrder() {
return [
{
content: "sync current order",
trigger: "body",
run: async () => {
const currentOrder = posmodel.getOrder();
const order = await posmodel.syncAllOrders({ orders: [currentOrder] });
if (!order[0].isSynced) {
throw new Error("Order ID is not a number after sync.");
}
},
},
];
}
export function isInvoiceButtonUnchecked() {
return [
{
content: "check invoice button is not highlighted",
trigger: ".js_invoice:not(.highlight)",
},
];
}

View file

@ -0,0 +1,149 @@
/* global posmodel */
import { _t } from "@web/core/l10n/translation";
import { renderToElement } from "@web/core/utils/render";
export async function generateReceiptsToPrint(order, orderChange) {
const { orderData, changes } = posmodel.generateOrderChange(
order,
orderChange,
Array.from(posmodel.config.printerCategories),
false
);
const receiptsData = await posmodel.generateReceiptsDataToPrint(
orderData,
changes,
orderChange
);
const groupedReceiptsData = await posmodel.prepareReceiptGroupedData(receiptsData);
return groupedReceiptsData.map((data) =>
renderToElement("point_of_sale.OrderChangeReceipt", {
data: data,
})
);
}
// Return rendered order change receipts that will be printed when clicking "Order" button
export async function generatePreparationReceipts() {
const order = posmodel.getOrder();
const orderChange = posmodel.changesToOrder(order, posmodel.config.printerCategories, false);
return await generateReceiptsToPrint(order, orderChange);
}
// Return rendered fire course receipts that will be printed when clicking "Fire course" button
export async function generateFireCourseReceipts() {
const order = posmodel.getOrder();
const course = order.getSelectedCourse();
const orderChange = {
new: [],
cancelled: [],
noteUpdate: course.lines.map((line) => ({ product_id: line.getProduct().id })),
noteUpdateTitle: _t("Course %s fired", "" + course.index),
printNoteUpdateData: false,
};
return await generateReceiptsToPrint(order, orderChange);
}
export function checkPreparationTicketData(
data,
opts = {
visibleInDom: [],
invisibleInDom: [],
lineOrder: [],
fireCourse: false,
}
) {
const check = async () => {
let tickets = [];
if (opts.fireCourse) {
tickets = await generateFireCourseReceipts();
} else {
tickets = await generatePreparationReceipts();
}
if (
!tickets[0] &&
!data.length &&
!opts.invisibleInDom?.length &&
!opts.visibleInDom?.length &&
!opts.lineOrder?.length &&
!opts.fireCourse
) {
return true;
}
const lines = tickets[0].querySelectorAll(".orderline");
const lineNames = [];
let idx = 0;
for (const line of lines) {
const name = line.firstChild.children[1].innerHTML;
const qty = line.firstChild.children[0].innerHTML;
const domAttrs = Object.values(line.children[1]?.children || []);
const attrs = domAttrs.map((c) => c.innerHTML).filter(Boolean);
const values = data[idx];
if (values.qty != qty) {
throw new Error(
`Ticket data mismatch for ${name}: expected ${values.qty}, got ${qty}`
);
}
if (values.name != name) {
throw new Error(
`Ticket data mismatch for ${name}: expected ${values.name}, got ${name}, maybe lines ordering ?`
);
}
if (values.attributes) {
for (const attr of values.attributes) {
const found = attrs.find((a) => a.includes(attr));
if (!found) {
throw new Error(
`Attribute ${attr} not found in printed receipt for ${name}`
);
}
}
}
if (!values) {
throw new Error(`Received ${name} but no check data found`);
}
lineNames.push(name);
idx++;
}
if (opts.visibleInDom) {
for (const inDom of opts.visibleInDom) {
let found = false;
for (const ticket of tickets) {
if (ticket.innerHTML.includes(inDom)) {
found = true;
}
}
if (!found) {
throw new Error(`${inDom} not found in printed receipt`);
}
}
}
if (opts.invisibleInDom) {
for (const notInDom of opts.invisibleInDom) {
for (const ticket of tickets) {
if (ticket.innerHTML.includes(notInDom)) {
throw new Error(`${notInDom} should not be in printed receipt`);
}
}
}
}
};
return [
{
trigger: "body",
run: async () => await check(),
},
];
}

View file

@ -0,0 +1,110 @@
/* global posmodel */
function assert(condition, message) {
if (!condition) {
throw message || "Assertion failed";
}
}
function assertProductPrice(product, pricelist_name, quantity, expected_price) {
return function () {
var pricelist = posmodel.data.models["product.pricelist"].find(
(pricelist) => pricelist.name === pricelist_name
);
var frontend_price = product.getPrice(
pricelist,
quantity,
0,
false,
product.product_variant_ids[0]
);
const ProductPrice = posmodel.data.models["decimal.precision"].find(
(dp) => dp.name === "Product Price"
);
frontend_price = ProductPrice.round(frontend_price);
var diff = Math.abs(expected_price - frontend_price);
assert(
diff < 0.001,
JSON.stringify({
product: product.id,
product_display_name: product.display_name,
pricelist_name: pricelist_name,
quantity: quantity,
}) +
" DOESN'T MATCH -> " +
expected_price +
" != " +
frontend_price
);
return Promise.resolve();
};
}
export function setUp() {
return [
// The global posmodel is only present when the posmodel is instantiated
// So, wait for everything to be loaded
{
content: "waiting for loading to finish",
trigger: "body:not(:has(.pos-loader))", // Pos has finished loading
run: function () {
var product_wall_shelf = posmodel.data.models["product.template"]
.getAll()
.find((p) => p.display_name === "Wall Shelf Unit");
var product_small_shelf = posmodel.data.models["product.template"]
.getAll()
.find((p) => p.display_name === "Small Shelf");
var product_magnetic_board = posmodel.data.models["product.template"]
.getAll()
.find((p) => p.display_name === "Magnetic Board");
var product_monitor_stand = posmodel.data.models["product.template"]
.getAll()
.find((p) => p.display_name === "Monitor Stand");
var product_desk_pad = posmodel.data.models["product.template"]
.getAll()
.find((p) => p.display_name === "Desk Pad");
var product_letter_tray = posmodel.data.models["product.template"]
.getAll()
.find((p) => p.display_name === "Letter Tray");
assertProductPrice(product_letter_tray, "Public Pricelist", 0, 4.8)()
.then(assertProductPrice(product_letter_tray, "Public Pricelist", 1, 4.8))
.then(assertProductPrice(product_letter_tray, "Fixed", 1, 1))
.then(assertProductPrice(product_letter_tray, "Fixed", -1, 1))
.then(assertProductPrice(product_wall_shelf, "Fixed", 1, 2))
.then(assertProductPrice(product_small_shelf, "Fixed", 1, 13.95))
.then(assertProductPrice(product_wall_shelf, "Percentage", 1, 0))
.then(assertProductPrice(product_small_shelf, "Percentage", 1, 0.03))
.then(assertProductPrice(product_magnetic_board, "Percentage", 1, 1.98))
.then(assertProductPrice(product_wall_shelf, "Formula", 1, 6.86))
.then(assertProductPrice(product_small_shelf, "Formula", 1, 2.99))
.then(assertProductPrice(product_magnetic_board, "Formula", 1, 11.98))
.then(assertProductPrice(product_monitor_stand, "Formula", 1, 8.19))
.then(assertProductPrice(product_desk_pad, "Formula", 1, 6.98))
.then(assertProductPrice(product_wall_shelf, "min_quantity ordering", 1, 2))
.then(assertProductPrice(product_wall_shelf, "min_quantity ordering", 2, 1))
.then(assertProductPrice(product_letter_tray, "Category vs no category", 1, 2))
.then(assertProductPrice(product_letter_tray, "Category", 1, 2))
.then(assertProductPrice(product_wall_shelf, "Product template", 1, 1))
.then(assertProductPrice(product_wall_shelf, "Dates", 1, 2))
.then(
assertProductPrice(product_small_shelf, "Pricelist base rounding", 1, 13.95)
)
.then(function () {
document.querySelector(".pos").classList.add("done-testing");
});
},
},
];
}
export function waitForUnitTest() {
return [
{
content: "wait for unit tests to finish",
trigger: ".pos.done-testing",
},
];
}

View file

@ -0,0 +1,218 @@
export function pickRadio(name) {
return [
{
content: `picking radio attribute with name ${name}`,
trigger: `.modal .attribute-name-cell:contains('${name}') input`,
run: "click",
},
];
}
export function selectedRadio(name) {
return [
{
content: `checking selected radio attribute with name ${name}`,
trigger: `.modal .attribute-name-cell:contains('${name}') input:checked`,
},
];
}
export function pickMulti(name) {
return [
{
content: `picking multi attribute with name ${name}`,
trigger: `.modal label[for^="multi-"]:contains('${name}')`,
run: "click",
},
];
}
export function selectedMulti(name) {
return [
{
content: `checking selected multi attribute with name ${name}`,
trigger: `.modal label[for^="multi-"].active:contains('${name}')`,
},
];
}
export function pickSelect(name) {
return [
{
content: `picking select attribute with name ${name}`,
trigger: `.modal .configurator_select:has(option:contains('${name}'))`,
run: ({ queryAll }) => {
const selects = queryAll`.modal .configurator_select`;
for (const select of selects) {
const option = Array.from(select.options).find(
(opt) => opt.textContent.trim() === name
);
if (option) {
select.value = option.value;
// Manually trigger change event
select.dispatchEvent(new Event("change", { bubbles: true }));
return;
}
}
throw new Error(`Option "${name}" not found in any select`);
},
},
];
}
export function selectedSelect(name) {
return [
{
content: `check selected value for select containing option "${name}"`,
trigger: `.modal .configurator_select:has(option:contains(${name}))`,
run: ({ queryAll }) => {
const selects = queryAll`.modal .configurator_select:has(option:contains(${name}))`;
for (const select of selects) {
const selected = select.options[select.selectedIndex];
if (selected?.textContent.trim() === name) {
return true;
}
}
throw new Error(`No select found with option "${name}" selected`);
},
},
];
}
export function pickColor(name) {
return [
{
content: `picking color attribute with name ${name}`,
trigger: `.modal .configurator_color[data-color='${name}']`,
run: "click",
},
];
}
export function selectedColor(name) {
return [
{
content: `checking selected color attribute with name ${name}`,
trigger: `.modal .configurator_color[data-color='${name}'].active`,
},
];
}
export function fillCustomAttribute(value) {
return [
{
content: `filling custom attribute with value ${value}`,
trigger: `.modal .custom_value`,
run: `edit ${value}`,
},
];
}
export function selectedCustomAttribute(value) {
return [
{
content: `checking selected custom attribute with value "${value}"`,
// trigger: `.modal .custom_value:contains('${value}')`,
trigger: `.modal .custom_value`,
run: ({ queryAll }) => {
const inputs = queryAll(".modal .custom_value");
for (const input of inputs) {
const actual = input.value?.trim();
if (actual === value) {
return true;
}
}
throw new Error(`No custom input found with value "${value}"`);
},
},
];
}
export function numberRadioOptions(number) {
return [
{
trigger: `.attribute-name-cell`,
run: () => {
const radio_options = document.querySelectorAll(".attribute-name-cell").length;
if (radio_options !== number) {
throw new Error(`Expected ${number} radio options, got ${radio_options}`);
}
},
},
];
}
export function isOptionShown(option) {
return [
{
content: `option ${option} is shown`,
trigger: `.form-check-label:contains('${option}')`,
},
];
}
export function isUnavailable(option) {
return [
{
content: `option ${option} is unavailable`,
trigger: `.modal .attribute span.text-muted:contains('${option}')`,
},
];
}
export function isAddDisabled() {
return [
{
content: "Add button is disabled",
trigger: ".modal .btn-primary.disabled",
},
];
}
export function isAddEnabled() {
return [
{
content: "Add button is enabled",
trigger: ".modal .btn-primary:not(.disabled)",
},
];
}
export function checkImageVariantVisible() {
return [
{
content: `Check that the image is displayed`,
trigger: `.configurator_color.rounded-3`,
},
];
}
export function checkImageVariantTextVisible(variantName) {
return [
{
content: `Check that the variant is visible`,
trigger: `.text-center.mt-2.small span:contains("${variantName}")`,
},
];
}
export function checkImagePriceExtraVisible(price) {
return [
{
content: `Check that the extra price is displayed`,
trigger: `.price_extra.px-2.py-1.rounded-pill.text-bg-info:contains("${price}")`,
},
];
}
export function isRadioDisabled(name) {
return [
{
content: `check radio attribute with name ${name}`,
trigger: `.modal .attribute-name-cell:contains('${name}') input:disabled`,
},
];
}
export function priceIs(price) {
return [
{
content: `checking that total price is ${price}`,
trigger: `.modal .modal-title:contains('${price}')`,
},
];
}

View file

@ -0,0 +1,15 @@
import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import { back, selectButton } from "@point_of_sale/../tests/pos/tours/utils/common";
export function searchCustomerValueAndClear(val) {
return [
ProductScreen.clickPartnerButton(),
PartnerList.searchCustomerValue(val),
selectButton("Discard"),
{
isActive: ["mobile"],
...back(),
},
].flat();
}

View file

@ -0,0 +1,231 @@
export function clickNextOrder() {
return [
{
isActive: ["desktop"],
content: "go to next screen",
trigger: ".receipt-screen .button.next.highlight[name='done']",
run: "click",
},
{
isActive: ["mobile"],
content: "go to next screen",
trigger: ".receipt-screen .btn-switchpane.validation-button.highlight[name='done']",
run: "click",
},
];
}
export function clickContinueOrder() {
return [
{
content: "go to next screen",
trigger: ".receipt-screen .button.next.highlight[name='resume']",
run: "click",
},
];
}
export function setEmail(email) {
return [
{
trigger: ".receipt-screen .send-receipt-email-input",
run: `edit ${email}`,
},
];
}
export function clickSend() {
return [
{
run: "click",
trigger: `.receipt-screen button i.fa-paper-plane`,
},
];
}
export function clickBack() {
return [
{
trigger: ".receipt-screen .button.back",
run: "click",
},
];
}
export function isShown() {
return [
{
content: "receipt screen is shown",
trigger: ".pos .receipt-screen",
},
];
}
export function receiptIsThere() {
return [
{
content: "there should be the receipt",
trigger: ".receipt-screen .pos-receipt",
},
];
}
export function totalAmountContains(value) {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: `.receipt-screen .o_payment_successful:contains("${value}")`,
},
{
isActive: ["mobile"], // On mobile, at least wait for the receipt screen to show
trigger: `.receipt-screen`,
},
];
}
export function receiptAmountTotalIs(value) {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: `.receipt-screen .receipt-total:contains("${value}")`,
},
{
isActive: ["mobile"], // On mobile, at least wait for the receipt screen to show
trigger: `.receipt-screen`,
},
];
}
export function receiptRoundingAmountIs(value) {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: `.receipt-screen .receipt-rounding:contains("${value}")`,
},
];
}
export function paymentLineContains(paymentMethodName, amount) {
return [
{
content: `Check if payment line contains ${paymentMethodName} with amount ${amount}`,
trigger: `.receipt-screen .paymentlines:contains("${paymentMethodName}"):has(.pos-receipt-right-align:contains("${amount}"))`,
},
];
}
export function receiptToPayAmountIs(value) {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: `.receipt-screen .receipt-to-pay:contains("${value}")`,
},
];
}
export function receiptToPayAmountIsNotThere() {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: ".receipt-screen",
run: function () {
if (document.querySelector(".receipt-to-pay")) {
throw new Error("An amount to pay has been found in receipt.");
}
},
},
];
}
export function receiptChangeAmountIs(value) {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: `.receipt-screen .receipt-change:contains("${value}")`,
},
];
}
export function receiptChangeAmountIsNotThere() {
return [
{
isActive: ["desktop"], // not rendered on mobile
trigger: ".receipt-screen",
run: function () {
if (document.querySelector(".receipt-change")) {
throw new Error("An change amount has been found in receipt.");
}
},
},
];
}
export function emailIsSuccessful() {
return [
{
trigger: `.receipt-screen .notice .text-success`,
},
];
}
export function trackingMethodIsLot(lot) {
return [
{
content: `tracking method is Lot`,
trigger: `li.lot-number:contains("Lot Number ${lot}")`,
run: function () {
if (document.querySelectorAll("li.lot-number").length !== 1) {
throw new Error(`Expected exactly one 'Lot Number ${lot}' element.`);
}
},
},
];
}
export function noDiscountAmount() {
return [
{
trigger: `.pos-receipt:not(:contains("Discounts"))`,
run: () => {},
},
];
}
export function shippingDateExists() {
return [
{
content: "Shipping date must be printed",
trigger: ".pos-receipt-order-data:contains('Expected delivery:')",
run: "click",
},
];
}
export function shippingDateIsToday() {
// format the date in US, the language used by the tests
const expectedDelivery = new Date().toLocaleString("en-US", luxon.DateTime.DATE_SHORT);
return [
{
content: "Shipping date must be today",
trigger: `.pos-receipt-order-data:contains('Expected delivery:') > div:contains('${expectedDelivery}')`,
},
];
}
export function checkOrderlineTaxGroupLabel(label) {
return {
content: `Verify that the tax group "${label}" appears on the receipt order line.`,
trigger: `.pos-receipt .line-details:contains("${label}")`,
};
}
export function checkTaxSummaryTaxGroupLabel(label) {
return {
content: `Verify that the tax group "${label}" appears in the receipt tax summary.`,
trigger: `.pos-receipt-taxes:contains('${label}')`,
};
}
export function cashierNameExists(name) {
return [
{
content: `Cashier ${name} exists on the receipt`,
trigger: `.pos-receipt-contact .cashier:contains(Served by):contains(${name})`,
},
];
}
export function containsOrderLine(name, quantity, price_unit, line_price) {
return [
{
content: `Order line with name: ${name}, quantity: ${quantity}, price per unit: ${price_unit}, and line price: ${line_price} exists`,
trigger: `.pos-receipt .orderline:has(.product-name:contains('${name}')):has(.qty:contains('${quantity}')):has(.product-price:contains('${line_price}')):has(.price-per-unit:contains('${price_unit}'))`,
},
];
}

View file

@ -0,0 +1,269 @@
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import { isSyncStatusConnected } from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
export function nbOrdersIs(nb) {
return [
{
trigger: `.ticket-screen`,
run: () => {
const orders = document.querySelectorAll(".ticket-screen .order-row");
if (orders.length !== nb) {
throw new Error(`Expected ${nb} orders, but found ${orders.length}`);
}
},
},
];
}
export function clickDiscard() {
return {
content: "go back",
trigger: ".ticket-screen button.discard",
run: "click",
};
}
export function selectOrder(orderName) {
return [
{
trigger: `.ticket-screen .order-row:contains("${orderName}")`,
run: "click",
},
];
}
export function selectOrderByPrice(price) {
return [
{
trigger: `.ticket-screen .order-row:contains("${price}")`,
run: "click",
},
{
trigger: `.ticket-screen .order-row.active:contains("${price}")`,
},
];
}
export function doubleClickOrder(orderName) {
return [
{
trigger: `.ticket-screen .order-row:contains("${orderName}")`,
run: "dblclick",
},
];
}
export function loadSelectedOrder() {
return [
ProductScreen.clickReview(),
{
trigger: ".ticket-screen .pads .button.validation.load-order-button",
run: "click",
},
];
}
export function deleteOrder(orderName) {
return [
{
isActive: ["mobile"],
trigger: `.ticket-screen .order-row > div:contains("${orderName}")`,
run: "click",
},
{
isActive: ["mobile"],
trigger: `.ticket-screen .order-row:has(div:contains("${orderName}")) .btn-danger`,
run: "click",
},
{
isActive: ["desktop"],
trigger: `.ticket-screen .orders .order-row > td:contains("${orderName}") ~ td.text-end button.text-danger`,
run: "click",
},
];
}
export function selectFilter(name) {
return [
{
trigger: `.pos-search-bar .filter`,
run: "click",
},
{
trigger: `.pos-search-bar .filter ul`,
},
{
trigger: `.pos-search-bar .filter ul li:contains("${name}")`,
run: "click",
},
];
}
export function search(field, searchWord) {
return [
{
trigger: ".pos-search-bar input",
run: `edit ${
field !== "Invoice Number"
? searchWord
: "TSJ/" + new Date().getFullYear() + "/" + searchWord
}`,
},
{
trigger: `.pos-search-bar .search ul li:contains("${field}")`,
run: "click",
},
];
}
export function settleTips() {
return [
{
trigger: ".ticket-screen .controls .settle-tips",
run: "click",
},
isSyncStatusConnected(),
];
}
export function clickControlButton(name) {
return [
ProductScreen.clickReview(),
{
trigger: `.ticket-screen ${ProductScreen.controlButtonTrigger(name)}`,
run: "click",
},
];
}
export function confirmRefund() {
return [
ProductScreen.clickReview(),
{
trigger: ".ticket-screen .btn-primary.pay-order-button",
run: "click",
},
];
}
export function checkStatus(orderName, status) {
return [
{
isActive: ["desktop"],
trigger: `.ticket-screen tbody tr > td:contains("${orderName}") ~ td .badge:contains(${status})`,
},
{
isActive: ["mobile"],
trigger: `.ticket-screen .order-row > div:contains("${orderName}") ~ div .badge:contains(${status})`,
},
];
}
/**
* Check if the nth row contains the given string.
* Note that 1st row is the header-row.
* @param {boolean | undefined} viewMode true if in mobile view, false if in desktop, undefined if in both views.
*/
export function nthRowContains(n, string, viewMode) {
return [
{
isActive: [viewMode ? "mobile" : "desktop"],
trigger: `.ticket-screen .orders tbody .order-row:nth-child(${n}):contains("${string}")`,
},
];
}
export function nthRowIsHighlighted(n) {
return [
{
trigger: ".ticket-screen .order-row.highlight",
},
];
}
export function nthRowNotContains(n, string, viewMode) {
return [
{
isActive: [viewMode ? "mobile" : "desktop"],
trigger: `.ticket-screen .orders tbody .order-row:nth-child(${n}):not(:contains("${string}"))`,
},
];
}
export function contains(string) {
return [
{
trigger: `.ticket-screen .orders:contains("${string}")`,
},
];
}
export function filterIs(name) {
return [
{
trigger: `.ticket-screen .pos-search-bar .filter span:contains("${name}")`,
},
];
}
export function invoicePrinted() {
return [
{
trigger: ProductScreen.controlButtonTrigger("Reprint Invoice"),
},
];
}
export function toRefundTextContains(text) {
return inLeftSide({
trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`,
});
}
export function toRefundLineContains(product, text) {
return inLeftSide({
trigger: `.ticket-screen div:has(.product-name:contains("${product}")):has(.to-refund-highlight:contains("${text}"))`,
});
}
export function refundedNoteContains(text) {
return inLeftSide({
trigger: `.ticket-screen .refund-note:contains("${text}")`,
});
}
export function noLinesToRefund() {
return inLeftSide({
content: "No lines are marked for to refund or refunding",
trigger: ".ticket-screen:not(:has(.to-refund-highlight))",
});
}
export function tipContains(amount) {
return [
{
trigger: `.ticket-screen .tip-cell:contains("${amount}")`,
},
];
}
export function receiptTotalIs(amount) {
return [
{
trigger: `.receipt-screen .pos-receipt-amount:contains("${amount}")`,
},
];
}
export function receiptChangeIs(amount) {
return [
{
trigger: `.receipt-screen .receipt-change:contains("${amount}")`,
},
];
}
export function back() {
return {
isActive: ["mobile"],
trigger: ".back-button",
run: "click",
};
}
export function checkCameraIsOpen() {
return {
content: "Verify that the camera view is visible in the left pane.",
trigger: ".ticket-screen .leftpane .o_crop_container",
};
}
export function noOrderIsThere() {
return {
content: "No orders should be visible on the Ticket Screen",
trigger: ".ticket-screen:not(:has(.order-row))",
};
}
export function isShown() {
return [
{
content: "ticket screen is shown",
trigger: ".pos .ticket-screen",
},
];
}

View file

@ -1,55 +0,0 @@
odoo.define('point_of_sale.tour.BarcodeScanning', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
startSteps();
// Add a product with its barcode
ProductScreen.do.scan_barcode("0123456789");
ProductScreen.check.selectedOrderlineHas('Monitor Stand');
ProductScreen.do.scan_barcode("0123456789");
ProductScreen.check.selectedOrderlineHas('Monitor Stand', 2);
// Test "Prices product" EAN-13 `23.....{NNNDD}` barcode pattern
ProductScreen.do.scan_ean13_barcode("2305000000004");
ProductScreen.check.selectedOrderlineHas('Magnetic Board', 1, "0.00");
ProductScreen.do.scan_ean13_barcode("2305000123451");
ProductScreen.check.selectedOrderlineHas('Magnetic Board', 1, "123.45");
// Test "Weighted product" EAN-13 `21.....{NNDDD}` barcode pattern
ProductScreen.do.scan_ean13_barcode("2100005000000");
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', 0, "0.00");
ProductScreen.do.scan_ean13_barcode("2100005080002");
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', 8);
Tour.register('BarcodeScanningTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
// Add the Product 1 with GS1 barcode
ProductScreen.do.scan_barcode("0108431673020125100000001");
ProductScreen.check.selectedOrderlineHas('Product 1');
ProductScreen.do.scan_barcode("0108431673020125100000001");
ProductScreen.check.selectedOrderlineHas('Product 1', 2);
// Add the Product 2 with normal barcode
ProductScreen.do.scan_barcode("08431673020126");
ProductScreen.check.selectedOrderlineHas('Product 2');
ProductScreen.do.scan_barcode("08431673020126");
ProductScreen.check.selectedOrderlineHas('Product 2', 2);
// Add the Product 3 with normal barcode
ProductScreen.do.scan_barcode("3760171283370");
ProductScreen.check.selectedOrderlineHas('Product 3');
ProductScreen.do.scan_barcode("3760171283370");
ProductScreen.check.selectedOrderlineHas('Product 3', 2);
Tour.register('GS1BarcodeScanningTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -1,105 +0,0 @@
odoo.define('point_of_sale.tour.Chrome', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
// Order 1 is at Product Screen
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Pad', '1', '2', '2.0');
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0001', 'Ongoing');
// Order 2 is at Payment Screen
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Monitor Stand', '3', '4', '12.0');
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0002', 'Payment');
// Order 3 is at Receipt Screen
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6', '30.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0003', 'Receipt');
// Select order 1, should be at Product Screen
TicketScreen.do.selectOrder('-0001');
ProductScreen.check.productIsDisplayed('Desk Pad');
ProductScreen.check.selectedOrderlineHas('Desk Pad', '1.0', '2.0');
// Select order 2, should be at Payment Screen
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder('-0002');
PaymentScreen.check.emptyPaymentlines('12.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
// Select order 3, should be at Receipt Screen
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder('-0003');
ReceiptScreen.check.totalAmountContains('30.0');
// Pay order 1, with change
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder('-0001');
ProductScreen.check.isShown();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('2 0');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('18.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.totalAmountContains('2.0');
// Order 1 now should have Receipt status
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0001', 'Receipt');
// Select order 3, should still be at Receipt Screen
// and the total amount doesn't change.
TicketScreen.do.selectOrder('-0003');
ReceiptScreen.check.totalAmountContains('30.0');
// click next screen on order 3
// then delete the new empty order
ReceiptScreen.do.clickNextOrder();
ProductScreen.check.orderIsEmpty();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder('-0004');
TicketScreen.do.deleteOrder('-0001');
// After deleting order 1 above, order 2 became
// the 2nd-row order and it has payment status
TicketScreen.check.nthRowContains(2, 'Payment')
TicketScreen.do.deleteOrder('-0002');
Chrome.do.confirmPopup();
TicketScreen.do.clickNewTicket();
// Invoice an order
ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Tour.register('ChromeTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -1,291 +0,0 @@
odoo.define('point_of_sale.tour.PaymentScreen', function (require) {
'use strict';
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { ErrorPopup } = require('point_of_sale.tour.ErrorPopupTourMethods');
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 { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.exec.addOrderline('Letter Tray', '10');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '10.0');
ProductScreen.do.clickPayButton();
PaymentScreen.check.emptyPaymentlines('52.8');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('1 1');
PaymentScreen.check.selectedPaymentlineHas('Cash', '11.00');
PaymentScreen.check.remainingIs('41.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
// remove the selected paymentline with multiple backspace presses
PaymentScreen.do.pressNumpad('Backspace Backspace');
PaymentScreen.check.selectedPaymentlineHas('Cash', '0.00');
PaymentScreen.do.pressNumpad('Backspace');
PaymentScreen.check.emptyPaymentlines('52.8');
// Pay with bank, the selected line should have full amount
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
// remove the line using the delete button
PaymentScreen.do.clickPaymentlineDelButton('Bank', '52.8');
// Use +10 and +50 to increment the amount of the paymentline
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('+10');
PaymentScreen.check.remainingIs('42.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.pressNumpad('+50');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('7.2');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickPaymentlineDelButton('Cash', '60.0');
// Multiple paymentlines
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('1');
PaymentScreen.check.remainingIs('51.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('5');
PaymentScreen.check.remainingIs('46.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('2 0');
PaymentScreen.check.remainingIs('26.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
Tour.register('PaymentScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Letter Tray', '1', '10');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('1 0 0 0');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Tour.register('PaymentScreenTour2', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('2.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test', '-1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('-2.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Tour.register('PaymentScreenRoundingUp', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.95');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test', '-1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('-1.95');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Tour.register('PaymentScreenRoundingDown', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test 1.2', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test 1.25', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.5');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test 1.4', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.5');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test 1.2', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('2');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('1.0');
Tour.register('PaymentScreenRoundingHalfUp', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test 40', '1');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('40.00');
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('3 8');
PaymentScreen.check.remainingIs('2.0');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test 41', '1');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('41.00');
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('3 8');
PaymentScreen.check.remainingIs('3.0');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
Tour.register('PaymentScreenRoundingHalfUpCashAndBank', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.95');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('5');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('3.05');
PaymentScreen.check.totalDueIs('1.95');
Tour.register('PaymentScreenTotalDueWithOverPayment', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Magnetic Board', '1');
ProductScreen.do.clickPayButton();
// Check the popup error is shown when selecting another payment method
PaymentScreen.check.totalIs('1.90');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('1 .');
PaymentScreen.check.selectedPaymentlineHas('Cash', '1.00');
PaymentScreen.do.pressNumpad('2 4');
PaymentScreen.check.selectedPaymentlineHas('Cash', '1.24');
PaymentScreen.do.clickPaymentMethod('Bank');
ErrorPopup.check.isShown();
ErrorPopup.check.messageBodyContains( // Verify the value displayed are as expected
'The rounding precision is 0.10 so you should set 1.20 or 1.30 as payment amount instead of 1.24.'
);
Tour.register('CashRoundingPayment', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.exec.addOrderline('Letter Tray', '5');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '5.0');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('New Cash');
PaymentScreen.do.pressNumpad('5 5');
PaymentScreen.check.selectedPaymentlineHas('New Cash', '55');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
Tour.register('MultipleCashPaymentMethod', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -1,78 +0,0 @@
odoo.define('point_of_sale.tour.ProductConfigurator', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ProductConfigurator } = require('point_of_sale.tour.ProductConfiguratorTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
// signal to start generating steps
// when finished, steps can be taken from getSteps
startSteps();
ProductScreen.do.confirmOpeningPopup();
// Go by default to home category
ProductScreen.do.clickHomeCategory();
// Click on Configurable Chair product
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Cancel configuration, not product should be in order
ProductConfigurator.do.cancelAttributes();
ProductScreen.check.orderIsEmpty();
// Click on Configurable Chair product
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Pick Color
ProductConfigurator.do.pickColor('Red');
// Pick Radio
ProductConfigurator.do.pickSelect('Metal');
// Pick Select
ProductConfigurator.do.pickRadio('Other');
// Fill in custom attribute
ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
// Confirm configuration
ProductConfigurator.do.confirmAttributes();
// Check that the product has been added to the order with correct attributes and price
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '1.0', '11.0');
// Orderlines with the same attributes should be merged
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.do.pickColor('Red');
ProductConfigurator.do.pickSelect('Metal');
ProductConfigurator.do.pickRadio('Other');
ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
ProductConfigurator.do.confirmAttributes();
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '2.0', '22.0');
// Orderlines with different attributes shouldn't be merged
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.do.pickColor('Blue');
ProductConfigurator.do.pickSelect('Metal');
ProductConfigurator.do.pickRadio('Leather');
ProductConfigurator.do.confirmAttributes();
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Blue, Metal, Leather)', '1.0', '10.0');
Tour.register('ProductConfiguratorTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Option Other is active, Leather is not -> only 1 option available
ProductConfigurator.check.numberRadioOptions(1);
Tour.register('InactiveAttributeValueTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -1,258 +0,0 @@
odoo.define('point_of_sale.tour.ProductScreen', 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 { TextAreaPopup } = require('point_of_sale.tour.TextAreaPopupTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
// signal to start generating steps
// when finished, steps can be taken from getSteps
startSteps();
// Go by default to home category
ProductScreen.do.clickHomeCategory();
// Clicking product multiple times should increment quantity
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.10');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '2.0', '10.20');
// Clicking product should add new orderline and select the orderline
// If orderline exists, increment the quantity
ProductScreen.do.clickDisplayedProduct('Letter Tray');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0', '5.28');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3.0', '15.30');
// Check effects of clicking numpad buttons
ProductScreen.do.clickOrderline('Letter Tray', '1');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '0.0', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3', '15.30');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '0.0', '0.0');
ProductScreen.do.pressNumpad('1');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.1');
ProductScreen.do.pressNumpad('2');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '12.0', '61.2');
ProductScreen.do.pressNumpad('3');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.0', '627.3');
ProductScreen.do.pressNumpad('. 5');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '629.85');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('1');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '123.5');
ProductScreen.do.pressNumpad('1 .');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,358.5');
ProductScreen.do.pressNumpad('Disc');
ProductScreen.do.pressNumpad('5 .');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,290.58');
ProductScreen.do.pressNumpad('Qty');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.orderIsEmpty();
// Check different subcategories
ProductScreen.do.clickSubcategory('Desks');
ProductScreen.check.productIsDisplayed('Desk Pad');
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickSubcategory('Miscellaneous');
ProductScreen.check.productIsDisplayed('Whiteboard Pen');
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickSubcategory('Chairs');
ProductScreen.check.productIsDisplayed('Letter Tray');
ProductScreen.do.clickHomeCategory();
// Add two orderlines and update quantity
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit');
ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0');
ProductScreen.do.pressNumpad('2');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.0');
ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0');
ProductScreen.do.pressNumpad('2');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '2.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.orderIsEmpty();
// Add multiple orderlines then delete each of them until empty
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit');
ProductScreen.do.clickDisplayedProduct('Small Shelf');
ProductScreen.do.clickDisplayedProduct('Magnetic Board');
ProductScreen.do.clickDisplayedProduct('Monitor Stand');
ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.clickOrderline('Small Shelf', '1.0');
ProductScreen.check.selectedOrderlineHas('Small Shelf', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Small Shelf', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.clickOrderline('Magnetic Board', '1.0');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.orderIsEmpty();
// Test OrderlineCustomerNoteButton
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.do.clickOrderlineCustomerNoteButton();
TextAreaPopup.check.isShown();
TextAreaPopup.do.inputText('Test customer note');
TextAreaPopup.do.clickConfirm();
ProductScreen.check.orderlineHasCustomerNote('Desk Organizer', '1', 'Test customer note');
Tour.register('ProductScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product');
ProductScreen.check.totalAmountIs('100.00');
ProductScreen.do.changeFiscalPosition('No Tax');
ProductScreen.check.noDiscountApplied("100.00");
ProductScreen.check.totalAmountIs('86.96');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.check.noOrderlineContainsDiscount();
Tour.register('FiscalPositionNoTax', { 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());
});
odoo.define('point_of_sale.tour.OpenCloseCashCount', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.enterOpeningAmount('90');
ProductScreen.do.confirmOpeningPopup();
ProductScreen.check.checkSecondCashClosingDetailsLineAmount('10.00', '-');
Tour.register('CashClosingDetails', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.RoundGloballyTax', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product');
ProductScreen.check.totalAmountIs('115.00');
Tour.register('RoundGloballyAmoundTour', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.ShowTaxExcludedTour', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
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');
Tour.register('ShowTaxExcludedTour', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.limitedProductPricelistLoading', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.scan_barcode("0100100");
ProductScreen.check.selectedOrderlineHas('Test Product 1', '1.0', '80.0');
ProductScreen.do.scan_barcode("0100200");
ProductScreen.check.selectedOrderlineHas('Test Product 2', '1.0', '100.0');
ProductScreen.do.scan_barcode("0100300");
ProductScreen.check.selectedOrderlineHas('Test Product 3', '1.0', '50.0');
Tour.register('limitedProductPricelistLoading', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -1,104 +0,0 @@
odoo.define('point_of_sale.tour.ReceiptScreen', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
startSteps();
// press close button in receipt screen
ProductScreen.exec.addOrderline('Letter Tray', '10', '5');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '10');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
// letter tray has 10% tax (search SRC)
ReceiptScreen.check.totalAmountContains('55.0');
ReceiptScreen.do.clickNextOrder();
// send email in receipt screen
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Pad', '6', '5', '30.0');
ProductScreen.exec.addOrderline('Whiteboard Pen', '6', '6', '36.0');
ProductScreen.exec.addOrderline('Monitor Stand', '6', '1', '6.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('7 0');
PaymentScreen.check.remainingIs('2.0');
PaymentScreen.do.pressNumpad('0');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.check.changeIs('628.0');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
ReceiptScreen.check.totalAmountContains('72.0');
ReceiptScreen.do.setEmail('test@receiptscreen.com');
ReceiptScreen.do.clickSend();
ReceiptScreen.check.emailIsSuccessful();
ReceiptScreen.do.clickNextOrder();
// order with tip
// check if tip amount is displayed
ProductScreen.exec.addOrderline('Desk Pad', '6', '5');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickTipButton();
NumberPopup.do.pressNumpad('1');
NumberPopup.check.inputShownIs('1');
NumberPopup.do.clickConfirm();
PaymentScreen.check.emptyPaymentlines('31.0');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
ReceiptScreen.check.totalAmountContains('$ 30.00 + $ 1.00 tip');
ReceiptScreen.do.clickNextOrder();
// Test customer note in receipt
ProductScreen.exec.addOrderline('Desk Pad', '1', '5');
ProductScreen.exec.addCustomerNote('Test customer note')
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.customerNoteIsThere('Test customer note');
Tour.register('ReceiptScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Test Product', '1');
ProductScreen.do.clickPricelistButton();
ProductScreen.do.selectPriceList('special_pricelist');
ProductScreen.check.discountOriginalPriceIs('7.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.discountAmountIs('0.7');
ReceiptScreen.do.clickNextOrder();
ProductScreen.exec.addOrderline('Test Product', '1');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('9');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.noDiscountAmount();
Tour.register('ReceiptScreenDiscountWithPricelistTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.do.enterLotNumber('123456789');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.trackingMethodIsLot();
Tour.register('ReceiptTrackingMethodTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -1,204 +0,0 @@
odoo.define('point_of_sale.tour.TicketScreen', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { PartnerListScreen } = require('point_of_sale.tour.PartnerListScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { ErrorPopup } = require('point_of_sale.tour.ErrorPopupTourMethods');
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Pad', '1', '2');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, 'Nicole Ford');
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Desk Pad', '1', '3');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Brandon Freeman');
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(3, 'Brandon Freeman');
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Desk Pad', '2', '4');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(4, 'Receipt');
TicketScreen.do.selectFilter('Receipt');
TicketScreen.check.nthRowContains(2, 'Receipt');
TicketScreen.do.selectFilter('Payment');
TicketScreen.check.nthRowContains(2, 'Payment');
TicketScreen.do.selectFilter('Ongoing');
TicketScreen.check.nthRowContains(2, 'Ongoing');
TicketScreen.do.selectFilter('All active orders');
TicketScreen.check.nthRowContains(4, 'Receipt');
TicketScreen.do.search('Customer', 'Nicole');
TicketScreen.check.nthRowContains(2, 'Nicole');
TicketScreen.do.search('Customer', 'Brandon');
TicketScreen.check.nthRowContains(2, 'Brandon');
TicketScreen.do.search('Receipt Number', '-0003');
TicketScreen.check.nthRowContains(2, 'Receipt');
// Close the TicketScreen to see the current order which is in ReceiptScreen.
// This is just to remove the search string in the search bar.
TicketScreen.do.clickDiscard();
ReceiptScreen.check.isShown();
// Open again the TicketScreen to check the Paid filter.
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter('Paid');
TicketScreen.check.nthRowContains(2, '-0003');
// Pay the order that was in PaymentScreen.
TicketScreen.do.selectFilter('Payment');
TicketScreen.do.selectOrder('-0002');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.check.isShown();
// Check that the Paid filter will show the 2 synced orders.
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter('Paid');
TicketScreen.check.nthRowContains(2, 'Brandon Freeman');
TicketScreen.check.nthRowContains(3, '-0003');
// Invoice order
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.orderWidgetIsNotEmpty();
TicketScreen.do.clickControlButton('Invoice');
Chrome.do.confirmPopup();
PartnerListScreen.check.isShown();
PartnerListScreen.do.clickPartner('Colleen Diaz');
TicketScreen.check.partnerIs('Colleen Diaz');
// Reprint receipt
TicketScreen.do.clickControlButton('Print Receipt');
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickBack();
// When going back, the ticket screen should be in its previous state.
TicketScreen.check.filterIs('Paid');
// Test refund //
TicketScreen.do.clickDiscard();
ProductScreen.check.isShown();
ProductScreen.check.orderIsEmpty();
ProductScreen.do.clickRefund();
// Filter should be automatically 'Paid'.
TicketScreen.check.filterIs('Paid');
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.partnerIs('Colleen Diaz');
TicketScreen.do.clickOrderline('Desk Pad');
TicketScreen.do.pressNumpad('3');
// Error should show because 2 is more than the number
// that can be refunded.
ErrorPopup.do.clickConfirm();
TicketScreen.do.clickDiscard();
ProductScreen.check.isShown();
ProductScreen.check.orderIsEmpty();
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0003');
TicketScreen.do.clickOrderline('Desk Pad');
TicketScreen.do.pressNumpad('1');
TicketScreen.check.toRefundTextContains('To Refund: 1.00');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
ProductScreen.check.selectedOrderlineHas('Desk Pad', '-1.00');
// Try changing the refund line to positive number.
// Error popup should show.
ProductScreen.do.pressNumpad('2');
ErrorPopup.do.clickConfirm();
// Change the refund line quantity to -3 -- not allowed
// so error popup.
ProductScreen.do.pressNumpad('+/- 3');
ErrorPopup.do.clickConfirm();
// Change the refund line quantity to -2 -- allowed.
ProductScreen.do.pressNumpad('+/- 2');
ProductScreen.check.selectedOrderlineHas('Desk Pad', '-2.00');
// Check if the amount being refunded changed to 2.
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.toRefundTextContains('Refunding 2.00');
TicketScreen.do.clickDiscard();
// Pay the refund order.
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
// Check refunded quantity.
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.refundedNoteContains('2.00 Refunded');
Tour.register('TicketScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product Test');
ProductScreen.check.totalAmountIs('100.00');
ProductScreen.do.changeFiscalPosition('No Tax');
ProductScreen.check.totalAmountIs('86.96');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0001');
TicketScreen.do.clickOrderline('Product Test');
TicketScreen.do.pressNumpad('1');
TicketScreen.check.toRefundTextContains('To Refund: 1.00');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
ProductScreen.check.totalAmountIs('-86.96');
Tour.register('FiscalPositionNoTaxRefund', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.do.enterLotNumber('123456789');
ProductScreen.check.selectedOrderlineHas('Product A', '1.00');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0001');
TicketScreen.do.clickOrderline('Product A');
TicketScreen.do.pressNumpad('1');
TicketScreen.check.toRefundTextContains('To Refund: 1.00');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
ProductScreen.do.clickLotIcon();
ProductScreen.check.checkFirstLotNumber('123456789');
Tour.register('LotRefundTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct("Test Product");
ProductScreen.check.checkTaxAmount("9.09");
ProductScreen.check.totalAmountIs("100.00");
ProductScreen.do.changeFiscalPosition("test fp");
ProductScreen.check.totalAmountIs("100.00");
ProductScreen.check.checkTaxAmount("4.76");
Tour.register("FiscalPositionTwoTaxIncluded", { test: true, url: "/pos/ui" }, getSteps());
});

View file

@ -1,29 +0,0 @@
odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
confirmPopup() {
return [
{
content: 'confirm popup',
trigger: '.popups .modal-dialog .button.confirm',
},
];
}
clickTicketButton() {
return [
{
trigger: '.pos-topheader .ticket-button',
},
{
trigger: '.subwindow .ticket-screen',
run: () => {},
},
];
}
}
return createTourMethods('Chrome', Do);
});

View file

@ -1,39 +0,0 @@
odoo.define('point_of_sale.tour.ErrorPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickConfirm() {
return [
{
content: 'click confirm button',
trigger: '.popup-error .footer .cancel',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'error popup is shown',
trigger: '.modal-dialog .popup-error',
run: () => {},
},
];
}
messageBodyContains(text) {
return [
{
content: `check '${text}' is in the body of the popup`,
trigger: `.modal-dialog .popup-error .body:contains(${text})`,
}
];
}
}
return createTourMethods('ErrorPopup', Do, Check);
});

View file

@ -1,72 +0,0 @@
odoo.define('point_of_sale.tour.NumberPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
/**
* Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours.
*
* @param {String} keys space-separated input keys
*/
pressNumpad(keys) {
const numberChars = '0 1 2 3 4 5 6 7 8 9 C'.split(' ');
const modeButtons = '+1 +10 +2 +20 +5 +50'.split(' ');
const decimalSeparators = ', .'.split(' ');
function generateStep(key) {
let trigger;
if (numberChars.includes(key)) {
trigger = `.popup-numpad .number-char:contains("${key}")`;
} else if (modeButtons.includes(key)) {
trigger = `.popup-numpad .mode-button:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.popup-numpad .numpad-backspace`;
} else if (decimalSeparators.includes(key)) {
trigger = `.popup-numpad .number-char.dot`;
}
return {
content: `'${key}' pressed in numpad`,
trigger,
};
}
return keys.split(' ').map(generateStep);
}
clickConfirm() {
return [
{
content: 'click confirm button',
trigger: '.popup-number .footer .confirm',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'number popup is shown',
trigger: '.modal-dialog .popup-number',
run: () => {},
},
];
}
inputShownIs(val) {
return [
{
content: 'number input element check',
trigger: '.modal-dialog .popup-number .popup-input',
run: () => {},
},
{
content: `input shown is '${val}'`,
trigger: `.modal-dialog .popup-number .popup-input:contains("${val}")`,
run: () => {},
},
];
}
}
return createTourMethods('NumberPopup', Do, Check);
});

View file

@ -1,47 +0,0 @@
odoo.define('point_of_sale.tour.PartnerListScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickPartner(name) {
return [
{
content: `click partner '${name}' from partner list screen`,
trigger: `.partnerlist-screen .partner-list-contents .partner-line td:contains("${name}")`,
},
];
}
clickPartnerDetailsButton(name) {
return [
{
content: `click partner details '${name}' from partner list screen`,
trigger: `.partner-line:contains('${name}') .edit-partner-button`,
}
]
}
clickBack() {
return [
{
trigger: ".partnerlist-screen .button.back",
},
];
}
}
class Check {
isShown() {
return [
{
content: 'partner list screen is shown',
trigger: '.pos-content .partnerlist-screen',
run: () => {},
},
];
}
}
class Execute {}
return createTourMethods('PartnerListScreen', Do, Check, Execute);
});

View file

@ -1,251 +0,0 @@
odoo.define('point_of_sale.tour.PaymentScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickPaymentMethod(name) {
return [
{
content: `click '${name}' payment method`,
trigger: `.paymentmethods .button.paymentmethod:contains("${name}")`,
},
];
}
/**
* Delete the paymentline having the given payment method name and amount.
* @param {String} name payment method
* @param {String} amount
*/
clickPaymentlineDelButton(name, amount) {
return [
{
content: `delete ${name} paymentline with ${amount} amount`,
trigger: `.paymentlines .paymentline .payment-name:contains("${name}") ~ .delete-button`,
},
];
}
clickEmailButton() {
return [
{
content: `click email button`,
trigger: `.payment-buttons .js_email`,
},
];
}
clickInvoiceButton() {
return [{ content: 'click invoice button', trigger: '.payment-buttons .js_invoice' }];
}
clickValidate() {
return [
{
content: 'validate payment',
trigger: `.payment-screen .button.next.highlight`,
},
];
}
/**
* Press the numpad in sequence based on the given space-separated keys.
* Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours.
*
* @param {String} keys space-separated numpad keys
*/
pressNumpad(keys) {
const numberChars = '. +/- 0 1 2 3 4 5 6 7 8 9'.split(' ');
const modeButtons = '+10 +20 +50'.split(' ');
function generateStep(key) {
let trigger;
if (numberChars.includes(key)) {
trigger = `.payment-numpad .number-char:contains("${key}")`;
} else if (modeButtons.includes(key)) {
trigger = `.payment-numpad .mode-button:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.payment-numpad .number-char img[alt="Backspace"]`;
}
return {
content: `'${key}' pressed in payment numpad`,
trigger,
};
}
return keys.split(' ').map(generateStep);
}
clickBack() {
return [
{
content: 'click back button',
trigger: '.payment-screen .button.back',
},
];
}
clickTipButton() {
return [
{
trigger: '.payment-screen .button.js_tip',
},
]
}
clickShipLaterButton() {
return [
{
content: 'click ship later button',
trigger: '.button:contains("Ship Later")',
},
]
}
}
class Check {
isShown() {
return [
{
content: 'payment screen is shown',
trigger: '.pos .payment-screen',
run: () => {},
},
];
}
/**
* Check if change is the provided amount.
* @param {String} amount
*/
changeIs(amount) {
return [
{
content: `change is ${amount}`,
trigger: `.payment-status-change .amount:contains("${amount}")`,
run: () => {},
},
];
}
/**
* Check if the remaining is the provided amount.
* @param {String} amount
*/
remainingIs(amount) {
return [
{
content: `remaining amount is ${amount}`,
trigger: `.payment-status-remaining .amount:contains("${amount}")`,
run: () => {},
},
];
}
/**
* Check if validate button is highlighted.
* @param {Boolean} isHighlighted
*/
validateButtonIsHighlighted(isHighlighted = true) {
return [
{
content: `validate button is ${
isHighlighted ? 'highlighted' : 'not highligted'
}`,
trigger: isHighlighted
? `.payment-screen .button.next.highlight`
: `.payment-screen .button.next:not(:has(.highlight))`,
run: () => {},
},
];
}
/**
* Check if the paymentlines are empty. Also provide the amount to pay.
* @param {String} amountToPay
*/
emptyPaymentlines(amountToPay) {
return [
{
content: `there are no paymentlines`,
trigger: `.paymentlines-empty`,
run: () => {},
},
{
content: `amount to pay is '${amountToPay}'`,
trigger: `.paymentlines-empty .total:contains("${amountToPay}")`,
run: () => {},
},
];
}
/**
* Check if the selected paymentline has the given payment method and amount.
* @param {String} paymentMethodName
* @param {String} amount
*/
selectedPaymentlineHas(paymentMethodName, amount) {
return [
{
content: `line paid via '${paymentMethodName}' is selected`,
trigger: `.paymentlines .paymentline.selected .payment-name:contains("${paymentMethodName}")`,
run: () => {},
},
{
content: `amount tendered in the line is '${amount}'`,
trigger: `.paymentlines .paymentline.selected .payment-amount:contains("${amount}")`,
run: () => {},
},
];
}
totalIs(amount) {
return [
{
content: `total is ${amount}`,
trigger: `.total:contains("${amount}")`,
run: () => {},
},
];
}
totalDueIs(amount) {
return [
{
content: `total due is ${amount}`,
trigger: `.payment-status-total-due:contains("${amount}")`,
run: () => {},
},
];
}
isInvoiceButtonChecked() {
return [
{
content: 'check invoice button is checked',
trigger: '.js_invoice.highlight',
run: () => {},
}
]
}
isInvoiceButtonNotChecked() {
return [
{
content: "check invoice button is checked",
trigger: ".js_invoice:not(.highlight)",
run: () => {},
},
];
}
}
class Execute {
pay(method, amount) {
const steps = [];
steps.push(...this._do.clickPaymentMethod(method));
for (let char of amount.split('')) {
steps.push(...this._do.pressNumpad(char));
}
steps.push(...this._check.validateButtonIsHighlighted());
steps.push(...this._do.clickValidate());
return steps;
}
}
return createTourMethods('PaymentScreen', Do, Check, Execute);
});

View file

@ -1,91 +0,0 @@
odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
pickRadio(name) {
return [
{
content: `picking radio attribute with name ${name}`,
trigger: `.product-configurator-popup .attribute-name-cell label:contains('${name}')`,
},
];
}
pickSelect(name) {
return [
{
content: `picking select attribute with name ${name}`,
trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`,
run: `text ${name}`,
},
];
}
pickColor(name) {
return [
{
content: `picking color attribute with name ${name}`,
trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`,
},
];
}
fillCustomAttribute(value) {
return [
{
content: `filling custom attribute with value ${value}`,
trigger: `.product-configurator-popup .custom_value`,
run: `text ${value}`,
},
];
}
confirmAttributes() {
return [
{
content: `confirming product configuration`,
trigger: `.product-configurator-popup .button.confirm`,
},
];
}
cancelAttributes() {
return [
{
content: `canceling product configuration`,
trigger: `.product-configurator-popup .button.cancel`,
},
];
}
}
class Check {
isShown() {
return [
{
content: 'product configurator is shown',
trigger: '.product-configurator-popup:not(:has(.oe_hidden))',
run: () => {},
},
];
}
numberRadioOptions(number) {
return [
{
trigger: `.product-configurator-popup .attribute-name-cell`,
run: () => {
const radio_options = $('.product-configurator-popup .attribute-name-cell').length;
if (radio_options !== number) {
throw new Error(`Expected ${number} radio options, got ${radio_options}`);
}
}
},
];
}
}
return createTourMethods('ProductConfigurator', Do, Check);
});

View file

@ -1,429 +0,0 @@
odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
const { TextAreaPopup } = require('point_of_sale.tour.TextAreaPopupTourMethods');
class Do {
clickDisplayedProduct(name) {
return [
{
content: `click product '${name}'`,
trigger: `.product-list .product-name:contains("${name}")`,
},
];
}
clickOrderline(name, quantity) {
return [
{
content: `selecting orderline with product '${name}' and quantity '${quantity}'`,
trigger: `.order .orderline:not(:has(.selected)) .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
},
{
content: `orderline with product '${name}' and quantity '${quantity}' has been selected`,
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
run: () => {},
},
];
}
clickSubcategory(name) {
return [
{
content: `selecting '${name}' subcategory`,
trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`,
},
{
content: `'${name}' subcategory selected`,
trigger: `.breadcrumbs .breadcrumb-button:contains("${name}")`,
run: () => {},
},
];
}
clickHomeCategory() {
return [
{
content: `click Home subcategory`,
trigger: `.breadcrumbs .breadcrumb-home`,
},
];
}
/**
* Press the numpad in sequence based on the given space-separated keys.
* NOTE: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours.
*
* @param {String} keys space-separated numpad keys
*/
pressNumpad(keys) {
const numberChars = '. 0 1 2 3 4 5 6 7 8 9'.split(' ');
const modeButtons = 'Qty Price Disc'.split(' ');
function generateStep(key) {
let trigger;
if (numberChars.includes(key)) {
trigger = `.numpad .number-char:contains("${key}")`;
} else if (modeButtons.includes(key)) {
trigger = `.numpad .mode-button:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.numpad .numpad-backspace`;
} else if (key === '+/-') {
trigger = `.numpad .numpad-minus`;
}
return {
content: `'${key}' pressed in product screen numpad`,
trigger,
};
}
return keys.split(' ').map(generateStep);
}
clickPayButton(shouldCheck = true) {
const steps = [{ content: 'click pay button', trigger: '.product-screen .actionpad .button.pay' }];
if (shouldCheck) {
steps.push({
content: 'now in payment screen',
trigger: '.pos-content .payment-screen',
run: () => {},
});
}
return steps;
}
clickPartnerButton() {
return [
{ content: 'click customer button', trigger: '.actionpad .button.set-partner' },
{
content: 'partner screen is shown',
trigger: '.pos-content .partnerlist-screen',
run: () => {},
},
];
}
clickCustomer(name) {
return [
{
content: `select customer '${name}'`,
trigger: `.partnerlist-screen .partner-line td:contains("${name}")`,
},
];
}
clickOrderlineCustomerNoteButton() {
return [
{
content: 'click customer note button',
trigger: '.control-buttons .control-button span:contains("Customer Note")',
}
]
}
clickRefund() {
return [
{
trigger: '.control-button:contains("Refund")',
},
];
}
confirmOpeningPopup() {
return [{ trigger: '.opening-cash-control .button:contains("Open session")' }];
}
clickPricelistButton() {
return [{ trigger: '.o_pricelist_button' }];
}
selectPriceList(name) {
return [
{
content: `select price list '${name}'`,
trigger: `.selection-item:contains("${name}")`,
},
];
}
enterOpeningAmount(amount) {
return [
{
content: 'enter opening amount',
trigger: '.cash-input-sub-section > .pos-input',
run: 'text ' + amount,
},
];
}
changeFiscalPosition(name) {
return [
{
content: 'click fiscal position button',
trigger: '.o_fiscal_position_button',
},
{
content: 'fiscal position screen is shown',
trigger: `.selection-item:contains("${name}")`,
},
];
}
scan_barcode(barcode) {
return [
{
content: `input barcode '${barcode}'`,
trigger: "input.ean",
run: `text ${barcode}`,
},
{
content: `button scan barcode '${barcode}'`,
trigger: "li.barcode",
run: 'click',
}
];
}
scan_ean13_barcode(barcode) {
return [
{
content: `input barcode '${barcode}'`,
trigger: "input.ean",
run: `text ${barcode}`,
},
{
content: `button scan EAN-13 barcode '${barcode}'`,
trigger: "li.custom_ean",
run: 'click',
}
];
}
clickLotIcon() {
return [
{
content: 'click lot icon',
trigger: '.line-lot-icon',
},
];
}
enterLotNumber(number) {
return [
{
content: 'enter lot number',
trigger: '.list-line-input:first()',
run: 'text ' + number,
},
{
content: 'click validate lot number',
trigger: '.popup .button.confirm',
}
];
}
}
class Check {
isShown() {
return [
{
content: 'product screen is shown',
trigger: '.product-screen',
run: () => {},
},
];
}
selectedOrderlineHas(name, quantity, price) {
const res = [
{
// check first if the order widget is there and has orderlines
content: 'order widget has orderlines',
trigger: '.order .orderlines',
run: () => {},
},
{
content: `'${name}' is selected`,
trigger: `.order .orderline.selected .product-name:contains("${name}")`,
run: function () {}, // it's a check
},
];
if (quantity) {
res.push({
content: `selected line has ${quantity} quantity`,
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
run: function () {}, // it's a check
});
}
if (price) {
res.push({
content: `selected line has total price of ${price}`,
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .price:contains("${price}")`,
run: function () {}, // it's a check
});
}
return res;
}
orderIsEmpty() {
return [
{
content: `order is empty`,
trigger: `.order .order-empty`,
run: () => {},
},
];
}
productIsDisplayed(name) {
return [
{
content: `'${name}' should be displayed`,
trigger: `.product-list .product-name:contains("${name}")`,
run: () => {},
},
];
}
totalAmountIs(amount) {
return [
{
content: `order total amount is '${amount}'`,
trigger: `.order-container .order .summary .value:contains("${amount}")`,
run: () => {},
}
]
}
modeIsActive(mode) {
return [
{
content: `'${mode}' is active`,
trigger: `.numpad button.selected-mode:contains('${mode}')`,
run: function () {},
},
];
}
orderlineHasCustomerNote(name, quantity, note) {
return [
{
content: `line has ${quantity} quantity`,
trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
run: function () {}, // it's a check
},
{
content: `line has '${note}' as customer note`,
trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`,
run: function () {}, // it's a check
},
]
}
checkSecondCashClosingDetailsLineAmount(amount, sign) {
return [
{
content: 'Click close session button',
trigger: '.fa-sign-out',
},
{
content: 'Check closing details',
trigger: `.cash-overview tr:nth-child(2) td:contains("${amount}")`,
run: () => {}, // it's a check
},
{
content: 'Check closing details',
trigger: `.cash-overview tr:nth-child(2) .cash-sign:contains("${sign}")`,
run: () => {}, // it's a check
},
];
}
noDiscountApplied(originalPrice) {
return [
{
content: 'no discount is applied',
trigger: `.info:not(:contains(${originalPrice}))`,
},
];
}
discountOriginalPriceIs(original_price) {
return [
{
content: `discount original price is shown`,
trigger: `s:contains('${original_price}')`,
run: function () {},
},
];
}
checkFirstLotNumber(number) {
return [
{
content: 'Check lot number',
trigger: `.list-line-input:propValue('${number}')`,
run: () => {}, // it's a check
},
];
}
checkOrderlinesNumber(number) {
return [
{
content: `check orderlines number`,
trigger: `.order .orderlines .orderline`,
run: () => {
const orderline_amount = $('.order .orderlines .orderline').length;
if (orderline_amount !== number) {
throw new Error(`Expected ${number} orderlines, got ${orderline_amount}`);
}
},
},
];
}
checkTaxAmount(number) {
return [
{
content: `check order tax amount`,
trigger: `.subentry:contains("${number}")`,
},
];
}
}
class Execute {
/**
* Create an orderline for the given `productName` and `quantity`.
* - If `unitPrice` is provided, price of the product of the created line
* is changed to that value.
* - If `expectedTotal` is provided, the created orderline (which is the currently
* selected orderline) is checked if it contains the correct quantity and total
* price.
*
* @param {string} productName
* @param {string} quantity
* @param {string} unitPrice
* @param {string} expectedTotal
*/
addOrderline(productName, quantity, unitPrice = undefined, expectedTotal = undefined) {
const res = this._do.clickDisplayedProduct(productName);
if (unitPrice) {
res.push(...this._do.pressNumpad('Price'));
res.push(...this._check.modeIsActive('Price'));
res.push(...this._do.pressNumpad(unitPrice.toString().split('').join(' ')));
res.push(...this._do.pressNumpad('Qty'));
res.push(...this._check.modeIsActive('Qty'));
}
for (let char of (quantity.toString() == '1' ? '' : quantity.toString())) {
if ('.0123456789'.includes(char)) {
res.push(...this._do.pressNumpad(char));
} else if ('-'.includes(char)) {
res.push(...this._do.pressNumpad('+/-'));
}
}
if (expectedTotal) {
res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal));
} else {
res.push(...this._check.selectedOrderlineHas(productName, quantity));
}
return res;
}
addMultiOrderlines(...list) {
const steps = [];
for (let [product, qty, price] of list) {
steps.push(...this.addOrderline(product, qty, price));
}
return steps;
}
addCustomerNote(note) {
const res = [];
res.push(...this._do.clickOrderlineCustomerNoteButton());
res.push(...TextAreaPopup._do.inputText(note));
res.push(...TextAreaPopup._do.clickConfirm());
return res;
}
}
return createTourMethods('ProductScreen', Do, Check, Execute);
});

View file

@ -1,127 +0,0 @@
odoo.define('point_of_sale.tour.ReceiptScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickNextOrder() {
return [
{
content: 'go to next screen',
trigger: '.receipt-screen .button.next.highlight',
},
];
}
setEmail(email) {
return [
{
trigger: '.receipt-screen .input-email input',
run: `text ${email}`,
},
];
}
clickSend(isHighlighted = true) {
return [
{
trigger: `.receipt-screen .input-email .send${isHighlighted ? '.highlight' : ''}`,
},
];
}
clickBack() {
return [
{
trigger: '.receipt-screen .button.back',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'receipt screen is shown',
trigger: '.pos .receipt-screen',
run: () => {},
},
];
}
receiptIsThere() {
return [
{
content: 'there should be the receipt',
trigger: '.receipt-screen .pos-receipt',
run: () => {},
},
];
}
totalAmountContains(value) {
return [
{
trigger: `.receipt-screen .top-content h1:contains("${value}")`,
run: () => {},
},
];
}
emailIsSuccessful() {
return [
{
trigger: `.receipt-screen .notice .successful`,
run: () => {},
},
];
}
customerNoteIsThere(note) {
return [
{
trigger: `.receipt-screen .orderlines .pos-receipt-left-padding:contains("${note}")`
}
]
}
discountAmountIs(value) {
return [
{
trigger: `.pos-receipt>div:contains("Discounts")>span:contains("${value}")`,
run: () => {},
},
];
}
noDiscountAmount() {
return [
{
trigger: `.pos-receipt:not(:contains("Discounts"))`,
run: () => {},
},
];
}
noOrderlineContainsDiscount() {
return [
{
trigger: `.orderlines:not(:contains('->'))`,
run: () => { },
},
];
}
trackingMethodIsLot() {
return [
{
content: `tracking method is Lot`,
trigger: `li:contains("Lot Number")`,
run: () => {},
},
];
}
}
class Execute {
nextOrder() {
return [...this._check.isShown(), ...this._do.clickNextOrder()];
}
}
return createTourMethods('ReceiptScreen', Do, Check, Execute);
});

View file

@ -1,39 +0,0 @@
odoo.define('point_of_sale.tour.SelectionPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickItem(name) {
return [
{
content: `click selection '${name}'`,
trigger: `.selection-item:contains("${name}")`,
},
];
}
}
class Check {
hasSelectionItem(name) {
return [
{
content: `selection popup has '${name}'`,
trigger: `.selection-item:contains("${name}")`,
run: () => {},
},
];
}
isShown() {
return [
{
content: 'selection popup is shown',
trigger: '.modal-dialog .popup-selection',
run: () => {},
},
];
}
}
return createTourMethods('SelectionPopup', Do, Check);
});

View file

@ -1,39 +0,0 @@
odoo.define('point_of_sale.tour.TextAreaPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
inputText(val) {
return [
{
content: `input text '${val}'`,
trigger: `.modal-dialog .popup-textarea textarea`,
run: `text ${val}`,
},
];
}
clickConfirm() {
return [
{
content: 'confirm text input popup',
trigger: '.modal-dialog .confirm',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'text input popup is shown',
trigger: '.modal-dialog .popup-textarea',
run: () => {},
},
];
}
}
return createTourMethods('TextAreaPopup', Do, Check);
});

View file

@ -1,39 +0,0 @@
odoo.define('point_of_sale.tour.TextInputPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
inputText(val) {
return [
{
content: `input text '${val}'`,
trigger: `.modal-dialog .popup-textinput input`,
run: `text ${val}`,
},
];
}
clickConfirm() {
return [
{
content: 'confirm text input popup',
trigger: '.modal-dialog .confirm',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'text input popup is shown',
trigger: '.modal-dialog .popup-textinput',
run: () => {},
},
];
}
}
return createTourMethods('TextInputPopup', Do, Check);
});

View file

@ -1,195 +0,0 @@
odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickNewTicket() {
return [{ trigger: '.ticket-screen .highlight' }];
}
clickDiscard() {
return [{ trigger: '.ticket-screen button.discard' }];
}
selectOrder(orderName) {
return [
{
trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}")`,
},
];
}
deleteOrder(orderName) {
return [
{
trigger: `.ticket-screen .orders > .order-row > .col:contains("${orderName}") ~ .col[name="delete"]`,
},
];
}
selectFilter(name) {
return [
{
trigger: `.pos-search-bar .filter`,
},
{
trigger: `.pos-search-bar .filter ul`,
run: () => {},
},
{
trigger: `.pos-search-bar .filter ul li:contains("${name}")`,
},
];
}
search(field, searchWord) {
return [
{
trigger: '.pos-search-bar input',
run: `text ${searchWord}`,
},
{
/**
* Manually trigger keyup event to show the search field list
* because the previous step do not trigger keyup event.
*/
trigger: '.pos-search-bar input',
run: function () {
document
.querySelector('.pos-search-bar input')
.dispatchEvent(new KeyboardEvent('keyup', { key: '' }));
},
},
{
trigger: `.pos-search-bar .search ul li:contains("${field}")`,
},
];
}
settleTips() {
return [
{
trigger: '.ticket-screen .buttons .settle-tips',
},
];
}
clickControlButton(name) {
return [
{
trigger: `.ticket-screen .control-button:contains("${name}")`,
},
];
}
clickOrderline(name) {
return [
{
trigger: `.ticket-screen .orderline:not(:has(.selected)) .product-name:contains("${name}")`,
},
{
trigger: `.ticket-screen .orderline.selected .product-name:contains("${name}")`,
run: () => {},
},
];
}
pressNumpad(key) {
let trigger;
if ('.0123456789'.includes(key)) {
trigger = `.numpad .number-char:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.numpad .numpad-backspace`;
} else if (key === '+/-') {
trigger = `.numpad .numpad-minus`;
}
return [
{
trigger,
},
];
}
confirmRefund() {
return [
{
trigger: '.ticket-screen .button.pay',
},
];
}
}
class Check {
checkStatus(orderName, status) {
return [
{
trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}") ~ .col:nth-child(6):contains(${status})`,
run: () => {},
},
];
}
/**
* Check if the nth row contains the given string.
* Note that 1st row is the header-row.
*/
nthRowContains(n, string) {
return [
{
trigger: `.ticket-screen .orders > .order-row:nth-child(${n}):contains("${string}")`,
run: () => {},
},
];
}
noNewTicketButton() {
return [
{
trigger: '.ticket-screen .controls .buttons:nth-child(1):has(.discard)',
run: () => {},
},
];
}
orderWidgetIsNotEmpty() {
return [
{
trigger: '.ticket-screen:not(:has(.order-empty))',
run: () => {},
},
];
}
filterIs(name) {
return [
{
trigger: `.ticket-screen .pos-search-bar .filter span:contains("${name}")`,
run: () => {},
},
];
}
partnerIs(name) {
return [
{
trigger: `.ticket-screen .set-partner:contains("${name}")`,
run: () => {},
},
];
}
toRefundTextContains(text) {
return [
{
trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`,
run: () => {},
},
];
}
refundedNoteContains(text) {
return [
{
trigger: `.ticket-screen .refund-note:contains("${text}")`,
run: () => {},
},
];
}
tipContains(amount) {
return [
{
trigger: `.ticket-screen .tip-cell:contains("${amount}")`,
run: () => {},
},
];
}
}
class Execute {}
return createTourMethods('TicketScreen', Do, Check, Execute);
});

View file

@ -1,153 +0,0 @@
odoo.define('point_of_sale.tour.utils', function (require) {
'use strict';
const config = require('web.config');
/**
* USAGE
* -----
*
* ```
* const { startSteps, getSteps, createTourMethods } = require('point_of_sale.utils');
* const { Other } = require('point_of_sale.tour.OtherMethods');
*
* // 1. Define classes Do, Check and Execute having methods that
* // each return array of tour steps.
* class Do {
* click() {
* return [{ content: 'click button', trigger: '.button' }];
* }
* }
* class Check {
* isHighligted() {
* return [{ content: 'button is highlighted', trigger: '.button.highlight', run: () => {} }];
* }
* }
* // Notice that Execute has access to methods defined in Do and Check classes
* // Also, we can compose steps from other module.
* class Execute {
* complexSteps() {
* return [...this._do.click(), ...this._check.isHighlighted(), ...Other._exec.complicatedSteps()];
* }
* }
*
* // 2. Instantiate these class definitions using `createTourMethods`.
* // The returned object gives access to the defined methods above
* // thru the do, check and exec properties.
* // - do gives access to the methods defined in Do class
* // - check gives access to the methods defined in Check class
* // - exec gives access to the methods defined in Execute class
* const Screen = createTourMethods('Screen', Do, Check, Execute);
*
* // 3. Call `startSteps` to start empty steps.
* startSteps();
*
* // 4. Call the tour methods to populate the steps created by `startSteps`.
* Screen.do.click(); // return of this method call is added to steps created by startSteps
* Screen.check.isHighlighted() // same as above
* Screen.exec.complexSteps() // same as above
*
* // 5. Call `getSteps` which returns the generated tour steps.
* const steps = getSteps();
* ```
*/
let steps = [];
function startSteps() {
// always start by waiting for loading to finish
steps = [
{
content: 'wait for loading to finish',
trigger: 'body:not(:has(.loader))',
run: function () {},
},
];
}
function getSteps() {
return steps;
}
// this is the method decorator
// when the method is called, the generated steps are added
// to steps
const methodProxyHandler = {
apply(target, thisArg, args) {
const res = target.call(thisArg, ...args);
if (config.isDebug()) {
// This step is added before the real steps.
// Very useful when debugging because we know which
// method call failed and what were the parameters.
const constructor = thisArg.constructor.name.split(' ')[1];
const methodName = target.name.split(' ')[1];
const argList = args
.map((a) => (typeof a === 'string' ? `'${a}'` : `${a}`))
.join(', ');
steps.push({
content: `DOING "${constructor}.${methodName}(${argList})"`,
trigger: '.pos',
run: () => {},
});
}
steps.push(...res);
return res;
},
};
// we proxy get of the method to decorate the method call
const proxyHandler = {
get(target, key) {
const method = target[key];
if (!method) {
throw new Error(`Tour method '${key}' is not available.`);
}
return new Proxy(method.bind(target), methodProxyHandler);
},
};
/**
* Creates an object with `do`, `check` and `exec` properties which are instances of
* the given `Do`, `Check` and `Execute` classes, respectively. Calling methods
* automatically adds the returned steps to the steps created by `startSteps`.
*
* There are however underscored version (_do, _check, _exec).
* Calling methods thru the underscored version does not automatically
* add the returned steps to the current steps array. Useful when composing
* steps from other methods.
*
* @param {String} name
* @param {Function} Do class containing methods which return array of tour steps
* @param {Function} Check similar to Do class but the steps are mainly for checking
* @param {Function} Execute class containing methods which return array of tour steps
* but has access to methods of Do and Check classes via .do and .check,
* respectively. Here, we define methods that return tour steps based
* on the combination of steps from Do and Check.
*/
function createTourMethods(name, Do, Check = class {}, Execute = class {}) {
Object.defineProperty(Do, 'name', { value: `${name}.do` });
Object.defineProperty(Check, 'name', { value: `${name}.check` });
Object.defineProperty(Execute, 'name', {
value: `${name}.exec`,
});
const methods = { do: new Do(), check: new Check(), exec: new Execute() };
// Allow Execute to have access to methods defined in Do and Check
// via do and exec, respectively.
methods.exec._do = methods.do;
methods.exec._check = methods.check;
return {
Do,
Check,
Execute,
[name]: {
do: new Proxy(methods.do, proxyHandler),
check: new Proxy(methods.check, proxyHandler),
exec: new Proxy(methods.exec, proxyHandler),
_do: methods.do,
_check: methods.check,
_exec: methods.exec,
},
};
}
return { startSteps, getSteps, createTourMethods };
});

View file

@ -1,408 +0,0 @@
/* global posmodel */
odoo.define('point_of_sale.tour.pricelist', function (require) {
"use strict";
var Tour = require('web_tour.tour');
var utils = require('web.utils');
var round_di = utils.round_decimals;
function assert (condition, message) {
if (! condition) {
throw message || "Assertion failed";
}
}
function assertProductPrice(product, pricelist_name, quantity, expected_price) {
return function () {
var pricelist = _.findWhere(posmodel.pricelists, {name: pricelist_name});
var frontend_price = product.get_price(pricelist, quantity);
frontend_price = round_di(frontend_price, posmodel.dp['Product Price']);
var diff = Math.abs( expected_price - frontend_price );
assert(diff < 0.001,
JSON.stringify({
product: product.id,
product_display_name: product.display_name,
pricelist_name: pricelist_name,
quantity: quantity
}) + ' DOESN\'T MATCH -> ' + expected_price + ' != ' + frontend_price);
return Promise.resolve();
};
}
// The global posmodel is only present when the posmodel is instanciated
// So, wait for everythiong to be loaded
var steps = [{ // Leave category displayed by default
content: 'waiting for loading to finish',
extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading
trigger: 'body:not(:has(.o_loading_indicator))', // WebClient has finished Loading
run: function () {
var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0];
var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0];
var product_magnetic_board = posmodel.db.search_product_in_category(0, 'Magnetic Board')[0];
var product_monitor_stand = posmodel.db.search_product_in_category(0, 'Monitor Stand')[0];
var product_desk_pad = posmodel.db.search_product_in_category(0, 'Desk Pad')[0];
var product_letter_tray = posmodel.db.search_product_in_category(0, 'Letter Tray')[0];
var product_whiteboard = posmodel.db.search_product_in_category(0, 'Whiteboard')[0];
assertProductPrice(product_letter_tray, 'Public Pricelist', 0, 4.8)()
.then(assertProductPrice(product_letter_tray, 'Public Pricelist', 1, 4.8))
.then(assertProductPrice(product_letter_tray, 'Fixed', 1, 1))
.then(assertProductPrice(product_wall_shelf, 'Fixed', 1, 2))
.then(assertProductPrice(product_small_shelf, 'Fixed', 1, 13.95))
.then(assertProductPrice(product_wall_shelf, 'Percentage', 1, 0))
.then(assertProductPrice(product_small_shelf, 'Percentage', 1, 0.03))
.then(assertProductPrice(product_magnetic_board, 'Percentage', 1, 1.98))
.then(assertProductPrice(product_wall_shelf, 'Formula', 1, 6.86))
.then(assertProductPrice(product_small_shelf, 'Formula', 1, 2.99))
.then(assertProductPrice(product_magnetic_board, 'Formula', 1, 11.98))
.then(assertProductPrice(product_monitor_stand, 'Formula', 1, 8.19))
.then(assertProductPrice(product_desk_pad, 'Formula', 1, 6.98))
.then(assertProductPrice(product_wall_shelf, 'min_quantity ordering', 1, 2))
.then(assertProductPrice(product_wall_shelf, 'min_quantity ordering', 2, 1))
.then(assertProductPrice(product_letter_tray, 'Category vs no category', 1, 2))
.then(assertProductPrice(product_letter_tray, 'Category', 1, 2))
.then(assertProductPrice(product_wall_shelf, 'Product template', 1, 1))
.then(assertProductPrice(product_wall_shelf, 'Dates', 1, 2))
.then(assertProductPrice(product_small_shelf, 'Pricelist base rounding', 1, 13.95))
.then(assertProductPrice(product_whiteboard, 'Public Pricelist', 1, 3.2))
.then(function () {
$('.pos').addClass('done-testing');
});
},
}, {
trigger: '.opening-cash-control .button:contains("Open session")',
}];
steps = steps.concat([{
content: "wait for unit tests to finish",
trigger: ".pos.done-testing",
run: function () {}, // it's a check
}, {
content: "click category switch",
trigger: ".breadcrumb-home",
run: 'click',
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "verify default pricelist is set",
trigger: ".selection-item.selected:contains('Public Pricelist')",
run: function () {}, // it's a check
}, {
content: "select fixed pricelist",
trigger: ".selection-item:contains('Fixed')",
}, {
content: "open partner list",
trigger: "button.set-partner",
}, {
content: "select Deco Addict",
trigger: ".partner-line:contains('Deco Addict')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "verify pricelist changed",
trigger: ".selection-item.selected:contains('Public Pricelist')",
run: function () {}, // it's a check
}, {
content: "cancel pricelist dialog",
trigger: ".button.cancel:visible",
}, {
content: "open customer list",
trigger: "button.set-partner",
}, {
content: "select Lumber Inc",
trigger: ".partner-line:contains('Lumber Inc')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "verify pricelist remained public pricelist ('Not loaded' is not available)",
trigger: ".selection-item.selected:contains('Public Pricelist')",
run: function () {}, // it's a check
}, {
content: "cancel pricelist dialog",
trigger: ".button.cancel:visible",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select fixed pricelist",
trigger: ".selection-item:contains('min_quantity ordering')",
}, {
content: "order 1 kg shelf",
trigger: ".product:contains('Wall Shelf')",
}, {
content: "change qty to 2 kg",
trigger: ".numpad button.input-button:visible:contains('2')",
}, {
content: "qty of Wall Shelf line should be 2",
trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')",
extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('2.0')",
run: function() {},
}, {
content: "verify that unit price of shelf changed to $1",
trigger: ".total > .value:contains('$ 2.00')",
run: function() {},
}, {
content: "order different shelf",
trigger: ".product:contains('Small Shelf')",
}, {
content: "Small Shelf line should be selected with quantity 1",
trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')",
extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .info-list .info em:contains('1.0')",
run: function() {}
}, {
content: "change to price mode",
trigger: ".numpad button:contains('Price')",
}, {
content: "make sure price mode is activated",
trigger: ".numpad button.selected-mode:contains('Price')",
run: function() {},
}, {
content: "manually override the unit price of these shelf to $5",
trigger: ".numpad button.input-button:visible:contains('5')",
}, {
content: "Small Shelf line should be selected with unit price of 5",
trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')",
extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .price:contains('5.0')",
}, {
content: "change back to qty mode",
trigger: ".numpad button:contains('Qty')",
}, {
content: "make sure qty mode is activated",
trigger: ".numpad button.selected-mode:contains('Qty')",
run: function() {},
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select public pricelist",
trigger: ".selection-item:contains('Public Pricelist')",
}, {
content: "verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)",
trigger: ".total > .value:contains('$ 8.96')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select fixed pricelist",
trigger: ".selection-item:contains('min_quantity ordering')",
}, {
content: "close the Point of Sale frontend",
trigger: ".header-button",
}, {
content: "confirm closing the frontend",
trigger: ".header-button",
run: function() {}, //it's a check,
}]);
Tour.register('pos_pricelist', { test: true, url: '/pos/ui' }, steps);
});
odoo.define('point_of_sale.tour.acceptance', function (require) {
"use strict";
var Tour = require("web_tour.tour");
function add_product_to_order(product_name) {
return [{
content: 'buy ' + product_name,
trigger: '.product-list .product-name:contains("' + product_name + '")',
}, {
content: 'the ' + product_name + ' have been added to the order',
trigger: '.order .product-name:contains("' + product_name + '")',
run: function () {},
}];
}
function set_fiscal_position_on_order(fp_name) {
return [{
content: 'set fiscal position',
trigger: '.control-button.o_fiscal_position_button',
}, {
content: 'choose fiscal position ' + fp_name + ' to add to the order',
trigger: '.popups .popup .selection .selection-item:contains("' + fp_name + '")',
}, {
content: 'the fiscal position ' + fp_name + ' has been set to the order',
trigger: '.control-button.o_fiscal_position_button:contains("' + fp_name + '")',
run: function () {},
}];
}
function press_payment_numpad(val) {
return [{
content: `press ${val} on payment screen numpad`,
trigger: `.payment-numpad .input-button:contains("${val}"):visible`,
}]
}
function press_product_numpad(val) {
return [{
content: `press ${val} on product screen numpad`,
trigger: `.numpad .input-button:contains("${val}"):visible`,
}]
}
function selected_payment_has(name, val) {
return [{
content: `selected payment is ${name} and has ${val}`,
trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}")`,
extra_trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}") ~ .payment-amount:contains("${val}")`,
run: function () {},
}]
}
function selected_orderline_has({ product, price = null, quantity = null }) {
const result = [];
if (price !== null) {
result.push({
content: `Selected line has product '${product}' and price '${price}'`,
trigger: `.order-container .orderlines .orderline.selected .product-name:contains("${product}") ~ span.price:contains("${price}")`,
run: function () {},
});
}
if (quantity !== null) {
result.push({
content: `Selected line has product '${product}' and quantity '${quantity}'`,
trigger: `.order-container .orderlines .orderline.selected .product-name:contains('${product}') ~ .info-list .info em:contains('${quantity}')`,
run: function () {},
});
}
return result;
}
function verify_order_total(total_str) {
return [{
content: 'order total contains ' + total_str,
trigger: '.order .total .value:contains("' + total_str + '")',
run: function () {}, // it's a check
}];
}
function goto_payment_screen_and_select_payment_method() {
return [{
content: "go to payment screen",
trigger: '.button.pay',
}, {
content: "pay with cash",
trigger: '.paymentmethod:contains("Cash")',
}];
}
function finish_order() {
return [{
content: "validate the order",
trigger: '.payment-screen .button.next.highlight:visible',
}, {
content: "verify that the order has been successfully sent to the backend",
trigger: ".js_connected:visible",
run: function () {},
}, {
content: "click Next Order",
trigger: '.receipt-screen .button.next.highlight:visible',
}, {
content: "check if we left the receipt screen",
trigger: '.pos-content .screen:not(:has(.receipt-screen))',
run: function () {},
}];
}
var steps = [{
content: 'waiting for loading to finish',
trigger: 'body:not(:has(.loader))',
run: function () {},
}, { // Leave category displayed by default
content: "click category switch",
trigger: ".breadcrumb-home",
}];
steps = steps.concat(add_product_to_order('Desk Organizer'));
steps = steps.concat(verify_order_total('5.10'));
steps = steps.concat(add_product_to_order('Desk Organizer'));
steps = steps.concat(verify_order_total('10.20'));
steps = steps.concat(goto_payment_screen_and_select_payment_method());
/* add payment line of only 5.20
status:
order-total := 10.20
total-payment := 11.70
expect:
remaining := 0.00
change := 1.50
*/
steps = steps.concat(press_payment_numpad('5'));
steps = steps.concat(selected_payment_has('Cash', '5.0'));
steps = steps.concat([{
content: "verify remaining",
trigger: '.payment-status-remaining .amount:contains("5.20")',
run: function () {},
}, {
content: "verify change",
trigger: '.payment-status-change .amount:contains("0.00")',
run: function () {},
}]);
/* make additional payment line of 6.50
status:
order-total := 10.20
total-payment := 11.70
expect:
remaining := 0.00
change := 1.50
*/
steps = steps.concat([{
content: "pay with cash",
trigger: '.paymentmethod:contains("Cash")',
}]);
steps = steps.concat(selected_payment_has('Cash', '5.2'));
steps = steps.concat(press_payment_numpad('6'))
steps = steps.concat(selected_payment_has('Cash', '6.0'));
steps = steps.concat([{
content: "verify remaining",
trigger: '.payment-status-remaining .amount:contains("0.00")',
run: function () {},
}, {
content: "verify change",
trigger: '.payment-status-change .amount:contains("0.80")',
run: function () {},
}]);
steps = steps.concat(finish_order());
// test opw-672118 orderline subtotal rounding
steps = steps.concat(add_product_to_order('Desk Organizer'));
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '1.0'}));
steps = steps.concat(press_product_numpad('.'))
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.0', price: '0.0'}));
steps = steps.concat(press_product_numpad('9'))
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.9', price: '4.59'}));
steps = steps.concat(press_product_numpad('9'))
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.99', price: '5.05'}));
steps = steps.concat(goto_payment_screen_and_select_payment_method());
steps = steps.concat(selected_payment_has('Cash', '5.05'));
steps = steps.concat(finish_order());
// Test fiscal position one2many map (align with backend)
steps = steps.concat(add_product_to_order('Letter Tray'));
steps = steps.concat(selected_orderline_has({product: 'Letter Tray', quantity: '1.0'}));
steps = steps.concat(verify_order_total('5.28'));
steps = steps.concat(set_fiscal_position_on_order('FP-POS-2M'));
steps = steps.concat(verify_order_total('5.52'));
steps = steps.concat([{
content: "open closing the Point of Sale frontend popup",
trigger: ".header-button",
}, {
content: "close the Point of Sale frontend",
trigger: ".close-pos-popup .button:contains('Discard')",
run: function() {}, //it's a check,
}]);
Tour.register('pos_basic_order', { test: true, url: '/pos/ui' }, steps);
});

View file

@ -0,0 +1,46 @@
import { test, expect } from "@odoo/hoot";
import { expectFormattedPrice, setupPosEnv } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { getFilledOrderForPriceCheck } from "./utils";
definePosModels();
test("Taxes object should contain no discount values", async () => {
const store = await setupPosEnv();
const order = await getFilledOrderForPriceCheck(store);
order.lines[0].setDiscount(10);
order.lines[1].setDiscount(20);
const details = order.prices.taxDetails;
const line1 = order.lines[0].prices;
const line2 = order.lines[1].prices;
// Order prices
expect(details.base_amount).toBe(980); // Base amount is 980 = (1000 - 10%) + (100 - 20%)
expect(details.tax_amount).toBe(257); // Tax amount is 257 = (250 - 10%) + (15 - 20%) + (25 - 20%)
expect(details.total_amount).toBe(1237); // Total amount is 1237 = 980 + 257
// Formatted prices
expectFormattedPrice(order.currencyDisplayPrice, "$ 1,237.00");
expectFormattedPrice(order.currencyAmountTaxes, "$ 257.00");
expectFormattedPrice(order.lines[0].currencyDisplayPrice, "$ 1,125.00");
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnit, "$ 1,125.00");
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnitExcl, "$ 900.00");
expectFormattedPrice(order.lines[1].currencyDisplayPrice, "$ 112.00");
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnit, "$ 112.00");
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnitExcl, "$ 80.00");
// First line (25% on 1000) - no discount
expect(line1.no_discount_total_included).toBe(1250);
expect(line1.no_discount_total_excluded).toBe(1000);
expect(line1.no_discount_taxes_data[0].tax_amount).toBe(250);
expect(line1.no_discount_taxes_data[0].tax.amount).toBe(25);
// Second line (15% + 25% on 100) - no discount
expect(line2.no_discount_total_included).toBe(140);
expect(line2.no_discount_total_excluded).toBe(100);
expect(line2.no_discount_taxes_data[0].tax_amount).toBe(15);
expect(line2.no_discount_taxes_data[0].tax.amount).toBe(15);
expect(line2.no_discount_taxes_data[1].tax_amount).toBe(25);
expect(line2.no_discount_taxes_data[1].tax.amount).toBe(25);
});

View file

@ -0,0 +1,363 @@
/**
* This file contains old tour tests related to accounting that were migrated to Hoot.
* These tours were not checking anything on the Python side, so they were simply
* converted to Hoot tests without any additional checks.
*/
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { prepareRoundingVals } from "./utils";
definePosModels();
test("[Old Tour] pos_basic_order_01_multi_payment_and_change", async () => {
const store = await setupPosEnv();
const product1 = store.models["product.template"].get(15);
product1.list_price = 5.1;
product1.product_variant_ids[0].lst_price = 5.1;
product1.taxes_id = [];
const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count);
const cardPm = store.models["pos.payment.method"].find((pm) => !pm.is_cash_count);
const order = store.addNewOrder();
order.pricelist_id = false;
// Add products
await store.addLineToOrder({ product_tmpl_id: product1, qty: 2 }, order);
order.addPaymentline(cashPm);
order.payment_ids[0].setAmount(5);
expect(order.remainingDue).toBe(5.2);
order.addPaymentline(cardPm);
order.payment_ids[1].setAmount(6);
expect(order.change).toBe(-0.8);
});
test("[Old Tour] PaymentScreenRoundingHalfUp", async () => {
const store = await setupPosEnv();
const product1_2 = store.models["product.template"].get(12);
const product1_25 = store.models["product.template"].get(13);
const product1_40 = store.models["product.template"].get(14);
const { cashPm } = prepareRoundingVals(store, 0.5, "HALF-UP", true);
product1_2.list_price = 1.2;
product1_2.product_variant_ids[0].lst_price = 1.2;
product1_2.taxes_id = [];
product1_25.list_price = 1.25;
product1_25.product_variant_ids[0].lst_price = 1.25;
product1_25.taxes_id = [];
product1_40.list_price = 1.4;
product1_40.product_variant_ids[0].lst_price = 1.4;
product1_40.taxes_id = [];
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product1_2, qty: 1 }, order);
expect(order.totalDue).toBe(1.2);
order.addPaymentline(cashPm);
expect(order.amountPaid).toBe(1.0);
expect(order.appliedRounding).toBe(-0.2);
expect(order.change).toBe(0.0);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product1_25, qty: 1 }, order2);
expect(order2.totalDue).toBe(1.25);
order2.addPaymentline(cashPm);
expect(order2.amountPaid).toBe(1.5);
expect(order2.appliedRounding).toBe(0.25);
expect(order2.change).toBe(0.0);
const order3 = store.addNewOrder();
order3.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product1_40, qty: 1 }, order3);
expect(order3.totalDue).toBe(1.4);
order3.addPaymentline(cashPm);
expect(order3.amountPaid).toBe(1.5);
expect(order3.appliedRounding).toBe(0.1);
expect(order3.change).toBe(0.0);
const order4 = store.addNewOrder();
order4.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product1_2, qty: 1 }, order4);
expect(order4.totalDue).toBe(1.2);
order4.addPaymentline(cashPm);
order4.payment_ids[0].setAmount(2);
expect(order4.amountPaid).toBe(2.0);
expect(order4.change).toBe(-1.0);
});
const prepareProduct = (store) => {
const product = store.models["product.template"].get(15);
const tax15 = store.models["account.tax"].get(1);
product.list_price = 13.67;
product.product_variant_ids[0].lst_price = 13.67;
product.taxes_id = [tax15];
return product;
};
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_not_only_round_cash_method", async () => {
const store = await setupPosEnv();
const { cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.7);
order.addPaymentline(cardPm);
expect(order.amountPaid).toBe(15.7);
expect(order.appliedRounding).toBe(-0.02);
expect(order.change).toBe(0.0);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.7);
order2.addPaymentline(cardPm);
expect(order2.amountPaid).toBe(-15.7);
expect(order2.appliedRounding).toBe(0.02);
expect(order2.change).toBe(0.0);
});
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_not_only_round_cash_method_pay_by_bank_and_cash", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.7);
order.addPaymentline(cardPm);
order.payment_ids[0].setAmount(0.68);
expect(order.amountPaid).toBe(0.68);
expect(order.remainingDue).toBe(15.02); // Order is rounded globaly so remaining due is rounded
order.addPaymentline(cashPm);
expect(order.payment_ids[1].amount).toBe(15.02);
expect(order.amountPaid).toBe(15.7);
expect(order.appliedRounding).toBe(-0.02);
expect(order.change).toBe(0.0);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order2.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.7);
order2.addPaymentline(cardPm);
order2.payment_ids[0].setAmount(-0.68);
expect(order2.amountPaid).toBe(-0.68);
expect(order2.remainingDue).toBe(-15.02); // Order is rounded globaly so remaining due is rounded
order2.addPaymentline(cashPm);
expect(order2.payment_ids[1].amount).toBe(-15.02);
expect(order2.amountPaid).toBe(-15.7);
expect(order2.appliedRounding).toBe(0.02);
expect(order2.change).toBe(0.0);
});
test("[Old Tour] test_cash_rounding_down_add_invoice_line_not_only_round_cash_method_no_rounding_left", async () => {
const store = await setupPosEnv();
const { cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.7);
order.addPaymentline(cardPm);
order.payment_ids[0].setAmount(0.67);
expect(order.amountPaid).toBe(0.67);
expect(order.remainingDue).toBe(15.03);
order.addPaymentline(cardPm);
expect(order.payment_ids[1].amount).toBe(15.03);
expect(order.amountPaid).toBe(15.7);
expect(order.appliedRounding).toBe(-0.02);
expect(order.change).toBe(0.0);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order2.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.7);
order2.addPaymentline(cardPm);
order2.payment_ids[0].setAmount(-0.67);
expect(order2.amountPaid).toBe(-0.67);
expect(order2.remainingDue).toBe(-15.03);
order2.addPaymentline(cardPm);
expect(order2.payment_ids[1].amount).toBe(-15.03);
expect(order2.amountPaid).toBe(-15.7);
expect(order2.appliedRounding).toBe(0.02);
expect(order2.change).toBe(0.0);
});
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_only_round_cash_method", async () => {
const store = await setupPosEnv();
const { cashPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.72);
order.addPaymentline(cashPm);
expect(order.amountPaid).toBe(15.7);
expect(order.appliedRounding).toBe(-0.02);
expect(order.change).toBe(0.0);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order2.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.72);
order2.addPaymentline(cashPm);
expect(order2.amountPaid).toBe(-15.7);
expect(order2.appliedRounding).toBe(0.02);
expect(order2.change).toBe(0.0);
});
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_only_round_cash_method_pay_by_bank_and_cash", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.72);
order.addPaymentline(cardPm);
order.payment_ids[0].setAmount(0.68);
expect(order.amountPaid).toBe(0.68);
expect(order.remainingDue).toBe(15.04);
order.addPaymentline(cashPm);
expect(order.payment_ids[1].amount).toBe(15.05);
expect(order.amountPaid).toBe(15.73);
expect(order.appliedRounding).toBe(0.01);
expect(order.change).toBe(0.0);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order2.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.72);
order2.addPaymentline(cardPm);
order2.payment_ids[0].setAmount(-0.68);
expect(order2.amountPaid).toBe(-0.68);
expect(order2.remainingDue).toBe(-15.04);
order2.addPaymentline(cashPm);
expect(order2.payment_ids[1].amount).toBe(-15.05);
expect(order2.amountPaid).toBe(-15.73);
expect(order2.appliedRounding).toBe(-0.01);
expect(order2.change).toBe(0.0);
});
test("[Old Tour] test_cash_rounding_with_change", async () => {
const store = await setupPosEnv();
const { cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.7);
order.addPaymentline(cardPm);
order.payment_ids[0].setAmount(20);
expect(order.amountPaid).toBe(20);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(-4.3);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order2.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.7);
order2.addPaymentline(cardPm);
order2.payment_ids[0].setAmount(-20);
expect(order2.amountPaid).toBe(-20);
expect(order2.appliedRounding).toBe(0);
expect(order2.change).toBe(4.3);
});
test("[Old Tour] test_cash_rounding_only_cash_method_with_change", async () => {
const store = await setupPosEnv();
const { cashPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
const product = prepareProduct(store);
const order = store.addNewOrder();
order.pricelist_id = false;
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
expect(order.displayPrice).toBe(15.72);
expect(order.priceExcl).toBe(13.67);
expect(order.totalDue).toBe(15.72);
order.addPaymentline(cashPm);
order.payment_ids[0].setAmount(20);
expect(order.amountPaid).toBe(20);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(-4.3);
const order2 = store.addNewOrder();
order2.pricelist_id = false;
order2.is_refund = true;
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
expect(order2.displayPrice).toBe(-15.72);
expect(order2.priceExcl).toBe(-13.67);
expect(order2.totalDue).toBe(-15.72);
order2.addPaymentline(cashPm);
order2.payment_ids[0].setAmount(-20);
expect(order2.amountPaid).toBe(-20);
expect(order2.appliedRounding).toBe(0);
expect(order2.change).toBe(4.3);
});
test(["[Old Tour] test_cash_rounding_up_with_change"], async () => {
const store = await setupPosEnv();
const { cashPm } = prepareRoundingVals(store, 1, "UP", true);
const order = store.addNewOrder();
order.pricelist_id = false;
const tax = store.models["account.tax"].get(3);
const productA = store.models["product.template"].get(15);
const productB = store.models["product.template"].get(16);
productA.list_price = 95;
productA.product_variant_ids[0].lst_price = 95;
productA.taxes_id = [tax];
productB.list_price = 42;
productB.product_variant_ids[0].lst_price = 42;
productB.taxes_id = [tax];
await store.addLineToOrder({ product_tmpl_id: productA, qty: 1 }, order);
await store.addLineToOrder({ product_tmpl_id: productB, qty: 2 }, order);
expect(order.displayPrice).toBe(179);
expect(order.totalDue).toBe(179);
order.addPaymentline(cashPm);
order.payment_ids[0].setAmount(200);
expect(order.amountPaid).toBe(200);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(-21);
});

View file

@ -0,0 +1,82 @@
import { test, expect } from "@odoo/hoot";
import { expectFormattedPrice, setupPosEnv } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { getFilledOrderForPriceCheck } from "./utils";
definePosModels();
test("Prices includes", async () => {
const store = await setupPosEnv();
const order = await getFilledOrderForPriceCheck(store);
const details = order.prices.taxDetails;
const line1 = order.lines[0].prices;
const line2 = order.lines[1].prices;
// Order prices
expect(details.base_amount).toBe(1100);
expect(details.tax_amount).toBe(290);
expect(details.total_amount).toBe(1390);
// First line (25% on 1000)
expect(line1.total_included).toBe(1250);
expect(line1.total_excluded).toBe(1000);
expect(line1.taxes_data[0].tax_amount).toBe(250);
expect(line1.taxes_data[0].tax.amount).toBe(25);
// Second line (15% + 25% on 100)
expect(line2.total_included).toBe(140);
expect(line2.total_excluded).toBe(100);
expect(line2.taxes_data[0].tax_amount).toBe(15);
expect(line2.taxes_data[0].tax.amount).toBe(15);
expect(line2.taxes_data[1].tax_amount).toBe(25);
expect(line2.taxes_data[1].tax.amount).toBe(25);
// Formatted prices
expectFormattedPrice(order.currencyDisplayPrice, "$ 1,390.00");
expectFormattedPrice(order.currencyAmountTaxes, "$ 290.00");
expectFormattedPrice(order.lines[0].currencyDisplayPrice, "$ 1,250.00");
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnit, "$ 1,250.00");
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnitExcl, "$ 1,000.00");
expectFormattedPrice(order.lines[1].currencyDisplayPrice, "$ 140.00");
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnit, "$ 140.00");
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnitExcl, "$ 100.00");
});
test("Prices excludes", async () => {
const store = await setupPosEnv();
store.config.iface_tax_included = "subtotal";
const order = await getFilledOrderForPriceCheck(store);
// Formatted prices
expectFormattedPrice(order.currencyDisplayPrice, "$ 1,100.00");
expectFormattedPrice(order.lines[0].currencyDisplayPrice, "$ 1,000.00");
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnit, "$ 1,000.00");
expectFormattedPrice(order.lines[1].currencyDisplayPrice, "$ 100.00");
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnit, "$ 100.00");
});
test("Combo prices incl and excl", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const template = store.models["product.template"].get(7);
const comboProduct = store.models["product.combo.item"].get(1);
await store.addLineToOrder(
{
product_tmpl_id: template,
payload: [[{ combo_item_id: comboProduct, qty: 1 }]],
qty: 1,
},
order
);
order.setOrderPrices();
const [comboParentLine, comboChildLine] = order.lines;
expect(comboParentLine.comboTotalPrice).toBe(3.75);
expect(comboParentLine.comboTotalPriceWithoutTax).toBe(3);
expect(comboChildLine.comboTotalPrice).toBe(3.75);
expect(comboChildLine.comboTotalPriceWithoutTax).toBe(3);
});

View file

@ -0,0 +1,150 @@
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
definePosModels();
test("Pricelist: Precedence Rules (Variant > Template > Category > Global)", async () => {
const store = await setupPosEnv();
const pricelist = store.models["product.pricelist"].create({
name: "Test Pricelist",
});
const category = store.models["product.category"].create({ name: "Test Category" });
const productTemplate = store.models["product.template"].create({
name: "Test Template",
list_price: 100,
categ_id: category,
});
const product = store.models["product.product"].create({
product_tmpl_id: productTemplate,
lst_price: 100,
});
// 1. Global Rule
const globalRule = store.models["product.pricelist.item"].create({
pricelist_id: pricelist,
compute_price: "fixed",
fixed_price: 90,
});
pricelist.update({ item_ids: [globalRule] });
pricelist.computeRuleIndexes();
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(90);
// 2. Category Rule (should win over Global)
const categoryRule = store.models["product.pricelist.item"].create({
pricelist_id: pricelist,
categ_id: category,
compute_price: "fixed",
fixed_price: 80,
});
pricelist.update({ item_ids: [globalRule, categoryRule] });
pricelist.computeRuleIndexes();
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(80);
// 3. Template Rule (should win over Category)
const templateRule = store.models["product.pricelist.item"].create({
pricelist_id: pricelist,
product_tmpl_id: productTemplate,
compute_price: "fixed",
fixed_price: 70,
});
pricelist.update({ item_ids: [globalRule, categoryRule, templateRule] });
pricelist.computeRuleIndexes();
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(70);
// 4. Variant Rule (should win over Template)
const variantRule = store.models["product.pricelist.item"].create({
pricelist_id: pricelist,
product_id: product,
compute_price: "fixed",
fixed_price: 60,
});
pricelist.update({ item_ids: [globalRule, categoryRule, templateRule, variantRule] });
pricelist.computeRuleIndexes();
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(60);
});
test("Pricelist: Min Quantity logic", async () => {
const store = await setupPosEnv();
const pricelist = store.models["product.pricelist"].create({
name: "Qty Pricelist",
});
const productTemplate = store.models["product.template"].create({
name: "Qty Product",
list_price: 100,
});
const product = store.models["product.product"].create({
product_tmpl_id: productTemplate,
lst_price: 100,
});
const ruleSmall = store.models["product.pricelist.item"].create({
pricelist_id: pricelist,
product_id: product,
compute_price: "fixed",
fixed_price: 50,
min_quantity: 0,
});
const ruleLarge = store.models["product.pricelist.item"].create({
pricelist_id: pricelist,
product_id: product,
compute_price: "fixed",
fixed_price: 20,
min_quantity: 10,
});
pricelist.update({ item_ids: [ruleSmall, ruleLarge] });
pricelist.computeRuleIndexes();
// Qty 1 -> should use ruleSmall (50)
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(50);
// Qty 10 -> should use ruleLarge (20)
expect(product.getPrice(pricelist, 10, 0, false, product)).toBe(20);
// Qty 15 -> should use ruleLarge (20)
expect(product.getPrice(pricelist, 15, 0, false, product)).toBe(20);
});
test("Pricelist: Nested Pricelists (Pricelist of Pricelist)", async () => {
const store = await setupPosEnv();
// Base Pricelist: -10% discount
const basePricelist = store.models["product.pricelist"].create({
name: "Base Pricelist",
});
const baseRule = store.models["product.pricelist.item"].create({
pricelist_id: basePricelist,
compute_price: "percentage",
percent_price: 10,
base: "list_price",
});
basePricelist.update({ item_ids: [baseRule] });
basePricelist.computeRuleIndexes();
// Nested Pricelist: Base + another -5$ surcharge
const nestedPricelist = store.models["product.pricelist"].create({
name: "Nested Pricelist",
});
const nestedRule = store.models["product.pricelist.item"].create({
pricelist_id: nestedPricelist,
compute_price: "formula", // formula to use base + surcharge
base: "pricelist",
base_pricelist_id: basePricelist,
price_surcharge: 5,
});
nestedPricelist.update({ item_ids: [nestedRule] });
nestedPricelist.computeRuleIndexes();
const productTemplate = store.models["product.template"].create({
name: "Nested Test Product",
list_price: 100,
});
const product = store.models["product.product"].create({
product_tmpl_id: productTemplate,
lst_price: 100,
});
// Calculation: (100 - 10%) + 5 = 90 + 5 = 95
expect(product.getPrice(nestedPricelist, 1, 0, false, product)).toBe(95);
});

View file

@ -0,0 +1,376 @@
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { getFilledOrderForPriceCheck, prepareRoundingVals } from "./utils";
definePosModels();
test("Rounding sale HALF-UP 0.05 (cash only)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(52.54);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(52.55);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.01);
expect(order.change).toBe(0);
});
test("Rounding sale HALF-UP 0.05 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(52.55);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.01);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(52.55);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.01);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
order.addPaymentline(cashPm);
order.payment_ids[0].setAmount(52.5);
expect(order.payment_ids[0].amount).toBe(52.5);
expect(order.appliedRounding).toBe(0);
expect(order.remainingDue).toBe(0.05);
expect(order.canBeValidated()).toBe(false);
});
test("Rounding sale UP 10 (cash only)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", true);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(52.54);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(60);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(7.46);
expect(order.change).toBe(0);
});
test("Rounding sale UP 10 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", false);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(60);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(7.46);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(60);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(7.46);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(60);
order.payment_ids[0].setAmount(70);
expect(order.payment_ids[0].amount).toBe(70);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(-10);
});
test("Rounding sale DOWN 10 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "DOWN", false);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(50);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-2.54);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(50);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-2.54);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(50);
order.payment_ids[0].setAmount(70);
expect(order.payment_ids[0].amount).toBe(70);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(-20);
});
test("Rounding sale DOWN 1 (cash only)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", true);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(52.54);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(52);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-0.54);
expect(order.change).toBe(0);
});
test("Rounding sale DOWN 1 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", false);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(52);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-0.54);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(52);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-0.54);
expect(order.change).toBe(0);
});
test("Rounding refund HALF-UP 0.05 (cash only)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
const order = await getFilledOrderForPriceCheck(store);
order.is_refund = true;
order.lines.map((line) => line.setQuantity(-line.qty));
expect(order.displayPrice).toBe(-52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(-52.54);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(-52.55);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-0.01);
expect(order.change).toBe(0);
});
test("Rounding refund HALF-UP 0.05 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const order = await getFilledOrderForPriceCheck(store);
order.is_refund = true;
order.lines.map((line) => line.setQuantity(-line.qty));
expect(order.displayPrice).toBe(-52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(-52.55);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-0.01);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(-52.55);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-0.01);
expect(order.change).toBe(0);
});
test("Rounding refund UP 10 (cash only)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", true);
const order = await getFilledOrderForPriceCheck(store);
order.is_refund = true;
order.lines.map((line) => line.setQuantity(-line.qty));
expect(order.displayPrice).toBe(-52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(-52.54);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(-60);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-7.46);
expect(order.change).toBe(0);
});
test("Rounding refund UP 10 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", false);
const order = await getFilledOrderForPriceCheck(store);
order.is_refund = true;
order.lines.map((line) => line.setQuantity(-line.qty));
expect(order.displayPrice).toBe(-52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(-60);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-7.46);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(-60);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(-7.46);
expect(order.change).toBe(0);
});
test("Rounding refund DOWN 1 (cash only)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", true);
const order = await getFilledOrderForPriceCheck(store);
order.is_refund = true;
order.lines.map((line) => line.setQuantity(-line.qty));
expect(order.displayPrice).toBe(-52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(-52.54);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(-52);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.54);
expect(order.change).toBe(0);
});
test("Rounding refund DOWN 1 (all methods)", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", false);
const order = await getFilledOrderForPriceCheck(store);
order.is_refund = true;
order.lines.map((line) => line.setQuantity(-line.qty));
expect(order.displayPrice).toBe(-52.54);
order.addPaymentline(cardPm);
expect(order.payment_ids[0].amount).toBe(-52);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.54);
expect(order.change).toBe(0);
order.payment_ids[0].delete();
expect(order.canBeValidated()).toBe(false);
order.addPaymentline(cashPm);
expect(order.payment_ids[0].amount).toBe(-52);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.54);
expect(order.change).toBe(0);
});
test("Rouding sale HALF-UP 0.05 with two payment method", async () => {
const store = await setupPosEnv();
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
const order = await getFilledOrderForPriceCheck(store);
expect(order.displayPrice).toBe(52.54);
// only_round_cash_method is false so the order due is 52.55
order.addPaymentline(cardPm);
order.payment_ids[0].setAmount(2.54);
expect(order.payment_ids[0].amount).toBe(2.54);
expect(order.canBeValidated()).toBe(false);
expect(order.remainingDue).toBe(50.01);
order.addPaymentline(cashPm);
// Cash rounding is not applied on the cash payment line but on the order due
expect(order.payment_ids[1].amount).toBe(50.01);
expect(order.remainingDue).toBe(0);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0.01);
expect(order.change).toBe(0);
// Set only_round_cash_method to true and check that the order due is now 52.54
order.config_id.only_round_cash_method = true;
order.payment_ids = [];
order.addPaymentline(cardPm);
order.payment_ids[0].setAmount(2.54);
expect(order.payment_ids[0].amount).toBe(2.54);
expect(order.canBeValidated()).toBe(false);
expect(order.remainingDue).toBe(50);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
order.addPaymentline(cashPm);
expect(order.payment_ids[1].amount).toBe(50);
expect(order.remainingDue).toBe(0);
expect(order.canBeValidated()).toBe(true);
expect(order.appliedRounding).toBe(0);
expect(order.change).toBe(0);
});

View file

@ -0,0 +1,66 @@
const { DateTime } = luxon;
/**
* We use a dedicated method for the price check because we don't want to use
* getFilledOrder in case of modification of this method breaks the price test.
*
* This method is a copy of getFilledOrder from utils.js
*/
export const getFilledOrderForPriceCheck = async (store, data = {}) => {
const order = store.addNewOrder(data);
// This product1 has a 25% tax with a 100.0 price
// This product2 has a 15% + 25% tax with a 1000.0 price
const product1 = store.models["product.template"].get(15);
const product2 = store.models["product.template"].get(16);
const date = DateTime.now();
order.write_date = date;
order.create_date = date;
order.pricelist_id = false;
await store.addLineToOrder(
{
product_tmpl_id: product1,
qty: 1,
write_date: date,
create_date: date,
},
order
);
await store.addLineToOrder(
{
product_tmpl_id: product2,
qty: 1,
write_date: date,
create_date: date,
},
order
);
store.addPendingOrder([order.id]);
return order;
};
export const prepareRoundingVals = (store, roundingAmount, roundingMethod, onlyCash = true) => {
const config = store.config;
const product1 = store.models["product.template"].get(15);
const product2 = store.models["product.template"].get(16);
const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count);
const cardPm = store.models["pos.payment.method"].find((pm) => !pm.is_cash_count);
// Changes prices to have a non rounded change
product1.list_price = 15.73;
product2.list_price = 23.49;
product1.product_variant_ids[0].lst_price = 15.73;
product2.product_variant_ids[0].lst_price = 23.49;
config.cash_rounding = true;
config.only_round_cash_method = onlyCash;
config.rounding_method = store.models["account.cash.rounding"].create({
name: "roudning",
rounding: roundingAmount,
rounding_method: roundingMethod,
strategy: "add_invoice_line",
});
return { config, cashPm, cardPm };
};

View file

@ -0,0 +1,37 @@
import { test, expect, animationFrame } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
import { setupPosEnv, getFilledOrder } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { queryAll, queryOne } from "@odoo/hoot-dom";
definePosModels();
test("getNewLine", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const orderSummary = await mountWithCleanup(OrderSummary, {});
order.getSelectedOrderline().uiState.savedQuantity = 5;
const newLine = orderSummary.getNewLine();
expect(newLine.order_id.id).toBe(order.id);
expect(newLine.qty).toBe(0);
});
test("Display tax include/exclude subtotal label", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
order.config.iface_tax_included = "total";
await mountWithCleanup(OrderSummary, {});
const total = queryOne(".total");
const subtotal = queryAll(".subtotal");
expect(subtotal).toHaveLength(0);
expect(total.innerHTML).toBe("$&nbsp;17.85");
order.config.iface_tax_included = "subtotal";
await animationFrame();
const total2 = queryOne(".total");
const subtotal2 = queryOne(".subtotal");
expect(total2.innerHTML).toBe("$&nbsp;17.85");
expect(subtotal2.innerHTML).toBe("$&nbsp;15.00");
});

View file

@ -0,0 +1,38 @@
import { test, expect } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
import { expectFormattedPrice, setupPosEnv } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
definePosModels();
test("orderline.js", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const product = store.models["product.template"].get(5);
const line = await store.addLineToOrder(
{
product_tmpl_id: product,
qty: 3,
note: '[{"text":"Test 1","colorIndex":0},{"text":"Test 2","colorIndex":0}]',
},
order
);
const comp = await mountWithCleanup(Orderline, {
props: { line },
});
const lineData = comp.lineScreenValues;
expect(comp.line.id).toEqual(line.id);
expectFormattedPrice(comp.line.currencyDisplayPrice, "$ 10.35");
expect(lineData.internalNote).toEqual([
{
text: "Test 1",
colorIndex: 0,
},
{
text: "Test 2",
colorIndex: 0,
},
]);
});

View file

@ -0,0 +1,54 @@
import { expect, test } from "@odoo/hoot";
import { setupPosEnv, dialogActions } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { click } from "@odoo/hoot-dom";
import { InternalNoteButton } from "@point_of_sale/app/screens/product_screen/control_buttons/orderline_note_button/orderline_note_button";
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
definePosModels();
test("orderline_note_button.js", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productTmplCombo = store.models["product.template"].get(7);
const productComboSteps = [
() => click("#article_product_8"), // Wood Chair 1/2
() => click("#article_product_8"), // Wood Chair 2/2
() => click("#article_product_10"), // Wood desk
() => click(".confirm"), // Confirm combo configuration
];
const lineAction = async () =>
await store.addLineToCurrentOrder({
product_tmpl_id: productTmplCombo,
qty: 1,
});
const line = await dialogActions(lineAction, productComboSteps);
expect(order.lines[0].qty).toBe(1);
expect(order.lines[1].qty).toBe(1);
expect(order.lines[2].qty).toBe(2);
const orderSummary = await mountWithCleanup(OrderSummary, { props: {} });
orderSummary._setValue(4);
expect(order.lines[0].qty).toBe(4);
expect(order.lines[1].qty).toBe(4);
expect(order.lines[2].qty).toBe(8);
const comp = await mountWithCleanup(InternalNoteButton, { props: { label: "" } });
await comp.setChanges(line, '[{"1":"Test","colorIndex":0}]');
order.updateLastOrderChange();
orderSummary._setValue(9);
const noteAction = async () => await comp.setChanges(line, '[{"2":"Test","colorIndex":0}]');
await dialogActions(noteAction, productComboSteps);
// Check quantity
expect(order.lines[0].qty).toBe(4);
expect(order.lines[1].qty).toBe(4);
expect(order.lines[2].qty).toBe(8);
expect(order.lines[3].qty).toBe(5);
expect(order.lines[4].qty).toBe(5);
expect(order.lines[5].qty).toBe(10);
// Check notes (only on parent lines)
expect(order.lines[0].note).toBe('[{"1":"Test","colorIndex":0}]');
expect(order.lines[3].note).toBe('[{"2":"Test","colorIndex":0}]');
});

View file

@ -0,0 +1,27 @@
import { test, animationFrame } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { setupPosEnv, getFilledOrder, expectFormattedPrice } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { queryOne } from "@odoo/hoot-dom";
import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
definePosModels();
test("Change always incl", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const firstPm = store.models["pos.payment.method"].getFirst();
order.config.iface_tax_included = "total";
const comp = await mountWithCleanup(PaymentScreen, {
props: { orderUuid: order.uuid },
});
await comp.addNewPaymentLine(firstPm);
order.payment_ids[0].setAmount(20);
await animationFrame();
const total = queryOne(".amount");
expectFormattedPrice(total.attributes.amount.value, "$ -2.15");
order.config.iface_tax_included = "subtotal";
await animationFrame();
const subtotal = queryOne(".amount");
expectFormattedPrice(subtotal.attributes.amount.value, "$ -2.15");
});

View file

@ -0,0 +1,43 @@
import { test, expect } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { setupPosEnv } from "../utils";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { definePosModels } from "../data/generate_model_definitions";
definePosModels();
test("_getProductByBarcode", async () => {
const store = await setupPosEnv();
store.addNewOrder();
const order = store.getOrder();
const comp = await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
await comp.addProductToOrder(store.models["product.template"].get(5));
expect(order.displayPrice).toBe(3.45);
expect(comp.total).toBe("$\u00a03.45");
expect(comp.items).toBe("1");
const productByBarcode = await comp._getProductByBarcode({ base_code: "test_test" });
expect(productByBarcode.id).toEqual(5);
});
test("fastValidate", async () => {
const store = await setupPosEnv();
store.addNewOrder();
const order = store.getOrder();
const fastPaymentMethod = order.config.fast_payment_method_ids[0];
const productScreen = await mountWithCleanup(ProductScreen, {
props: { orderUuid: order.uuid },
});
await productScreen.addProductToOrder(store.models["product.template"].get(5));
expect(order.displayPrice).toBe(3.45);
expect(productScreen.total).toBe("$\u00a03.45");
expect(productScreen.items).toBe("1");
await productScreen.fastValidate(fastPaymentMethod);
expect(order.payment_ids[0].payment_method_id).toEqual(fastPaymentMethod);
expect(order.state).toBe("paid");
expect(order.amount_paid).toBe(3.45);
});

View file

@ -0,0 +1,22 @@
import { test, expect } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { setupPosEnv, getFilledOrder } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { queryOne } from "@odoo/hoot-dom";
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
definePosModels();
test("Total on receipt always incl", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
order.config.iface_tax_included = "total";
await mountWithCleanup(ReceiptScreen, {
props: { orderUuid: order.uuid },
});
const total = queryOne(".pos-receipt-amount .pos-receipt-right-align");
expect(total.innerHTML).toBe("$&nbsp;17.85");
order.config.iface_tax_included = "subtotal";
const subtotal = queryOne(".pos-receipt-amount .pos-receipt-right-align");
expect(subtotal.innerHTML).toBe("$&nbsp;17.85");
});

View file

@ -0,0 +1,36 @@
import { test, expect } from "@odoo/hoot";
import { getFilledOrder, setupPosEnv, expectFormattedPrice } from "../utils";
import { definePosModels } from "../data/generate_model_definitions";
import { CustomerDisplayPosAdapter } from "@point_of_sale/app/customer_display/customer_display_adapter";
definePosModels();
test("getOrderlineData", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const adapter = new CustomerDisplayPosAdapter();
adapter.formatOrderData(order);
expect(adapter.data.lines).toHaveLength(2);
expect(adapter.data.lines[0].isSelected).toBe(false);
expect(adapter.data.lines[1].isSelected).toBe(true);
});
test("order amounts summary", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const adapter = new CustomerDisplayPosAdapter();
adapter.formatOrderData(order);
expectFormattedPrice(adapter.data.amount, "$ 17.85");
expectFormattedPrice(adapter.data.amountTaxes, "$ 2.85");
expect(adapter.data.subtotal).toBe(false);
store.config.iface_tax_included = "subtotal";
adapter.formatOrderData(order);
expectFormattedPrice(adapter.data.amount, "$ 17.85");
expectFormattedPrice(adapter.data.amountTaxes, "$ 2.85");
expectFormattedPrice(adapter.data.subtotal, "$ 15.00");
});

View file

@ -0,0 +1,9 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountCashRounding extends models.ServerModel {
_name = "account.cash.rounding";
_load_pos_data_fields() {
return [];
}
}

View file

@ -0,0 +1,23 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountFiscalPosition extends models.ServerModel {
_name = "account.fiscal.position";
_load_pos_data_fields() {
return ["id", "name", "display_name", "tax_map", "tax_ids"];
}
_records = [
{
id: 1,
name: "Domestic",
display_name: "Domestic",
tax_ids: [1],
},
{
id: 2,
name: "No tax fp",
display_name: "No tax fp",
},
];
}

View file

@ -0,0 +1,19 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountJournal extends models.ServerModel {
_name = "account.journal";
_load_pos_data_fields() {
return [];
}
_records = [
{
id: 1,
name: "Point of Sale",
type: "sale",
code: "POS",
company_id: 250,
},
];
}

View file

@ -0,0 +1,9 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountMove extends models.ServerModel {
_name = "account.move";
_load_pos_data_fields() {
return ["id"];
}
}

View file

@ -0,0 +1,81 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountTax extends models.ServerModel {
_name = "account.tax";
_load_pos_data_fields() {
return [
"id",
"name",
"price_include",
"include_base_amount",
"is_base_affected",
"has_negative_factor",
"amount_type",
"children_tax_ids",
"amount",
"company_id",
"id",
"sequence",
"tax_group_id",
];
}
_records = [
{
id: 1,
name: "15%",
price_include: false,
include_base_amount: false,
is_base_affected: true,
has_negative_factor: false,
amount_type: "percent",
children_tax_ids: [],
amount: 15.0,
company_id: 250,
sequence: 1,
tax_group_id: 1,
},
{
id: 2,
name: "25%",
price_include: false,
include_base_amount: false,
is_base_affected: true,
has_negative_factor: false,
amount_type: "percent",
children_tax_ids: [],
amount: 25.0,
company_id: 250,
sequence: 1,
tax_group_id: 3,
},
{
id: 3,
name: "tax incl",
type_tax_use: "sale",
amount_type: "percent",
amount: 7,
price_include_override: "tax_included",
include_base_amount: true,
has_negative_factor: true,
company_id: 250,
is_base_affected: true,
tax_group_id: 4,
},
{
id: 4,
name: "15% incl",
type_tax_use: "sale",
amount_type: "percent",
amount: 15,
price_include_override: "tax_included",
price_include: true,
include_base_amount: true,
has_negative_factor: false,
company_id: 250,
is_base_affected: true,
tax_group_id: 5,
},
];
}

View file

@ -0,0 +1,37 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountTaxGroup extends models.ServerModel {
_name = "account.tax.group";
_load_pos_data_fields() {
return ["id", "name", "pos_receipt_label"];
}
_records = [
{
id: 1,
name: "Tax 15%",
pos_receipt_label: false,
},
{
id: 2,
name: "Tax 0%",
pos_receipt_label: false,
},
{
id: 3,
name: "Tax 25%",
pos_receipt_label: false,
},
{
id: 4,
name: "No group",
pos_receipt_label: false,
},
{
id: 5,
name: "15% incl",
pos_receipt_label: false,
},
];
}

View file

@ -0,0 +1,9 @@
import { models } from "@web/../tests/web_test_helpers";
export class BarcodeNomenclature extends models.ServerModel {
_name = "barcode.nomenclature";
_load_pos_data_fields() {
return [];
}
}

View file

@ -0,0 +1,47 @@
import { models } from "@web/../tests/web_test_helpers";
export class DecimalPrecision extends models.ServerModel {
_name = "decimal.precision";
_load_pos_data_fields() {
return ["id", "name", "digits"];
}
_records = [
{
id: 1,
name: "Product Unit",
digits: 2,
},
{
id: 2,
name: "Percentage Analytic",
digits: 2,
},
{
id: 3,
name: "Product Price",
digits: 2,
},
{
id: 4,
name: "Discount",
digits: 2,
},
{
id: 5,
name: "Stock Weight",
digits: 2,
},
{
id: 6,
name: "Volume",
digits: 2,
},
{
id: 7,
name: "Payment Terms",
digits: 6,
},
];
}

View file

@ -0,0 +1,114 @@
import { defineModels, onRpc } from "@web/../tests/web_test_helpers";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { ResourceCalendar } from "./resource_calendar.data";
import { AccountMove } from "./account_move.data";
import { PosSession } from "./pos_session.data";
import { PosConfig } from "./pos_config.data";
import { PosPreset } from "./pos_preset.data";
import { ResourceCalendarAttendance } from "./resource_calendar_attendance";
import { PosOrder } from "./pos_order.data";
import { PosOrderLine } from "./pos_order_line.data";
import { PosPackOperationLot } from "./pos_pack_operation_lot.data";
import { PosPayment } from "./pos_payment.data";
import { PosPaymentMethod } from "./pos_payment_method.data";
import { PosPrinter } from "./pos_printer.data";
import { PosCategory } from "./pos_category.data";
import { PosBill } from "./pos_bill.data";
import { ResCompany } from "./res_company.data";
import { AccountTax } from "./account_tax.data";
import { AccountTaxGroup } from "./account_tax_group.data";
import { ProductTemplate } from "./product_template.data";
import { ProductProduct } from "./product_product.data";
import { ProductAttribute } from "./product_attribute.data";
import { ProductAttributeCustomValue } from "./product_attribute_custom_value.data";
import { ProductTemplateAttributeLine } from "./product_template_attribute_line.data";
import { ProductTemplateAttributeValue } from "./product_template_attribute_value.data";
import { ProductTemplateAttributeExclusion } from "./product_template_attribute_exclusion.data";
import { ProductCombo } from "./product_combo.data";
import { ProductComboItem } from "./product_combo_item.data";
import { ResUsers } from "./res_users.data";
import { ResPartner } from "./res_partner.data";
import { ProductUom } from "./product_uom.data";
import { DecimalPrecision } from "./decimal_precision.data";
import { UomUom } from "./uom_uom.data";
import { ResCountry } from "./res_country.data";
import { ResCountryState } from "./res_country_state.data";
import { ResLang } from "./res_lang.data";
import { ProductCategory } from "./product_category.data";
import { ProductPricelist } from "./product_pricelist.data";
import { ProductPricelistItem } from "./product_pricelist_item.data";
import { AccountCashRounding } from "./account_cash_rounding.data";
import { AccountFiscalPosition } from "./account_fiscal_position.data";
import { StockPickingType } from "./stock_picking_type.data";
import { ResCurrency } from "./res_currency.data";
import { PosNote } from "./pos_note.data";
import { ProductTag } from "./product_tag.data";
import { IrModuleModule } from "./ir_module_module.data";
import { AccountJournal } from "./account_journal.data";
import { IrSequence } from "./ir_sequence.data";
import { StockWarehouse } from "./stock_warehouse.data";
import { StockRoute } from "./stock_route.data";
import { BarcodeNomenclature } from "./barcode_nomenclature.data";
import { ProductAttributeValue } from "./product_attribute_value.data";
export const hootPosModels = [
ResCountry,
ResCountryState,
ResCurrency,
ResCompany,
ResPartner,
ResUsers,
ResLang,
PosSession,
PosConfig,
PosPreset,
ResourceCalendarAttendance,
PosOrder,
PosOrderLine,
PosPackOperationLot,
PosPayment,
PosPaymentMethod,
PosPrinter,
PosCategory,
PosBill,
AccountTax,
AccountTaxGroup,
AccountMove,
ProductCategory,
ProductTemplate,
ProductProduct,
ProductAttribute,
ProductAttributeValue,
ProductAttributeCustomValue,
ProductTemplateAttributeLine,
ProductTemplateAttributeValue,
ProductTemplateAttributeExclusion,
ProductCombo,
ProductComboItem,
ProductUom,
ProductTag,
ProductPricelist,
ProductPricelistItem,
DecimalPrecision,
StockWarehouse,
StockRoute,
UomUom,
AccountCashRounding,
AccountFiscalPosition,
StockPickingType,
IrSequence,
PosNote,
IrModuleModule,
AccountJournal,
ResourceCalendar,
BarcodeNomenclature,
];
export const definePosModels = () => {
const posModelNames = hootPosModels.map((modelClass) => modelClass.prototype.constructor._name);
const modelsFromMail = Object.values(mailModels).filter(
(modelClass) => !posModelNames.includes(modelClass.prototype.constructor._name)
);
onRpc("/pos/ping", () => {});
defineModels([...modelsFromMail, ...hootPosModels]);
};

View file

@ -0,0 +1,42 @@
import { registry } from "@web/core/registry";
import { createRelatedModels } from "@point_of_sale/app/models/related_models";
import { DataServiceOptions } from "@point_of_sale/app/models/data_service_options";
import { MockServer } from "@web/../tests/web_test_helpers";
export const getModelDefinitions = () => {
const session = MockServer.current._models["pos.session"];
const params = session.load_data_params();
return Object.entries(params).reduce((acc, [modelName, params]) => {
acc[modelName] = params.relations;
return acc;
}, {});
};
let generatedModels = null;
export const getRelatedModelsInstance = (useModelClass = true) => {
if (generatedModels) {
return generatedModels;
}
const options = new DataServiceOptions();
const relations = getModelDefinitions();
const modelClasses = {};
if (useModelClass) {
for (const posModel of registry.category("pos_available_models").getAll()) {
const pythonModel = posModel.pythonModel;
const extraFields = posModel.extraFields || {};
modelClasses[pythonModel] = posModel;
relations[pythonModel] = {
...relations[pythonModel],
...extraFields,
};
}
}
const models = createRelatedModels(relations, useModelClass ? modelClasses : {}, options);
generatedModels = models.models;
return models.models;
};

View file

@ -0,0 +1,17 @@
import { models } from "@web/../tests/web_test_helpers";
export class IrModuleModule extends models.ServerModel {
_name = "ir.module.module";
_load_pos_data_fields() {
return ["id", "name", "state"];
}
_records = [
{
id: 901,
name: "pos_settle_due",
state: "installed",
},
];
}

View file

@ -0,0 +1,9 @@
import { models } from "@web/../tests/web_test_helpers";
export class IrSequence extends models.ServerModel {
_name = "ir.sequence";
_load_pos_data_fields() {
return [];
}
}

Some files were not shown because too many files have changed in this diff Show more