mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 00:12:03 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -0,0 +1,4 @@
|
|||
.sale-order-info td {
|
||||
padding: 2px 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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]);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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());
|
||||
});
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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.)`);
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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",
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
@ -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"];
|
||||
},
|
||||
});
|
||||
|
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -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"];
|
||||
},
|
||||
});
|
||||
|
|
@ -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]);
|
||||
|
|
@ -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]);
|
||||
|
|
@ -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)
|
||||
});
|
||||
});
|
||||
|
|
@ -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.)`);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue