[REF] point_of_sale: unify order components
At the moment, the pos displays the selected order in multiple screens, such as the `ProductScreen`, the `TicketScreen`, the `SplitBillScreen`, the `self_order` app, but uses completely different implementations for each. This means that when a change needs to be made to the one, it generally needs to be made to the other as well, which is error-prone. In this commit we create generic `OrderWidget` and `Orderline` components, that have not dependencies on POS objects and we refactor the `ProductScreen`, the `TicketScreen` and the `SplitBillScreen` to use these components. The `OrderSummary` component was removed and it's contents are now in the `OrderWidget`. We also introduce a very simple new component called `CenteredIcon` that can be used whenever we want to show an icon that fills a whole div. In this commit we use it for showing the cart icon when the order is empty. In a future commit, the `pos_self_order` will be refactored in order to use the same `OrderWidget` and `Orderline` compoenents as the POS. Task: 3470365 In addition, this commit solves the issue from the task 3491303. closes odoo/odoo#132248 Related: odoo/enterprise#45972 Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="l10n_fr_pos_cert.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension">
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="inside">
|
||||
<t t-if="pos.is_french_country() !== false and props.line.price_manually_set === 'manual'">
|
||||
<li class="info">
|
||||
Old unit price:
|
||||
<span class="oldPrice">
|
||||
<s>
|
||||
<t t-out="env.utils.formatCurrency(props.line.get_taxed_lst_unit_price())" /> / Units
|
||||
</s>
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="l10n_fr_pos_cert.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//Orderline" position="inside" >
|
||||
<t t-if="pos.is_french_country() !== false and line.price_manually_set === 'manual'">
|
||||
<li class="info">
|
||||
Old unit price:
|
||||
<span class="oldPrice">
|
||||
<s>
|
||||
<t t-esc="env.utils.formatCurrency(line.get_taxed_lst_unit_price())" /> / Units
|
||||
</s>
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
export class CenteredIcon extends Component {
|
||||
static props = {
|
||||
icon: String,
|
||||
text: { type: String, optional: true },
|
||||
class: { type: String, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
class: "",
|
||||
};
|
||||
static template = xml`
|
||||
<div t-attf-class="{{props.class}} d-flex flex-column align-items-center justify-content-center h-100 w-100 text-muted">
|
||||
<i t-attf-class="fa {{props.icon}}" role="img" />
|
||||
<h3 t-if="props.text" t-esc="props.text" class="mt-2"/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -5,19 +5,19 @@ import { useAutofocus } from "@web/core/utils/hooks";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
|
||||
/**
|
||||
This component is meant to provide a "batteries included" api for working
|
||||
with inputs. It is well suited to work as a search bar or as a monetary input.
|
||||
Optional props allow handling debouncing, toggling between mobile and desktop views,
|
||||
autofocus, validation, and more.
|
||||
The only required prop is tModel, which is an array that represents the
|
||||
state variable that this component should read and write to. The first
|
||||
element of the array is the object returned by `useState` and second is
|
||||
either a `string` or an `int` representing the key to access the value.
|
||||
ex: normally you would write
|
||||
```<input t-model="state.userValues[nThValue].searchString"/>```
|
||||
with this component you can write
|
||||
```<Input tModel="[state.userValues[nThValue], "searchString"]"/>```
|
||||
*/
|
||||
* This component is meant to provide a "batteries included" api for working
|
||||
* with inputs. It is well suited to work as a search bar or as a monetary input.
|
||||
* Optional props allow handling debouncing, toggling between mobile and desktop views,
|
||||
* autofocus, validation, and more.
|
||||
* The only required prop is tModel, which is an array that represents the
|
||||
* state variable that this component should read and write to. The first
|
||||
* element of the array is the object returned by `useState` and second is
|
||||
* either a `string` or an `int` representing the key to access the value.
|
||||
* ex: normally you would write
|
||||
* ```<input t-model="state.userValues[nThValue].searchString"/>```
|
||||
* with this component you can write
|
||||
* ```<Input tModel="[state.userValues[nThValue], "searchString"]"/>```
|
||||
*/
|
||||
export class Input extends Component {
|
||||
static template = "point_of_sale.input";
|
||||
static props = {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component, useEffect, useRef } from "@odoo/owl";
|
||||
import { CenteredIcon } from "@point_of_sale/app/generic_components/centered_icon/centered_icon";
|
||||
|
||||
export class OrderWidget extends Component {
|
||||
static template = "point_of_sale.OrderWidget";
|
||||
static props = {
|
||||
lines: { type: Array, element: Object },
|
||||
slots: { type: Object },
|
||||
total: { type: String, optional: true },
|
||||
tax: { type: String, optional: true },
|
||||
};
|
||||
static components = { CenteredIcon };
|
||||
setup() {
|
||||
this.scrollableRef = useRef("scrollable");
|
||||
useEffect(() => {
|
||||
this.scrollableRef.el
|
||||
?.querySelector(".orderline.selected")
|
||||
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="point_of_sale.OrderWidget">
|
||||
<t t-if="props.lines?.length">
|
||||
<div t-ref="scrollable" class="order-container bg-view overflow-y-auto flex-grow-1 d-flex flex-column text-start">
|
||||
<!-- this is similar to doing `for i in range(mylist)` -->
|
||||
<!-- we do this instead of looping directly on the lines because the lines might not have -->
|
||||
<!-- an element that we could use as a `t-key` -->
|
||||
<t t-foreach="props.lines.map((el, idx) => idx)" t-as="i" t-key="i">
|
||||
<t t-slot="default" line="props.lines[i]"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="props.total or props.tax" class="order-summary w-100 py-2 px-3 bg-100 text-end fw-bolder fs-2 lh-sm">
|
||||
Total:
|
||||
<span t-esc="props.total" class="total"/>
|
||||
<div t-if="props.tax" class="fs-6 text-muted subentry">
|
||||
Taxes: <span t-esc="props.tax" class="tax"/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-slot="details"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<CenteredIcon icon="'fa-shopping-cart fa-4x'" text="'This order is empty'"/>
|
||||
<t t-slot="details"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* How to extend this component:
|
||||
*/
|
||||
export class Orderline extends Component {
|
||||
static template = "point_of_sale.Orderline";
|
||||
static props = {
|
||||
class: { type: Object, optional: true },
|
||||
line: {
|
||||
type: Object,
|
||||
shape: {
|
||||
productName: String,
|
||||
price: String,
|
||||
qty: String,
|
||||
unit: { type: String, optional: true },
|
||||
unitPrice: String,
|
||||
discount: { type: String, optional: true },
|
||||
comboParent: { type: String, optional: true },
|
||||
oldUnitPrice: { type: String, optional: true },
|
||||
customerNote: { type: String, optional: true },
|
||||
internalNote: { type: String, optional: true },
|
||||
},
|
||||
},
|
||||
slots: { type: Object, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
class: {},
|
||||
};
|
||||
}
|
||||
+1
-6
@@ -1,5 +1,5 @@
|
||||
.orderline {
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: $o-gray-200;
|
||||
}
|
||||
@@ -7,9 +7,4 @@
|
||||
&.selected {
|
||||
background-color: $o-component-active-bg;
|
||||
}
|
||||
|
||||
&.skip-change {
|
||||
color: #7F82AC;
|
||||
border-color: #7F82AC !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="point_of_sale.Orderline">
|
||||
<t t-set="line" t-value="props.line" />
|
||||
<li class="orderline w-100 p-2 lh-sm cursor-pointer" t-att-class="props.class">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="product-name d-inline-block flex-grow-1 fw-bolder pe-1 text-truncate">
|
||||
<span t-esc="line.productName"/>
|
||||
<t t-slot="product-name"/>
|
||||
</div>
|
||||
<div t-esc="line.price" class="price fw-bolder"/>
|
||||
</div>
|
||||
<ul class="info-list ms-2">
|
||||
<li>
|
||||
<em t-esc="line.qty" class="qty fst-normal fw-bolder me-1" /> <t t-if="line.unit" t-esc="line.unit" />
|
||||
x <s t-esc="line.oldUnitPrice" t-if="line.oldUnitPrice" />
|
||||
<t t-esc="line.unitPrice" /> <span t-if="line.unit"> / <t t-esc="line.unit" /></span>
|
||||
</li>
|
||||
<li t-if="line.discount !== '0'">
|
||||
With a <em><t t-esc="line.discount" />% </em> discount
|
||||
</li>
|
||||
<li t-if="line.comboParent" class="combo-parent-name">
|
||||
<i class="fa fa-th-large me-2" role="img" aria-label="Combo" title="Combo"/>
|
||||
<t t-esc="line.comboParent" />
|
||||
</li>
|
||||
<t t-slot="default" />
|
||||
<li t-if="line.customerNote" class="customer-note w-50 p-2 my-1 rounded text-bg-warning text-warning bg-opacity-25 text-break">
|
||||
<i class="fa fa-sticky-note me-1" role="img" aria-label="Customer Note" title="Customer Note"/>
|
||||
<t t-esc="line.customerNote" />
|
||||
</li>
|
||||
<li t-if="line.internalNote" class="internal-note w-50 p-2 my-1 rounded text-bg-info text-info bg-opacity-25 text-break">
|
||||
<i class="fa fa-tag me-1" role="img" aria-label="Note" title="Note"/>
|
||||
<t t-esc="line.internalNote" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -5,7 +5,7 @@ import { Component } from "@odoo/owl";
|
||||
export class ProductCard extends Component {
|
||||
static template = "point_of_sale.ProductCard";
|
||||
static props = {
|
||||
class: String,
|
||||
class: { String, optional: true },
|
||||
name: String,
|
||||
productId: Number,
|
||||
price: String,
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<div class="pos dvh-100 d-flex flex-column">
|
||||
<div class="pos-receipt-print"></div>
|
||||
<Navbar />
|
||||
<div class="pos-content flex-grow-1 overflow-auto w-100 bg-200">
|
||||
<div class="pos-content flex-grow-1 overflow-auto bg-200">
|
||||
<!-- FIXME POSREF: better error handling in main screens (currently, a crash in owl lifecycle of a main screen blows up the application and the error can't be displayed) -->
|
||||
<t isShown="!pos.tempScreen" t-component="pos.mainScreen.component"
|
||||
t-props="pos.mainScreen.props"/>
|
||||
<ErrorHandler onError.bind="onTempScreenError">
|
||||
<t t-if="pos.tempScreen" t-component="pos.tempScreen.component"
|
||||
t-props="pos.tempScreen.props" t-key="pos.tempScreen.name" />
|
||||
t-props="pos.tempScreen.props" t-key="pos.tempScreen.name" />
|
||||
</ErrorHandler>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { EditListPopup } from "@point_of_sale/app/store/select_lot_popup/select_lot_popup";
|
||||
|
||||
import { Orderline } from "@point_of_sale/app/screens/product_screen/orderline/orderline";
|
||||
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
|
||||
import { Component, useEffect, useRef } from "@odoo/owl";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
|
||||
export class OrderWidget extends Component {
|
||||
static components = { Orderline, OrderSummary };
|
||||
static template = "point_of_sale.OrderWidget";
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
this.popup = useService("popup");
|
||||
this.numberBuffer = useService("number_buffer");
|
||||
this.scrollableRef = useRef("scrollable");
|
||||
useEffect(
|
||||
() => {
|
||||
const selectedLineEl =
|
||||
this.scrollableRef.el &&
|
||||
this.scrollableRef.el.querySelector(".orderline.selected");
|
||||
if (selectedLineEl) {
|
||||
selectedLineEl.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
},
|
||||
() => [this.order.selected_orderline]
|
||||
);
|
||||
}
|
||||
get order() {
|
||||
return this.pos.get_order();
|
||||
}
|
||||
get orderlinesArray() {
|
||||
return this.order ? this.order.get_orderlines() : [];
|
||||
}
|
||||
selectLine(orderline) {
|
||||
this.numberBuffer.reset();
|
||||
this.order.select_orderline(orderline);
|
||||
}
|
||||
// IMPROVEMENT: Might be better to lift this to ProductScreen
|
||||
// because there is similar operation when clicking a product.
|
||||
//
|
||||
// Furthermore, what if a number different from 1 (or -1) is specified
|
||||
// to an orderline that has product tracked by lot. Lot tracking (based
|
||||
// on the current implementation) requires that 1 item per orderline is
|
||||
// allowed.
|
||||
async editPackLotLines(orderline) {
|
||||
const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot();
|
||||
const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot);
|
||||
const { confirmed, payload } = await this.popup.add(EditListPopup, {
|
||||
title: _t("Lot/Serial Number(s) Required"),
|
||||
name: orderline.product.display_name,
|
||||
isSingleItem: isAllowOnlyOneLot,
|
||||
array: packLotLinesToEdit,
|
||||
});
|
||||
if (confirmed) {
|
||||
// Segregate the old and new packlot lines
|
||||
const modifiedPackLotLines = Object.fromEntries(
|
||||
payload.newArray.filter((item) => item.id).map((item) => [item.id, item.text])
|
||||
);
|
||||
const newPackLotLines = payload.newArray
|
||||
.filter((item) => !item.id)
|
||||
.map((item) => ({ lot_name: item.text }));
|
||||
|
||||
orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines });
|
||||
}
|
||||
this.order.select_orderline(orderline);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.OrderWidget">
|
||||
<div class="order-container w-100 h-100 overflow-y-auto bg-view" t-ref="scrollable">
|
||||
<div class="order d-flex flex-column h-100 text-start">
|
||||
<t t-if="orderlinesArray.length === 0" >
|
||||
<div class="order-empty d-flex flex-column align-items-center justify-content-center h-100 bg-100 text-muted">
|
||||
<i class="fa fa-4x fa-shopping-cart" role="img" aria-label="Shopping cart"
|
||||
title="Shopping cart"/>
|
||||
<h3 class="mt-2">This order is empty</h3>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<ul class="orderlines flex-grow-1 overflow-auto">
|
||||
<t t-foreach="orderlinesArray" t-as="orderline" t-key="orderline.id">
|
||||
<Orderline
|
||||
editPackLotLines.bind="editPackLotLines"
|
||||
selectLine.bind="selectLine"
|
||||
line="orderline" />
|
||||
</t>
|
||||
</ul>
|
||||
<OrderSummary order="order" />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
import { floatIsZero } from "@web/core/utils/numbers";
|
||||
|
||||
export class OrderSummary extends Component {
|
||||
static template = "point_of_sale.OrderSummary";
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
}
|
||||
getTotal() {
|
||||
return this.env.utils.formatCurrency(this.props.order.get_total_with_tax());
|
||||
}
|
||||
getTax() {
|
||||
const total = this.props.order.get_total_with_tax();
|
||||
const totalWithoutTax = this.props.order.get_total_without_tax();
|
||||
const taxAmount = total - totalWithoutTax;
|
||||
return {
|
||||
hasTax: !floatIsZero(taxAmount, this.pos.currency.decimal_places),
|
||||
displayAmount: this.env.utils.formatCurrency(taxAmount),
|
||||
};
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.OrderSummary">
|
||||
<div class="summary clearfix w-100 py-2 px-3 bg-200 bg-opacity-50 text-end fw-bolder">
|
||||
<t t-set="_total" t-value="getTotal()" />
|
||||
<t t-set="_tax" t-value="getTax()" />
|
||||
<div class="line">
|
||||
<div class="entry total fs-2 lh-sm">
|
||||
Total:
|
||||
<span class="value">
|
||||
<t t-esc="_total" />
|
||||
</span>
|
||||
<div t-if="_tax.hasTax" class="subentry fs-6 text-muted">
|
||||
Taxes:
|
||||
<span class="value">
|
||||
<t t-esc="_tax.displayAmount" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,48 +0,0 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
|
||||
export class Orderline extends Component {
|
||||
static template = "point_of_sale.Orderline";
|
||||
|
||||
/**
|
||||
* if doubleclick, change hasChange to skipChange
|
||||
*
|
||||
* IMPROVEMENT: Instead of handling both double click and click in single
|
||||
* method, perhaps we can separate double click from single click.
|
||||
*/
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
}
|
||||
selectLine() {
|
||||
const line = this.props.line; // the orderline
|
||||
if (this.pos.get_order().selected_orderline.id !== line.id) {
|
||||
this.mp_dbclk_time = new Date().getTime();
|
||||
} else if (!this.mp_dbclk_time) {
|
||||
this.mp_dbclk_time = new Date().getTime();
|
||||
} else if (this.mp_dbclk_time + 500 > new Date().getTime()) {
|
||||
line.toggleSkipChange();
|
||||
this.mp_dbclk_time = 0;
|
||||
} else {
|
||||
this.mp_dbclk_time = new Date().getTime();
|
||||
}
|
||||
|
||||
this.props.selectLine(this.props.line);
|
||||
}
|
||||
lotIconClicked() {
|
||||
this.props.editPackLotLines(this.props.line);
|
||||
}
|
||||
get addedClasses() {
|
||||
return {
|
||||
selected: this.props.line.selected,
|
||||
"has-change ps-2 text-success border-start border-success border-4":
|
||||
this.props.line.hasChange && this.pos.config.module_pos_restaurant,
|
||||
"skip-change ps-2 border-start border-secondary border-4":
|
||||
this.props.line.skipChange && this.pos.config.module_pos_restaurant,
|
||||
};
|
||||
}
|
||||
get customerNote() {
|
||||
return this.props.line.get_customer_note();
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.Orderline">
|
||||
<li t-on-click="selectLine" class="orderline w-100 m-0 py-2 px-3 lh-sm cursor-pointer transition-base" t-att-class="addedClasses">
|
||||
<span class="product-name d-inline-block flex-grow-1 fw-bolder text-truncate">
|
||||
<t t-esc="props.line.get_full_product_name()"/>
|
||||
<t t-if="props.line.get_product().tracking!=='none' and (pos.picking_type.use_create_lots || pos.picking_type.use_existing_lots)">
|
||||
<t t-if="props.line.has_valid_product_lot()">
|
||||
<i t-on-click.stop="lotIconClicked"
|
||||
class="oe_link_icon fa fa-list oe_icon line-lot-icon ms-1 text-success"
|
||||
aria-label="Valid product lot"
|
||||
role="img"
|
||||
title="Valid product lot"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i t-on-click.stop="lotIconClicked"
|
||||
class="oe_link_icon fa fa-list oe_icon line-lot-icon ms-1 text-danger"
|
||||
aria-label="Invalid product lot"
|
||||
role="img"
|
||||
title="Invalid product lot"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</span>
|
||||
<span class="price fw-bolder float-end">
|
||||
<t t-esc="env.utils.formatCurrency(props.line.get_display_price())"/>
|
||||
</span>
|
||||
<ul class="info-list text-dark">
|
||||
<t t-if="props.line.get_quantity_str() !== '1' || props.line.selected ">
|
||||
<li class="info ms-2">
|
||||
<em class="fst-normal fw-bolder">
|
||||
<t t-esc="props.line.get_quantity_str()" />
|
||||
</em>
|
||||
<span> </span><t t-esc="props.line.get_unit().name" />
|
||||
at
|
||||
<t t-if="props.line.display_discount_policy() == 'without_discount' and
|
||||
env.utils.roundCurrency(props.line.get_unit_display_price()) lt env.utils.roundCurrency(props.line.get_taxed_lst_unit_price())">
|
||||
<s>
|
||||
<t t-esc="env.utils.formatCurrency(props.line.get_taxed_lst_unit_price())" />
|
||||
</s>
|
||||
<t t-esc="env.utils.formatCurrency(props.line.get_unit_display_price())" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="env.utils.formatCurrency(props.line.get_unit_display_price())" />
|
||||
</t>
|
||||
/
|
||||
<t t-esc="props.line.get_unit().name" />
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="props.line.get_discount_str() !== '0'">
|
||||
<li class="info ms-2">
|
||||
With a
|
||||
<em>
|
||||
<t t-esc="props.line.get_discount_str()" />%
|
||||
</em>
|
||||
discount
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="customerNote">
|
||||
<li class="info orderline-note p-2 my-1 rounded text-bg-warning text-warning bg-opacity-25 text-break">
|
||||
<i class="fa fa-sticky-note me-1" role="img" aria-label="Customer Note" title="Customer Note"/>
|
||||
<t t-esc="customerNote" />
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="props.line.comboParent" t-call="point_of_sale.ComboParentName"/>
|
||||
</ul>
|
||||
<t t-if="props.line.pack_lot_lines">
|
||||
<ul class="info-list ms-2">
|
||||
<t t-foreach="props.line.get_lot_lines()" t-as="lot" t-key="lot.cid">
|
||||
<li>
|
||||
SN <t t-esc="lot.lot_name"/>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<t t-name="point_of_sale.ComboParentName" owl="1">
|
||||
<li class="info ms-2 combo-parent-name">
|
||||
<i class="fa fa-th-large me-2" role="img" aria-label="Combo" title="Combo"/>
|
||||
<t t-esc="props.line.comboParent.get_full_product_name()" />
|
||||
</li>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -17,17 +17,18 @@ import { Component, onMounted, useState } from "@odoo/owl";
|
||||
import { ErrorBarcodePopup } from "@point_of_sale/app/barcode/error_popup/barcode_error_popup";
|
||||
|
||||
import { Numpad } from "@point_of_sale/app/generic_components/numpad/numpad";
|
||||
import { OrderWidget } from "@point_of_sale/app/screens/product_screen/order/order";
|
||||
import { ProductsWidget } from "@point_of_sale/app/screens/product_screen/product_list/product_list";
|
||||
import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad";
|
||||
|
||||
import { Orderline } from "@point_of_sale/app/generic_components/orderline/orderline";
|
||||
import { OrderWidget } from "@point_of_sale/app/generic_components/order_widget/order_widget";
|
||||
export class ProductScreen extends ControlButtonsMixin(Component) {
|
||||
static template = "point_of_sale.ProductScreen";
|
||||
static components = {
|
||||
ActionpadWidget,
|
||||
Numpad,
|
||||
OrderWidget,
|
||||
ProductsWidget,
|
||||
Orderline,
|
||||
OrderWidget,
|
||||
};
|
||||
static numpadActionName = _t("Payment");
|
||||
|
||||
@@ -51,7 +52,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
|
||||
gs1: this._barcodeGS1Action,
|
||||
});
|
||||
|
||||
// Call `resset` when the `onMounted` callback in `numberBuffer.use` is done.
|
||||
// Call `reset` when the `onMounted` callback in `numberBuffer.use` is done.
|
||||
// We don't do this in the `mounted` lifecycle method because it is called before
|
||||
// the callbacks in `onMounted` hook.
|
||||
onMounted(() => this.numberBuffer.reset());
|
||||
@@ -97,6 +98,11 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
|
||||
this.numberBuffer.sendKey(buttonValue);
|
||||
}
|
||||
|
||||
|
||||
selectLine(orderline) {
|
||||
this.numberBuffer.reset();
|
||||
this.currentOrder.select_orderline(orderline);
|
||||
}
|
||||
/**
|
||||
* To be overridden by modules that checks availability of
|
||||
* connected scale.
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.ProductScreen">
|
||||
<div class="product-screen d-flex h-100 bg-100" t-att-class="{ 'd-none': !props.isShown }">
|
||||
<div class="leftpane d-flex flex-column border-end bg-200"
|
||||
t-if="!ui.isSmall || pos.mobile_pane === 'left'"
|
||||
t-att-class="{'flex-grow-1': ui.isSmall}">
|
||||
<OrderWidget/>
|
||||
<div t-if="!ui.isSmall || pos.mobile_pane === 'left'"
|
||||
t-att-class="{'flex-grow-1': ui.isSmall}"
|
||||
class="leftpane d-flex flex-column border-end bg-200" >
|
||||
<OrderWidget lines="currentOrder.orderlines" t-slot-scope="scope"
|
||||
total="env.utils.formatCurrency(currentOrder.get_total_with_tax())"
|
||||
tax="!env.utils.floatIsZero(currentOrder.get_total_tax()) and env.utils.formatCurrency(currentOrder.get_total_tax()) or ''">
|
||||
<t t-set="line" t-value="scope.line" />
|
||||
<Orderline line="line.getDisplayData()"
|
||||
t-on-click="() => this.selectLine(line)"
|
||||
class="{ ...line.getDisplayClasses(), 'selected' : line.selected }">
|
||||
<t t-set-slot="product-name">
|
||||
<i t-if="line.get_product().isTracked()"
|
||||
t-on-click.stop="() => line.editPackLotLines()" role="img"
|
||||
t-attf-class="{{ line.has_valid_product_lot() ? 'text-success' : 'text-danger'}} fa fa-list line-lot-icon ms-1"
|
||||
t-attf-title="{{ line.has_valid_product_lot() ? 'Valid product lot' : 'Invalid product lot'}}" />
|
||||
</t>
|
||||
<ul t-if="line.pack_lot_lines">
|
||||
<li t-foreach="line.get_lot_lines()" t-as="lot" t-key="lot.cid">
|
||||
SN <t t-esc="lot.lot_name"/>
|
||||
</li>
|
||||
</ul>
|
||||
</Orderline>
|
||||
<t t-set-slot="details" />
|
||||
</OrderWidget>
|
||||
<div class="pads border-top">
|
||||
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300"
|
||||
t-if="!ui.isSmall">
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { OrderlineDetails } from "@point_of_sale/app/screens/ticket_screen/order_details/orderline_details";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* @props {models.Order} order
|
||||
*/
|
||||
export class OrderDetails extends Component {
|
||||
static components = { OrderlineDetails };
|
||||
static template = "point_of_sale.OrderDetails";
|
||||
|
||||
get order() {
|
||||
return this.props.order;
|
||||
}
|
||||
get orderlines() {
|
||||
return this.order ? this.order.orderlines : [];
|
||||
}
|
||||
get total() {
|
||||
return this.env.utils.formatCurrency(this.order ? this.order.get_total_with_tax() : 0);
|
||||
}
|
||||
get tax() {
|
||||
return this.env.utils.formatCurrency(this.order ? this.order.get_total_tax() : 0);
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.OrderDetails">
|
||||
<div class="order-container h-100 overflow-y-auto bg-view">
|
||||
<div t-ref="scrollable" class="order-scroller touch-scrollable h-100">
|
||||
<div class="order d-flex flex-column h-100 text-start">
|
||||
<t t-if="!order">
|
||||
<div class="order-empty d-flex flex-column align-items-center justify-content-center h-100 text-muted">
|
||||
<i class="fa fa fa-4x fa-shopping-cart" role="img" aria-label="Shopping cart"
|
||||
title="Shopping cart" />
|
||||
<h3 class="mt-2">Select an order</h3>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="orderlines.length === 0">
|
||||
<div class="order-empty d-flex flex-column align-items-center justify-content-center h-100 text-muted">
|
||||
<i class="fa fa fa-4x fa-shopping-cart" role="img" aria-label="Shopping cart"
|
||||
title="Shopping cart" />
|
||||
<h3 class="mt-2">Order is empty</h3>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-if="order.locked" class="header-note text-bg-view py-2 px-3 border-bottom" t-att-class="{ 'highlight text-danger': props.highlightHeaderNote }">
|
||||
Select the product(s) to refund and set the quantity
|
||||
</div>
|
||||
<ul class="orderlines flex-grow-1 overflow-auto">
|
||||
<t t-foreach="orderlines" t-as="orderline" t-key="orderline.id">
|
||||
<OrderlineDetails
|
||||
onClickOrderline.bind="this.props.onClickOrderline"
|
||||
onClickRefundOrderUid.bind="props.onClickRefundOrderUid"
|
||||
line="orderline"
|
||||
isSelected="orderline.id == props.selectedOrderlineId" />
|
||||
</t>
|
||||
</ul>
|
||||
<!-- Not using OrderSummary because we don't need it's overrides in this view. -->
|
||||
<div class="summary clearfix sticky-top w-100 py-2 px-3 bg-200 bg-opacity-50 text-end fw-bolder">
|
||||
<div class="line">
|
||||
<div class="entry total fs-2 lh-sm">
|
||||
Total:
|
||||
<span class="value">
|
||||
<t t-esc="total" />
|
||||
</span>
|
||||
<div t-if="tax" class="subentry fs-6 text-muted">
|
||||
Taxes:
|
||||
<span class="value">
|
||||
<t t-esc="tax" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { formatFloat, roundPrecision as round_pr } from "@web/core/utils/numbers";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
|
||||
/**
|
||||
* @props {pos.order.line} line
|
||||
*/
|
||||
export class OrderlineDetails extends Component {
|
||||
static template = "point_of_sale.OrderlineDetails";
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
}
|
||||
get line() {
|
||||
const line = this.props.line;
|
||||
const formatQty = (line) => {
|
||||
const quantity = line.get_quantity();
|
||||
const unit = line.get_unit();
|
||||
const decimals = this.pos.dp["Product Unit of Measure"];
|
||||
const rounding = Math.max(unit.rounding, Math.pow(10, -decimals));
|
||||
const roundedQuantity = round_pr(quantity, rounding);
|
||||
return formatFloat(roundedQuantity, { digits: [69, decimals] });
|
||||
};
|
||||
return {
|
||||
productName: line.get_full_product_name(),
|
||||
totalPrice: line.get_price_with_tax(),
|
||||
quantity: formatQty(line),
|
||||
unit: line.get_unit().name,
|
||||
unitPrice: line.get_unit_price(),
|
||||
};
|
||||
}
|
||||
get productName() {
|
||||
return this.line.productName;
|
||||
}
|
||||
get totalPrice() {
|
||||
return this.env.utils.formatCurrency(this.line.totalPrice);
|
||||
}
|
||||
get quantity() {
|
||||
return this.line.quantity;
|
||||
}
|
||||
get unitPrice() {
|
||||
return this.env.utils.formatCurrency(this.line.unitPrice);
|
||||
}
|
||||
get unit() {
|
||||
return this.line.unit;
|
||||
}
|
||||
get pricePerUnit() {
|
||||
return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`;
|
||||
}
|
||||
get customerNote() {
|
||||
return this.props.line.get_customer_note();
|
||||
}
|
||||
getToRefundDetail() {
|
||||
return this.pos.toRefundLines[this.props.line.id];
|
||||
}
|
||||
hasRefundedQty() {
|
||||
return !this.pos.isProductQtyZero(this.props.line.refunded_qty);
|
||||
}
|
||||
getFormattedRefundedQty() {
|
||||
return this.env.utils.formatProductQty(this.props.line.refunded_qty);
|
||||
}
|
||||
hasToRefundQty() {
|
||||
const toRefundDetail = this.getToRefundDetail();
|
||||
return !this.pos.isProductQtyZero(toRefundDetail && toRefundDetail.qty);
|
||||
}
|
||||
getFormattedToRefundQty() {
|
||||
const toRefundDetail = this.getToRefundDetail();
|
||||
return this.env.utils.formatProductQty(toRefundDetail && toRefundDetail.qty);
|
||||
}
|
||||
getRefundingMessage() {
|
||||
return _t("Refunding %s in ", this.getFormattedToRefundQty());
|
||||
}
|
||||
getToRefundMessage() {
|
||||
return _t("To Refund: %s", this.getFormattedToRefundQty());
|
||||
}
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.OrderlineDetails">
|
||||
<li class="orderline w-100 m-0 py-2 px-3 lh-sm cursor-pointer"
|
||||
t-on-click="() => this.props.onClickOrderline(props.line)"
|
||||
t-att-class="{
|
||||
'selected': props.isSelected,
|
||||
'ps-3': this.props.line.isPartOfCombo()
|
||||
}">
|
||||
<span class="product-name d-inline-block flex-grow-1 fw-bolder text-truncate">
|
||||
<t t-esc="productName" />
|
||||
</span>
|
||||
<span class="price fw-bolder float-end">
|
||||
<t t-esc="totalPrice" />
|
||||
</span>
|
||||
<ul class="info-list">
|
||||
<li class="info ms-2">
|
||||
<strong>
|
||||
<t t-esc="quantity" />
|
||||
</strong>
|
||||
<span><t t-esc="pricePerUnit" /></span>
|
||||
</li>
|
||||
<t t-if="customerNote">
|
||||
<li class="info orderline-note p-2 my-1 rounded text-bg-warning text-warning bg-opacity-25 text-break">
|
||||
<i class="fa fa-sticky-note me-1" role="img" aria-label="Customer Note" title="Customer Note"/>
|
||||
<t t-esc="customerNote" />
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="props.line.comboParent" t-call="point_of_sale.ComboParentName"/>
|
||||
<t t-if="hasRefundedQty()">
|
||||
<li class="info refund-note mt-1">
|
||||
<strong>
|
||||
<t t-esc="getFormattedRefundedQty()" />
|
||||
</strong>
|
||||
<span> Refunded</span>
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="hasToRefundQty()">
|
||||
<li class="info to-refund-highlight refund-note mt-1 fw-bold text-primary">
|
||||
<t t-set="_destinationOrderUid" t-value="getToRefundDetail().destinationOrderUid"/>
|
||||
<t t-if="_destinationOrderUid">
|
||||
<t t-esc="getRefundingMessage()" />
|
||||
<span t-esc="_destinationOrderUid" class="order-link text-decoration-underline cursor-pointer" t-on-click.stop="() => this.props.onClickRefundOrderUid(_destinationOrderUid)" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="getToRefundMessage()" />
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -12,7 +12,9 @@ import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_pop
|
||||
|
||||
import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad";
|
||||
import { InvoiceButton } from "@point_of_sale/app/screens/ticket_screen/invoice_button/invoice_button";
|
||||
import { OrderDetails } from "@point_of_sale/app/screens/ticket_screen/order_details/order_details";
|
||||
import { Orderline } from "@point_of_sale/app/generic_components/orderline/orderline";
|
||||
import { OrderWidget } from "@point_of_sale/app/generic_components/order_widget/order_widget";
|
||||
import { CenteredIcon } from "@point_of_sale/app/generic_components/centered_icon/centered_icon";
|
||||
import { ReprintReceiptButton } from "@point_of_sale/app/screens/ticket_screen/reprint_receipt_button/reprint_receipt_button";
|
||||
import { SearchBar } from "@point_of_sale/app/screens/ticket_screen/search_bar/search_bar";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
@@ -27,7 +29,9 @@ export class TicketScreen extends Component {
|
||||
static components = {
|
||||
ActionpadWidget,
|
||||
InvoiceButton,
|
||||
OrderDetails,
|
||||
Orderline,
|
||||
OrderWidget,
|
||||
CenteredIcon,
|
||||
ReprintReceiptButton,
|
||||
SearchBar,
|
||||
Numpad,
|
||||
|
||||
@@ -105,12 +105,7 @@
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="empty-order-list m-5 text-center text-muted">
|
||||
<i role="img" aria-label="Shopping cart" title="Shopping cart" class="fa fa-4x fa-shopping-cart"></i>
|
||||
<h3 class="mt-2">No orders found</h3>
|
||||
</div>
|
||||
</t>
|
||||
<CenteredIcon t-else="" icon="'fa-shopping-cart fa-4x'" text="'No orders found'"/>
|
||||
</div>
|
||||
<div class="switchpane d-flex w-100" t-if="ui.isSmall">
|
||||
<t t-set="_selectedSyncedOrder" t-value="getSelectedOrder()" />
|
||||
@@ -125,43 +120,64 @@
|
||||
<div class="leftpane d-flex flex-column flex-grow-1 w-100 h-100 h-md-100 bg-200" t-if="!ui.isSmall || pos.ticket_screen_mobile_pane === 'right'">
|
||||
<t t-set="_selectedSyncedOrder" t-value="getSelectedOrder()" />
|
||||
<t t-set="_selectedOrderlineId" t-value="getSelectedOrderlineId()" />
|
||||
<t t-if="isOrderSynced">
|
||||
<OrderDetails
|
||||
onClickOrderline.bind="onClickOrderline"
|
||||
onClickRefundOrderUid.bind="onClickRefundOrderUid"
|
||||
order="_selectedSyncedOrder"
|
||||
selectedOrderlineId="_selectedOrderlineId"
|
||||
highlightHeaderNote="_state.ui.highlightHeaderNote" />
|
||||
<t t-if="_selectedSyncedOrder?.get_orderlines()?.length" >
|
||||
<div t-if="isOrderSynced" t-att-class="{ 'highlight text-danger': _state.ui.highlightHeaderNote }" class="text-bg-view py-2 px-3 border-bottom" >
|
||||
Select the product(s) to refund and set the quantity
|
||||
</div>
|
||||
<OrderWidget lines="_selectedSyncedOrder.orderlines" t-slot-scope="scope"
|
||||
total="env.utils.formatCurrency(_selectedSyncedOrder.get_total_with_tax())"
|
||||
tax="!env.utils.floatIsZero(_selectedSyncedOrder.get_total_tax()) and env.utils.formatCurrency(_selectedSyncedOrder.get_total_tax()) or ''">
|
||||
<t t-set="line" t-value="scope.line" />
|
||||
<Orderline line="line.getDisplayData()"
|
||||
class="{'selected': line.id === getSelectedOrderlineId()}"
|
||||
t-on-click="() => this.onClickOrderline(line)">
|
||||
<t t-set="toRefundDetail" t-value="line.pos.toRefundLines[line.id]" />
|
||||
<li t-if="!line.pos.isProductQtyZero(line.refunded_qty)"
|
||||
class="info refund-note mt-1">
|
||||
<strong t-esc="env.utils.formatProductQty(line.refunded_qty)" />
|
||||
<span> Refunded</span>
|
||||
</li>
|
||||
<li t-if="!line.pos.isProductQtyZero(toRefundDetail?.qty)" class="info to-refund-highlight refund-note mt-1 fw-bold text-primary">
|
||||
<t t-set="_destinationOrderUid" t-value="toRefundDetail.destinationOrderUid"/>
|
||||
<t t-set="refundQty" t-value="env.utils.formatProductQty(toRefundDetail.qty)"/>
|
||||
<t t-if="_destinationOrderUid">
|
||||
Refunding <t t-esc="refundQty" /> in
|
||||
<span t-esc="_destinationOrderUid"
|
||||
class="order-link ms-1 text-decoration-underline cursor-pointer"
|
||||
t-on-click.stop="() => this.onClickRefundOrderUid(_destinationOrderUid)" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
To Refund: <t t-esc="refundQty" />
|
||||
</t>
|
||||
</li>
|
||||
</Orderline>
|
||||
</OrderWidget>
|
||||
<div class="pads border-top">
|
||||
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300">
|
||||
<InvoiceButton
|
||||
onInvoiceOrder.bind="onInvoiceOrder"
|
||||
order="_selectedSyncedOrder" />
|
||||
<ReprintReceiptButton order="_selectedSyncedOrder" />
|
||||
</div>
|
||||
<div class="subpads d-flex">
|
||||
<ActionpadWidget
|
||||
partner="getSelectedPartner()"
|
||||
actionName="constructor.numpadActionName"
|
||||
actionType="'refund'"
|
||||
actionToTrigger.bind="onDoRefund"
|
||||
isActionButtonHighlighted="getHasItemsToRefund()" />
|
||||
<Numpad buttons="getNumpadButtons()" class="'max-width-325px'"/>
|
||||
<t t-if="isOrderSynced">
|
||||
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300">
|
||||
<InvoiceButton
|
||||
onInvoiceOrder.bind="onInvoiceOrder"
|
||||
order="_selectedSyncedOrder" />
|
||||
<ReprintReceiptButton order="_selectedSyncedOrder" />
|
||||
</div>
|
||||
<div class="subpads d-flex">
|
||||
<ActionpadWidget
|
||||
partner="getSelectedPartner()"
|
||||
actionName="constructor.numpadActionName"
|
||||
actionType="'refund'"
|
||||
actionToTrigger.bind="onDoRefund"
|
||||
isActionButtonHighlighted="getHasItemsToRefund()" />
|
||||
<Numpad buttons="getNumpadButtons()" class="'max-width-325px'"/>
|
||||
</div>
|
||||
</t>
|
||||
<div t-else="" class="pads border-top" >
|
||||
<button class="button validation load-order-button w-100 btn btn-lg btn-primary rounded-0 fw-bolder py-3" t-on-click="() => this._setOrder(_selectedSyncedOrder)">
|
||||
<span class="fs-1 d-block">Load Order</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<OrderDetails
|
||||
onClickOrderline.bind="onClickOrderline"
|
||||
onClickRefundOrderUid.bind="onClickRefundOrderUid"
|
||||
order="_selectedSyncedOrder"
|
||||
highlightHeaderNote="_state.ui.highlightHeaderNote" />
|
||||
<div class="pads border-top" t-if="_selectedSyncedOrder">
|
||||
<button class="button validation load-order-button w-100 btn btn-lg btn-primary rounded-0 fw-bolder py-3" t-on-click="() => this._setOrder(_selectedSyncedOrder)">
|
||||
<span class="fs-1 d-block">Load Order</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<CenteredIcon t-else="" icon="'fa-shopping-cart fa-4x'" text="'Select an order'" class="'bg-100'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
|
||||
import { ProductConfiguratorPopup } from "@point_of_sale/app/store/product_configurator_popup/product_configurator_popup";
|
||||
import { ComboConfiguratorPopup } from "./combo_configurator_popup/combo_configurator_popup";
|
||||
import { EditListPopup } from "@point_of_sale/app/store/select_lot_popup/select_lot_popup";
|
||||
import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_popup";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
@@ -113,6 +112,12 @@ export class Product extends PosModel {
|
||||
""
|
||||
);
|
||||
}
|
||||
isTracked() {
|
||||
return (
|
||||
["serial", "lot"].includes(this.tracking) &&
|
||||
(this.pos.picking_type.use_create_lots || this.pos.picking_type.use_existing_lots)
|
||||
);
|
||||
}
|
||||
get_unit() {
|
||||
var unit_id = this.uom_id;
|
||||
if (!unit_id) {
|
||||
@@ -179,24 +184,15 @@ export class Product extends PosModel {
|
||||
comboLines = payload;
|
||||
}
|
||||
// Gather lot information if required.
|
||||
if (
|
||||
["serial", "lot"].includes(this.tracking) &&
|
||||
(this.pos.picking_type.use_create_lots || this.pos.picking_type.use_existing_lots)
|
||||
) {
|
||||
const isAllowOnlyOneLot = this.isAllowOnlyOneLot();
|
||||
if (isAllowOnlyOneLot) {
|
||||
packLotLinesToEdit = [];
|
||||
} else {
|
||||
const orderline = this.pos.selectedOrder
|
||||
.get_orderlines()
|
||||
.filter((line) => !line.get_discount())
|
||||
.find((line) => line.product.id === this.id);
|
||||
if (orderline) {
|
||||
packLotLinesToEdit = orderline.getPackLotLinesToEdit();
|
||||
} else {
|
||||
packLotLinesToEdit = [];
|
||||
}
|
||||
}
|
||||
if (this.isTracked()) {
|
||||
packLotLinesToEdit =
|
||||
(!this.isAllowOnlyOneLot() &&
|
||||
this.pos.selectedOrder
|
||||
.get_orderlines()
|
||||
.filter((line) => !line.get_discount())
|
||||
.find((line) => line.product.id === this.id)
|
||||
?.getPackLotLinesToEdit()) ||
|
||||
[];
|
||||
// if the lot information exists in the barcode, we don't need to ask it from the user.
|
||||
if (code && code.type === "lot") {
|
||||
// consider the old and new packlot lines
|
||||
@@ -206,31 +202,14 @@ export class Product extends PosModel {
|
||||
const newPackLotLines = [{ lot_name: code.code }];
|
||||
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
|
||||
} else {
|
||||
const { confirmed, payload } = await this.pos.env.services.popup.add(
|
||||
EditListPopup,
|
||||
{
|
||||
title: _t("Lot/Serial Number(s) Required"),
|
||||
name: this.display_name,
|
||||
isSingleItem: isAllowOnlyOneLot,
|
||||
array: packLotLinesToEdit,
|
||||
}
|
||||
draftPackLotLines = await this.pos.getEditedPackLotLines(
|
||||
this.isAllowOnlyOneLot(),
|
||||
packLotLinesToEdit,
|
||||
this.display_name
|
||||
);
|
||||
if (confirmed) {
|
||||
// Segregate the old and new packlot lines
|
||||
const modifiedPackLotLines = Object.fromEntries(
|
||||
payload.newArray
|
||||
.filter((item) => item.id)
|
||||
.map((item) => [item.id, item.text])
|
||||
);
|
||||
const newPackLotLines = payload.newArray
|
||||
.filter((item) => !item.id)
|
||||
.map((item) => ({ lot_name: item.text }));
|
||||
|
||||
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
|
||||
} else {
|
||||
// We don't proceed on adding product.
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!draftPackLotLines) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,6 +449,9 @@ export class Orderline extends PosModel {
|
||||
orderline.customerNote = this.customerNote;
|
||||
return orderline;
|
||||
}
|
||||
getDisplayClasses() {
|
||||
return {};
|
||||
}
|
||||
getPackLotLinesToEdit(isAllowOnlyOneLot) {
|
||||
const currentPackLotLines = this.pack_lot_lines;
|
||||
let nExtraLines = Math.abs(this.quantity) - currentPackLotLines.length;
|
||||
@@ -487,6 +469,23 @@ export class Orderline extends PosModel {
|
||||
);
|
||||
return isAllowOnlyOneLot ? [tempLines[0]] : tempLines;
|
||||
}
|
||||
// What if a number different from 1 (or -1) is specified
|
||||
// to an orderline that has product tracked by lot? Lot tracking (based
|
||||
// on the current implementation) requires that 1 item per orderline is
|
||||
// allowed.
|
||||
async editPackLotLines() {
|
||||
const isAllowOnlyOneLot = this.product.isAllowOnlyOneLot();
|
||||
const editedPackLotLines = await this.pos.getEditedPackLotLines(
|
||||
isAllowOnlyOneLot,
|
||||
this.getPackLotLinesToEdit(isAllowOnlyOneLot),
|
||||
this.product.display_name
|
||||
);
|
||||
if (!editedPackLotLines) {
|
||||
return;
|
||||
}
|
||||
this.setPackLotLines(editedPackLotLines);
|
||||
this.order.select_orderline(this);
|
||||
}
|
||||
/**
|
||||
* @param { modifiedPackLotLines, newPackLotLines }
|
||||
* @param {Object} modifiedPackLotLines key-value pair of String (the cid) & String (the new lot_name)
|
||||
@@ -534,11 +533,6 @@ export class Orderline extends PosModel {
|
||||
setNote(note) {
|
||||
this.note = note;
|
||||
}
|
||||
toggleSkipChange() {
|
||||
if (this.hasChange || this.skipChange) {
|
||||
this.skipChange = !this.skipChange;
|
||||
}
|
||||
}
|
||||
setHasChange(isChange) {
|
||||
this.hasChange = isChange;
|
||||
}
|
||||
@@ -879,6 +873,18 @@ export class Orderline extends PosModel {
|
||||
return this.get_all_prices(1).priceWithoutTax;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This is the price that will appear as striked through.
|
||||
* @returns {number | false}
|
||||
*/
|
||||
get_old_unit_display_price() {
|
||||
return (
|
||||
this.display_discount_policy() === "without_discount" &&
|
||||
this.env.utils.roundCurrency(this.get_unit_display_price()) <
|
||||
this.env.utils.roundCurrency(this.get_taxed_lst_unit_price()) &&
|
||||
this.get_taxed_lst_unit_price()
|
||||
);
|
||||
}
|
||||
getUnitDisplayPriceBeforeDiscount() {
|
||||
if (this.pos.config.iface_tax_included === "total") {
|
||||
return this.get_all_prices(1).priceWithTaxBeforeDiscount;
|
||||
@@ -1120,6 +1126,20 @@ export class Orderline extends PosModel {
|
||||
isPartOfCombo() {
|
||||
return Boolean(this.comboParent || this.comboLines?.length);
|
||||
}
|
||||
getDisplayData() {
|
||||
return {
|
||||
productName: this.get_full_product_name(),
|
||||
price: this.env.utils.formatCurrency(this.get_display_price()),
|
||||
qty: this.get_quantity_str(),
|
||||
unit: this.get_unit().name,
|
||||
unitPrice: this.env.utils.formatCurrency(this.get_unit_display_price()),
|
||||
oldUnitPrice: this.env.utils.formatCurrency(this.get_old_unit_display_price()),
|
||||
discount: this.get_discount_str(),
|
||||
customerNote: this.get_customer_note(),
|
||||
internalNote: this.getNote(),
|
||||
comboParent: this.comboParent?.get_full_product_name(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Packlotline extends PosModel {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product
|
||||
import { renderToString } from "@web/core/utils/render";
|
||||
import { batched } from "@web/core/utils/timing";
|
||||
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
|
||||
import { EditListPopup } from "./select_lot_popup/select_lot_popup";
|
||||
|
||||
/* Returns an array containing all elements of the given
|
||||
* array corresponding to the rule function {agg} and without duplicates
|
||||
@@ -1823,6 +1824,28 @@ export class PosStore extends Reactive {
|
||||
await this.addProductFromUi(product, options);
|
||||
this.numberBuffer.reset();
|
||||
}
|
||||
|
||||
async getEditedPackLotLines(isAllowOnlyOneLot, packLotLinesToEdit, productName) {
|
||||
const { confirmed, payload } = await this.env.services.popup.add(EditListPopup, {
|
||||
title: _t("Lot/Serial Number(s) Required"),
|
||||
name: productName,
|
||||
isSingleItem: isAllowOnlyOneLot,
|
||||
array: packLotLinesToEdit,
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
// Segregate the old and new packlot lines
|
||||
const modifiedPackLotLines = Object.fromEntries(
|
||||
payload.newArray.filter((item) => item.id).map((item) => [item.id, item.text])
|
||||
);
|
||||
const newPackLotLines = payload.newArray
|
||||
.filter((item) => !item.id)
|
||||
.map((item) => ({ lot_name: item.text }));
|
||||
|
||||
return { modifiedPackLotLines, newPackLotLines };
|
||||
}
|
||||
|
||||
// FIXME POSREF get rid of temp screens entirely?
|
||||
showTempScreen(name, props = {}) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -38,7 +38,7 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", {
|
||||
isCheck: true,
|
||||
},
|
||||
combo.confirm(),
|
||||
...ProductScreen.do.clickOrderline("Office Combo"),
|
||||
...ProductScreen.check.selectedOrderlineHas("Office Combo"),
|
||||
...ProductScreen.do.clickOrderline("Combo Product 3"),
|
||||
...ProductScreen.check.selectedOrderlineHas(
|
||||
"Combo Product 3",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScre
|
||||
import { TextAreaPopup } from "@point_of_sale/../tests/tours/helpers/TextAreaPopupTourMethods";
|
||||
import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
registry.category("web_tour.tours").add("ProductScreenTour", {
|
||||
test: true,
|
||||
@@ -78,11 +79,9 @@ registry.category("web_tour.tours").add("ProductScreenTour", {
|
||||
ProductScreen.do.clickDisplayedProduct("Whiteboard Pen");
|
||||
ProductScreen.do.clickDisplayedProduct("Wall Shelf Unit");
|
||||
ProductScreen.do.clickOrderline("Whiteboard Pen", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "1.0");
|
||||
ProductScreen.do.pressNumpad("2");
|
||||
ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "2.0");
|
||||
ProductScreen.do.clickOrderline("Wall Shelf Unit", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "1.0");
|
||||
ProductScreen.do.pressNumpad("2");
|
||||
ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "2.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
@@ -101,25 +100,21 @@ registry.category("web_tour.tours").add("ProductScreenTour", {
|
||||
ProductScreen.do.clickDisplayedProduct("Magnetic Board");
|
||||
ProductScreen.do.clickDisplayedProduct("Monitor Stand");
|
||||
ProductScreen.do.clickOrderline("Whiteboard Pen", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "1.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Whiteboard Pen", "0.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0");
|
||||
ProductScreen.do.clickOrderline("Wall Shelf Unit", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "1.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Wall Shelf Unit", "0.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0");
|
||||
ProductScreen.do.clickOrderline("Small Shelf", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Small Shelf", "1.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Small Shelf", "0.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Monitor Stand", "1.0");
|
||||
ProductScreen.do.clickOrderline("Magnetic Board", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Magnetic Board", "1.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
ProductScreen.check.selectedOrderlineHas("Magnetic Board", "0.0");
|
||||
ProductScreen.do.pressNumpad("⌫");
|
||||
@@ -136,7 +131,13 @@ registry.category("web_tour.tours").add("ProductScreenTour", {
|
||||
TextAreaPopup.check.isShown();
|
||||
TextAreaPopup.do.inputText("Test customer note");
|
||||
TextAreaPopup.do.clickConfirm();
|
||||
ProductScreen.check.orderlineHasCustomerNote("Desk Organizer", "1", "Test customer note");
|
||||
Order.hasLine({
|
||||
productName: "Desk Organizer",
|
||||
quantity: "1.0",
|
||||
customerNote: "Test customer note",
|
||||
withClass: ".selected",
|
||||
});
|
||||
ProductScreen.check.isShown();
|
||||
return getSteps();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScre
|
||||
import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods";
|
||||
import { PartnerListScreen } from "@point_of_sale/../tests/tours/helpers/PartnerListScreenTourMethods";
|
||||
import { TicketScreen } from "@point_of_sale/../tests/tours/helpers/TicketScreenTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
import { ErrorPopup } from "@point_of_sale/../tests/tours/helpers/ErrorPopupTourMethods";
|
||||
import { Chrome } from "@point_of_sale/../tests/tours/helpers/ChromeTourMethods";
|
||||
import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
@@ -125,7 +126,7 @@ registry.category("web_tour.tours").add("TicketScreenTour", {
|
||||
TicketScreen.check.filterIs("Paid");
|
||||
TicketScreen.do.selectOrder("-0005");
|
||||
TicketScreen.check.partnerIs("Colleen Diaz");
|
||||
TicketScreen.do.clickOrderline("Desk Pad");
|
||||
Order.hasLine({ productName: "Desk Pad", withClass: ".selected" });
|
||||
ProductScreen.do.pressNumpad("3");
|
||||
// Error should show because 2 is more than the number
|
||||
// that can be refunded.
|
||||
@@ -136,7 +137,7 @@ registry.category("web_tour.tours").add("TicketScreenTour", {
|
||||
ProductScreen.check.orderIsEmpty();
|
||||
ProductScreen.do.clickRefund();
|
||||
TicketScreen.do.selectOrder("-0005");
|
||||
TicketScreen.do.clickOrderline("Desk Pad");
|
||||
Order.hasLine({ productName: "Desk Pad", withClass: ".selected" });
|
||||
ProductScreen.do.pressNumpad("1");
|
||||
TicketScreen.check.toRefundTextContains("To Refund: 1.00");
|
||||
TicketScreen.do.confirmRefund();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
|
||||
import { Numpad } from "@point_of_sale/../tests/tours/helpers/NumpadTourMethods";
|
||||
import { registry } from "@web/core/registry";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
function add_product_to_order(product_name) {
|
||||
return [
|
||||
@@ -15,11 +16,7 @@ function add_product_to_order(product_name) {
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "the " + product_name + " have been added to the order",
|
||||
trigger: '.order .product-name:contains("' + product_name + '")',
|
||||
run: function () {},
|
||||
},
|
||||
...Order.hasLine({ productName: product_name }),
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".floor-button",
|
||||
@@ -130,20 +127,7 @@ function selected_orderline_has({ product, price = null, quantity = null }) {
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
if (price !== null) {
|
||||
result.push({
|
||||
content: `Selected line has product '${product}' and price '${price}'`,
|
||||
trigger: `.order-container .orderlines .orderline.selected .product-name:contains("${product}") ~ span.price:contains("${price}")`,
|
||||
run: function () {},
|
||||
});
|
||||
}
|
||||
if (quantity !== null) {
|
||||
result.push({
|
||||
content: `Selected line has product '${product}' and quantity '${quantity}'`,
|
||||
trigger: `.order-container .orderlines .orderline.selected .product-name:contains('${product}') ~ .info-list .info em:contains('${quantity}')`,
|
||||
run: function () {},
|
||||
});
|
||||
}
|
||||
result.push(...Order.hasLine({ productName: product, quantity, price, withClass: ".selected" }));
|
||||
result.push({
|
||||
content: "go back to the products",
|
||||
trigger: ".floor-button",
|
||||
@@ -159,11 +143,7 @@ function verify_order_total(total_str) {
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "order total contains " + total_str,
|
||||
trigger: '.order .total .value:contains("' + total_str + '")',
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
Order.hasTotal(total_str),
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".floor-button",
|
||||
|
||||
@@ -3,6 +3,35 @@
|
||||
import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
import { TextAreaPopup } from "@point_of_sale/../tests/tours/helpers/TextAreaPopupTourMethods";
|
||||
import { Numpad } from "@point_of_sale/../tests/tours/helpers/NumpadTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
export function adaptForMobile(steps) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
...[steps].flat(),
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function clickLine(productName, quantity = "1.0") {
|
||||
return adaptForMobile([
|
||||
...Order.hasLine({
|
||||
withoutClass: ".selected",
|
||||
run: "click",
|
||||
productName,
|
||||
quantity,
|
||||
}),
|
||||
...Order.hasLine({ withClass: ".selected", productName, quantity }),
|
||||
]);
|
||||
}
|
||||
class Do {
|
||||
clickDisplayedProduct(name) {
|
||||
return [
|
||||
@@ -13,27 +42,9 @@ class Do {
|
||||
];
|
||||
}
|
||||
|
||||
clickOrderline(name, quantity = "1.0") {
|
||||
clickOrderline(productName, quantity = "1.0") {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: `selecting orderline with product '${name}' and quantity '${quantity}'`,
|
||||
trigger: `.order .orderline:not(:has(.selected)) .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
|
||||
},
|
||||
{
|
||||
content: `orderline with product '${name}' and quantity '${quantity}' has been selected`,
|
||||
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
...clickLine(productName, quantity),
|
||||
{
|
||||
content: "Check the product page",
|
||||
trigger: ".product-list-container .product-list",
|
||||
@@ -73,19 +84,7 @@ class Do {
|
||||
* @param {...String} keys space-separated numpad keys
|
||||
*/
|
||||
pressNumpad(...keys) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
...keys.map(Numpad.click),
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile(keys.map(Numpad.click));
|
||||
}
|
||||
|
||||
clickPayButton(shouldCheck = true) {
|
||||
@@ -183,12 +182,7 @@ class Do {
|
||||
return [{ trigger: '.opening-cash-control .button:contains("Open session")' }];
|
||||
}
|
||||
selectPriceList(name) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
return adaptForMobile([
|
||||
{
|
||||
content: "click more button",
|
||||
trigger: ".mobile-more-button",
|
||||
@@ -199,12 +193,7 @@ class Do {
|
||||
content: `select price list '${name}'`,
|
||||
trigger: `.selection-item:contains("${name}")`,
|
||||
},
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
]);
|
||||
}
|
||||
enterOpeningAmount(amount) {
|
||||
return [
|
||||
@@ -309,74 +298,25 @@ class Check {
|
||||
];
|
||||
}
|
||||
selectedOrderlineHas(name, quantity, price, comboParent) {
|
||||
const res = [
|
||||
return [
|
||||
...adaptForMobile(
|
||||
Order.hasLine({
|
||||
withClass: ".selected",
|
||||
productName: name,
|
||||
quantity,
|
||||
price,
|
||||
comboParent,
|
||||
})
|
||||
),
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
// check first if the order widget is there and has orderlines
|
||||
content: "order widget has orderlines",
|
||||
trigger: ".order .orderlines",
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: `'${name}' is selected`,
|
||||
trigger: `.order .orderline.selected .product-name:contains("${name}")`,
|
||||
run: function () {}, // it's a check
|
||||
content: "Check the product page",
|
||||
trigger: ".product-list-container .product-list",
|
||||
isCheck: true,
|
||||
},
|
||||
];
|
||||
if (quantity) {
|
||||
res.push({
|
||||
content: `selected line has ${quantity} quantity`,
|
||||
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
|
||||
run: function () {}, // it's a check
|
||||
});
|
||||
}
|
||||
if (price) {
|
||||
res.push({
|
||||
content: `selected line has total price of ${price}`,
|
||||
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .price:contains("${price}")`,
|
||||
run: function () {}, // it's a check
|
||||
});
|
||||
}
|
||||
if (comboParent) {
|
||||
res.push({
|
||||
content: `selected line has ${comboParent} as combo parent`,
|
||||
trigger: `.order .orderline.selected .info-list .combo-parent-name:contains("${comboParent}")`,
|
||||
isCheck: true,
|
||||
});
|
||||
}
|
||||
res.push({
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
}, {
|
||||
content: "Check the product page",
|
||||
trigger: ".product-list-container .product-list",
|
||||
isCheck: true,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
orderIsEmpty() {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: `order is empty`,
|
||||
trigger: `.order .order-empty`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile(Order.doesNotHaveLine());
|
||||
}
|
||||
|
||||
productIsDisplayed(name) {
|
||||
@@ -390,75 +330,19 @@ class Check {
|
||||
}
|
||||
totalAmountIs(amount) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: `order total amount is '${amount}'`,
|
||||
trigger: `.order-container .order .summary .value:contains("${amount}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
...adaptForMobile(Order.hasTotal(amount)),
|
||||
{
|
||||
content: "Check the product page",
|
||||
trigger: ".product-list-container .product-list",
|
||||
isCheck: true,
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
totalTaxIs(amount) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: `order total tax is '${amount}'`,
|
||||
trigger: `.summary .subentry .value:contains("${amount}")`,
|
||||
isCheck: true,
|
||||
},
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile(Order.hasTax(amount));
|
||||
}
|
||||
modeIsActive(mode) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
Numpad.isActive(mode),
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
orderlineHasCustomerNote(name, quantity, note) {
|
||||
return [
|
||||
{
|
||||
content: `line has ${quantity} quantity`,
|
||||
trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
{
|
||||
content: `line has '${note}' as customer note`,
|
||||
trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
];
|
||||
return adaptForMobile(Numpad.isActive(mode));
|
||||
}
|
||||
checkSecondCashClosingDetailsLineAmount(amount, sign) {
|
||||
return [
|
||||
@@ -483,22 +367,10 @@ class Check {
|
||||
];
|
||||
}
|
||||
noDiscountApplied(originalPrice) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "no discount is applied",
|
||||
trigger: `.info:not(:contains(${originalPrice}))`,
|
||||
},
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile({
|
||||
content: "no discount is applied",
|
||||
trigger: `.orderline .info-list:not(:contains(${originalPrice}))`,
|
||||
});
|
||||
}
|
||||
discountOriginalPriceIs(original_price) {
|
||||
return [
|
||||
@@ -576,11 +448,7 @@ class Execute {
|
||||
if (quantity.toString() !== "1") {
|
||||
res.push(...numpadWrite(quantity));
|
||||
}
|
||||
if (expectedTotal) {
|
||||
res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal));
|
||||
} else {
|
||||
res.push(...this._check.selectedOrderlineHas(productName, quantity));
|
||||
}
|
||||
res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal));
|
||||
return res;
|
||||
}
|
||||
addMultiOrderlines(...list) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
import {
|
||||
adaptForMobile,
|
||||
clickLine,
|
||||
} from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
class Do {
|
||||
clickNewTicket() {
|
||||
@@ -130,25 +135,7 @@ class Do {
|
||||
];
|
||||
}
|
||||
clickOrderline(name) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
trigger: `.ticket-screen .orderline:not(:has(.selected)) .product-name:contains("${name}")`,
|
||||
},
|
||||
{
|
||||
trigger: `.ticket-screen .orderline.selected .product-name:contains("${name}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "go back",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return clickLine(name);
|
||||
}
|
||||
confirmRefund() {
|
||||
return [
|
||||
@@ -179,6 +166,9 @@ class Check {
|
||||
},
|
||||
];
|
||||
}
|
||||
hasLine(options) {
|
||||
return adaptForMobile(Order.hasLine(options));
|
||||
}
|
||||
/**
|
||||
* Check if the nth row contains the given string.
|
||||
* Note that 1st row is the header-row.
|
||||
@@ -210,22 +200,7 @@ class Check {
|
||||
];
|
||||
}
|
||||
orderWidgetIsNotEmpty() {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
trigger: ".ticket-screen:not(:has(.order-empty))",
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "go back",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile(Order.hasLine());
|
||||
}
|
||||
filterIs(name) {
|
||||
return [
|
||||
@@ -244,58 +219,22 @@ class Check {
|
||||
];
|
||||
}
|
||||
partnerIs(name) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
trigger: `.ticket-screen .set-partner:contains("${name}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "Go back to main TicketScreen when in mobile",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile({
|
||||
trigger: `.ticket-screen .set-partner:contains("${name}")`,
|
||||
isCheck: true,
|
||||
});
|
||||
}
|
||||
toRefundTextContains(text) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "Go back to main TicketScreen when in mobile",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile({
|
||||
trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`,
|
||||
run: () => {},
|
||||
});
|
||||
}
|
||||
refundedNoteContains(text) {
|
||||
return [
|
||||
{
|
||||
content: "click review button",
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
trigger: `.ticket-screen .refund-note:contains("${text}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "Go back to main TicketScreen when in mobile",
|
||||
trigger: ".pos-rightheader .floor-button",
|
||||
mobile: true,
|
||||
},
|
||||
];
|
||||
return adaptForMobile({
|
||||
trigger: `.ticket-screen .refund-note:contains("${text}")`,
|
||||
run: () => {},
|
||||
});
|
||||
}
|
||||
tipContains(amount) {
|
||||
return [
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { negate } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* withClass?: string, // ex: withClass: ".selected.blue"
|
||||
* withoutClass?: string,
|
||||
* run?: function | string,
|
||||
* productName?: string,
|
||||
* quantity?: string,
|
||||
* price?: string,
|
||||
* customerNote?: string,
|
||||
* comboParent?: string,
|
||||
* }} LineOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LineOptions} options
|
||||
* @returns {import("@web_tour/tour_service/tour_service").TourStep[]}
|
||||
*/
|
||||
export function hasLine({
|
||||
withClass = "",
|
||||
withoutClass = "",
|
||||
run = () => {},
|
||||
productName,
|
||||
quantity,
|
||||
price,
|
||||
customerNote,
|
||||
comboParent,
|
||||
} = {}) {
|
||||
let trigger = `.order-container .orderline${withClass}`;
|
||||
if (withoutClass) {
|
||||
trigger += `:not(${withoutClass})`;
|
||||
}
|
||||
if (productName) {
|
||||
trigger += `:has(.product-name:contains("${productName}"))`;
|
||||
}
|
||||
if (quantity) {
|
||||
trigger += `:has(em.qty:contains("${quantity}"))`;
|
||||
}
|
||||
if (price) {
|
||||
trigger += `:has(.price:contains("${price}"))`;
|
||||
}
|
||||
if (customerNote) {
|
||||
trigger += `:has(.info-list .customer-note:contains("${customerNote}"))`;
|
||||
}
|
||||
if (comboParent) {
|
||||
trigger += `:has(.info-list .combo-parent-name:contains("${comboParent}"))`;
|
||||
}
|
||||
const args = JSON.stringify(arguments[0]);
|
||||
return [
|
||||
{
|
||||
content: `Check orderline with attributes: ${args}`,
|
||||
trigger,
|
||||
run: typeof run === "string" ? run : () => run(trigger),
|
||||
},
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @param {LineOptions} options
|
||||
* @returns {import("@web_tour/tour_service/tour_service").TourStep}
|
||||
*/
|
||||
export function doesNotHaveLine(options = {}) {
|
||||
const step = hasLine(options)[0];
|
||||
return [{ ...step, trigger: negate(step.trigger) }];
|
||||
}
|
||||
|
||||
// TODO: there are instances where we have no selected orderline. Fix those instances
|
||||
|
||||
export function hasTotal(amount) {
|
||||
return {
|
||||
content: `order total amount is '${amount}'`,
|
||||
trigger: `.product-screen .order-summary .total:contains("${amount}")`,
|
||||
isCheck: true,
|
||||
};
|
||||
}
|
||||
export function hasTax(amount) {
|
||||
return {
|
||||
content: `order total tax is '${amount}'`,
|
||||
trigger: `.order-summary .tax:contains("${amount}")`,
|
||||
isCheck: true,
|
||||
};
|
||||
}
|
||||
@@ -143,3 +143,7 @@ export function createTourMethods(name, Do, Check = class {}, Execute = class {}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function negate(selector) {
|
||||
return `body:not(:has(${selector}))`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScre
|
||||
import { roundDecimals as round_di } from "@web/core/utils/numbers";
|
||||
import { nbsp } from "@web/core/utils/strings";
|
||||
import { Numpad } from "./helpers/NumpadTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
@@ -192,13 +193,7 @@ steps = steps.concat([
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')",
|
||||
extra_trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('1.0')",
|
||||
run: function () {},
|
||||
},
|
||||
...Order.hasLine({ productName: "Wall Shelf", quantity: "1.0", withClass: ".selected" }),
|
||||
{
|
||||
content: "click more button",
|
||||
trigger: ".mobile-more-button",
|
||||
@@ -213,19 +208,9 @@ steps = steps.concat([
|
||||
trigger: ".selection-item:contains('min_quantity ordering')",
|
||||
},
|
||||
Numpad.click("2"),
|
||||
{
|
||||
content: "qty of Wall Shelf line should be 2",
|
||||
trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')",
|
||||
extra_trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('2.0')",
|
||||
run: function () {},
|
||||
},
|
||||
{
|
||||
content: "verify that unit price of shelf changed to $1",
|
||||
trigger: `.total > .value:contains('$${nbsp}2.00')`,
|
||||
run: function () {},
|
||||
},
|
||||
...Order.hasLine({ productName: "Wall Shelf", quantity: "2.0", withClass: ".selected" }),
|
||||
// verify that unit price of shelf changed to $1
|
||||
Order.hasTotal(`$${nbsp}2.00`),
|
||||
{
|
||||
content: "go back to the products",
|
||||
trigger: ".floor-button",
|
||||
@@ -240,24 +225,11 @@ steps = steps.concat([
|
||||
trigger: ".btn-switchpane:contains('Review')",
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
content: "Small Shelf line should be selected with quantity 1",
|
||||
trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')",
|
||||
extra_trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .info-list .info em:contains('1.0')",
|
||||
run: function () {},
|
||||
},
|
||||
...Order.hasLine({ productName: "Small Shelf", quantity: "1.0", withClass: ".selected" }),
|
||||
Numpad.click("Price"),
|
||||
Numpad.isActive("Price"),
|
||||
Numpad.click("5"),
|
||||
{
|
||||
content: "Small Shelf line should be selected with unit price of 5",
|
||||
trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')",
|
||||
extra_trigger:
|
||||
".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .price:contains('5.0')",
|
||||
},
|
||||
...Order.hasLine({ productName: "Small Shelf", price: "5.0", withClass: ".selected" }),
|
||||
Numpad.click("Qty"),
|
||||
Numpad.isActive("Qty"),
|
||||
{
|
||||
@@ -273,11 +245,8 @@ steps = steps.concat([
|
||||
content: "select public pricelist",
|
||||
trigger: ".selection-item:contains('Public Pricelist')",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)",
|
||||
trigger: `.total > .value:contains('$${nbsp}8.96')`,
|
||||
},
|
||||
// verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)
|
||||
Order.hasTotal(`$${nbsp}8.96`),
|
||||
{
|
||||
content: "click more button",
|
||||
trigger: ".mobile-more-button",
|
||||
|
||||
@@ -10,6 +10,22 @@ import { EventBus } from "@odoo/owl";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { popupService } from "@point_of_sale/app/popup/popup_service";
|
||||
|
||||
const mockContextualUtilsService = {
|
||||
dependencies: ["pos", "localization"],
|
||||
start(env, { pos, localization }) {
|
||||
const formatCurrency = (value, hasSymbol = true) => {
|
||||
return "dummy";
|
||||
};
|
||||
const floatIsZero = (value) => {
|
||||
return value === 0;
|
||||
};
|
||||
env.utils = {
|
||||
formatCurrency,
|
||||
floatIsZero,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
QUnit.module("Chrome", {
|
||||
beforeEach() {
|
||||
registry
|
||||
@@ -19,6 +35,7 @@ QUnit.module("Chrome", {
|
||||
.add("barcode_reader", barcodeReaderService)
|
||||
.add("ui", uiService)
|
||||
.add("popup", popupService)
|
||||
.add("contextual_utils_service", mockContextualUtilsService)
|
||||
.add("barcode", {
|
||||
start() {
|
||||
return { bus: new EventBus() };
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pos .order {
|
||||
.pos .orderlines {
|
||||
|
||||
.active-programs {
|
||||
padding-top: 0.5em;
|
||||
@@ -78,10 +78,6 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.orderline.program-reward {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-receipt .pos-coupon-rewards {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(OrderSummary.prototype, {
|
||||
getLoyaltyPoints() {
|
||||
const order = this.pos.get_order();
|
||||
return order.getLoyaltyPoints();
|
||||
},
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_loyalty.OrderSummary" t-inherit="point_of_sale.OrderSummary" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('summary')]" position="after">
|
||||
<t t-set="_loyaltyStats" t-value="getLoyaltyPoints()"/>
|
||||
<t t-foreach="_loyaltyStats" t-as="_loyaltyStat" t-key="_loyaltyStat.couponId">
|
||||
<t t-if="_loyaltyStat.points.won || _loyaltyStat.points.spent">
|
||||
<div class="summary clearfix w-100 border-top text-end fw-bolder">
|
||||
<div class='d-flex flex-column px-3 py-2 bg-200 text-start'>
|
||||
<div class="loyalty-points-title text-center mb-1">
|
||||
<t t-esc="_loyaltyStat.points.name"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-around gap-2 mt-1">
|
||||
<div
|
||||
t-if='_loyaltyStat.points.won'
|
||||
class="loyalty-points-won d-flex flex-column align-items-center justify-content-center flex-grow-1 rounded bg-300 px-3 py-2" >
|
||||
<span class="text-muted">Points Won</span>
|
||||
<span class='value text-success '>+<t t-esc='_loyaltyStat.points.won'/></span>
|
||||
</div>
|
||||
<div
|
||||
t-if='_loyaltyStat.points.spent'
|
||||
class="loyalty-points-won d-flex flex-column align-items-center justify-content-center flex-grow-1 rounded bg-300 px-3 py-2" >
|
||||
<span class="text-muted">Points Spent</span>
|
||||
<span class='value text-danger'>-<t t-esc='_loyaltyStat.points.spent'/></span>
|
||||
</div>
|
||||
<div class="loyalty-points-total d-flex flex-column align-items-center justify-content-center flex-grow-1 rounded bg-300 px-3 py-2">
|
||||
<span class="text-muted">New Total</span>
|
||||
<span class='value text-primary'><t t-esc='_loyaltyStat.points.total'/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,31 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Orderline } from "@point_of_sale/app/screens/product_screen/orderline/orderline";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Orderline.prototype, {
|
||||
get addedClasses() {
|
||||
return Object.assign(
|
||||
{ "program-reward": this.props.line.is_reward_line },
|
||||
super.addedClasses
|
||||
);
|
||||
},
|
||||
_isGiftCardOrEWalletReward() {
|
||||
const coupon = this.pos.couponCache[this.props.line.coupon_id];
|
||||
if (coupon) {
|
||||
const program = this.pos.program_by_id[coupon.program_id];
|
||||
return (
|
||||
["ewallet", "gift_card"].includes(program.program_type) &&
|
||||
this.props.line.is_reward_line
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
_getGiftCardOrEWalletBalance() {
|
||||
const coupon = this.pos.couponCache[this.props.line.coupon_id];
|
||||
if (coupon) {
|
||||
return this.env.utils.formatCurrency(coupon.balance);
|
||||
}
|
||||
return this.env.utils.formatCurrency(0);
|
||||
},
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_loyalty.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension">
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="attributes">
|
||||
<attribute name="t-if">!_isGiftCardOrEWalletReward()</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="after">
|
||||
<ul t-else="" class="info-list">
|
||||
Current Balance: <span t-esc="_getGiftCardOrEWalletBalance()"/>
|
||||
</ul>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_loyalty.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//Orderline" position="inside" >
|
||||
<li t-if="line.isGiftCardOrEWalletReward()">
|
||||
Current Balance: <t t-esc="line.getGiftCardOrEWalletBalance()"/>
|
||||
</li>
|
||||
</xpath>
|
||||
<xpath expr="//OrderWidget/t[@t-set-slot='details']" position="inside">
|
||||
<t t-foreach="pos.get_order()?.getLoyaltyPoints() or []" t-as="_loyaltyStat" t-key="_loyaltyStat.couponId">
|
||||
<div t-if="_loyaltyStat.points.won || _loyaltyStat.points.spent" class="w-100 border-top mt-auto fw-bolder d-flex flex-column px-3 py-2 bg-200">
|
||||
<div t-esc="_loyaltyStat.points.name" class="loyalty-points-title text-center mb-1" />
|
||||
<div class="d-flex justify-content-around gap-2 mt-1">
|
||||
<div t-if='_loyaltyStat.points.won' class="loyalty-points-won d-flex flex-column align-items-center justify-content-center flex-grow-1 rounded bg-300 px-3 py-2" >
|
||||
<span class="text-muted">Points Won</span>
|
||||
<span class='value text-success '>+<t t-esc='_loyaltyStat.points.won'/></span>
|
||||
</div>
|
||||
<div t-if='_loyaltyStat.points.spent' class="loyalty-points-won d-flex flex-column align-items-center justify-content-center flex-grow-1 rounded bg-300 px-3 py-2" >
|
||||
<span class="text-muted">Points Spent</span>
|
||||
<span class='value text-danger'>-<t t-esc='_loyaltyStat.points.spent'/></span>
|
||||
</div>
|
||||
<div class="loyalty-points-total d-flex flex-column align-items-center justify-content-center flex-grow-1 rounded bg-300 px-3 py-2">
|
||||
<span class="text-muted">New Total</span>
|
||||
<span class='value text-primary'><t t-esc='_loyaltyStat.points.total'/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,35 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { Orderline } from "@point_of_sale/app/store/models";
|
||||
|
||||
patch(Orderline.prototype, {
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isGiftCardOrEWalletReward() {
|
||||
const coupon = this.pos.couponCache[this.coupon_id];
|
||||
if (!coupon || !this.is_reward_line) {
|
||||
return false;
|
||||
}
|
||||
const program = this.pos.program_by_id[coupon.program_id];
|
||||
return ["ewallet", "gift_card"].includes(program.program_type);
|
||||
},
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getGiftCardOrEWalletBalance() {
|
||||
const coupon = this.pos.couponCache[this.coupon_id];
|
||||
return this.env.utils.formatCurrency(coupon?.balance || 0);
|
||||
},
|
||||
/**
|
||||
* override
|
||||
*/
|
||||
getDisplayClasses() {
|
||||
return {
|
||||
...super.getDisplayClasses(),
|
||||
"fst-italic":
|
||||
this.pos.mainScreen.component.name === "ProductScreen" && this.is_reward_line,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -8,96 +8,90 @@ import { registry } from "@web/core/registry";
|
||||
import { TicketScreen } from "@point_of_sale/../tests/tours/helpers/TicketScreenTourMethods";
|
||||
|
||||
//#region GiftCardProgramCreateSetTour1
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("GiftCardProgramCreateSetTour1", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
ProductScreen.do.clickDisplayedProduct("Gift Card");
|
||||
PosLoyalty.check.orderTotalIs("50.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "50");
|
||||
return getSteps();
|
||||
}
|
||||
});
|
||||
registry.category("web_tour.tours").add("GiftCardProgramCreateSetTour1", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
ProductScreen.do.clickDisplayedProduct("Gift Card");
|
||||
PosLoyalty.check.orderTotalIs("50.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "50");
|
||||
return getSteps();
|
||||
},
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region GiftCardProgramCreateSetTour2
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("GiftCardProgramCreateSetTour2", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
|
||||
startSteps();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
ProductScreen.do.clickDisplayedProduct("Whiteboard Pen");
|
||||
PosLoyalty.do.enterCode("044123456");
|
||||
PosLoyalty.check.orderTotalIs("0.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "0");
|
||||
return getSteps();
|
||||
}
|
||||
});
|
||||
registry.category("web_tour.tours").add("GiftCardProgramCreateSetTour2", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
ProductScreen.do.clickDisplayedProduct("Whiteboard Pen");
|
||||
PosLoyalty.do.enterCode("044123456");
|
||||
PosLoyalty.check.orderTotalIs("0.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "0");
|
||||
return getSteps();
|
||||
},
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region GiftCardProgramScanUseTour
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("GiftCardProgramScanUseTour", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
// Pay the 5$ gift card.
|
||||
ProductScreen.do.clickDisplayedProduct("Gift Card");
|
||||
TextInputPopup.check.isShown();
|
||||
TextInputPopup.do.inputText("044123456");
|
||||
TextInputPopup.do.clickConfirm();
|
||||
PosLoyalty.check.orderTotalIs("5.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "5");
|
||||
// Partially use the gift card. (4$)
|
||||
ProductScreen.exec.addOrderline("Desk Pad", "2", "2", "4.0");
|
||||
PosLoyalty.do.enterCode("044123456");
|
||||
PosLoyalty.check.orderTotalIs("0.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "0");
|
||||
// Use the remaining of the gift card. (5$ - 4$ = 1$)
|
||||
ProductScreen.exec.addOrderline("Whiteboard Pen", "6", "6", "36.0");
|
||||
PosLoyalty.do.enterCode("044123456");
|
||||
PosLoyalty.check.orderTotalIs("35.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "35");
|
||||
return getSteps();
|
||||
}
|
||||
});
|
||||
registry.category("web_tour.tours").add("GiftCardProgramScanUseTour", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
// Pay the 5$ gift card.
|
||||
ProductScreen.do.clickDisplayedProduct("Gift Card");
|
||||
TextInputPopup.check.isShown();
|
||||
TextInputPopup.do.inputText("044123456");
|
||||
TextInputPopup.do.clickConfirm();
|
||||
PosLoyalty.check.orderTotalIs("5.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "5");
|
||||
// Partially use the gift card. (4$)
|
||||
ProductScreen.exec.addOrderline("Desk Pad", "2", "2", "4.0");
|
||||
PosLoyalty.do.enterCode("044123456");
|
||||
PosLoyalty.check.orderTotalIs("0.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "0");
|
||||
// Use the remaining of the gift card. (5$ - 4$ = 1$)
|
||||
ProductScreen.exec.addOrderline("Whiteboard Pen", "6", "6", "36.0");
|
||||
PosLoyalty.do.enterCode("044123456");
|
||||
PosLoyalty.check.orderTotalIs("35.00");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "35");
|
||||
return getSteps();
|
||||
},
|
||||
});
|
||||
//#endregion
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("GiftCardWithRefundtTour", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
ProductScreen.exec.addOrderline("Magnetic Board", "1"); // 1.98
|
||||
PosLoyalty.check.orderTotalIs("1.98");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "20");
|
||||
ProductScreen.do.clickRefund();
|
||||
TicketScreen.do.selectOrder("-0001");
|
||||
TicketScreen.do.clickOrderline("Magnetic Board");
|
||||
ProductScreen.do.pressNumpad("1");
|
||||
TicketScreen.do.confirmRefund();
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.selectedOrderlineHas("Magnetic Board", "-1.00");
|
||||
ProductScreen.exec.addOrderline("Gift Card", "1");
|
||||
ProductScreen.check.selectedOrderlineHas("Gift Card", "1");
|
||||
PosLoyalty.check.orderTotalIs("0.0");
|
||||
return getSteps();
|
||||
}
|
||||
});
|
||||
registry.category("web_tour.tours").add("GiftCardWithRefundtTour", {
|
||||
test: true,
|
||||
url: "/pos/web",
|
||||
steps: () => {
|
||||
startSteps();
|
||||
ProductScreen.do.confirmOpeningPopup();
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
ProductScreen.exec.addOrderline("Magnetic Board", "1"); // 1.98
|
||||
PosLoyalty.check.orderTotalIs("1.98");
|
||||
PosLoyalty.exec.finalizeOrder("Cash", "20");
|
||||
ProductScreen.do.clickRefund();
|
||||
TicketScreen.do.selectOrder("-0001");
|
||||
TicketScreen.check.hasLine({
|
||||
withClass: ".selected",
|
||||
productName: "Magnetic Board",
|
||||
});
|
||||
ProductScreen.do.pressNumpad("1");
|
||||
TicketScreen.do.confirmRefund();
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.selectedOrderlineHas("Magnetic Board", "-1.00");
|
||||
ProductScreen.exec.addOrderline("Gift Card", "1");
|
||||
ProductScreen.check.selectedOrderlineHas("Gift Card", "1");
|
||||
PosLoyalty.check.orderTotalIs("0.0");
|
||||
return getSteps();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Do as ProductScreenDo } from "@point_of_sale/../tests/tours/helpers/Pro
|
||||
import { Do as PaymentScreenDo } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods";
|
||||
import { Do as ReceiptScreenDo } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods";
|
||||
import { Do as ChromeDo } from "@point_of_sale/../tests/tours/helpers/ChromeTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
const ProductScreen = { do: new ProductScreenDo() };
|
||||
const PaymentScreen = { do: new PaymentScreenDo() };
|
||||
@@ -14,15 +15,16 @@ const Chrome = { do: new ChromeDo() };
|
||||
class Do {
|
||||
selectRewardLine(rewardName) {
|
||||
return [
|
||||
{
|
||||
content: "select reward line",
|
||||
trigger: `.orderline.program-reward .product-name:contains("${rewardName}")`,
|
||||
},
|
||||
{
|
||||
content: "check reward line if selected",
|
||||
trigger: `.orderline.selected.program-reward .product-name:contains("${rewardName}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
...Order.hasLine({
|
||||
withClass: ".fst-italic",
|
||||
withoutClass: ".selected",
|
||||
run: "click",
|
||||
productName: rewardName,
|
||||
}),
|
||||
...Order.hasLine({
|
||||
withClass: ".selected.fst-italic",
|
||||
productName: rewardName,
|
||||
}),
|
||||
];
|
||||
}
|
||||
enterCode(code) {
|
||||
@@ -99,35 +101,15 @@ class Do {
|
||||
|
||||
class Check {
|
||||
hasRewardLine(rewardName, amount, qty) {
|
||||
const steps = [
|
||||
{
|
||||
content: "check if reward line is there",
|
||||
trigger: `.orderline.program-reward span.product-name:contains("${rewardName}")`,
|
||||
run: function () {},
|
||||
},
|
||||
{
|
||||
content: "check if the reward price is correct",
|
||||
trigger: `.orderline.program-reward span.price:contains("${amount}")`,
|
||||
run: function () {},
|
||||
},
|
||||
];
|
||||
if (qty) {
|
||||
steps.push({
|
||||
content: "check if the reward qty is correct",
|
||||
trigger: `.order .orderline.program-reward .product-name:contains("${rewardName}") ~ .info-list em:contains("${qty}")`,
|
||||
run: function () {},
|
||||
});
|
||||
}
|
||||
return steps;
|
||||
return Order.hasLine({
|
||||
withClass: ".fst-italic",
|
||||
productName: rewardName,
|
||||
price: amount,
|
||||
quantity: qty,
|
||||
});
|
||||
}
|
||||
orderTotalIs(total_str) {
|
||||
return [
|
||||
{
|
||||
content: "order total contains " + total_str,
|
||||
trigger: '.order .total .value:contains("' + total_str + '")',
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
];
|
||||
return [Order.hasTotal(total_str)];
|
||||
}
|
||||
checkNoClaimableRewards() {
|
||||
return [
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { OrderWidget } from "@point_of_sale/app/screens/product_screen/order/order";
|
||||
import { AlertPopup } from "@point_of_sale/app/utils/alert_popup/alert_popup";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(OrderWidget.prototype, {
|
||||
releaseTable() {
|
||||
const currentTable = this.pos.table;
|
||||
const orderOnTable = this.pos.orders.filter(
|
||||
(o) => o.tableId === currentTable.id && o.finalized === false
|
||||
);
|
||||
const orderOnTableWithOrderline = orderOnTable.find((o) => o.orderlines.length > 0);
|
||||
|
||||
if (orderOnTableWithOrderline) {
|
||||
this.popup.add(AlertPopup, {
|
||||
title: _t("Open orders"),
|
||||
body: _t(
|
||||
"There are open orders on this table. Please close them before releasing the table."
|
||||
),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const order of orderOnTable) {
|
||||
this.pos.removeOrder(order);
|
||||
}
|
||||
|
||||
this.pos.showScreen("FloorScreen");
|
||||
},
|
||||
get showReleaseBtn() {
|
||||
if (!this.pos.config.module_pos_restaurant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTable = this.pos.table;
|
||||
const noEmptyOrderOnTable = this.pos.orders.filter(
|
||||
(o) => o.tableId === currentTable.id && o.finalized === false && o.orderlines.length > 0
|
||||
);
|
||||
return noEmptyOrderOnTable.length ? false : true;
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.OrderWidget" t-inherit="point_of_sale.OrderWidget" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('order-empty')]" position="after">
|
||||
<button t-if="showReleaseBtn" class="btn btn-primary py-2 rounded-0" style="border:none; font-size: 20px;" t-on-click="releaseTable">Release table</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,25 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
|
||||
patch(ProductScreen.prototype, {
|
||||
releaseTable() {
|
||||
const orderOnTable = this.pos.orders.filter(
|
||||
(o) => o.tableId === this.pos.table.id && o.finalized === false
|
||||
);
|
||||
for (const order of orderOnTable) {
|
||||
this.pos.removeOrder(order);
|
||||
}
|
||||
this.pos.showScreen("FloorScreen");
|
||||
},
|
||||
showReleaseBtn() {
|
||||
return (
|
||||
this.pos.config.module_pos_restaurant &&
|
||||
!this.pos.orders.some(
|
||||
(o) =>
|
||||
o.tableId === this.pos.table.id && o.finalized === false && o.orderlines.length
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//OrderWidget/t[@t-set-slot='details']" position="inside">
|
||||
<button t-if="showReleaseBtn()" class="btn btn-primary py-2 rounded-0" style="border:none; font-size: 20px;" t-on-click="releaseTable">Release table</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
import { Order } from "@point_of_sale/app/store/models";
|
||||
|
||||
import { SplitOrderline } from "@pos_restaurant/app/split_bill_screen/split_orderline/split_orderline";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { Orderline } from "@point_of_sale/app/generic_components/orderline/orderline";
|
||||
import { OrderWidget } from "@point_of_sale/app/generic_components/order_widget/order_widget";
|
||||
import { groupBy } from "@web/core/utils/arrays";
|
||||
|
||||
export class SplitBillScreen extends Component {
|
||||
static template = "pos_restaurant.SplitBillScreen";
|
||||
static components = { SplitOrderline };
|
||||
static components = { Orderline, OrderWidget };
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
@@ -38,6 +39,13 @@ export class SplitBillScreen extends Component {
|
||||
this._updateNewOrder(l);
|
||||
}
|
||||
}
|
||||
getLineData(line) {
|
||||
const splitQty = this.splitlines[line.id].quantity;
|
||||
if (!splitQty) {
|
||||
return line.getDisplayData();
|
||||
}
|
||||
return { ...line.getDisplayData(), qty: `${splitQty} / ${line.get_quantity_str()}` };
|
||||
}
|
||||
back() {
|
||||
this.pos.showScreen("ProductScreen");
|
||||
}
|
||||
|
||||
@@ -13,20 +13,17 @@
|
||||
<h2 class="mb-0">Bill Splitting</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="newOrder" class="main d-flex flex-nowrap flex-grow-1 overflow-hidden">
|
||||
<div class="lines d-flex flex-shrink-1 flex-grow-1 flex-basis-0 justify-content-center border-end overflow-y-auto ">
|
||||
<div class="order w-100 h-auto py-2 m-4 border rounded bg-view text-start">
|
||||
<ul class="orderlines flex-grow-1 h-100 overflow-auto">
|
||||
<t t-foreach="orderlines" t-as="line" t-key="line.cid">
|
||||
<SplitOrderline
|
||||
onClickLine.bind="onClickLine"
|
||||
line="line"
|
||||
split="splitlines[line.id]" />
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-grow-1 mw-50 m-3 bg-view border rounded w-50">
|
||||
<OrderWidget lines="orderlines" t-slot-scope="scope">
|
||||
<t t-set="line" t-value="scope.line" />
|
||||
<Orderline line="getLineData(line)"
|
||||
t-on-click="() => this.onClickLine(line)"
|
||||
class="{'selected text-bg-primary': splitlines[line.id].quantity !== 0}" />
|
||||
</OrderWidget>
|
||||
</div>
|
||||
<div class="controls d-flex flex-column flex-nowrap flex-grow-1 flex-shrink-1 flex-basis-0">
|
||||
<div class="controls border-start flex-column flex-nowrap flex-grow-1 flex-shrink-1 flex-basis-0">
|
||||
<div class="order-info py-4 border-bottom text-center text-success">
|
||||
<span class="subtotal">
|
||||
<t t-esc="env.utils.formatCurrency(newOrder.get_subtotal())" />
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class SplitOrderline extends Component {
|
||||
static template = "pos_restaurant.SplitOrderline";
|
||||
|
||||
setup() {}
|
||||
get isSelected() {
|
||||
return this.props.split.quantity !== 0;
|
||||
}
|
||||
click() {
|
||||
this.props.onClickLine(this.props.line);
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_restaurant.SplitOrderline">
|
||||
<li class="orderline w-100 m-0 py-2 px-3 lh-sm cursor-pointer" t-att-class="{ 'selected text-bg-primary' : isSelected, partially: props.split.quantity !== props.line.get_quantity() }" t-on-click="() => this.click()">
|
||||
<span class="product-name d-inline-block flex-grow-1 fw-bolder text-truncate">
|
||||
<t t-esc="props.line.get_product().display_name" />
|
||||
</span>
|
||||
<span class="price fw-bolder float-end">
|
||||
<t t-esc="env.utils.formatCurrency(props.line.get_display_price())" />
|
||||
</span>
|
||||
<ul class="info-list ms-2">
|
||||
<t t-if="props.line.get_quantity_str() !== '1'">
|
||||
<li class="info">
|
||||
<t t-if="isSelected and props.line.is_pos_groupable()">
|
||||
<em class="big fs-3 fst-normal fw-bolder me-1">
|
||||
<t t-esc="props.split.quantity" />
|
||||
</em>
|
||||
/
|
||||
<t t-esc="props.line.get_quantity_str()" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<em class="fst-normal fw-bolder me-1">
|
||||
<t t-esc="props.line.get_quantity_str()" />
|
||||
</em>
|
||||
</t>
|
||||
<t t-esc="props.line.get_unit().name" />
|
||||
at
|
||||
<t t-esc="env.utils.formatCurrency(props.line.get_unit_price())" />
|
||||
/
|
||||
<t t-esc="props.line.get_unit().name" />
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="props.line.get_discount_str() !== '0'">
|
||||
<li class="info">
|
||||
<span>With a </span>
|
||||
<em>
|
||||
<t t-esc="props.line.get_discount_str()" />
|
||||
<span>%</span>
|
||||
</em>
|
||||
<span> discount</span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_restaurant.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension">
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="inside">
|
||||
<t t-if="props.line.getNote()">
|
||||
<li class="info orderline-note p-2 my-1 rounded text-bg-info text-info bg-opacity-25 text-break">
|
||||
<i class="fa fa-tag me-1" role="img" aria-label="Note" title="Note"/>
|
||||
<t t-esc="props.line.getNote()" />
|
||||
</li>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+5
@@ -22,5 +22,10 @@
|
||||
</button>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//Orderline" position="attributes">
|
||||
<attribute name="t-on-dblclick">
|
||||
() => line.toggleSkipChange()
|
||||
</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -92,6 +92,18 @@ patch(Orderline.prototype, {
|
||||
return "" + this.id;
|
||||
}
|
||||
},
|
||||
toggleSkipChange() {
|
||||
if (this.hasChange || this.skipChange) {
|
||||
this.skipChange = !this.skipChange;
|
||||
}
|
||||
},
|
||||
getDisplayClasses() {
|
||||
return {
|
||||
...super.getDisplayClasses(),
|
||||
"has-change text-success border-start border-success border-4": this.hasChange,
|
||||
"skip-change text-primary border-start border-primary border-4": this.skipChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
patch(Payment.prototype, {
|
||||
|
||||
@@ -7,62 +7,65 @@ import { FloorScreen } from "@pos_restaurant/../tests/tours/helpers/FloorScreenT
|
||||
import { ProductScreen } from "@pos_restaurant/../tests/tours/helpers/ProductScreenTourMethods";
|
||||
import { SplitBillScreen } from "@pos_restaurant/../tests/tours/helpers/SplitBillScreenTourMethods";
|
||||
import { BillScreen } from "@pos_restaurant/../tests/tours/helpers/BillScreenTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry
|
||||
.category("web_tour.tours")
|
||||
.add("ControlButtonsTour", {
|
||||
test: true,
|
||||
url: "/pos/ui",
|
||||
steps: () => {
|
||||
// signal to start generating steps
|
||||
// when finished, steps can be taken from getSteps
|
||||
startSteps();
|
||||
|
||||
// Test TransferOrderButton
|
||||
FloorScreen.do.clickTable("2");
|
||||
ProductScreen.exec.addOrderline("Water", "5", "2", "10.0");
|
||||
ProductScreen.do.clickTransferButton();
|
||||
FloorScreen.do.clickTable("4");
|
||||
ProductScreen.do.clickOrderline("Water", "5", "2");
|
||||
Chrome.do.backToFloor();
|
||||
FloorScreen.do.clickTable("2");
|
||||
ProductScreen.check.orderIsEmpty();
|
||||
Chrome.do.backToFloor();
|
||||
FloorScreen.do.clickTable("4");
|
||||
ProductScreen.do.clickOrderline("Water", "5", "2");
|
||||
|
||||
// Test SplitBillButton
|
||||
ProductScreen.do.clickSplitBillButton();
|
||||
SplitBillScreen.do.clickBack();
|
||||
|
||||
// Test OrderlineNoteButton
|
||||
ProductScreen.do.clickNoteButton();
|
||||
TextAreaPopup.check.isShown();
|
||||
TextAreaPopup.do.inputText("test note");
|
||||
TextAreaPopup.do.clickConfirm();
|
||||
ProductScreen.check.orderlineHasNote("Water", "5", "test note");
|
||||
ProductScreen.exec.addOrderline("Water", "8", "1", "8.0");
|
||||
|
||||
// Test PrintBillButton
|
||||
ProductScreen.do.clickPrintBillButton();
|
||||
BillScreen.check.isShown();
|
||||
BillScreen.do.clickOk();
|
||||
|
||||
// Test GuestButton
|
||||
ProductScreen.do.clickGuestButton();
|
||||
NumberPopup.do.enterValue("15");
|
||||
NumberPopup.check.inputShownIs("15");
|
||||
NumberPopup.do.clickConfirm();
|
||||
ProductScreen.check.guestNumberIs("15");
|
||||
|
||||
ProductScreen.do.clickGuestButton();
|
||||
NumberPopup.do.enterValue("5");
|
||||
NumberPopup.check.inputShownIs("5");
|
||||
NumberPopup.do.clickConfirm();
|
||||
ProductScreen.check.guestNumberIs("5");
|
||||
registry.category("web_tour.tours").add("ControlButtonsTour", {
|
||||
test: true,
|
||||
url: "/pos/ui",
|
||||
steps: () => {
|
||||
// signal to start generating steps
|
||||
// when finished, steps can be taken from getSteps
|
||||
startSteps();
|
||||
|
||||
return getSteps();
|
||||
}
|
||||
});
|
||||
// Test TransferOrderButton
|
||||
FloorScreen.do.clickTable("2");
|
||||
ProductScreen.exec.addOrderline("Water", "5", "2", "10.0");
|
||||
ProductScreen.do.clickTransferButton();
|
||||
FloorScreen.do.clickTable("4");
|
||||
Chrome.do.backToFloor();
|
||||
FloorScreen.do.clickTable("2");
|
||||
ProductScreen.check.orderIsEmpty();
|
||||
Chrome.do.backToFloor();
|
||||
FloorScreen.do.clickTable("4");
|
||||
|
||||
// Test SplitBillButton
|
||||
ProductScreen.do.clickSplitBillButton();
|
||||
SplitBillScreen.do.clickBack();
|
||||
|
||||
// Test OrderlineNoteButton
|
||||
ProductScreen.do.clickNoteButton();
|
||||
TextAreaPopup.check.isShown();
|
||||
TextAreaPopup.do.inputText("test note");
|
||||
TextAreaPopup.do.clickConfirm();
|
||||
Order.hasLine({
|
||||
productName: "Water",
|
||||
quantity: "5",
|
||||
price: "10.0",
|
||||
customerNote: "test note",
|
||||
withClass: ".selected",
|
||||
});
|
||||
ProductScreen.exec.addOrderline("Water", "8", "1", "8.0");
|
||||
|
||||
// Test PrintBillButton
|
||||
ProductScreen.do.clickPrintBillButton();
|
||||
BillScreen.check.isShown();
|
||||
BillScreen.do.clickOk();
|
||||
|
||||
// Test GuestButton
|
||||
ProductScreen.do.clickGuestButton();
|
||||
NumberPopup.do.enterValue("15");
|
||||
NumberPopup.check.inputShownIs("15");
|
||||
NumberPopup.do.clickConfirm();
|
||||
ProductScreen.check.guestNumberIs("15");
|
||||
|
||||
ProductScreen.do.clickGuestButton();
|
||||
NumberPopup.do.enterValue("5");
|
||||
NumberPopup.check.inputShownIs("5");
|
||||
NumberPopup.do.clickConfirm();
|
||||
ProductScreen.check.guestNumberIs("5");
|
||||
|
||||
return getSteps();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,7 +34,9 @@ registry.category("web_tour.tours").add("RefundStayCurrentTableTour", {
|
||||
ProductScreen.check.orderIsEmpty();
|
||||
ProductScreen.do.clickRefund();
|
||||
TicketScreen.do.selectOrder("-0001");
|
||||
TicketScreen.do.clickOrderline("Coca-Cola");
|
||||
TicketScreen.check.hasLine({
|
||||
productName: "Coca-Cola",
|
||||
});
|
||||
ProductScreen.do.pressNumpad("2");
|
||||
TicketScreen.check.toRefundTextContains("To Refund: 2.00");
|
||||
TicketScreen.do.confirmRefund();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScre
|
||||
import { ReceiptScreen } from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods";
|
||||
import { Chrome } from "@pos_restaurant/../tests/tours/helpers/ChromeTourMethods";
|
||||
import { FloorScreen } from "@pos_restaurant/../tests/tours/helpers/FloorScreenTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
import { ProductScreen } from "@pos_restaurant/../tests/tours/helpers/ProductScreenTourMethods";
|
||||
import { SplitBillScreen } from "@pos_restaurant/../tests/tours/helpers/SplitBillScreenTourMethods";
|
||||
import { TicketScreen } from "@point_of_sale/../tests/tours/helpers/TicketScreenTourMethods";
|
||||
@@ -92,7 +93,7 @@ registry.category("web_tour.tours").add("SplitBillScreenTour2", {
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.selectOrder("-0001");
|
||||
TicketScreen.do.loadSelectedOrder();
|
||||
ProductScreen.do.clickOrderline("Minute Maid", "1.0");
|
||||
Order.hasLine({ productName: "Minute Maid", quantity: "1.0", withClass: ".selected" });
|
||||
ProductScreen.check.totalAmountIs("2.00");
|
||||
return getSteps();
|
||||
},
|
||||
@@ -125,7 +126,7 @@ registry.category("web_tour.tours").add("SplitBillScreenTour3", {
|
||||
|
||||
// Check if there is still water in the order
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.do.clickOrderline("Water", "1.0");
|
||||
ProductScreen.check.selectedOrderlineHas("Water", "1.0");
|
||||
ProductScreen.do.clickPayButton(true);
|
||||
PaymentScreen.do.clickPaymentMethod("Bank");
|
||||
PaymentScreen.do.clickValidate();
|
||||
@@ -184,7 +185,7 @@ registry.category("web_tour.tours").add("SplitBillScreenTour4PosCombo", {
|
||||
// Check if there is still water in the order
|
||||
...ProductScreen.check.isShown(),
|
||||
// now we check that all the lines that remained in the order are correct
|
||||
...ProductScreen.do.clickOrderline("Minute Maid", "1.0"),
|
||||
...ProductScreen.check.selectedOrderlineHas("Minute Maid", "1.0"),
|
||||
...ProductScreen.do.clickOrderline("Office Combo"),
|
||||
...ProductScreen.do.clickOrderline("Combo Product 2"),
|
||||
...ProductScreen.check.selectedOrderlineHas(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
import { Do, Check, Execute } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
class DoExt extends Do {
|
||||
clickSplitBillButton() {
|
||||
@@ -13,13 +14,7 @@ class DoExt extends Do {
|
||||
];
|
||||
}
|
||||
doubleClickOrderline(name) {
|
||||
return [
|
||||
{
|
||||
content: "first click orderline",
|
||||
trigger: `.order .orderline .product-name:contains("${name}")`,
|
||||
run: "dblclick"
|
||||
},
|
||||
];
|
||||
return Order.hasLine({ productName: name, run: "dblclick" });
|
||||
}
|
||||
clickTransferButton() {
|
||||
return [
|
||||
@@ -77,13 +72,7 @@ class DoExt extends Do {
|
||||
|
||||
class CheckExt extends Check {
|
||||
orderlinesHaveNoChange() {
|
||||
return [
|
||||
{
|
||||
content: "Orderlines have no change",
|
||||
trigger: ".orderlines .orderline:not(.has-change)",
|
||||
run: function () {},
|
||||
},
|
||||
];
|
||||
return Order.doesNotHaveLine({ withClass: ".has-change" });
|
||||
}
|
||||
isPrintingError() {
|
||||
// because we don't have printer in the test.
|
||||
@@ -95,36 +84,16 @@ class CheckExt extends Check {
|
||||
];
|
||||
}
|
||||
orderlineIsToOrder(name) {
|
||||
return [
|
||||
{
|
||||
content: `Line is to order`,
|
||||
trigger: `.order .orderline.has-change .product-name:contains("${name}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
];
|
||||
return Order.hasLine({
|
||||
productName: name,
|
||||
withClass: ".has-change.text-success.border-start.border-success.border-4",
|
||||
});
|
||||
}
|
||||
orderlineIsToSkip(name) {
|
||||
return [
|
||||
{
|
||||
content: `Line is to order`,
|
||||
trigger: `.order .orderline.skip-change .product-name:contains("${name}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
];
|
||||
}
|
||||
orderlineHasNote(name, quantity, note) {
|
||||
return [
|
||||
{
|
||||
content: `line has ${quantity} quantity`,
|
||||
trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
{
|
||||
content: `line has '${note}' note`,
|
||||
trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`,
|
||||
run: function () {}, // it's a check
|
||||
},
|
||||
];
|
||||
return Order.hasLine({
|
||||
withClass: ".skip-change.text-primary.border-start.border-primary.border-4",
|
||||
productName: name,
|
||||
});
|
||||
}
|
||||
guestNumberIs(numberInString) {
|
||||
return [
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { createTourMethods } from "@point_of_sale/../tests/tours/helpers/utils";
|
||||
import * as Order from "@point_of_sale/../tests/tours/helpers/generic_components/OrderWidgetMethods";
|
||||
|
||||
class Do {
|
||||
clickOrderline(name, totalQuantity) {
|
||||
let trigger = `.splitbill-screen li.orderline .product-name:contains("${name}")`;
|
||||
if (totalQuantity) {
|
||||
trigger += ` ~ .info-list .info:contains("${totalQuantity}")`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
content: `click '${name}' orderline with total quantity of '${totalQuantity}'`,
|
||||
trigger,
|
||||
},
|
||||
];
|
||||
clickOrderline(productName) {
|
||||
return Order.hasLine({ productName, run: "click" });
|
||||
}
|
||||
clickBack() {
|
||||
return [
|
||||
@@ -35,18 +27,10 @@ class Do {
|
||||
|
||||
class Check {
|
||||
orderlineHas(name, totalQuantity, splitQuantity) {
|
||||
return [
|
||||
{
|
||||
content: `'${name}' orderline has total quantity of '${totalQuantity}'`,
|
||||
trigger: `.splitbill-screen li.orderline .product-name:contains("${name}") ~ .info-list .info:contains("${totalQuantity}")`,
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: `'${name}' orderline has '${splitQuantity}' quantity to split`,
|
||||
trigger: `.splitbill-screen li.orderline .product-name:contains("${name}") ~ .info-list .info em:contains("${splitQuantity}")`,
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
return Order.hasLine({
|
||||
productName: name,
|
||||
quantity: splitQuantity != 0 ? `${splitQuantity} / ${totalQuantity}` : totalQuantity,
|
||||
});
|
||||
}
|
||||
subtotalIs(amount) {
|
||||
return [
|
||||
|
||||
@@ -28,8 +28,8 @@ registry.category("web_tour.tours").add("pos_restaurant_sync", {
|
||||
ProductScreen.check.orderlineIsToOrder("Water");
|
||||
ProductScreen.check.orderlineIsToSkip("Coca-Cola");
|
||||
ProductScreen.do.clickOrderButton();
|
||||
ProductScreen.check.orderlinesHaveNoChange();
|
||||
ProductScreen.check.isPrintingError();
|
||||
ProductScreen.check.orderlinesHaveNoChange();
|
||||
ProductScreen.check.totalAmountIs("4.40");
|
||||
|
||||
// Create 2nd order (paid)
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
<?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="props.line.get_sale_order()">
|
||||
<li class="info orderline-sale-order ms-2">
|
||||
<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 ms-2">
|
||||
<t t-foreach='props.line.get_sale_order().details' t-as='line' t-key='line_index'>
|
||||
<tr>
|
||||
<td class="text-truncate"><t t-esc="line['product_uom_qty']"/>x</td>
|
||||
<td class="text-truncate" style="max-width: 275px;">
|
||||
<t t-esc="line['product_name']" />
|
||||
</td>
|
||||
<td class="text-truncate">:</td>
|
||||
<td class="text-truncate"><t t-esc="env.utils.formatCurrency(line['total'])" /> (tax incl.)</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_sale.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//Orderline" position="inside" >
|
||||
<t t-if="line.get_sale_order()">
|
||||
<li class="info orderline-sale-order ms-2">
|
||||
<i class="fa fa-shopping-basket me-1" role="img" aria-label="SO" title="SO"/>
|
||||
<t t-esc="line.get_sale_order().name" />
|
||||
</li>
|
||||
<table t-if="line.get_sale_order().details" class="sale-order-info ms-2">
|
||||
<tr t-foreach="line.get_sale_order()?.details" t-as="soLine" t-key="soLine_index">
|
||||
<td class="text-truncate"><t t-esc="soLine.product_uom_qty"/>x</td>
|
||||
<td class="text-truncate" style="max-width: 275px;"
|
||||
t-esc="soLine.product_name" />
|
||||
<td class="text-truncate">: </td>
|
||||
<td class="text-truncate"><t t-esc="env.utils.formatCurrency(soLine.total)" /> (tax incl.)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -105,7 +105,7 @@ registry
|
||||
// Filter should be automatically 'Paid'.
|
||||
TicketScreen.check.filterIs('Paid');
|
||||
TicketScreen.do.selectOrder('-0001');
|
||||
TicketScreen.do.clickOrderline('Down Payment');
|
||||
TicketScreen.check.hasLine({productName: 'Down Payment', withClass: '.selected', quantity: '1.0'});
|
||||
ProductScreen.do.pressNumpad('1');
|
||||
TicketScreen.do.confirmRefund();
|
||||
ProductScreen.do.clickPayButton();
|
||||
@@ -150,7 +150,6 @@ registry
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user