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,4 @@
.sale-order-info td {
padding: 2px 8px;
white-space: normal;
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_sale.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension">
<xpath expr="//ul[hasclass('info-list')]" position="inside" >
<t t-if="line.sale_order_origin_id?.name">
<li>
<i class="fa fa-shopping-basket me-1" role="img" aria-label="SO" title="SO"/>
<t t-esc="line.sale_order_origin_id?.name" />
</li>
<table t-if="line.saleDetails" class="sale-order-info">
<tr t-foreach="line.saleDetails" t-as="soLine" t-key="soLine_index">
<td class="text-truncate"><t t-esc="soLine.product_uom_qty"/>x</td>
<td class="text-truncate product-name" t-esc="soLine.product_name" />
<td class="text-truncate">: </td>
<td t-if="!props.basic_receipt" class="text-truncate"><t t-esc="soLine.total" /> (tax incl.)</td>
</tr>
</table>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,36 @@
import { patch } from "@web/core/utils/patch";
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
patch(ControlButtons.prototype, {
onClickQuotation() {
const context = {};
if (this.partner) {
context["search_default_partner_id"] = this.partner.id;
}
let domain = [
["state", "!=", "cancel"],
["invoice_status", "!=", "invoiced"],
["currency_id", "=", this.pos.currency.id],
["amount_unpaid", ">", 0],
];
if (this.pos.getOrder()?.getPartner()) {
domain = [
...domain,
["partner_id", "any", [["id", "child_of", [this.pos.getOrder().getPartner().id]]]],
];
}
this.dialog.add(SelectCreateDialog, {
resModel: "sale.order",
noCreate: true,
multiSelect: false,
domain,
context: context,
onSelected: async (resIds) => {
await this.pos.onClickSaleOrder(resIds[0]);
},
});
},
});

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_sale.ControlButtons" t-inherit="point_of_sale.ControlButtons" t-inherit-mode="extension">
<xpath
expr="//t[@t-if='props.showRemainingButtons']/div/button[hasclass('o_pricelist_button')]"
position="before">
<button t-if="this.pos.cashier._role != 'minimal'" t-att-class="buttonClass" t-on-click="() => this.onClickQuotation()">
<i class="fa fa-link me-1" role="img" aria-label="Set Sale Order" title="Set Sale Order" /> Quotation/Order
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,28 @@
import { DataServiceOptions } from "@point_of_sale/app/models/data_service_options";
import { patch } from "@web/core/utils/patch";
patch(DataServiceOptions.prototype, {
get databaseTable() {
return {
...super.databaseTable,
"sale.order": {
key: "id",
condition: (record) =>
record.models["pos.order.line"].find(
(l) => l.sale_order_origin_id?.id === record.id
),
getRecordsBasedOnLines: (orderlines) =>
orderlines.map((line) => line.sale_order_origin_id).filter((so) => so),
},
"sale.order.line": {
key: "id",
condition: (record) =>
record.models["pos.order.line"].find(
(l) => l.sale_order_line_id?.id === record.id
),
getRecordsBasedOnLines: (orderlines) =>
orderlines.map((line) => line.sale_order_line_id).filter((sol) => sol),
},
};
},
});

View file

@ -0,0 +1,13 @@
import { PosOrder } from "@point_of_sale/app/models/pos_order";
import { patch } from "@web/core/utils/patch";
patch(PosOrder.prototype, {
//@override
_getIgnoredProductIdsTotalDiscount() {
const productIds = super._getIgnoredProductIdsTotalDiscount(...arguments);
if (this.config.down_payment_product_id) {
productIds.push(this.config.down_payment_product_id.id);
}
return productIds;
},
});

View file

@ -0,0 +1,48 @@
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
import { formatCurrency } from "@point_of_sale/app/models/utils/currency";
import { patch } from "@web/core/utils/patch";
patch(PosOrderline.prototype, {
setup(_defaultObj) {
super.setup(...arguments);
// It is possible that this orderline is initialized using server data,
// meaning, it is loaded from localStorage or from server. This means
// that some fields has already been assigned. Therefore, we only set the options
// when the original value is falsy.
if (this.sale_order_origin_id?.shipping_date) {
this.order_id.setShippingDate(this.sale_order_origin_id.shipping_date);
}
},
get saleDetails() {
let down_payment_details = [];
// FIXME: This is a hack to handle the case where the down_payment_details is a stringified JSON.
try {
down_payment_details = JSON.parse(this.down_payment_details);
} catch {
down_payment_details = this.down_payment_details;
}
return down_payment_details?.map?.((detail) => ({
product_uom_qty: detail.product_uom_qty,
product_name: detail.product_name,
total: formatCurrency(detail.total, this.currency),
}));
},
/**
* Set quantity based on the give sale order line.
* @param {'sale.order.line'} saleOrderLine
*/
async setQuantityFromSOL(saleOrderLine) {
if (
this.product_id.type === "service" &&
!["sent", "draft"].includes(this.sale_order_origin_id.state)
) {
this.setQuantity(saleOrderLine.qty_to_invoice);
} else {
this.setQuantity(
saleOrderLine.product_uom_qty -
Math.max(saleOrderLine.qty_delivered, saleOrderLine.qty_invoiced)
);
}
},
});

View file

@ -0,0 +1,418 @@
import { _t } from "@web/core/l10n/translation";
import { parseFloat } from "@web/views/fields/parsers";
import { SelectionPopup } from "@point_of_sale/app/components/popups/selection_popup/selection_popup";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { NumberPopup } from "@point_of_sale/app/components/popups/number_popup/number_popup";
import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
import { enhancedButtons, DECIMAL } from "@point_of_sale/app/components/numpad/numpad";
import { patch } from "@web/core/utils/patch";
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { accountTaxHelpers } from "@account/helpers/account_tax";
patch(PosStore.prototype, {
async onClickSaleOrder(clickedOrderId) {
const sale_order = await this._getSaleOrder(clickedOrderId);
await this._processSaleOrder(sale_order);
},
async _processSaleOrder(sale_order) {
const currentSaleOrigin = this.getOrder()
.getOrderlines()
.find((line) => line.sale_order_origin_id)?.sale_order_origin_id;
if (currentSaleOrigin?.id) {
const linkedSO = await this._getSaleOrder(currentSaleOrigin.id);
if (
linkedSO.partner_id?.id !== sale_order.partner_id?.id ||
linkedSO.partner_invoice_id?.id !== sale_order.partner_invoice_id?.id ||
linkedSO.partner_shipping_id?.id !== sale_order.partner_shipping_id?.id
) {
this.addNewOrder({
partner_id: sale_order.partner_id,
});
this.notification.add(_t("A new order has been created."));
}
}
if (sale_order.partner_id) {
this.getOrder().setPartner(sale_order.partner_id);
}
// Fiscal position should be set after the partner is set
// to ensure that the fiscal position is correctly computed
// based on sale order.
const orderFiscalPos = sale_order.fiscal_position_id;
this.getOrder().update({
fiscal_position_id: orderFiscalPos,
});
//Add a down payment for transactions that were already done online
if (sale_order.amount_paid > 0) {
await this.addDownPaymentProductOrderlineToOrder(
sale_order,
-sale_order.amount_paid,
false
);
}
const selectedOption = await makeAwaitable(this.dialog, SelectionPopup, {
title: _t("What do you want to do?"),
list: [
{ id: "0", label: _t("Settle the order"), item: "settle" },
{
id: "1",
label: _t("Apply a down payment (percentage)"),
item: "dpPercentage",
},
{
id: "2",
label: _t("Apply a down payment (fixed amount)"),
item: "dpAmount",
},
],
});
if (!selectedOption) {
return;
}
selectedOption == "settle"
? await this.settleSO(sale_order, orderFiscalPos)
: await this.downPaymentSO(sale_order, selectedOption == "dpPercentage");
this.selectOrderLine(this.getOrder(), this.getOrder().lines.at(-1));
},
async _getSaleOrder(id) {
const result = await this.data.callRelated("sale.order", "load_sale_order_from_pos", [
id,
this.config.id,
]);
return result["sale.order"][0];
},
async settleSO(sale_order, orderFiscalPos) {
if (sale_order.pricelist_id) {
this.getOrder().setPricelist(sale_order.pricelist_id);
}
let useLoadedLots = false;
let userWasAskedAboutLoadedLots = false;
let previousProductLine = null;
const converted_lines = await this.data.call("sale.order.line", "read_converted", [
sale_order.order_line.map((l) => l.id),
]);
for (const line of sale_order.order_line) {
if (line.display_type === "line_note") {
if (previousProductLine) {
const previousNote = previousProductLine.customer_note;
previousProductLine.customer_note = previousNote
? previousNote + "--" + line.name
: line.name;
}
continue;
}
if (line.is_downpayment) {
line.product_id = this.config.down_payment_product_id;
}
const taxes = orderFiscalPos?.getTaxesAfterFiscalPosition(line.tax_ids) || line.tax_ids;
const newLineValues = {
product_tmpl_id: line.product_id?.product_tmpl_id,
product_id: line.product_id,
qty: line.product_uom_qty,
price_unit: line.price_unit,
price_type: "automatic",
tax_ids: taxes.map((tax) => ["link", tax]),
sale_order_origin_id: sale_order,
sale_order_line_id: line,
customer_note: line.customer_note,
description: line.name,
order_id: this.getOrder(),
custom_attribute_value_ids: Object.values(
line.product_custom_attribute_value_ids || {}
).map((value_line) => [
"create",
{
custom_product_template_attribute_value_id:
value_line.custom_product_template_attribute_value_id,
custom_value: value_line.custom_value,
},
]),
};
if (line.display_type === "line_section") {
continue;
}
newLineValues.attribute_value_ids = (line.product_custom_attribute_value_ids || []).map(
(value_line) => {
if (value_line?.custom_product_template_attribute_value_id) {
return ["link", value_line.custom_product_template_attribute_value_id];
}
}
);
const newLine = await this.addLineToCurrentOrder(newLineValues, {}, false);
previousProductLine = newLine;
const converted_line = converted_lines.find((l) => l.id === line.id);
if (
newLine.getProduct().tracking !== "none" &&
(this.pickingType.use_create_lots || this.pickingType.use_existing_lots) &&
converted_line.lot_names.length > 0
) {
if (!useLoadedLots && !userWasAskedAboutLoadedLots) {
useLoadedLots = await ask(this.dialog, {
title: _t("SN/Lots Loading"),
body: _t("Do you want to load the SN/Lots linked to the Sales Order?"),
});
userWasAskedAboutLoadedLots = true;
}
if (useLoadedLots) {
newLine.setPackLotLines({
modifiedPackLotLines: [],
newPackLotLines: (converted_line.lot_names || []).map((name) => ({
lot_name: name,
})),
});
}
}
newLine.setQuantityFromSOL(converted_line);
newLine.setUnitPrice(converted_line.price_unit);
newLine.setDiscount(line.discount);
const lot_splitted_lines = [];
const product_unit = line.product_id.uom_id;
if (product_unit && !product_unit.is_pos_groupable) {
let remaining_quantity = newLine.qty;
newLineValues.product_id = newLine.product_id;
const priceUnit = newLine.price_unit;
newLine.delete();
while (!product_unit.isZero(remaining_quantity)) {
const splitted_line = this.models["pos.order.line"].create({
...newLineValues,
});
splitted_line.setQuantity(Math.min(remaining_quantity, 1.0), true);
splitted_line.setUnitPrice(priceUnit);
splitted_line.setDiscount(line.discount);
remaining_quantity -= splitted_line.qty;
if (splitted_line.product_id.tracking == "lot") {
lot_splitted_lines.push(splitted_line);
}
}
}
// Order line can only hold one lot, so we need to split the line if there are multiple lots
if (
line.product_id.tracking == "lot" &&
converted_line.lot_names.length > 0 &&
useLoadedLots
) {
const priceUnit = newLine.price_unit;
newLine.delete();
let total_lot_quantity = 0;
for (const lot of converted_line.lot_names) {
let lot_remaining_quantity = converted_line.lot_qty_by_name[lot] || 0;
while (lot_splitted_lines.length && lot_remaining_quantity > 0) {
const splitted_line = lot_splitted_lines.shift();
splitted_line.setPackLotLines({
modifiedPackLotLines: [],
newPackLotLines: [{ lot_name: lot }],
setQuantity: false,
});
total_lot_quantity += splitted_line.qty;
lot_remaining_quantity -= splitted_line.qty;
}
if (lot_remaining_quantity > 0 && lot_splitted_lines.length == 0) {
const splitted_line = this.models["pos.order.line"].create({
...newLineValues,
});
splitted_line.setQuantity(lot_remaining_quantity, true);
splitted_line.setUnitPrice(priceUnit);
splitted_line.setDiscount(line.discount);
splitted_line.setPackLotLines({
modifiedPackLotLines: [],
newPackLotLines: [{ lot_name: lot }],
setQuantity: false,
});
total_lot_quantity += lot_remaining_quantity;
}
}
if (total_lot_quantity < newLineValues.qty && lot_splitted_lines.length == 0) {
const splitted_line = this.models["pos.order.line"].create({
...newLineValues,
});
splitted_line.setQuantity(newLineValues.qty - total_lot_quantity, true);
splitted_line.setDiscount(line.discount);
}
}
}
},
prepareSoBaseLineForTaxesComputationExtraValues(so, soLine) {
const extraValues = { currency_id: so.currency_id || this.company.currency_id };
return {
...extraValues,
quantity: soLine.product_uom_qty,
tax_ids: soLine.tax_ids,
partner_id: so.partner_id,
product_id: soLine.product_id,
extra_tax_data: soLine.extra_tax_data,
};
},
async downPaymentSO(saleOrder, isPercentage) {
const colorClassMap = {
[DECIMAL.value]: "o_colorlist_item_numpad_color_6",
Backspace: "o_colorlist_item_numpad_color_1",
"+10": "o_colorlist_item_numpad_color_10",
"+20": "o_colorlist_item_numpad_color_10",
"+50": "o_colorlist_item_numpad_color_10",
"-": "o_colorlist_item_numpad_color_3",
};
const payload = await makeAwaitable(this.dialog, NumberPopup, {
title: _t("Down Payment"),
subtitle: _t("Due balance: %s", this.env.utils.formatCurrency(saleOrder.amount_unpaid)),
buttons: enhancedButtons().map((button) => ({
...button,
class: `${colorClassMap[button.value] || ""}`,
})),
confirmButtonLabel: _t("Apply"),
formatDisplayedValue: (x) => (isPercentage ? `% ${x}` : x),
feedback: (buffer) =>
isPercentage && buffer
? `(${this.env.utils.formatCurrency(
(saleOrder.amount_unpaid * parseFloat(buffer)) / 100
)})`
: "",
});
if (!payload) {
return;
}
const amount = parseFloat(payload);
await this.addDownPaymentProductOrderlineToOrder(saleOrder, amount, isPercentage);
},
async loadDownPaymentProduct() {
if (!this.config.down_payment_product_id && this.config.raw.down_payment_product_id) {
await this.data.read("product.product", [this.config.raw.down_payment_product_id]);
}
if (!this.config.down_payment_product_id) {
this.dialog.add(AlertDialog, {
title: _t("No down payment product"),
body: _t(
"It seems that you didn't configure a down payment product in your point of sale. You can go to your point of sale configuration to choose one."
),
});
return false;
}
return true;
},
async addDownPaymentProductOrderlineToOrder(saleOrder, amount, isPercentage) {
const result = await this.loadDownPaymentProduct();
if (!result) {
return;
}
const saleOrderLines = saleOrder.order_line.filter((soLine) => !soLine.display_type);
const baseLines = [];
for (const saleOrderLine of saleOrderLines) {
baseLines.push(
accountTaxHelpers.prepare_base_line_for_taxes_computation(
saleOrderLine,
this.prepareSoBaseLineForTaxesComputationExtraValues(saleOrder, saleOrderLine)
)
);
}
accountTaxHelpers.add_tax_details_in_base_lines(baseLines, this.company);
accountTaxHelpers.round_base_lines_tax_details(baseLines, this.company);
if (isPercentage) {
const percentage = amount / 100.0;
amount = baseLines.length ? saleOrder.amount_unpaid * percentage : 0.0;
}
const downPaymentProduct = this.config.down_payment_product_id;
const groupingFunction = (base_line) => ({
grouping_key: { product_id: downPaymentProduct },
raw_grouping_key: { product_id: downPaymentProduct.id },
});
const downPaymentBaseLines = accountTaxHelpers.prepare_down_payment_lines(
baseLines,
this.company,
"fixed",
amount,
{
computation_key: "down_payment", // TODO: won't work with multiple down payment on the same order... is it a problem?
grouping_function: groupingFunction,
}
);
// Update the pos order.
for (const baseLine of downPaymentBaseLines) {
// Find the sale order lines that are impacted by this down payment line.
const taxIds = new Set(baseLine.tax_ids.map((tax) => tax.id));
const matchedSaleOrderLines = [];
for (const saleOrderLine of saleOrderLines) {
// TODO: use '!saleOrderLine.is_down_payment' instead?
// TODO: 'product_id' is always set on a SO line, correct?
if (
!saleOrderLine.product_id ||
saleOrderLine.product_id.id === downPaymentProduct.id
) {
continue;
}
const saleOrderLineTaxIds = saleOrderLine.tax_ids.map((tax) => tax.id);
if (
saleOrderLineTaxIds.length === taxIds.size &&
saleOrderLineTaxIds.every((taxId) => taxIds.has(taxId))
) {
matchedSaleOrderLines.push(saleOrderLine);
}
}
this.addLineToCurrentOrder({
pos: this,
order: saleOrder,
product_id: baseLine.product_id,
product_tmpl_id: baseLine.product_id.product_tmpl_id,
price: baseLine.price_unit,
price_unit: baseLine.price_unit,
price_type: "automatic",
sale_order_origin_id: saleOrder,
down_payment_details: matchedSaleOrderLines.map((saleOrderLine) => ({
product_name: saleOrderLine.product_id.display_name,
product_uom_qty: saleOrderLine.product_uom_qty,
price_unit: saleOrderLine.price_unit,
total: saleOrderLine.price_total,
})),
tax_ids: [["link", ...baseLine.tax_ids]],
extra_tax_data: accountTaxHelpers.export_base_line_extra_tax_data(baseLine),
});
}
},
selectOrderLine(order, line) {
super.selectOrderLine(...arguments);
if (
line &&
this.config.down_payment_product_id &&
line.product_id.id === this.config.down_payment_product_id.id
) {
this.numpadMode = "price";
}
},
setPartnerToCurrentOrder(partner) {
if (partner.sale_warn_msg) {
this.dialog.add(AlertDialog, {
title: _t("Warning for %s", partner.name),
body: partner.sale_warn_msg,
});
}
super.setPartnerToCurrentOrder(partner);
},
addLineToCurrentOrder(vals, opt = {}, configure = true) {
if (!vals.product_tmpl_id && vals.product_id) {
vals.product_tmpl_id = vals.product_id.product_tmpl_id;
}
const productTemplate = vals.product_tmpl_id;
if (productTemplate.sale_line_warn_msg) {
this.dialog.add(AlertDialog, {
title: _t("Warning for %s", productTemplate.name),
body: productTemplate.sale_line_warn_msg,
});
}
return super.addLineToCurrentOrder(vals, opt, configure);
},
});

View file

@ -0,0 +1,18 @@
import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation";
import { patch } from "@web/core/utils/patch";
patch(OrderPaymentValidation.prototype, {
async afterOrderValidation() {
const lines = this.order.lines.filter(
(e) => e.sale_order_origin_id && e.down_payment_details
);
if (lines.length > 0) {
const orders = [...new Set(lines.map((e) => e.sale_order_origin_id))];
await this.pos.data.read(
"sale.order.line",
orders.flatMap((o) => o.order_line).map((ol) => ol.id)
);
}
await super.afterOrderValidation();
},
});

View file

@ -0,0 +1,16 @@
.product-info-popup .extra {
display: flex;
}
.product-info-popup .extra div {
width: 100%;
}
@media screen and (max-width: 768px) {
.product-info-popup .extra{
flex-direction: column;
}
.product-info-popup .section-optional-product-body td {
width: 50%;
}
}

View file

@ -1,94 +0,0 @@
.sale-order-info td {
padding-right: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-management-screen .flex-container {
display: flex;
flex-direction: column;
height: 100%;
}
.order-management-screen .control-panel {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 0.5rem;
}
.order-management-screen .control-panel .item {
font-size: medium;
}
.order-management-screen .control-panel .search-box {
flex: 1;
position: relative;
text-align: center;
margin: 0.2rem;
}
.order-management-screen .control-panel .search-box .clear {
position: relative;
right: 25px;
cursor: pointer;
color: #808080;
font-size: 13px;
}
.order-management-screen .control-panel .search-box .icon {
position: relative;
left: 25px;
color: #808080;
font-size: 13px;
}
.order-management-screen .control-panel .search-box input {
border: 1px solid #cecbcb;
padding: 10px 30px;
margin: auto;
background-color: white;
border-radius: 20px;
font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial;
font-size: 13px;
}
.order-management-screen .control-panel .search-box input:focus {
outline: none;
box-shadow: 0px 0px 0px 2px rgb(153, 153, 255) inset;
color: rgb(153, 153, 255);
}
.order-management-screen .control-panel .button {
line-height: 32px;
padding: 3px 13px;
font-size: 20px;
background: rgb(230, 230, 230);
border-radius: 3px;
border: solid 1px rgb(209, 209, 209);
cursor: pointer;
transition: all 150ms linear;
}
.order-management-screen .control-panel .button:hover {
background: #efefef;
}
.order-management-screen .back-to-list {
font-size: large;
padding: 10px;
background-color: #6EC89B;
color: white;
}
/* Utility classes */
.order-management-screen .self-end {
align-self: flex-end;
}
.order-management-screen .text-gray {
color: #808080;
}

View file

@ -1,26 +0,0 @@
odoo.define('point_of_sale.MobileSaleOrderManagementScreen', function (require) {
const SaleOrderManagementScreen = require('pos_sale.SaleOrderManagementScreen');
const Registries = require('point_of_sale.Registries');
const { useListener } = require("@web/core/utils/hooks");
const { useState } = owl;
const MobileSaleOrderManagementScreen = (SaleOrderManagementScreen) => {
class MobileSaleOrderManagementScreen extends SaleOrderManagementScreen {
setup() {
super.setup();
useListener('click-order', this._onShowDetails)
this.mobileState = useState({ showDetails: false });
}
_onShowDetails() {
this.mobileState.showDetails = true;
}
}
MobileSaleOrderManagementScreen.template = 'MobileSaleOrderManagementScreen';
return MobileSaleOrderManagementScreen;
};
Registries.Component.addByExtending(MobileSaleOrderManagementScreen, SaleOrderManagementScreen);
return MobileSaleOrderManagementScreen;
});

View file

@ -1,137 +0,0 @@
odoo.define('pos_sale.SaleOrderFetcher', function (require) {
'use strict';
const { Gui } = require('point_of_sale.Gui');
const { isConnectionError } = require('point_of_sale.utils');
const { EventBus } = owl;
class SaleOrderFetcher extends EventBus {
constructor() {
super();
this.currentPage = 1;
this.ordersToShow = [];
this.totalCount = 0;
}
/**
* for nPerPage = 10
* +--------+----------+
* | nItems | lastPage |
* +--------+----------+
* | 2 | 1 |
* | 10 | 1 |
* | 11 | 2 |
* | 30 | 3 |
* | 35 | 4 |
* +--------+----------+
*/
get lastPage() {
const nItems = this.totalCount;
return Math.trunc(nItems / (this.nPerPage + 1)) + 1;
}
get orderFields(){
return ['name', 'partner_id', 'amount_total', 'date_order', 'state', 'user_id', 'amount_unpaid']
}
/**
* Calling this methods populates the `ordersToShow` then trigger `update` event.
* @related get
*
* NOTE: This is tightly-coupled with pagination. So if the current page contains all
* active orders, it will not fetch anything from the server but only sets `ordersToShow`
* to the active orders that fits the current page.
*/
async fetch() {
try {
let limit, offset;
// Show orders from the backend.
offset =
this.nPerPage +
(this.currentPage - 1 - 1) *
this.nPerPage;
limit = this.nPerPage;
this.ordersToShow = await this._fetch(limit, offset);
this.trigger('update');
} catch (error) {
if (isConnectionError(error)) {
Gui.showPopup('ErrorPopup', {
title: this.comp.env._t('Network Error'),
body: this.comp.env._t('Unable to fetch orders if offline.'),
});
Gui.setSyncStatus('error');
} else {
throw error;
}
}
}
/**
* This returns the orders from the backend that needs to be shown.
* If the order is already in cache, the full information about that
* order is not fetched anymore, instead, we use info from cache.
*
* @param {number} limit
* @param {number} offset
*/
async _fetch(limit, offset) {
const sale_orders = await this._getOrderIdsForCurrentPage(limit, offset);
this.totalCount = sale_orders.length;
return sale_orders;
}
async _getOrderIdsForCurrentPage(limit, offset) {
let domain = [['currency_id', '=', this.comp.env.pos.currency.id]].concat(this.searchDomain || []);
const saleOrders = await this.rpc({
model: 'sale.order',
method: 'search_read',
args: [domain, this.orderFields, offset, limit],
context: this.comp.env.session.user_context,
});
return saleOrders;
}
nextPage() {
if (this.currentPage < this.lastPage) {
this.currentPage += 1;
this.fetch();
}
}
prevPage() {
if (this.currentPage > 1) {
this.currentPage -= 1;
this.fetch();
}
}
/**
* @param {integer|undefined} id id of the cached order
* @returns {Array<models.Order>}
*/
get(id) {
return this.ordersToShow;
}
setSearchDomain(searchDomain) {
this.searchDomain = searchDomain;
}
setComponent(comp) {
this.comp = comp;
return this;
}
setNPerPage(val) {
this.nPerPage = val;
}
setPage(page) {
this.currentPage = page;
}
async rpc() {
Gui.setSyncStatus('connecting');
const result = await this.comp.rpc(...arguments);
Gui.setSyncStatus('connected');
return result;
}
}
return new SaleOrderFetcher();
});

View file

@ -1,32 +0,0 @@
odoo.define('pos_sale.SaleOrderList', function (require) {
'use strict';
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { useState } = owl;
/**
* @props {models.Order} [initHighlightedOrder] initially highligted order
* @props {Array<models.Order>} orders
*/
class SaleOrderList extends PosComponent {
setup() {
super.setup();
useListener('click-order', this._onClickOrder);
this.state = useState({ highlightedOrder: this.props.initHighlightedOrder || null });
}
get highlightedOrder() {
return this.state.highlightedOrder;
}
_onClickOrder({ detail: order }) {
this.state.highlightedOrder = order;
}
}
SaleOrderList.template = 'SaleOrderList';
Registries.Component.add(SaleOrderList);
return SaleOrderList;
});

View file

@ -1,127 +0,0 @@
odoo.define('pos_sale.SaleOrderManagementControlPanel', function (require) {
'use strict';
const { useAutofocus, useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const SaleOrderFetcher = require('pos_sale.SaleOrderFetcher');
const contexts = require('point_of_sale.PosContext');
const { useState } = owl;
// NOTE: These are constants so that they are only instantiated once
// and they can be used efficiently by the OrderManagementControlPanel.
const VALID_SEARCH_TAGS = new Set(['date', 'customer', 'client', 'name', 'order']);
const FIELD_MAP = {
date: 'date_order',
customer: 'partner_id.display_name',
client: 'partner_id.display_name',
name: 'name',
order: 'name',
};
const SEARCH_FIELDS = ['name', 'partner_id.display_name', 'date_order'];
/**
* @emits close-screen
* @emits prev-page
* @emits next-page
* @emits search
*/
class SaleOrderManagementControlPanel extends PosComponent {
setup() {
super.setup();
this.orderManagementContext = useState(contexts.orderManagement);
useListener('clear-search', this._onClearSearch);
useAutofocus();
let currentPartner = this.env.pos.get_order().get_partner();
if (currentPartner) {
this.orderManagementContext.searchString = currentPartner.name;
}
SaleOrderFetcher.setSearchDomain(this._computeDomain());
}
onInputKeydown(event) {
if (event.key === 'Enter') {
this.trigger('search', this._computeDomain());
}
}
get showPageControls() {
return SaleOrderFetcher.lastPage > 1;
}
get pageNumber() {
const currentPage = SaleOrderFetcher.currentPage;
const lastPage = SaleOrderFetcher.lastPage;
return isNaN(lastPage) ? '' : `(${currentPage}/${lastPage})`;
}
get validSearchTags() {
return VALID_SEARCH_TAGS;
}
get fieldMap() {
return FIELD_MAP;
}
get searchFields() {
return SEARCH_FIELDS;
}
/**
* E.g. 1
* ```
* searchString = 'Customer 1'
* result = [
* '|',
* '|',
* ['pos_reference', 'ilike', '%Customer 1%'],
* ['partner_id.display_name', 'ilike', '%Customer 1%'],
* ['date_order', 'ilike', '%Customer 1%']
* ]
* ```
*
* E.g. 2
* ```
* searchString = 'date: 2020-05'
* result = [
* ['date_order', 'ilike', '%2020-05%']
* ]
* ```
*
* E.g. 3
* ```
* searchString = 'customer: Steward, date: 2020-05-01'
* result = [
* ['partner_id.display_name', 'ilike', '%Steward%'],
* ['date_order', 'ilike', '%2020-05-01%']
* ]
* ```
*/
_computeDomain() {
let domain = [['state', '!=', 'cancel'],['invoice_status', '!=', 'invoiced']];
const input = this.orderManagementContext.searchString.trim();
if (!input) return domain;
const searchConditions = this.orderManagementContext.searchString.split(/[,&]\s*/);
if (searchConditions.length === 1) {
let cond = searchConditions[0].split(/:\s*/);
if (cond.length === 1) {
domain = domain.concat(Array(this.searchFields.length - 1).fill('|'));
domain = domain.concat(this.searchFields.map((field) => [field, 'ilike', `%${cond[0]}%`]));
return domain;
}
}
for (let cond of searchConditions) {
let [tag, value] = cond.split(/:\s*/);
if (!this.validSearchTags.has(tag)) continue;
domain.push([this.fieldMap[tag], 'ilike', `%${value}%`]);
}
return domain;
}
_onClearSearch() {
this.orderManagementContext.searchString = '';
this.onInputKeydown({ key: 'Enter' });
}
}
SaleOrderManagementControlPanel.template = 'SaleOrderManagementControlPanel';
Registries.Component.add(SaleOrderManagementControlPanel);
return SaleOrderManagementControlPanel;
});

View file

@ -1,357 +0,0 @@
odoo.define('pos_sale.SaleOrderManagementScreen', function (require) {
'use strict';
const { sprintf } = require('web.utils');
const { parse } = require('web.field_utils');
const { _t } = require('@web/core/l10n/translation');
const { useListener } = require("@web/core/utils/hooks");
const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const Registries = require('point_of_sale.Registries');
const SaleOrderFetcher = require('pos_sale.SaleOrderFetcher');
const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
const contexts = require('point_of_sale.PosContext');
const utils = require('web.utils');
const { Orderline } = require('point_of_sale.models');
const { onMounted, onWillUnmount, useState } = owl;
/**
* ID getter to take into account falsy many2one value.
* @param {[id: number, display_name: string] | false} fieldVal many2one field value
* @returns {number | false}
*/
function getId(fieldVal) {
return fieldVal && fieldVal[0];
}
class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) {
setup() {
super.setup();
useListener('close-screen', this.close);
useListener('click-sale-order', this._onClickSaleOrder);
useListener('next-page', this._onNextPage);
useListener('prev-page', this._onPrevPage);
useListener('search', this._onSearch);
SaleOrderFetcher.setComponent(this);
this.orderManagementContext = useState(contexts.orderManagement);
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
onMounted() {
SaleOrderFetcher.on('update', this, this.render);
// calculate how many can fit in the screen.
// It is based on the height of the header element.
// So the result is only accurate if each row is just single line.
const flexContainer = this.el.querySelector('.flex-container');
const cpEl = this.el.querySelector('.control-panel');
const headerEl = this.el.querySelector('.header-row');
const val = Math.trunc(
(flexContainer.offsetHeight - cpEl.offsetHeight - headerEl.offsetHeight) /
headerEl.offsetHeight
);
SaleOrderFetcher.setNPerPage(val);
// Fetch the order after mounting so that order management screen
// is shown while fetching.
setTimeout(() => SaleOrderFetcher.fetch(), 0);
}
onWillUnmount() {
SaleOrderFetcher.off('update', this);
}
get selectedPartner() {
const order = this.orderManagementContext.selectedOrder;
return order ? order.get_partner() : null;
}
get orders() {
return SaleOrderFetcher.get();
}
async _setNumpadMode(event) {
const { mode } = event.detail;
this.numpadMode = mode;
NumberBuffer.reset();
}
_onNextPage() {
SaleOrderFetcher.nextPage();
}
_onPrevPage() {
SaleOrderFetcher.prevPage();
}
_onSearch({ detail: domain }) {
SaleOrderFetcher.setSearchDomain(domain);
SaleOrderFetcher.setPage(1);
SaleOrderFetcher.fetch();
}
_getSaleOrderOrigin(order) {
for (const line of order.get_orderlines()) {
if (line.sale_order_origin_id) {
return line.sale_order_origin_id
}
}
return false;
}
async _onClickSaleOrder(event) {
const clickedOrder = event.detail;
const { confirmed, payload: selectedOption } = await this.showPopup('SelectionPopup',
{
title: this.env._t('What do you want to do?'),
list: [{id:"0", label: this.env._t("Apply a down payment"), item: false}, {id:"1", label: this.env._t("Settle the order"), item: true}],
});
if(confirmed){
let currentPOSOrder = this.env.pos.get_order();
let sale_order = await this._getSaleOrder(clickedOrder.id);
const currentSaleOrigin = this._getSaleOrderOrigin(currentPOSOrder);
const currentSaleOriginId = currentSaleOrigin && currentSaleOrigin.id;
if (currentSaleOriginId) {
const linkedSO = await this._getSaleOrder(currentSaleOriginId);
if (
getId(linkedSO.partner_id) !== getId(sale_order.partner_id) ||
getId(linkedSO.partner_invoice_id) !== getId(sale_order.partner_invoice_id) ||
getId(linkedSO.partner_shipping_id) !== getId(sale_order.partner_shipping_id)
) {
currentPOSOrder = this.env.pos.add_new_order();
this.showNotification(this.env._t("A new order has been created."));
}
}
let order_partner = this.env.pos.db.get_partner_by_id(sale_order.partner_id[0])
if(order_partner){
currentPOSOrder.set_partner(order_partner);
} else {
try {
await this.env.pos._loadPartners([sale_order.partner_id[0]]);
}
catch (_error){
const title = this.env._t('Customer loading error');
const body = _.str.sprintf(this.env._t('There was a problem in loading the %s customer.'), sale_order.partner_id[1]);
await this.showPopup('ErrorPopup', { title, body });
}
currentPOSOrder.set_partner(this.env.pos.db.get_partner_by_id(sale_order.partner_id[0]));
}
let orderFiscalPos = sale_order.fiscal_position_id ? this.env.pos.fiscal_positions.find(
(position) => position.id === sale_order.fiscal_position_id[0]
)
: false;
if (orderFiscalPos){
currentPOSOrder.fiscal_position = orderFiscalPos;
}
let orderPricelist = sale_order.pricelist_id ? this.env.pos.pricelists.find(
(pricelist) => pricelist.id === sale_order.pricelist_id[0]
)
: false;
if (orderPricelist){
currentPOSOrder.set_pricelist(orderPricelist);
}
if (selectedOption){
// settle the order
let lines = sale_order.order_line;
let product_to_add_in_pos = lines.filter(line => !this.env.pos.db.get_product_by_id(line.product_id[0])).map(line => line.product_id[0]);
if (product_to_add_in_pos.length){
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Products not available in POS'),
body:
this.env._t(
'Some of the products in your Sale Order are not available in POS, do you want to import them?'
),
confirmText: this.env._t('Yes'),
cancelText: this.env._t('No'),
});
if (confirmed){
await this.env.pos._addProducts(product_to_add_in_pos);
}
}
/**
* This variable will have 3 values, `undefined | false | true`.
* Initially, it is `undefined`. When looping thru each sale.order.line,
* when a line comes with lots (`.lot_names`), we use these lot names
* as the pack lot of the generated pos.order.line. We ask the user
* if he wants to use the lots that come with the sale.order.lines to
* be used on the corresponding pos.order.line only once. So, once the
* `useLoadedLots` becomes true, it will be true for the succeeding lines,
* and vice versa.
*/
let useLoadedLots;
for (var i = 0; i < lines.length; i++) {
let line = lines[i];
if (!this.env.pos.db.get_product_by_id(line.product_id[0])){
continue;
}
const line_values = {
pos: this.env.pos,
order: this.env.pos.get_order(),
product: this.env.pos.db.get_product_by_id(line.product_id[0]),
description: line.product_id[1],
price: line.price_unit,
tax_ids: orderFiscalPos ? undefined : line.tax_id,
price_automatically_set: true,
price_manually_set: false,
sale_order_origin_id: clickedOrder,
sale_order_line_id: line,
customer_note: line.customer_note,
};
let new_line = Orderline.create({}, line_values);
if (
new_line.get_product().tracking !== 'none' &&
(this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots) &&
line.lot_names.length > 0
) {
// Ask once when `useLoadedLots` is undefined, then reuse it's value on the succeeding lines.
const { confirmed } =
useLoadedLots === undefined
? await this.showPopup('ConfirmPopup', {
title: this.env._t('SN/Lots Loading'),
body: this.env._t(
'Do you want to load the SN/Lots linked to the Sales Order?'
),
confirmText: this.env._t('Yes'),
cancelText: this.env._t('No'),
})
: { confirmed: useLoadedLots };
useLoadedLots = confirmed;
if (useLoadedLots) {
new_line.setPackLotLines({
modifiedPackLotLines: [],
newPackLotLines: (line.lot_names || []).map((name) => ({ lot_name: name })),
});
}
}
new_line.setQuantityFromSOL(line);
new_line.set_unit_price(line.price_unit);
new_line.set_discount(line.discount);
const product = this.env.pos.db.get_product_by_id(line.product_id[0]);
const product_unit = product.get_unit();
if (product_unit && !product.get_unit().is_pos_groupable) {
//loop for value of quantity
let remaining_quantity = new_line.quantity;
while (!utils.float_is_zero(remaining_quantity, 6)) {
let splitted_line = Orderline.create({}, line_values);
splitted_line.set_quantity(Math.min(remaining_quantity, 1.0), true);
splitted_line.set_discount(line.discount);
remaining_quantity -= splitted_line.quantity;
this.env.pos.get_order().add_orderline(splitted_line);
}
}
else {
this.env.pos.get_order().add_orderline(new_line);
}
}
}
else {
// apply a downpayment
if (this.env.pos.config.down_payment_product_id){
let lines = sale_order.order_line;
let tab = [];
for (let i=0; i<lines.length; i++) {
tab[i] = {
'product_name': lines[i].product_id[1],
'product_uom_qty': lines[i].product_uom_qty,
'price_unit': lines[i].price_unit,
'total': lines[i].price_total,
};
}
let down_payment_product = this.env.pos.db.get_product_by_id(this.env.pos.config.down_payment_product_id[0]);
if (!down_payment_product) {
await this.env.pos._addProducts([this.env.pos.config.down_payment_product_id[0]]);
down_payment_product = this.env.pos.db.get_product_by_id(this.env.pos.config.down_payment_product_id[0]);
}
let down_payment_tax = this.env.pos.taxes_by_id[down_payment_product.taxes_id] || false ;
let down_payment;
if (down_payment_tax) {
down_payment = down_payment_tax.price_include ? sale_order.amount_total : sale_order.amount_untaxed;
}
else{
down_payment = sale_order.amount_total;
}
const { confirmed, payload } = await this.showPopup('NumberPopup', {
title: sprintf(this.env._t("Percentage of %s"), this.env.pos.format_currency(sale_order.amount_total)),
startingValue: 0,
});
if (confirmed){
down_payment = down_payment * parse.float(payload) / 100;
}
if (down_payment > sale_order.amount_unpaid) {
const errorBody = sprintf(
this.env._t("You have tried to charge a down payment of %s but only %s remains to be paid, %s will be applied to the purchase order line."),
this.env.pos.format_currency(down_payment),
this.env.pos.format_currency(sale_order.amount_unpaid),
sale_order.amount_unpaid > 0 ? this.env.pos.format_currency(sale_order.amount_unpaid) : this.env.pos.format_currency(0),
);
await this.showPopup('ErrorPopup', { title: _t('Error amount too high'), body: errorBody });
down_payment = sale_order.amount_unpaid > 0 ? sale_order.amount_unpaid : 0;
}
let new_line = Orderline.create({}, {
pos: this.env.pos,
order: this.env.pos.get_order(),
product: down_payment_product,
price: down_payment,
price_automatically_set: true,
sale_order_origin_id: clickedOrder,
down_payment_details: tab,
});
new_line.set_unit_price(down_payment);
this.env.pos.get_order().add_orderline(new_line);
}
else {
const title = this.env._t('No down payment product');
const body = this.env._t(
"It seems that you didn't configure a down payment product in your point of sale.\
You can go to your point of sale configuration to choose one."
);
await this.showPopup('ErrorPopup', { title, body });
}
}
this.close();
}
}
async _getSaleOrder(id) {
const sale_order = await this.rpc({
model: 'sale.order',
method: 'read',
args: [[id],['order_line', 'partner_id', 'pricelist_id', 'fiscal_position_id', 'amount_total', 'amount_untaxed', 'amount_unpaid', 'partner_shipping_id', 'partner_invoice_id']],
context: this.env.session.user_context,
});
const sale_lines = await this._getSOLines(sale_order[0].order_line);
sale_order[0].order_line = sale_lines;
return sale_order[0];
}
async _getSOLines(ids) {
let so_lines = await this.rpc({
model: 'sale.order.line',
method: 'read_converted',
args: [ids],
context: this.env.session.user_context,
});
return so_lines;
}
}
SaleOrderManagementScreen.template = 'SaleOrderManagementScreen';
SaleOrderManagementScreen.hideOrderSelector = true;
Registries.Component.add(SaleOrderManagementScreen);
return SaleOrderManagementScreen;
});

View file

@ -1,71 +0,0 @@
odoo.define('pos_sale.SaleOrderRow', function (require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const utils = require('web.utils');
const { deserializeDateTime } = require("@web/core/l10n/dates");
/**
* @props {models.Order} order
* @props columns
* @emits click-order
*/
class SaleOrderRow extends PosComponent {
get order() {
return this.props.order;
}
get highlighted() {
const highlightedOrder = this.props.highlightedOrder;
return !highlightedOrder ? false : highlightedOrder.backendId === this.props.order.backendId;
}
// Column getters //
get name() {
return this.order.name;
}
get date() {
return deserializeDateTime(this.order.date_order).toFormat("yyyy-MM-dd HH:mm a");
}
get partner() {
const partner = this.order.partner_id;
return partner ? partner[1] : null;
}
get total() {
return this.env.pos.format_currency(this.order.amount_total);
}
/**
* Returns true if the order has unpaid amount, but the unpaid amount
* should not be the same as the total amount.
* @returns {boolean}
*/
get showAmountUnpaid() {
const isFullAmountUnpaid = utils.float_is_zero(Math.abs(this.order.amount_total - this.order.amount_unpaid), this.env.pos.currency.decimal_places);
return !isFullAmountUnpaid && !utils.float_is_zero(this.order.amount_unpaid, this.env.pos.currency.decimal_places);
}
get amountUnpaidRepr() {
return this.env.pos.format_currency(this.order.amount_unpaid);
}
get state() {
let state_mapping = {
'draft': this.env._t('Quotation'),
'sent': this.env._t('Quotation Sent'),
'sale': this.env._t('Sales Order'),
'done': this.env._t('Locked'),
'cancel': this.env._t('Cancelled'),
};
return state_mapping[this.order.state];
}
get salesman() {
const salesman = this.order.user_id;
return salesman ? salesman[1] : null;
}
}
SaleOrderRow.template = 'SaleOrderRow';
Registries.Component.add(SaleOrderRow);
return SaleOrderRow;
});

View file

@ -1,59 +0,0 @@
odoo.define('pos_sale.SetSaleOrderButton', function(require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
const { isConnectionError } = require('point_of_sale.utils');
const { Gui } = require('point_of_sale.Gui');
class SetSaleOrderButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
}
get currentOrder() {
return this.env.pos.get_order();
}
async onClick() {
try {
// ping the server, if no error, show the screen
// Use rpc from services which resolves even when this
// component is destroyed (removed together with the popup).
await this.env.services.rpc({
model: 'sale.order',
method: 'browse',
args: [[]],
kwargs: { context: this.env.session.user_context },
});
// LegacyComponent doesn't work the same way as before.
// We need to use Gui here to show the screen. This will work
// because ui methods in Gui is bound to the root component.
const screen = this.env.isMobile ? 'MobileSaleOrderManagementScreen' : 'SaleOrderManagementScreen';
Gui.showScreen(screen);
} catch (error) {
if (isConnectionError(error)) {
this.showPopup('ErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Cannot access order management screen if offline.'),
});
} else {
throw error;
}
}
}
}
SetSaleOrderButton.template = 'SetSaleOrderButton';
ProductScreen.addControlButton({
component: SetSaleOrderButton,
condition: function() {
return true;
},
});
Registries.Component.add(SetSaleOrderButton);
return SetSaleOrderButton;
});

View file

@ -1,83 +0,0 @@
odoo.define('pos_sale.models', function (require) {
"use strict";
var { Order, Orderline } = require('point_of_sale.models');
const Registries = require('point_of_sale.Registries');
const PosSaleOrder = (Order) => class PosSaleOrder extends Order {
//@override
select_orderline(orderline) {
super.select_orderline(...arguments);
if (orderline && orderline.product.id === this.pos.config.down_payment_product_id[0]) {
this.pos.numpadMode = 'price';
}
}
//@override
_get_ignored_product_ids_total_discount() {
const productIds = super._get_ignored_product_ids_total_discount(...arguments);
productIds.push(this.pos.config.down_payment_product_id[0]);
return productIds;
}
}
Registries.Model.extend(Order, PosSaleOrder);
const PosSaleOrderline = (Orderline) => class PosSaleOrderline extends Orderline {
constructor(obj, options) {
super(...arguments);
// It is possible that this orderline is initialized using `init_from_JSON`,
// meaning, it is loaded from localStorage or from export_for_ui. This means
// that some fields has already been assigned. Therefore, we only set the options
// when the original value is falsy.
this.sale_order_origin_id = this.sale_order_origin_id || options.sale_order_origin_id;
this.sale_order_line_id = this.sale_order_line_id || options.sale_order_line_id;
this.down_payment_details = this.down_payment_details || options.down_payment_details;
this.customerNote = this.customerNote || options.customer_note;
}
init_from_JSON(json) {
super.init_from_JSON(...arguments);
this.sale_order_origin_id = json.sale_order_origin_id;
this.sale_order_line_id = json.sale_order_line_id;
this.down_payment_details = json.down_payment_details && JSON.parse(json.down_payment_details);
}
export_as_JSON() {
const json = super.export_as_JSON(...arguments);
json.sale_order_origin_id = this.sale_order_origin_id;
json.sale_order_line_id = this.sale_order_line_id;
json.down_payment_details = this.down_payment_details && JSON.stringify(this.down_payment_details);
return json;
}
get_sale_order(){
if(this.sale_order_origin_id) {
let value = {
'name': this.sale_order_origin_id.name,
'details': this.down_payment_details || false
}
return value;
}
return false;
}
export_for_printing() {
var json = super.export_for_printing(...arguments);
json.down_payment_details = this.down_payment_details;
if (this.sale_order_origin_id) {
json.so_reference = this.sale_order_origin_id.name;
}
return json;
}
/**
* Set quantity based on the give sale order line.
* @param {'sale.order.line'} saleOrderLine
*/
setQuantityFromSOL(saleOrderLine) {
if (this.product.type === 'service' && !['sent', 'draft'].includes(this.sale_order_origin_id.state)) {
this.set_quantity(saleOrderLine.qty_to_invoice);
} else {
this.set_quantity(saleOrderLine.product_uom_qty - Math.max(saleOrderLine.qty_delivered, saleOrderLine.qty_invoiced));
}
}
}
Registries.Model.extend(Orderline, PosSaleOrderline);
});

View file

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="MobileSaleOrderManagementScreen" class="screen-full-width" owl="1">
<div class="order-management-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
<div t-if="mobileState.showDetails" class="leftpane">
<OrderDetails order="orderManagementContext.selectedOrder" />
<div class="pads">
<div class="control-buttons">
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
<t t-component="cb.component" t-key="cb.name" />
</t>
</div>
<div class="subpads">
<ActionpadWidget partner="selectedPartner" />
<NumpadWidget />
</div>
</div>
<div class="back-to-list" t-on-click="() => { mobileState.showDetails = false; }">
<span>Back to list</span>
</div>
</div>
<div t-else="" class="rightpane">
<div class="flex-container">
<SaleOrderManagementControlPanel />
<SaleOrderList orders="orders" initHighlightedOrder="orderManagementContext.selectedOrder" />
</div>
</div>
</div>
</div>
</templates>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SaleOrderList" owl="1">
<div class="orders">
<div class="header-row" t-att-class="{ oe_hidden: env.isMobile }">
<div class="col name">Order</div>
<div class="col date">Date</div>
<div class="col customer">Customer</div>
<div class="col salesman">Salesperson</div>
<div class="col end total">Total</div>
<div class="col state">State</div>
</div>
<div class="order-list">
<t t-foreach="props.orders" t-as="order" t-key="order.id">
<SaleOrderRow order="order" highlightedOrder="highlightedOrder" />
</t>
</div>
</div>
</t>
</templates>

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SaleOrderManagementControlPanel" owl="1">
<div class="control-panel">
<div class="item button back" t-on-click="() => this.trigger('close-screen')">
<i class="fa fa-angle-double-left"></i>
<span t-if="!env.isMobile"> Back</span>
</div>
<div class="item search-box">
<span class="icon">
<i class="fa fa-search" />
</span>
<input type="text" t-ref="autofocus" t-model="orderManagementContext.searchString" t-on-keydown="onInputKeydown" placeholder="E.g. customer: Steward, date: 2020-05-09" />
<span class="clear" t-on-click="() => this.trigger('clear-search')">
<i class="fa fa-remove" />
</span>
</div>
<div t-if="showPageControls" class="item">
<div class="page-controls">
<div class="previous" t-on-click="() => this.trigger('prev-page')">
<i class="fa fa-fw fa-caret-left" role="img" aria-label="Previous Order List" title="Previous Order List"></i>
</div>
<div class="next" t-on-click="() => this.trigger('next-page')">
<i class="fa fa-fw fa-caret-right" role="img" aria-label="Next Order List" title="Next Order List"></i>
</div>
</div>
<div class="page">
<span><t t-esc="pageNumber" /></span>
</div>
</div>
<div t-else="" class="item"></div>
</div>
</t>
</templates>

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SaleOrderManagementScreen" owl="1">
<div class="order-management-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
<div class="rightpane">
<div class="flex-container">
<SaleOrderManagementControlPanel />
<SaleOrderList orders="orders" initHighlightedOrder="orderManagementContext.selectedOrder" />
</div>
</div>
</div>
</t>
</templates>

View file

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SaleOrderRow" owl="1">
<div class="order-row"
t-att-class="{ highlight: highlighted }"
t-on-click="() => this.trigger('click-sale-order', props.order)">
<div class="col name">
<div t-if="env.isMobile">Order</div>
<div><t t-esc="name"/></div>
</div>
<div class="col date">
<div t-if="env.isMobile">Date</div>
<div><t t-esc="date"/></div>
</div>
<div class="col partner">
<div t-if="env.isMobile">Customer</div>
<div><t t-esc="partner"/></div>
</div>
<div class="col salesman">
<div t-if="env.isMobile">Salesman</div>
<div><t t-esc="salesman"/></div>
</div>
<div class="col end total">
<div t-if="env.isMobile">Total</div>
<div class="flex-container">
<div class="self-end">
<t t-esc="total"/>
</div>
<div t-if="showAmountUnpaid" class="self-end text-gray">
(left: <t t-esc="amountUnpaidRepr"/>)
</div>
</div>
</div>
<div class="col state">
<div t-if="env.isMobile">State</div>
<div><t t-esc="state"/></div>
</div>
</div>
</t>
</templates>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
<xpath expr="//ul[hasclass('info-list')]" position="inside">
<t t-if="props.line.get_sale_order()">
<li class="info orderline-sale-order">
<i class="fa fa-basket" role="img" aria-label="SO" title="SO"/>
<t t-esc="props.line.get_sale_order().name" />
</li>
<table t-if="props.line.get_sale_order().details" class="sale-order-info">
<t t-foreach='props.line.get_sale_order().details' t-as='line' t-key='line_index'>
<tr>
<td><t t-esc="line['product_uom_qty']"/>x</td>
<td style="max-width: 275px;">
<t t-esc="line['product_name']" />
</td>
<td>:</td>
<td><t t-esc="env.pos.format_currency(line['total'])" /> (tax incl.)</td>
</tr>
</t>
</table>
</t>
</xpath>
</t>
</templates>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="OrderReceipt" t-inherit="point_of_sale.OrderLinesReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-foreach]" position="inside">
<div class="pos-receipt-left-padding" t-if="line.so_reference">From <t t-esc="line.so_reference"/></div>
<div class="pos-receipt-left-padding" t-if="line.down_payment_details">
<table class="sale-order-info">
<tr t-foreach='line.down_payment_details' t-as='line' t-key='line_index'>
<td><t t-esc="line['product_uom_qty']" />x</td>
<td style="max-width: 200px;">
<t t-esc="line['product_name']" />
</td>
</tr>
</table>
</div>
</xpath>
</t>
</templates>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SetSaleOrderButton" owl="1">
<div class="control-button o_sale_order_button">
<i class="fa fa-link" role="img" aria-label="Set Sale Order"
title="Set Sale Order" /> Quotation/Order
</div>
</t>
</templates>

View file

@ -1,84 +0,0 @@
odoo.define('pos_sale.tour.ProductScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
const { Do, Check, Execute } = require('point_of_sale.tour.ProductScreenTourMethods');
class DoExt extends Do {
clickQuotationButton() {
return [
{
content: 'click quotation button',
trigger: '.o_sale_order_button',
}
];
}
selectFirstOrder() {
return [
{
content: `select order`,
trigger: `.order-row .col.name:first`,
},
{
content: `click on select the order`,
trigger: `.selection-item:contains('Settle the order')`,
}
];
}
selectNthOrder(n) {
return [
{
content: `select order`,
trigger: `.order-list .order-row:nth-child(${n})`,
},
{
content: `click on select the order`,
trigger: `.selection-item:contains('Settle the order')`,
}
];
}
downPaymentFirstOrder() {
return [
{
content: `select order`,
trigger: `.order-row .col.name:first`,
},
{
content: `click on select the order`,
trigger: `.selection-item:contains('Apply a down payment')`,
},
{
content: `click on +10 button`,
trigger: `.mode-button.add:contains('+10')`,
},
{
content: `click on ok button`,
trigger: `.button.confirm`,
}
];
}
}
class CheckExt extends Check{
checkCustomerNotes(note) {
return [
{
content: `check customer notes`,
trigger: `.orderline-note:contains(${note})`,
}
];
}
checkOrdersListEmpty() {
return [
{
content: 'Check that the orders list is empty',
trigger: '.order-list:not(:has(.order-row))',
}
]
}
}
return createTourMethods('ProductScreen', DoExt, CheckExt, Execute);
});

View file

@ -1,19 +0,0 @@
odoo.define('pos_sale.tour.ReceiptScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
const { Do, Check, Execute } = require('point_of_sale.tour.ReceiptScreenTourMethods');
class CheckExt extends Check{
checkCustomerNotes(note) {
return [
{
content: `check customer notes`,
trigger: `.pos-receipt-customer-note:contains(${note})`,
}
];
}
}
return createTourMethods('ReceiptScreen', Do, CheckExt, Execute);
});

View file

@ -0,0 +1,662 @@
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_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 ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
import * as PosSale from "@pos_sale/../tests/tours/utils/pos_sale_utils";
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 * as Utils from "@point_of_sale/../tests/generic_helpers/utils";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("PosSettleOrder", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Pizza Chicken", 9),
ProductScreen.clickNumpad("Qty", "2"), // Change the quantity of the product to 2
ProductScreen.selectedOrderlineHas("Pizza Chicken", 2),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
Chrome.clickOrders(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrderIncompatiblePartner", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// The second item in the list is the first sale.order.
PosSale.settleNthOrder(2),
ProductScreen.selectedOrderlineHas("product1", 1),
ProductScreen.totalAmountIs("10.00"),
// The first item in the list is the second sale.order.
// Selecting the 2nd sale.order should use a new order,
// therefore, the total amount will change.
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("product2", 1),
ProductScreen.totalAmountIs("11.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrder2", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.clickOrderline("Product A", "1"),
ProductScreen.selectedOrderlineHas("Product A", "1"),
ProductScreen.clickOrderline("Product B", "1"),
ProductScreen.clickNumpad("Qty", "0"),
ProductScreen.selectedOrderlineHas("Product B", "0"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.0" }),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PosRefundDownpayment", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.downPaymentFirstOrder("+10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
...ProductScreen.clickRefund(),
// Filter should be automatically 'Paid'.
TicketScreen.filterIs("Paid"),
TicketScreen.selectOrder("001"),
Order.hasLine({
productName: "Down Payment",
withClass: ".selected",
quantity: "1",
}),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrderRealTime", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.totalAmountIs(40),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrder3", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Product A", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank", true, { remaining: "0.0" }),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrderNotGroupable", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.totalAmountIs(28.98), // 3.5 * 8 * 1.15 * 90%
ProductScreen.selectedOrderlineHas("Product A", "0.5"),
ProductScreen.checkOrderlinesNumber(4),
ProductScreen.selectedOrderlineHas("Product A", "0.5", "4.14"),
].flat(),
});
registry.category("web_tour.tours").add("test_import_lot_groupable_and_non_groupable", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1, { loadSN: true }),
PosSale.selectedOrderLinesHasLots("Groupable Product", []),
ProductScreen.checkOrderlinesNumber(5),
ProductScreen.totalAmountIs(60),
ProductScreen.selectedOrderlineHas("Groupable Product", "1", "10"),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrderWithNote", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
Order.hasLine({
customerNote: "Customer note 2--Customer note 3",
}),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
// Check in the receipt
Order.hasLine({
customerNote: "Customer note 2--Customer note 3",
}),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleAndInvoiceOrder", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
Order.hasLine({}),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleAndInvoiceOrder2", {
steps: () =>
[
Chrome.startPoS(),
PosSale.settleNthOrder(1),
Order.hasLine({}),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PosOrderDoesNotRemainInList", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
PosSale.checkOrdersListEmpty(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleDraftOrder", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Test service product", "1", "50.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleCustomPrice", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Product A", "1", "100"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner AAA"),
ProductScreen.selectedOrderlineHas("Product A", "1", "100"),
].flat(),
});
registry.category("web_tour.tours").add("PoSSaleOrderWithDownpayment", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Down Payment (POS)"),
ProductScreen.totalAmountIs(980.0),
].flat(),
});
registry.category("web_tour.tours").add("test_settle_so_with_non_pos_groupable_uom", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Pomme de Terre", "0.5", "5.00"),
].flat(),
});
registry.category("web_tour.tours").add("PoSDownPaymentLinesPerTax", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.downPaymentFirstOrder("+20"),
Order.hasLine({
productName: "Down Payment",
quantity: "1",
price: "2.20",
}),
Order.hasLine({
productName: "Down Payment",
quantity: "1",
price: "1.00",
}),
Order.hasLine({
productName: "Down Payment",
quantity: "1",
price: "3.00",
}),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PoSApplyDownpayment", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.downPaymentFirstOrder("+10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PoSApplyDownpaymentInvoice", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.downPaymentFirstOrder("+10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PoSApplyDownpaymentInvoice2", {
steps: () =>
[
Chrome.startPoS(),
PosSale.downPaymentFirstOrder("+10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PosShipLaterNoDefault", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.clickPayButton(),
PaymentScreen.isShown(),
Utils.negateStep(PaymentScreen.shippingLaterHighlighted()),
].flat(),
});
registry.category("web_tour.tours").add("PosSaleTeam", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product"),
ProductScreen.totalAmountIs("100.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PosOrdersListDifferentCurrency", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickControlButton("Quotation/Order"),
{
content: "Check that no orders are displayed",
trigger: '.o_nocontent_help p:contains("No record found")',
},
].flat(),
});
registry.category("web_tour.tours").add("PoSDownPaymentAmount", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.downPaymentFirstOrder("+20"),
Order.hasLine({
productName: "Down Payment",
quantity: "1",
price: "20.0",
}),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrder4", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Product A", "1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickShipLaterButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrderShipLater", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(2),
ProductScreen.clickPayButton(),
PaymentScreen.clickShipLaterButton(),
PaymentScreen.shippingLaterHighlighted(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
ReceiptScreen.clickNextOrder(),
PosSale.settleNthOrder(1),
ProductScreen.clickPayButton(),
PaymentScreen.clickShipLaterButton(),
PaymentScreen.shippingLaterHighlighted(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PosSettleOrder5", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Product A", 1),
].flat(),
});
registry.category("web_tour.tours").add("PosSaleWarning", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Customer 2"),
{
content: "Check warning popup are displayed",
trigger:
'.modal-dialog:has(.modal-header:contains("Warning for A Test Customer 2")):has(.modal-body:contains("Cannot afford our services"))',
},
{
trigger: ".modal-footer button",
run: "click",
},
ProductScreen.customerIsSelected("A Test Customer 2"),
ProductScreen.clickDisplayedProduct("Letter Tray", true, "1"),
ProductScreen.selectedOrderlineHas("Letter Tray", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Customer 1"),
{
content: "Check warning popup are displayed",
trigger:
'.modal-dialog:has(.modal-header:contains("Warning for A Test Customer 1")):has(.modal-body:contains("Highly infectious disease"))',
},
{
trigger: ".modal-footer button",
run: "click",
},
ProductScreen.customerIsSelected("A Test Customer 1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("PoSSettleQuotation", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("POSSalePaymentScreenInvoiceOrder", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product Test", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA - Test Partner invoice"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
Chrome.waitRequest(),
].flat(),
});
registry.category("web_tour.tours").add("test_settle_order_with_lot", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1, { loadSN: true }),
PosSale.selectedOrderLinesHasLots("Product A", ["1001", "1002"]),
].flat(),
});
registry.category("web_tour.tours").add("test_down_payment_displayed", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.downPaymentFirstOrder("+10"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
PosSale.settleNthOrder(1),
Order.hasLine({
productName: "Down Payment",
quantity: "1.0",
price: "-1.15",
}),
].flat(),
});
registry.category("web_tour.tours").add("test_sale_order_fp_different_from_partner_one", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleSaleOrderByPrice("20.00"),
ProductScreen.checkTaxAmount("10.00"),
ProductScreen.checkFiscalPosition("Partner FP"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.clickNextOrder(),
PosSale.settleSaleOrderByPrice("10.00"),
ProductScreen.checkTaxAmount("0.00"),
ProductScreen.checkFiscalPosition("Sale Order FP"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptIsThere(),
ReceiptScreen.clickNextOrder(),
].flat(),
});
registry.category("web_tour.tours").add("test_quantity_updated_settle", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
ProductScreen.clickNumpad("2"),
Order.hasLine({ productName: "Product A", quantity: "2.0" }),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.clickNextOrder(),
PosSale.settleNthOrder(1),
Order.hasLine({
productName: "Product A",
quantity: "3.0",
price: "34.50",
}),
].flat(),
});
registry.category("web_tour.tours").add("test_multiple_lots_sale_order_1", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1),
Order.hasLine({ productName: "Product", quantity: "6.0" }),
].flat(),
});
registry.category("web_tour.tours").add("test_multiple_lots_sale_order_2", {
steps: () =>
[
Chrome.startPoS(),
PosSale.settleNthOrder(1, { loadSN: false }),
Order.hasLine({ productName: "Product", quantity: "6.0" }),
{
content: "Check that the line-lot-icon has text-danger class",
trigger: `.order-container .orderline:has(.product-name:contains("Product")) .line-lot-icon.text-danger`,
},
].flat(),
});
registry.category("web_tour.tours").add("test_multiple_lots_sale_order_3", {
steps: () =>
[
Chrome.startPoS(),
PosSale.settleNthOrder(1, { loadSN: true }),
PosSale.selectedOrderLinesHasLots("Product", ["1002"]),
Utils.negateStep(...PosSale.selectedOrderLinesHasLots("Product", ["1001"])),
ProductScreen.selectedOrderlineHas("Product", "2.00"),
ProductScreen.clickOrderline("Product", "4"),
PosSale.selectedOrderLinesHasLots("Product", ["1001"]),
ProductScreen.selectedOrderlineHas("Product", "4.00"),
Utils.negateStep(...PosSale.selectedOrderLinesHasLots("Product", ["1002"])),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_selected_partner_quotation_loading", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner 1"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Product A", "1.00"),
Chrome.createFloatingOrder(),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner 2"),
PosSale.settleNthOrder(1),
ProductScreen.selectedOrderlineHas("Product B", "2.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_ecommerce_paid_order_is_hidden_in_pos", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner 1"),
PosSale.checkOrdersListEmpty(),
].flat(),
});
registry.category("web_tour.tours").add("test_ecommerce_unpaid_order_is_shown_in_pos", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner 1"),
PosSale.checkOrdersListNotEmpty(),
].flat(),
});
registry.category("web_tour.tours").add("test_settle_groupable_lot_total_amount", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
PosSale.settleNthOrder(1, { loadSN: true }),
Order.hasTotal("12.00"),
].flat(),
});

View file

@ -1,201 +0,0 @@
odoo.define('pos_sale.tour', function (require) {
'use strict';
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { ProductScreen } = require('pos_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('pos_sale.tour.ReceiptScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
// signal to start generating steps
// when finished, steps can be taken from getSteps
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.check.selectedOrderlineHas('Pizza Chicken', 9);
ProductScreen.do.pressNumpad('Qty 2'); // Change the quantity of the product to 2
ProductScreen.check.selectedOrderlineHas('Pizza Chicken', 2);
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
Chrome.do.clickTicketButton();
Tour.register('PosSettleOrder', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
// The second item in the list is the first sale.order.
ProductScreen.do.selectNthOrder(2);
ProductScreen.check.selectedOrderlineHas('product1', 1);
ProductScreen.check.totalAmountIs("10.00");
ProductScreen.do.clickQuotationButton();
// The first item in the list is the second sale.order.
// Selecting the 2nd sale.order should use a new order,
// therefore, the total amount will change.
ProductScreen.do.selectNthOrder(1);
ProductScreen.check.selectedOrderlineHas('product2', 1);
ProductScreen.check.totalAmountIs("11.00");
Tour.register('PosSettleOrderIncompatiblePartner', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.do.clickOrderline('[A001] Product A', '1');
ProductScreen.check.selectedOrderlineHas('[A001] Product A', '1.00');
ProductScreen.do.clickOrderline('[A002] Product B', '1');
ProductScreen.do.pressNumpad('Qty 0');
ProductScreen.check.selectedOrderlineHas('[A002] Product B', '0.00');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Tour.register('PosSettleOrder2', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.do.clickOrderline("Product A", "1");
ProductScreen.check.selectedOrderlineHas('Product A', '1.00');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Tour.register('PosSettleOrder3', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.check.totalAmountIs(40);
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
Chrome.do.clickTicketButton();
Tour.register('PosSettleOrderRealTime', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.downPaymentFirstOrder();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickRefund();
// Filter should be automatically 'Paid'.
TicketScreen.check.filterIs('Paid');
TicketScreen.do.selectOrder('-0001');
TicketScreen.do.clickOrderline('Down Payment');
TicketScreen.do.pressNumpad('1');
TicketScreen.do.confirmRefund();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.do.clickNextOrder();
Tour.register('PosRefundDownpayment', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.check.totalAmountIs(28.98); // 3.5 * 8 * 1.15 * 90%
ProductScreen.do.clickOrderline("Product A", '0.5');
ProductScreen.check.checkOrderlinesNumber(4);
ProductScreen.check.selectedOrderlineHas('Product A', '0.5', '4.14');
Tour.register('PosSettleOrderNotGroupable', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.check.checkCustomerNotes("Customer note 2--Customer note 3");
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.checkCustomerNotes("Customer note 2--Customer note 3");
ReceiptScreen.do.clickNextOrder();
Tour.register('PosSettleOrderWithNote', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickQuotationButton();
ProductScreen.check.checkOrdersListEmpty();
Tour.register('PosOrderDoesNotRemainInList', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.check.selectedOrderlineHas('product_a', '1', '100');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('partner_a');
ProductScreen.check.selectedOrderlineHas('product_a', '1', '100');
Tour.register('PosSettleCustomPrice', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.check.selectedOrderlineHas('Test service product', '1.00', '50.00');
Tour.register('PosSettleDraftOrder', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.selectFirstOrder();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickShipLaterButton()
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Tour.register('PosSettleOrderShipLater', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickQuotationButton();
ProductScreen.do.downPaymentFirstOrder();
ProductScreen.check.selectedOrderlineHas('Down Payment', '1', '10.00');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.do.clickNextOrder();
Tour.register('PoSDownPaymentAmount', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,324 @@
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 { escapeRegExp } from "@web/core/utils/strings";
import { registry } from "@web/core/registry";
export function clickDownPaymentNumpad(num) {
return {
content: `click discount numpad button: ${num}`,
trigger: `.o_dialog div.numpad button:contains(/^${escapeRegExp(num)}$/)`,
run: "click",
};
}
export function addDownPayment(percentage, soNth, downPaymentType) {
const steps = [
ProductScreen.clickControlButton("Quotation/Order"),
{
content: "Select the first SO",
trigger: `.o_sale_order .o_data_row:nth-child(${soNth}) .o_data_cell:nth-child(1)`,
run: "click",
},
];
if (downPaymentType === "percent") {
steps.push({
content: "Select 'Apply a down payment (percentage)'",
trigger: ".modal-body button:contains('percentage')",
run: "click",
});
} else {
steps.push({
content: "Select 'Apply a down payment (fixed amount)'",
trigger: ".modal-body button:contains('fixed amount')",
run: "click",
});
}
for (const num of percentage.split("")) {
steps.push(clickDownPaymentNumpad(num));
}
steps.push({
content: "Select 'Apply'",
trigger: ".modal-dialog button.btn-primary:contains('Apply')",
run: "click",
});
return steps;
}
export function payAndInvoice(totalAmount) {
return [
ProductScreen.clickPayButton(),
PaymentScreen.totalIs(totalAmount),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.remainingIs("0.0"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.receiptAmountTotalIs(totalAmount),
ReceiptScreen.clickNextOrder(),
];
}
registry
.category("web_tour.tours")
.add("test_taxes_l10n_in_pos_downpayment_round_per_line_price_excluded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.10"),
...payAndInvoice("0.73"),
...addDownPayment("0.73", 2, "fixed"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.10"),
...payAndInvoice("0.73"),
...addDownPayment("7", 3, "percent"),
ProductScreen.checkTotalAmount("2.56"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.56"),
...addDownPayment("2.56", 4, "fixed"),
ProductScreen.checkTotalAmount("2.56"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.56"),
...addDownPayment("18", 5, "percent"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
...addDownPayment("6.60", 6, "fixed"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_in_pos_downpayment_round_globally_price_excluded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.1"),
...payAndInvoice("0.73"),
...addDownPayment("0.73", 2, "fixed"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.1"),
...payAndInvoice("0.73"),
...addDownPayment("7", 3, "percent"),
ProductScreen.checkTotalAmount("2.57"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.57"),
...addDownPayment("2.57", 4, "fixed"),
ProductScreen.checkTotalAmount("2.57"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.57"),
...addDownPayment("18", 5, "percent"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
...addDownPayment("6.60", 6, "fixed"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_in_pos_downpayment_round_per_line_price_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.10"),
...payAndInvoice("0.73"),
...addDownPayment("0.73", 2, "fixed"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.10"),
...payAndInvoice("0.73"),
...addDownPayment("7", 3, "percent"),
ProductScreen.checkTotalAmount("2.56"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.56"),
...addDownPayment("2.56", 4, "fixed"),
ProductScreen.checkTotalAmount("2.56"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.56"),
...addDownPayment("18", 5, "percent"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
...addDownPayment("6.60", 6, "fixed"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_in_pos_downpayment_round_globally_price_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.10"),
...payAndInvoice("0.73"),
...addDownPayment("0.73", 2, "fixed"),
ProductScreen.checkTotalAmount("0.73"),
ProductScreen.checkTaxAmount("0.10"),
...payAndInvoice("0.73"),
...addDownPayment("7", 3, "percent"),
ProductScreen.checkTotalAmount("2.57"),
ProductScreen.checkTaxAmount("0.33"),
...payAndInvoice("2.57"),
...addDownPayment("2.57", 4, "fixed"),
ProductScreen.checkTotalAmount("2.57"),
ProductScreen.checkTaxAmount("0.34"),
...payAndInvoice("2.57"),
...addDownPayment("18", 5, "percent"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
...addDownPayment("6.60", 6, "fixed"),
ProductScreen.checkTotalAmount("6.60"),
ProductScreen.checkTaxAmount("0.87"),
...payAndInvoice("6.60"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_br_pos_downpayment_round_per_line_price_excluded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("1.92"),
ProductScreen.checkTaxAmount("0.63"),
...payAndInvoice("1.92"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_br_pos_downpayment_round_globally_price_excluded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("1.92"),
ProductScreen.checkTaxAmount("0.63"),
...payAndInvoice("1.92"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_br_pos_downpayment_round_per_line_price_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("1.92"),
ProductScreen.checkTaxAmount("0.63"),
...payAndInvoice("1.92"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_br_pos_downpayment_round_globally_price_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("1.92"),
ProductScreen.checkTaxAmount("0.63"),
...payAndInvoice("1.92"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_be_pos_downpayment_round_per_line_price_excluded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.86"),
ProductScreen.checkTaxAmount("0.15"),
...payAndInvoice("0.86"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_be_pos_downpayment_round_globally_price_excluded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.86"),
ProductScreen.checkTaxAmount("0.15"),
...payAndInvoice("0.86"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_be_pos_downpayment_round_per_line_price_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.86"),
ProductScreen.checkTaxAmount("0.15"),
...payAndInvoice("0.86"),
].flat(),
});
registry
.category("web_tour.tours")
.add("test_taxes_l10n_be_pos_downpayment_round_globally_price_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
...addDownPayment("2", 1, "percent"),
ProductScreen.checkTotalAmount("0.86"),
ProductScreen.checkTaxAmount("0.15"),
...payAndInvoice("0.86"),
].flat(),
});

View file

@ -0,0 +1,97 @@
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 Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
export function selectNthOrder(n) {
return [
...ProductScreen.clickControlButton("Quotation/Order"),
{
content: `select nth order`,
trigger: `.modal:not(.o_inactive_modal) table.o_list_table tbody tr.o_data_row:nth-child(${n}) td`,
run: "click",
},
];
}
export function settleSaleOrderByPrice(price) {
return [
...ProductScreen.clickControlButton("Quotation/Order"),
{
content: `select sale order with price ${price}`,
trigger: `.modal:not(.o_inactive_modal) table.o_list_table tbody tr.o_data_row td:contains('${price}')`,
run: "click",
},
{
content: `Choose to settle the order`,
trigger: `.modal:not(.o_inactive_modal) .selection-item:contains('Settle the order')`,
run: "click",
},
];
}
export function settleNthOrder(n, options = {}) {
const { loadSN } = options;
const step = [
...selectNthOrder(n),
{
content: `Choose to settle the order`,
trigger: `.modal:not(.o_inactive_modal) .selection-item:contains('Settle the order')`,
run: "click",
},
];
if (loadSN !== undefined) {
step.push({
content: `Choose to auto link the lot number to the order line`,
trigger: `.modal-content:contains('Do you want to load the SN/Lots linked to the Sales Order?') button:contains('${
loadSN ? "Ok" : "Cancel"
}')`,
run: "click",
});
}
step.push({
trigger: "body:not(:has(.modal))",
});
return step;
}
export function downPaymentFirstOrder(amount) {
return [
...selectNthOrder(1),
{
content: `click on select the order`,
trigger: `.selection-item:contains('Apply a down payment')`,
run: "click",
},
Numpad.click(amount),
Dialog.confirm("Apply"),
];
}
export function checkOrdersListEmpty() {
return [
...ProductScreen.clickControlButton("Quotation/Order"),
{
content: "Check that the orders list is empty",
trigger: "p:contains(No record found)",
},
];
}
export function selectedOrderLinesHasLots(productName, lots) {
const getSerialStep = (index, serialNumber) => ({
content: `check lot${index} is linked`,
trigger: `.info-list li:contains(${serialNumber})`,
});
const lotSteps = lots.reduce((acc, serial, i) => acc.concat(getSerialStep(i, serial)), []);
return [...ProductScreen.selectedOrderlineHas(productName), ...lotSteps];
}
export function checkOrdersListNotEmpty() {
return [
...ProductScreen.clickControlButton("Quotation/Order"),
{
content: "Check that the orders list is not empty",
trigger: ".o_data_row",
},
];
}

View file

@ -0,0 +1,55 @@
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
test("Displays the table with details of the down payment", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productDownPayment = store.models["product.template"].get(105);
const sol1 = store.models["sale.order.line"].get(1);
const sol2 = store.models["sale.order.line"].get(2);
const line = await store.addLineToOrder(
{
product_tmpl_id: productDownPayment,
sale_order_origin_id: 1,
down_payment_details: [
{
product_name: sol1.display_name,
product_uom_qty: sol1.product_uom_qty,
price_unit: sol1.price_unit,
total: sol1.price_total,
},
{
product_name: sol2.display_name,
product_uom_qty: sol2.product_uom_qty,
price_unit: sol2.price_unit,
total: sol2.price_total,
},
],
qty: 1,
},
order
);
const comp = await mountWithCleanup(Orderline, { props: { line } });
const saleOrderInfo = ".orderline .info-list .sale-order-info";
const cell = (tr, td) => `${saleOrderInfo} tr:nth-child(${tr}) td:nth-child(${td})`;
expect(comp.line).toEqual(line);
expect(saleOrderInfo).toBeVisible();
expect(`${saleOrderInfo} tr`).toHaveCount(2);
expect(cell(1, 1)).toHaveText("5x");
expect(cell(1, 2)).toHaveText("Product 1");
expect(cell(1, 4)).toHaveText(`$ 500.00 (tax incl.)`);
expect(cell(2, 1)).toHaveText("3x");
expect(cell(2, 2)).toHaveText("Product 2");
expect(cell(2, 4)).toHaveText(`$ 150.00 (tax incl.)`);
});

View file

@ -0,0 +1,11 @@
import { PosConfig } from "@point_of_sale/../tests/unit/data/pos_config.data";
PosConfig._records = PosConfig._records.map((config) => {
if (config.id === 1) {
return {
...config,
down_payment_product_id: 105,
};
}
return config;
});

View file

@ -0,0 +1,15 @@
import { patch } from "@web/core/utils/patch";
import { PosOrderLine } from "@point_of_sale/../tests/unit/data/pos_order_line.data";
patch(PosOrderLine.prototype, {
_load_pos_data_fields() {
return [
...super._load_pos_data_fields(),
"sale_order_origin_id",
"sale_order_line_id",
"down_payment_details",
"settled_order_id",
"settled_invoice_id",
];
},
});

View file

@ -0,0 +1,8 @@
import { patch } from "@web/core/utils/patch";
import { PosSession } from "@point_of_sale/../tests/unit/data/pos_session.data";
patch(PosSession.prototype, {
_load_pos_data_models() {
return [...super._load_pos_data_models(), "sale.order", "sale.order.line"];
},
});

View file

@ -0,0 +1,17 @@
import { ProductProduct } from "@point_of_sale/../tests/unit/data/product_product.data";
ProductProduct._records = [
...ProductProduct._records,
{
id: 105,
product_tmpl_id: 105,
lst_price: 0,
standard_price: 0,
display_name: "Down Payment (POS)",
product_tag_ids: [],
barcode: false,
default_code: false,
product_template_attribute_value_ids: [],
product_template_variant_value_ids: [],
},
];

View file

@ -0,0 +1,46 @@
import { patch } from "@web/core/utils/patch";
import { ProductTemplate } from "@point_of_sale/../tests/unit/data/product_template.data";
patch(ProductTemplate.prototype, {
_load_pos_data_fields() {
return [...super._load_pos_data_fields(), "sale_line_warn_msg", "invoice_policy"];
},
});
ProductTemplate._records = [
...ProductTemplate._records,
{
id: 105,
display_name: "Down Payment (POS)",
standard_price: 0,
categ_id: false,
pos_categ_ids: [],
taxes_id: [],
barcode: false,
name: "Down Payment (POS)",
list_price: 0,
is_favorite: false,
default_code: false,
to_weight: false,
uom_id: 1,
description_sale: false,
description: false,
tracking: "none",
type: "service",
service_tracking: "no",
is_storable: false,
write_date: "2025-07-03 17:04:14",
color: 0,
pos_sequence: 5,
available_in_pos: true,
attribute_line_ids: [],
active: true,
image_128: false,
sequence: 1,
combo_ids: [],
product_variant_ids: [7],
public_description: false,
pos_optional_product_ids: [],
product_tag_ids: [],
},
];

View file

@ -0,0 +1,8 @@
import { patch } from "@web/core/utils/patch";
import { ResPartner as MailResPartner } from "@mail/../tests/mock_server/mock_models/res_partner";
patch(MailResPartner.prototype, {
_load_pos_data_fields() {
return [...super._load_pos_data_fields(), "sale_warn_msg"];
},
});

View file

@ -0,0 +1,69 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models } from "@web/../tests/web_test_helpers";
export class SaleOrder extends models.ServerModel {
_name = "sale.order";
_load_pos_data_fields() {
return [
"name",
"state",
"user_id",
"order_line",
"partner_id",
"pricelist_id",
"fiscal_position_id",
"amount_total",
"amount_untaxed",
"amount_unpaid",
"picking_ids",
"partner_shipping_id",
"partner_invoice_id",
"date_order",
"write_date",
];
}
_records = [
{
id: 1,
name: "S00001",
state: "sale",
order_line: [1, 2],
partner_id: 3,
pricelist_id: 1,
fiscal_position_id: 1,
amount_total: 650,
amount_untaxed: 500,
amount_unpaid: 650,
partner_shipping_id: 3,
partner_invoice_id: 3,
date_order: "2025-07-03 17:04:14",
write_date: "2025-07-03 17:04:14",
},
];
async load_sale_order_from_pos(id, config_id) {
const order = this.env["sale.order"].find((order) => order.id === id);
const orderLines = this.env["sale.order.line"].filter((line) =>
order.order_line.includes(line.id)
);
const partner = this.env["res.partner"].find((partner) => partner.id === order.partner_id);
const productProducts = this.env["product.product"].filter((product) =>
orderLines.map((line) => line.product_id).includes(product.id)
);
const productTemplates = this.env["product.template"].filter((template) =>
productProducts.map((p) => p.product_tmpl_id).includes(template.id)
);
return {
"sale.order": [order],
"sale.order.line": orderLines,
"res.partner": [partner],
"product.product": productProducts,
"product.template": productTemplates,
};
}
}
patch(hootPosModels, [...hootPosModels, SaleOrder]);

View file

@ -0,0 +1,83 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models, MockServer } from "@web/../tests/web_test_helpers";
export class SaleOrderLine extends models.ServerModel {
_name = "sale.order.line";
_load_pos_data_fields() {
return [
"discount",
"display_name",
"price_total",
"price_unit",
"product_id",
"product_uom_qty",
"qty_delivered",
"qty_invoiced",
"qty_to_invoice",
"display_type",
"name",
"tax_ids",
"is_downpayment",
"extra_tax_data",
"write_date",
"is_repair_line",
];
}
_records = [
{
id: 1,
display_name: "Product 1",
product_id: 5,
product_uom_qty: 5,
price_unit: 100,
price_total: 500,
discount: 0,
qty_delivered: 0,
qty_invoiced: 0,
qty_to_invoice: 5,
display_type: false,
name: "Product 1",
tax_ids: [],
is_downpayment: false,
extra_tax_data: {},
write_date: "2025-07-03 17:04:14",
},
{
id: 2,
display_name: "Product 2",
product_id: 6,
product_uom_qty: 3,
price_unit: 50,
price_total: 150,
discount: 0,
qty_delivered: 0,
qty_invoiced: 0,
qty_to_invoice: 3,
display_type: false,
name: "Product 2",
tax_ids: [],
is_downpayment: false,
extra_tax_data: {},
write_date: "2025-07-03 17:04:14",
},
];
async read_converted(ids) {
const model = MockServer.env[this._name];
const posFields = model._load_pos_data_fields();
const records = model.search_read(
[["id", "in", ids]],
posFields,
false,
false,
false,
false
);
return records;
}
}
patch(hootPosModels, [...hootPosModels, SaleOrderLine]);

View file

@ -0,0 +1,118 @@
import { test, expect, describe } from "@odoo/hoot";
import { setupPosEnv, getFilledOrder } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
describe("saleDetails", () => {
test("down payment details as array", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productDownPayment = store.models["product.template"].get(105);
const line = await store.addLineToOrder(
{
product_tmpl_id: productDownPayment,
down_payment_details: [
{
product_name: "Product 1",
product_uom_qty: 2,
total: 100,
},
{
product_name: "Product 2",
product_uom_qty: 1,
total: 50,
},
],
qty: 1,
},
order
);
const saleDetails = line.saleDetails;
expect(saleDetails).toEqual([
{
product_uom_qty: 2,
product_name: "Product 1",
total: "$\u00a0100.00",
},
{
product_uom_qty: 1,
product_name: "Product 2",
total: "$\u00a050.00",
},
]);
});
test("down payment details as stringified JSON", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productDownPayment = store.models["product.template"].get(105);
const line = await store.addLineToOrder(
{
product_tmpl_id: productDownPayment,
down_payment_details: JSON.stringify([
{
product_name: "Product 1",
product_uom_qty: 2,
total: 100,
},
{
product_name: "Product 2",
product_uom_qty: 1,
total: 50,
},
]),
qty: 1,
},
order
);
const saleDetails = line.saleDetails;
expect(saleDetails).toEqual([
{
product_uom_qty: 2,
product_name: "Product 1",
total: "$\u00a0100.00",
},
{
product_uom_qty: 1,
product_name: "Product 2",
total: "$\u00a050.00",
},
]);
});
});
describe("setQuantityFromSOL", () => {
test("service product, state != sent/draft → qty_to_invoice", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const line = order.lines[0];
line.product_id.type = "service";
line.sale_order_origin_id = { state: "sale" }; // not 'sent' or 'draft'
const saleOrderLine = { qty_to_invoice: 2 };
await line.setQuantityFromSOL(saleOrderLine);
expect(line.qty).toBe(2);
});
test("non-service product → qty = uom_qty - max(delivered, invoiced)", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const line = order.lines[0];
line.product_id.type = "consu";
const saleOrderLine = {
product_uom_qty: 8,
qty_delivered: 1,
qty_invoiced: 2,
};
await line.setQuantityFromSOL(saleOrderLine);
expect(line.qty).toBe(6); // 8 - max(1,2)
});
});

View file

@ -0,0 +1,180 @@
import { test, expect, describe } from "@odoo/hoot";
import { setupPosEnv, getFilledOrder } from "@point_of_sale/../tests/unit/utils";
import { click, waitFor } from "@odoo/hoot-dom";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
describe("onClickSaleOrder", () => {
test("no selection → abort", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const button =
".modal-header:has(.modal-title:contains('What do you want to do?')) button[aria-label='Close']";
await waitFor(button);
await click(button);
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(2);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
});
test("settle → calls settleSO", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const button = ".modal-body button:contains('Settle the order')";
await waitFor(button);
await click(button);
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(4);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[0].price_unit).toBe(3);
expect(currentOrder.lines[0].prices.total_excluded).toBe(9);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
expect(currentOrder.lines[1].price_unit).toBe(3);
expect(currentOrder.lines[1].prices.total_excluded).toBe(6);
expect(currentOrder.lines[2].product_id.id).toBe(5);
expect(currentOrder.lines[2].qty).toBe(5);
expect(currentOrder.lines[2].price_unit).toBe(100);
expect(currentOrder.lines[2].prices.total_excluded).toBe(500);
expect(currentOrder.lines[3].product_id.id).toBe(6);
expect(currentOrder.lines[3].qty).toBe(3);
expect(currentOrder.lines[3].price_unit).toBe(50);
expect(currentOrder.lines[3].prices.total_excluded).toBe(150);
});
test("dpPercentage → calls downPaymentSO", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const buttonDownPaymentPercentage =
".modal-body button:contains('Apply a down payment (percentage)')";
await waitFor(buttonDownPaymentPercentage);
await click(buttonDownPaymentPercentage);
await waitFor(".modal-title:contains('Down Payment')");
await click(".modal-body .numpad .numpad-button[value='+50']");
await new Promise((resolve) => setTimeout(resolve, 50));
await click(".modal-footer .btn:contains('Apply')");
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(3);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[0].price_unit).toBe(3);
expect(currentOrder.lines[0].prices.total_excluded).toBe(9);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
expect(currentOrder.lines[1].price_unit).toBe(3);
expect(currentOrder.lines[1].prices.total_excluded).toBe(6);
expect(currentOrder.lines[2].product_id.id).toBe(105);
expect(currentOrder.lines[2].qty).toBe(1);
expect(currentOrder.lines[2].price_unit).toBe(325);
expect(currentOrder.lines[2].prices.total_excluded).toBe(325);
const comp = await mountWithCleanup(Orderline, {
props: { line: currentOrder.lines[2] },
});
const saleOrderInfo = ".orderline .info-list .sale-order-info";
const cell = (tr, td) => `${saleOrderInfo} tr:nth-child(${tr}) td:nth-child(${td})`;
expect(comp.line).toEqual(currentOrder.lines[2]);
expect(`${saleOrderInfo} tr`).toHaveCount(4);
expect(cell(1, 1)).toHaveText("5x");
expect(cell(1, 2)).toHaveText("TEST");
expect(cell(1, 4)).toHaveText(`$ 500.00 (tax incl.)`);
expect(cell(2, 1)).toHaveText("3x");
expect(cell(2, 2)).toHaveText("TEST 2");
expect(cell(2, 4)).toHaveText(`$ 150.00 (tax incl.)`);
});
test("dpAmount → calls downPaymentSO", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const buttonDownPaymentPercentage =
".modal-body button:contains('Apply a down payment (fixed amount)')";
await waitFor(buttonDownPaymentPercentage);
await click(buttonDownPaymentPercentage);
await waitFor(".modal-title:contains('Down Payment')");
await click(".modal-body .numpad .numpad-button[value='+50']");
await new Promise((resolve) => setTimeout(resolve, 50));
await click(".modal-footer .btn:contains('Apply')");
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(3);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[0].price_unit).toBe(3);
expect(currentOrder.lines[0].prices.total_excluded).toBe(9);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
expect(currentOrder.lines[1].price_unit).toBe(3);
expect(currentOrder.lines[1].prices.total_excluded).toBe(6);
expect(currentOrder.lines[2].product_id.id).toBe(105);
expect(currentOrder.lines[2].qty).toBe(1);
expect(currentOrder.lines[2].price_unit).toBe(50);
expect(currentOrder.lines[2].prices.total_excluded).toBe(50);
const comp = await mountWithCleanup(Orderline, {
props: { line: currentOrder.lines[2] },
});
const saleOrderInfo = ".orderline .info-list .sale-order-info";
const cell = (tr, td) => `${saleOrderInfo} tr:nth-child(${tr}) td:nth-child(${td})`;
expect(comp.line).toEqual(currentOrder.lines[2]);
expect(`${saleOrderInfo} tr`).toHaveCount(4);
expect(cell(1, 1)).toHaveText("5x");
expect(cell(1, 2)).toHaveText("TEST");
expect(cell(1, 4)).toHaveText(`$ 500.00 (tax incl.)`);
expect(cell(2, 1)).toHaveText("3x");
expect(cell(2, 2)).toHaveText("TEST 2");
expect(cell(2, 4)).toHaveText(`$ 150.00 (tax incl.)`);
});
});