[MERGE][REF] pos*: bootstrap refactor

This PR contains 2  commits. The first handles the bootstrap refactor and the
second refactors different views. The  changes  brought in the 2 commits are
described below.

**1st commit:**
Prior to this commit, the point_of_sale module relied on custom CSS.

To enhance user experience, increase flexibility, and simplify maintenance,
most of the custom css was replaced in this PR with Bootstrap utility classes.

**2nd commit**
In this commit we refactor some parts of the pos ui with the interest
of providing a simpler and cleaner codebase.

One pattern that is heavily used in the pos ui is that in which
views are changed based on the screen size at the component level.
This means that it becomes very difficult to understand what parts
of a specific template will be seen in mobile or in desktop views.
In most cases, the pos adapts for mobile screens by using the same
main components, but in different configurations. For example, a
component that was visible in desktop mode, might become accesible
through a menu in mobile mode.

In order to improve the situation, the proposed solution is to
separate the logical parts of the ui into `owl templates` and
to inject this templates wherever the need arises.
Thus, instead of constantly using `t-if="ui.isSmall"` in all places
where there is a difference between the mobile and desktop views,
we have a main conditional branch at the root level of the component
representing a certain screen and then build 2 separate views, one for
mobile and one for desktop, each calling the needed templates.

In this commit we implement this pattern for the `payment_screen`.
This will allow us to observe this pattern over a period of time,
before implementing it on the other screens.

In this commit we also:
- add small fixes and improvements relating to
  the previous commit, in which the pos was
  refactored to bootstrap;
- replace the `<br>` tags in the numpad component
  with the bootstrap grid system;
- adapt the `cash_move_popup` to rely more on `t-model`
  and less on `t-on-change`

Task: 3354582

closes odoo/odoo#129544

Related: odoo/enterprise#44197
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
Odoo's Mergebot
2023-07-28 16:28:46 +02:00
committed by GitHub
165 changed files with 2343 additions and 6953 deletions
+20 -12
View File
@@ -77,16 +77,32 @@
# Main PoS assets, they are loaded in the PoS UI and in the PoS unit tests
'point_of_sale._assets_pos': [
# scss variables and utilities
'point_of_sale/static/src/scss/pos_variables_extra.scss',
# 'preparation_display' bootstrap customization layer
'web/static/src/scss/functions.scss',
# 'point_of_sale/static/src/scss/primary_variables.scss', TO DO - CREATE
# 'webclient' bootstrap customization layer
('include', 'web._assets_helpers'),
'web/static/src/scss/bootstrap_overridden.scss',
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/fonts/fonts.scss',
# Import Bootstrap
('include', 'web._assets_bootstrap_backend'),
# Icons
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*',
'web/static/src/webclient/icons.scss',
# scss variables and utilities
'point_of_sale/static/src/scss/pos_variables_extra.scss',
'web/static/src/scss/bootstrap_overridden.scss',
'web/static/fonts/fonts.scss',
'web/static/src/legacy/scss/fontawesome_overridden.scss',
# JS framework
'web/static/src/boot.js',
'web/static/src/session.js',
@@ -228,14 +244,6 @@
'web/static/tests/main.js',
'web/static/tests/setup.js',
# These 2 lines below are taken from web.assets_frontend
# They're required for the web.frontend_legacy to work properly
# It is expected to add other lines coming from the web.assets_frontend
# if we need to add more and more legacy stuff that would require other scss or js.
('include', 'web._assets_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web.frontend_legacy'),
("include", "web.assets_backend_legacy_lazy"),
## END copy of web.tests_assets
@@ -3,20 +3,15 @@
<t t-name="point_of_sale.ErrorBarcodePopup" owl="1">
<div class="popup popup-barcode">
<header class="title drag-handle">
<span>Unknown Barcode</span>
<br />
<span class="barcode">
<t t-esc="props.code" />
</span>
<header class="title drag-handle modal-header">
<h5 class="modal-title">Unknown Barcode: <span class="fst-italic" t-esc="props.code"/>
</h5>
</header>
<main class="body">
<main class="modal-body">
<t t-esc="translatedMessage" />
</main>
<footer class="footer">
<div class="button cancel" t-on-click="confirm">
Ok
</div>
<footer class="footer modal-footer">
<button type="button" class="btn btn-primary" t-on-click="confirm">Ok</button>
</footer>
</div>
</t>
@@ -4,87 +4,6 @@
right: 10px;
top: 106px;
width: 200px;
font-size: 10px;
background: rgba(0, 0, 0, 0.82);
color: white;
padding-bottom: 10px;
cursor: move;
-webkit-transform: translate3d(0, 0, 0);
}
.debug-widget .toggle {
position: absolute;
font-size: 16px;
cursor: pointer;
top: 0px;
right: 0px;
padding: 10px;
padding-right: 15px;
}
.debug-widget .content {
overflow: hidden;
}
.debug-widget header {
position: relative;
}
.debug-widget h1 {
background: black;
padding-top: 10px;
padding-left: 10px;
margin-top: 0;
margin-bottom: 0;
}
.debug-widget .category {
background: black;
padding-left: 10px;
margin: 0px;
font-weight: bold;
padding-top: 3px;
padding-bottom: 3px;
}
.debug-widget .button {
padding: 5px;
padding-left: 15px;
display: block;
cursor: pointer;
}
.debug-widget .button:active {
background: rgba(96, 21, 177, 0.45);
}
.debug-widget input {
margin-left: 10px;
margin-top: 7px;
padding: 4px;
width: 180px;
border: none;
box-sizing: border-box;
-moz-box-sizing: border-box;
border-radius: 3px;
}
.debug-widget .status {
padding: 5px;
padding-left: 15px;
display: block;
cursor: default;
}
.debug-widget .status.on {
background-color: #6cd11d;
}
.debug-widget .event {
padding: 5px;
padding-left: 15px;
display: block;
cursor: default;
background-color: #1E1E1E;
}
};
@@ -3,88 +3,100 @@
<t t-name="point_of_sale.DebugWidget" owl="1">
<Transition visible="props.state.showWidget" name="'o-fade'" leaveDuration="200" t-slot-scope="transition">
<div class="debug-widget" t-att-class="transition.className" t-ref="root" t-att-style="style">
<div class="debug-widget bg-100 p-2 shadow-lg" t-att-class="transition.className" t-ref="root" t-att-style="style">
<header class="drag-handle">
<h1>Debug Window</h1>
<h2>Debug Window</h2>
</header>
<div class="toggle" t-on-click="() => debug.toggleWidget()" title="Dismiss"
role="img" aria-label="Dismiss"><i class="fa fa-times" /></div>
<div class="content">
<p class="category">Electronic Scale</p>
<ul>
<li>
<input t-model="state.weightInput" type="text" class="weight"></input>
</li>
<li class="button set_weight" t-on-click="setWeight">Set Weight</li>
<li class="button reset_weight" t-on-click="resetWeight">Reset</li>
</ul>
<div class="toggle position-absolute top-0 end-0 p-2" t-on-click="() => debug.toggleWidget()" title="Dismiss"
role="img" aria-label="Dismiss"><i class="fa fa-fw fa-times" /></div>
<div class="content overflow-hidden">
<p class="category">Barcode Scanner</p>
<ul>
<li>
<input t-model="state.barcodeInput" type="text" class="ean" t-att-disabled="!barcodeReader" t-attf-placeholder="{{ !barcodeReader ? 'No nomenclature' : '' }}"/>
</li>
<li class="button barcode" t-on-click="barcodeScan">Scan</li>
<li class="button custom_ean" t-on-click="barcodeScanEAN">Scan EAN-13</li>
</ul>
<div class="debug-electronic-scale">
<h5 class="category">Electronic Scale</h5>
<input t-model="state.weightInput" type="text" class="weight form-control"></input>
<ul class="d-flex flex-column align-items-start mb-3">
<li class="set_weight btn btn-link" t-on-click="setWeight">Set Weight</li>
<li class="reset_weight btn btn-link" t-on-click="resetWeight">Reset</li>
</ul>
</div>
<p class="category">Orders</p>
<ul>
<li class="button" t-on-click="deleteOrders">
Delete Paid Orders
</li>
<li class="button" t-on-click="deleteUnpaidOrders">
Delete Unpaid Orders
</li>
<li t-if="!state.isPaidOrdersReady" class="button"
t-on-click="preparePaidOrders">
Export Paid Orders
</li>
<a t-else="" t-att-download="paidOrdersFilename" t-att-href="paidOrdersURL"
t-on-click="() => { state.isPaidOrdersReady = !state.isPaidOrdersReady }">
<li class="button">
Download Paid Orders
<div class="debug-barcode-scanner">
<h5 class="category">Barcode Scanner</h5>
<ul class="d-flex flex-column align-items-start mb-3">
<li>
<input t-model="state.barcodeInput" type="text" class="ean form-control" t-att-disabled="!barcodeReader" t-attf-placeholder="{{ !barcodeReader ? 'No nomenclature' : '' }}"/>
</li>
</a>
<li t-if="!state.isUnpaidOrdersReady" class="button"
t-on-click="prepareUnpaidOrders">
Export Unpaid Orders
</li>
<a t-else="" t-att-download="unpaidOrdersFilename"
t-att-href="unpaidOrdersURL"
t-on-click="() => { state.isUnpaidOrdersReady = !state.isUnpaidOrdersReady }">
<li class="button">
Download Unpaid Orders
</li>
</a>
<li class="button import_orders" style="position:relative">
Import Orders
<input t-on-change="importOrders" type="file"
style="opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;margin:0;cursor:pointer" />
</li>
</ul>
<li class="button btn btn-link barcode" t-on-click="barcodeScan">Scan</li>
<li class="button btn btn-link custom_ean" t-on-click="barcodeScanEAN">Scan EAN-13</li>
</ul>
</div>
<p class="category">Hardware Status</p>
<ul>
<li class="status weighing">Weighing</li>
<li class="button display_refresh" t-on-click="refreshDisplay">
Refresh Display
</li>
</ul>
<p class="category">Hardware Events</p>
<ul>
<li class="event" t-ref="open_cashbox">Open Cashbox</li>
<li class="event" t-ref="print_receipt">Print Receipt</li>
<li class="event" t-ref="scale_read">Read Weighing Scale</li>
</ul>
<p class="category">Others</p>
<ul>
<li class="event">
<span>Buffer: </span>
<t t-esc="bufferRepr" />
</li>
</ul>
<div class="debug-orders">
<h5 class="category">Orders</h5>
<ul class="mb-3">
<li class="button btn btn-link" t-on-click="deleteOrders">
Delete Paid Orders
</li>
<li class="button btn btn-link" t-on-click="deleteUnpaidOrders">
Delete Unpaid Orders
</li>
<li t-if="!state.isPaidOrdersReady" class="button btn btn-link"
t-on-click="preparePaidOrders">
Export Paid Orders
</li>
<a t-else="" t-att-download="paidOrdersFilename" t-att-href="paidOrdersURL"
t-on-click="() => { state.isPaidOrdersReady = !state.isPaidOrdersReady }">
<li class="button btn btn-link">
Download Paid Orders
</li>
</a>
<li t-if="!state.isUnpaidOrdersReady" class="button btn btn-link"
t-on-click="prepareUnpaidOrders">
Export Unpaid Orders
</li>
<a t-else="" t-att-download="unpaidOrdersFilename"
t-att-href="unpaidOrdersURL"
t-on-click="() => { state.isUnpaidOrdersReady = !state.isUnpaidOrdersReady }">
<li class="button btn btn-link">
Download Unpaid Orders
</li>
</a>
<li class="button btn btn-link import_orders" style="position:relative">
Import Orders
<input t-on-change="importOrders" type="file"
style="opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;margin:0;cursor:pointer" />
</li>
</ul>
</div>
<div class="debug-hardware-status d-flex flex-column">
<h5 class="category">Hardware Status</h5>
<ul class="mb-3">
<li class="status weighing">Weighing</li>
<li class="button btn btn-link display_refresh" t-on-click="refreshDisplay">
Refresh Display
</li>
</ul>
</div>
<div class="debug-hardware-events">
<h5 class="category">Hardware Events</h5>
<ul class="mb-3">
<li class="event" t-ref="open_cashbox">Open Cashbox</li>
<li class="event" t-ref="print_receipt">Print Receipt</li>
<li class="event" t-ref="scale_read">Read Weighing Scale</li>
</ul>
</div>
<div class="debug-others">
<h5 class="category">Others</h5>
<ul class="mb-3">
<li class="event">
<span>Buffer: </span>
<t t-esc="bufferRepr" />
</li>
</ul>
</div>
</div>
</div>
</Transition>
@@ -3,10 +3,12 @@
<t t-name="point_of_sale.OrderImportPopup" owl="1">
<div class="popup popup-import">
<header class="title drag-handle">
<span>Finished Importing Orders</span>
</header>
<ul class="body">
<div class="modal-header">
<h4 class="modal-title title drag-handle">
Finished Importing Orders
</h4>
</div>
<main class="body modal-body">
<li>Successfully imported <b><t t-esc="props.report.paid or 0" /></b> paid orders</li>
<li>Successfully imported <b><t t-esc="props.report.unpaid or 0" /></b> unpaid orders</li>
<t t-if="unpaidSkipped">
@@ -23,9 +25,9 @@
</ul>
</li>
</t>
</ul>
<footer class="footer">
<div class="button cancel" t-on-click="confirm">
</main>
<footer class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
</footer>
@@ -3,14 +3,16 @@
<t t-name="point_of_sale.ErrorPopup" owl="1">
<div class="popup popup-error">
<p class="title">
<t t-esc="props.title" />
</p>
<p class="body">
<div class="modal-header">
<h4 class="modal-title title">
<t t-esc="props.title" />
</h4>
</div>
<main class="modal-body">
<t t-esc="props.body" />
</p>
<div class="footer">
<div class="button cancel" t-on-click="confirm">
</main>
<div class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary " t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
</div>
@@ -3,29 +3,32 @@
<t t-name="point_of_sale.ErrorTracebackPopup" owl="1">
<div class="popup popup-error">
<header class="title">
<t t-esc="props.title" />
</header>
<main class="body traceback">
<div class="modal-header">
<h4 class="modal-title title">
<t t-esc="props.title" />
</h4>
</div>
<main class="modal-body body traceback">
<t t-esc="props.body" />
</main>
<footer class="footer">
<div t-if="!props.exitButtonIsShown" class="button cancel" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<div t-if="props.exitButtonIsShown" class="button cancel" t-on-click="() => this.pos.closePos()">
<t t-esc="props.exitButtonText" />
</div>
<a t-att-download="tracebackFilename" t-att-href="tracebackUrl">
<div class="button icon download">
<i class="fa fa-download" role="img"
aria-label="Download error traceback"
title="Download error traceback"></i>
<footer class="footer modal-footer justify-content-between">
<div class="d-flex gap-1">
<a t-att-download="tracebackFilename" t-att-href="tracebackUrl">
<div class="button icon download btn btn-lg btn-secondary">
<i class="fa fa-download" role="img" aria-label="Download error traceback" title="Download error traceback"></i>
</div>
</a>
<div class="button icon email btn btn-lg btn-secondary" t-on-click="emailTraceback">
<i class="fa fa-paper-plane" role="img" aria-label="Send by email" title="Send by email"></i>
</div>
</div>
<div class="d-flex gap-1">
<div t-if="!props.exitButtonIsShown" class="button cancel btn btn-lg btn-primary" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<div t-if="props.exitButtonIsShown" class="button cancel btn btn-lg btn-secondary" t-on-click="() => this.pos.closePos()">
<t t-esc="props.exitButtonText" />
</div>
</a>
<div class="button icon email" t-on-click="emailTraceback">
<i class="fa fa-paper-plane" role="img" aria-label="Send by email"
title="Send by email"></i>
</div>
</footer>
</div>
@@ -3,14 +3,16 @@
<t t-name="point_of_sale.OfflineErrorPopup" owl="1">
<div class="popup offline">
<header class="title">
<t t-esc="props.title" />
</header>
<main class="body">
<div class="modal-header">
<h4 class="modal-title title">
<t t-esc="props.title" />
</h4>
</div>
<main class="modal-body">
<t t-esc=" props.body" />
</main>
<footer class="footer footer-flex">
<div class="button dont-show-again highlight" t-on-click="confirm">
<footer class="footer footer-flex modal-footer">
<div class="button dont-show-again highlight btn btn-lg btn-primary w-auto" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
</footer>
@@ -3,10 +3,10 @@
<t t-name="point_of_sale.BackButton" owl="1">
<span
class="order-button floor-button"
class="order-button floor-button d-flex align-items-center gap-2 px-4 fw-bolder cursor-pointer"
t-on-click="backToFloorScreen"
>
<i class="fa fa-angle-left" role="img" aria-label="Go Back" title="Go Back" />
<i class="fa fa-2x fa-angle-left" role="img" aria-label="Go Back" title="Go Back" />
<span t-if="!ui.isSmall">BACK</span>
</span>
</t>
@@ -31,7 +31,7 @@ export class CashMovePopup extends AbstractAwaitablePopup {
parsedAmount: 0,
});
this.amountInput = useAutofocus({ refName: "amountInput" });
useValidateCashInput('amountInput');
useValidateCashInput("amountInput");
}
async confirm() {
let amount;
@@ -117,13 +117,7 @@ export class CashMovePopup extends AbstractAwaitablePopup {
this.state.errorMessage = "";
this.amountInput.el.focus();
}
handleInputChange() {
if (this.amountInput.el.classList.contains('invalid-cash-input')) {
this.amountInput.el.parentElement.classList.add('invalid-cash-input');
this.amountInput.el.style.border = 'none';
return;
}
this.amountInput.el.parentElement.classList.remove('invalid-cash-input');
this.state.parsedAmount = parseFloat(this.state.amount);
format(value) {
return this.env.utils.isValidFloat(value) ? this.env.utils.formatCurrency(parseFloat(value)) : "!!!";
}
}
@@ -1,94 +0,0 @@
.cash-move-popup {
top: 20% !important;
.input-amount {
display: flex;
flex-flow: row;
.input-container {
padding: 0px;
padding-left: 5px;
padding-right: 5px;
display: flex;
align-items: center;
}
}
.input-type {
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 2px;
padding: 0 12px;
margin-right: 10px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
border: solid 1px rgba(60,60,60,0.1);
background: rgba(0,0,0,0.05);
white-space: nowrap;
&.highlight {
background: $primary !important;
border: solid 1px #64AF8A !important;
color: white !important;
}
&.red-highlight {
background: #bd2222 !important;
border: solid 1px #bd2222 !important;
color: white !important;
}
input {
width: 100%;
min-height: auto;
text-align: right;
padding-right: 30px;
}
}
textarea {
resize: none;
margin-top: 10px;
margin-bottom: 10px;
}
.error-message {
color: rgb(197, 52, 0);
text-align: left;
padding-left: 5px;
}
// FIXME POSREF increased selector specificity to override default style. Should just be "footer"
footer.cash-move.cash-move {
display: block;
width: calc(100% - 36px);
padding: 10px 18px 10px 18px;
.button.confirm {
float: left;
}
.button.cancel {
margin-right: 0;
}
}
.input-container .input-field,
.input-container .input-field:focus {
position: relative;
flex-grow: 1;
border: none;
outline: none;
box-shadow: none;
background-color: transparent;
padding: 0px;
padding-left: 5px;
}
}
.cash-move {
display: flex;
flex-flow: column;
}
@@ -3,32 +3,37 @@
<t t-name="point_of_sale.CashMovePopup" owl="1">
<div class="popup cash-move-popup">
<main class="body">
<div class="cash-move">
<div class="input-amount">
<span t-on-click="() => this.onClickButton('in')" class="input-type" t-att-class="{ highlight: state.type === 'in' }">
Cash In
</span>
<span t-on-click="() => this.onClickButton('out')" class="input-type" t-att-class="{ 'red-highlight': state.type === 'out' }">
Cash Out
</span>
<div class="input-container popup-input" t-on-input="handleInputChange" >
<main class="modal-body">
<div class="cash-move d-flex flex-column">
<div class="input-amount d-flex mb-2 gap-2">
<div class="input-group">
<button t-on-click="() => this.onClickButton('in')" class="input-type btn btn-secondary flex-fill py-2 fw-bolder" t-att-class="{ 'highlight btn-success': state.type === 'in' }">
Cash In
</button>
<button t-on-click="() => this.onClickButton('out')" class="input-type btn btn-secondary flex-fill py-2 fw-bolder" t-att-class="{ 'red-highlight btn-danger': state.type === 'out' }">
Cash Out
</button>
</div>
<div class="input-container popup-input align-items-center form-control d-flex">
<span class="currency" t-esc="pos.currency.symbol" t-if="pos.currency.position=='before'"/>
<input class="input-field" type="text" name="amount" t-model="state.amount" t-ref="amountInput" t-on-keypress="_onAmountKeypress"/>
<input class="input-field border-0 px-2" type="text" name="amount" t-model="state.amount" t-ref="amountInput" t-on-keypress="_onAmountKeypress"/>
<span class="currency" t-esc="pos.currency.symbol" t-if="pos.currency.position=='after'"/>
</div>
</div>
<textarea name="reason" t-model="state.reason" placeholder="Reason"></textarea>
<span t-if="state.errorMessage" class="error-message">
<div class="form-floating">
<textarea class="form-control" placeholder="Leave a reason here" name="reason" id="reason" t-model="state.reason" style="height:100px;" />
<label for="reason">Reason</label>
</div>
<span t-if="state.errorMessage" class="error-message alert alert-danger mt-2">
<t t-esc="state.errorMessage" />
</span>
</div>
</main>
<footer class="footer cash-move">
<div class="button confirm disable highlight dynamic-size" t-on-click="confirm">
Confirm <span t-esc="env.utils.formatCurrency(state.parsedAmount)"/>
<footer class="footer cash-move modal-footer">
<div class="button confirm disable highlight dynamic-size btn btn-lg btn-primary" t-on-click="confirm">
Confirm <span t-esc="format(state.amount)"/>
</div>
<div class="button cancel" t-on-click="cancel">
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
Discard
</div>
</footer>
@@ -2,8 +2,8 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CashierName" owl="1">
<div t-att-class="cssClass">
<span class="cashier-name"><img t-att-src="avatar" t-att-alt="username" class="avatar"/> <span t-if="!ui.isSmall" t-esc="username" class="username"/></span>
<div t-att-class="cssClass" class="btn btn-light border-0 rounded-0">
<span class="cashier-name d-flex align-items-center gap-1"><img t-att-src="avatar" t-att-alt="username" class="avatar rounded-3"/> <span t-if="!ui.isSmall" t-esc="username" class="username"/></span>
</div>
</t>
@@ -85,7 +85,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup {
this.hardwareProxy.openCashbox(action);
const { confirmed, payload } = await this.popup.add(MoneyDetailsPopup, {
moneyDetails: this.moneyDetails,
total: this.manualInputCashCount
total: this.manualInputCashCountpayments
? 0
: this.state.payments[this.defaultCashDetails.id].counted,
action: action,
@@ -3,22 +3,22 @@
<t t-name="point_of_sale.ClosePosPopup" owl="1">
<div class="popup close-pos-popup">
<header>
<div class="title">Closing Session</div>
<div class="total-orders">
<div class="modal-header">
<h4 class="modal-title">Closing Session</h4>
<div class="total-orders fw-bolder">
Total <t t-esc="ordersDetails.quantity"/> orders:
<span class="amount" t-esc="env.utils.formatCurrency(ordersDetails.amount)"/>
</div>
</header>
<main class="body">
<div class="payment-methods-overview">
<table>
</div>
<main class="modal-body">
<div class="payment-methods-overview overflow-auto">
<table class="text-start">
<thead>
<tr>
<tr class="text-dark">
<th>Payment Method</th>
<th>Expected</th>
<th style="width: 20%">Counted</th>
<th style="width: 20%">Difference</th>
<th style="width:20%">Counted</th>
<th>Difference</th>
</tr>
</thead>
<t t-if="cashControl">
@@ -26,37 +26,38 @@
<tr>
<td t-esc="defaultCashDetails.name"/>
<td t-esc="env.utils.formatCurrency(defaultCashDetails.amount)"/>
<td class="flex" t-on-input="(event) => this.handleInputChange(defaultCashDetails.id, event)">
<input class="pos-input" t-ref="closingCashInput" t-on-focus="ev=>ev.target.select()"/>
<div class="button icon ClosePosPopup" t-on-click="openDetailsPopup">
<i class="fa fa-money" role="img" title="Open the money details popup"/>
<td class="input-group" t-on-input="(event) => this.handleInputChange(defaultCashDetails.id, event)">
<input class="pos-input form-control text-end" t-ref="closingCashInput" t-on-focus="ev=>ev.target.select()"/>
<div class="button icon ClosePosPopup btn btn-secondary" t-on-click="openDetailsPopup">
<i class="fa fa-money fa-2x" role="img" title="Open the money details popup"/>
</div>
</td>
<td t-esc="env.utils.formatCurrency(state.payments[defaultCashDetails.id].difference)" t-att-class="{'warning': state.payments[defaultCashDetails.id].difference}"/>
<td t-esc="env.utils.formatCurrency(state.payments[defaultCashDetails.id].difference)"
t-att-class="{'warning text-danger fw-bolder': state.payments[defaultCashDetails.id].difference}"/>
</tr>
</tbody>
<tbody class="cash-overview">
<tbody class="cash-overview border-start small text-muted">
<tr>
<td>Opening</td>
<td t-esc="env.utils.formatCurrency(defaultCashDetails.opening)"/>
<td class="align-top" t-esc="env.utils.formatCurrency(defaultCashDetails.opening)"/>
</tr>
<tr t-foreach="defaultCashDetails.moves" t-as="move" t-key="move_index">
<td>
<div class="flex">
<div class="cash-sign" t-esc="move.amount &lt; 0 ? '-' : '+'"/>
<div class="flex d-flex flex-row">
<div class="cash-sign me-1" t-esc="move.amount &lt; 0 ? '-' : '+'"/>
<t t-esc="move.name"/>
</div>
</td>
<td t-esc="env.utils.formatCurrency(Math.abs(move.amount))"/>
<td class="align-top" t-esc="env.utils.formatCurrency(Math.abs(move.amount))"/>
</tr>
<tr t-if="defaultCashDetails.payment_amount">
<td>
<div class="flex">
<div class="cash-sign" t-esc="defaultCashDetails.payment_amount &lt; 0 ? '-' : '+'"/>
<div class="flex d-flex flex-row">
<div class="cash-sign me-1" t-esc="defaultCashDetails.payment_amount &lt; 0 ? '-' : '+'"/>
Payments in <t t-esc="defaultCashDetails.name"/>
</div>
</td>
<td t-esc="env.utils.formatCurrency(Math.abs(defaultCashDetails.payment_amount))"/>
<td class="align-top" t-esc="env.utils.formatCurrency(Math.abs(defaultCashDetails.payment_amount))"/>
</tr>
</tbody>
</t>
@@ -66,41 +67,43 @@
<td t-esc="env.utils.formatCurrency(pm.amount)"/>
<t t-set="_showDiff" t-value="_getShowDiff(pm)" />
<td t-if="_showDiff" t-on-input="(event) => this.handleInputChange(pm.id, event)">
<input class="pos-input" t-ref="closingCashInput_{{pm.id}}" t-on-focus="ev=>ev.target.select()"/>
<input class="pos-input form-control" t-ref="closingCashInput_{{pm.id}}" t-on-focus="ev=>ev.target.select()"/>
</td>
<td t-if="_showDiff" t-esc="env.utils.formatCurrency(state.payments[pm.id].difference)" t-att-class="{'warning': state.payments[pm.id].difference}"/>
<td t-if="_showDiff" t-esc="env.utils.formatCurrency(state.payments[pm.id].difference)" t-att-class="{'warning text-danger fw-bolder': state.payments[pm.id].difference}"/>
</tr>
</tbody>
</table>
</div>
</main>
<main class="body notes">
<div class="notes-container">
<div class="opening-notes-container" t-if="openingNotes" >
Opening note
<textarea class="opening-notes">
<div class="notes-container d-flex flex-column flex-sm-row gap-3 border-top mt-3 pt-3">
<div class="opening-notes-container d-flex flex-column flex-grow-1 align-items-start" t-if="openingNotes" >
<label class="form-label" for="openingNotes">Opening note</label>
<textarea class="opening-notes form-control" id="openingNotes">
<t t-esc="openingNotes"/>
</textarea>
</div>
<div class="closing-notes-container">
Closing note
<textarea class="closing-notes" placeholder="Add a closing note..." t-model="state.notes"/>
<div class="closing-notes-container d-flex flex-column flex-grow-1 align-items-start">
<label class="form-label" for="closingNotes">Closing note</label>
<textarea class="closing-notes form-control" id="closingNotes" placeholder="Add a closing note..." t-model="state.notes"/>
</div>
</div>
</main>
<footer class="footer">
<div class="button highlight" t-on-click="confirm">Close Session</div>
<div class="button" t-att-class="{'disabled': !canCancel()}" t-on-click="cancel">Discard</div>
<!-- Download Sale Details -->
<div class="small button icon"
t-on-click="downloadSalesReport"
title="Download a report with all the sales of the current PoS Session">
<div class="daily sale">Daily Sale</div>
<i class="fa fa-download" role="img"/>
<footer class="footer modal-footer justify-content-between">
<div class="modal-footer-left d-flex gap-2">
<button class="button highlight btn btn-lg btn-primary" t-on-click="confirm">Close Session</button>
<button class="button btn btn-lg btn-secondary" t-att-class="{'disabled': !canCancel()}" t-on-click="cancel">Discard</button>
</div>
<!-- Print Sale Details -->
<div t-if="hardwareProxy.printer" class="small button icon">
<SaleDetailsButton/>
<!-- Download Sale Details -->
<div class="modal-footer-right d-flex gap-2">
<button class="button icon btn btn-lg btn-secondary"
t-on-click="downloadSalesReport"
title="Download a report with all the sales of the current PoS Session">
Daily Sale
<i class="fa fa-download" role="img"/>
</button>
<!-- Print Sale Details -->
<button t-if="hardwareProxy.printer" class="button icon btn btn-lg btn-secondary">
<SaleDetailsButton/>
</button>
</div>
</footer>
</div>
@@ -2,20 +2,21 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CustomerFacingDisplayButton" owl="1">
<div class="menu-item navbar-button" t-on-click="() => {customerDisplay.connect(); this.displayMessage(message);}">
<div t-if="customerDisplay.status === 'warning'" class="js_warning oe_icon oe_orange">
<button class="menu-item navbar-button btn btn-light rounded-0 d-flex align-items-center px-3 cursor-pointer" t-on-click="() => {customerDisplay.connect(); this.displayMessage(message);}">
<div t-if="customerDisplay.status === 'warning'" class="js_warning oe_icon oe_orange text-warning">
<i class="fa fa-fw fa-desktop" role="img" t-attf-aria-label="Customer Screen Warning : {{message}}" t-attf-title="Customer Screen Warning : {{message}}"></i>
</div>
<div t-if="customerDisplay.status === 'failure'" class="js_disconnected oe_icon oe_red">
<div t-if="customerDisplay.status === 'failure'" class="js_disconnected oe_icon oe_red text-danger">
<i class="fa fa-fw fa-desktop" role="img" t-attf-aria-label="Customer Screen : {{message}}" t-attf-title="Customer Screen : {{message}}"></i>
</div>
<div t-if="customerDisplay.status === 'not_found'" class="js_disconnected oe_icon oe_red">
<div t-if="customerDisplay.status === 'not_found'" class="js_disconnected oe_icon oe_red text-danger">
<i class="fa fa-fw fa-desktop" role="img" t-attf-aria-label="Customer Screen Not Found : {{message}}" t-attf-title="Customer Screen Not Found : {{message}}"></i>
</div>
<div t-if="customerDisplay.status === 'success'" class="js_connected oe_icon oe_green">
<div t-if="customerDisplay.status === 'success'" class="js_connected oe_icon oe_green text-success">
<i class="fa fa-fw fa-desktop" role="img" aria-label="Customer Screen Connected" title="Customer Screen Connected"></i>
</div>
</div>
<span class="ms-1">Customer Screen</span>
</button>
</t>
</templates>
@@ -0,0 +1,31 @@
.pos-topheader {
height: $navbar-height;
}
.pos-branding{
max-width: $left-pane-width;
}
.status-buttons {
> div {
display: flex;
align-items: center;
padding: $o-horizontal-padding * 0.75;
}
.avatar {
width: 25px;
}
}
.menu {
z-index: $zindex-dropdown;
}
.sub-menu {
top: $navbar-height * 2/3;
right: $navbar-height * -1/3 ;
}
@@ -2,54 +2,59 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.Navbar" owl="1">
<div class="pos-topheader">
<div class="pos-topheader d-flex w-100 p-0 m-0 bg-white border-bottom">
<div t-if="pos.tempScreen" class="block-top-header" />
<div t-if="!ui.isSmall" class="pos-branding">
<img class="pos-logo" t-on-click="() => debug.toggleWidget()" src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
<div t-if="!ui.isSmall" class="pos-branding d-flex justify-content-start flex-grow-1 h-100 p-0 my-0 text-start">
<img class="pos-logo h-75 ms-3 me-auto align-self-center" t-on-click="() => debug.toggleWidget()" src="/web/static/img/logo.png" alt="Logo" />
</div>
<div class="pos-rightheader">
<div class="pos-rightheader d-flex flex-grow-1 h-100">
<BackButton t-if="showBackButton()"/>
<div class="status-buttons">
<div class="status-buttons d-flex flex-grow-1 flex-basis-0 justify-content-end h-100">
<CashierName />
<SaleDetailsButton t-if="hardwareProxy.printer" isHeaderButton="true"/>
<ProxyStatus t-if="pos.config.use_proxy" />
<CustomerFacingDisplayButton t-if="customerFacingDisplayButtonIsShown" />
<SyncNotification />
</div>
<div t-if="isBurgerMenuClosed()" class="navbar-button menu-button" t-on-click="openMenu">
<div t-if="isBurgerMenuClosed()" class="navbar-button menu-button btn btn-light rounded-0 d-flex align-items-center px-3 cursor-pointer" t-on-click="openMenu">
<i class="fa fa-bars" aria-hidden="true"></i>
</div>
<div t-else="" class="navbar-button menu menu-button" t-on-click="closeMenu">
<div t-else="" class="navbar-button menu menu-button btn btn-light rounded-0 d-flex align-items-center px-3 cursor-pointer" t-on-click="closeMenu">
<ul>
<li class="menu-item">
<li class="menu-item position-relative d-flex">
<i class="fa fa-bars" aria-hidden="true"></i>
<ul class="sub-menu">
<li class="menu-item navbar-button ticket-button" t-on-click="onTicketButtonClick">
<div class="with-badge" t-att-badge="orderCount">
<span>Orders</span>
</div>
<ul class="dropdown-menu sub-menu position-absolute d-flex flex-column align-items-stretch w-100 bg-white shadow-lg z-index-1">
<li class="ticket-button" t-on-click="onTicketButtonClick">
<a class="dropdown-item with-badge py-2">
Orders
<span class="badge text-bg-info rounded-pill py-1 ms-2" t-esc="orderCount"/>
</a>
</li>
<li t-if="showCashMoveButton" class="menu-item navbar-button" t-on-click="onCashMoveButtonClick">
<span>Cash In/Out</span>
<a class="dropdown-item py-2">
Cash In/Out
</a>
</li>
<CustomerFacingDisplayButton t-if="CustomerFacingDisplayButtonIsShown"/>
<li class="menu-item navbar-button backend-button" t-on-click="() => pos.closePos()">
<span>
<li class="backend-button" t-on-click="() => pos.closePos()">
<a class="dropdown-item py-2">
Backend
</span>
</a>
</li>
<li class="menu-item navbar-button close-button" t-on-click="closeSession">
<span>
<li class="close-button" t-on-click="closeSession">
<a class="dropdown-item py-2">
Close Session
</span>
</a>
</li>
<li t-if="showToggleProductView" class="menu-item navbar-button close-button" t-on-click="toggleProductView">
<span>
<a class="dropdown-item py-2">
Switch Product View
</span>
</a>
</li>
<li t-if="this.env.debug" t-on-click="() => debug.toggleWidget()" class="menu-item navbar-button">
<span>Debug Window</span>
<li t-if="this.env.debug" class="menu-item navbar-button" t-on-click="() => debug.toggleWidget()">
<a class="dropdown-item py-2">
Debug Window
</a>
</li>
</ul>
</li>
@@ -2,11 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProxyStatus" owl="1">
<div class="oe_status js_proxy" t-on-click="() => this.pos.connectToProxy()">
<button class="oe_status js_proxy btn btn-light" t-on-click="() => this.pos.connectToProxy()">
<span t-if="this.message and !ui.isSmall" class="js_msg">
<t t-esc="this.message" />
</span>
<span t-if="connectionInfo.status === 'connected' and !this.message" class="js_connected oe_green">
<span t-if="connectionInfo.status === 'connected' and !this.message" class="js_connected text-success">
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Connected"
title="Proxy Connected"></i>
</span>
@@ -14,15 +14,15 @@
<i class="fa fa-fw fa-spin fa-circle-o-notch" role="img" aria-label="Connecting to Proxy"
title="Connecting to Proxy"></i>
</span>
<span t-elif="connectionInfo.status === 'disconnected'" class="js_disconnected oe_red">
<span t-elif="connectionInfo.status === 'disconnected'" class="js_disconnected text-danger">
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Disconnected"
title="Proxy Disconnected"></i>
</span>
<span t-elif="connectionInfo.status === 'warning' or this.message" class="js_warning oe_orange">
<span t-elif="connectionInfo.status === 'warning' or this.message" class="js_warning text-warning">
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Warning"
title="Proxy Warning"></i>
</span>
</div>
</button>
</t>
</templates>
@@ -2,8 +2,8 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.SaleDetailsButton" owl="1">
<div t-att-class="{'oe_status': props.isHeaderButton }" t-on-click="onClick">
<div class="js_connected oe_icon"
<div t-att-class="{'oe_status cursor-pointer': props.isHeaderButton }" t-on-click="onClick">
<div class="js_connected oe_icon text-white"
title="Print a report with all the sales of the current PoS Session">
<i class="fa fa-fw fa-print" role="img" aria-label="Print"></i>
</div>
@@ -2,24 +2,24 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.SyncNotification" owl="1">
<div class="oe_status" t-on-click="onClick">
<div class="oe_status btn btn-light rounded-0" t-on-click="onClick">
<span t-if="sync.pending" class="js_msg">
<t t-esc="sync.pending" />
<span> </span>
</span>
<div t-if="sync.status === 'connected'" class="js_connected oe_icon oe_green">
<div t-if="sync.status === 'connected'" class="js_connected oe_icon text-success">
<i class="fa fa-fw fa-wifi" role="img" aria-label="Synchronisation Connected"
title="Synchronisation Connected"></i>
</div>
<div t-if="sync.status === 'connecting'" class="js_connecting oe_icon">
<div t-if="sync.status === 'connecting'" class="js_connecting oe_icon text-white">
<i class="fa fa-fw fa-spin fa-circle-o-notch" role="img"
aria-label="Synchronisation Connecting" title="Synchronisation Connecting"></i>
</div>
<div t-if="sync.status === 'disconnected'" class="js_disconnected oe_icon oe_orange">
<div t-if="sync.status === 'disconnected'" class="js_disconnected oe_icon text-warning">
<i class="fa fa-fw fa-wifi" role="img" aria-label="Synchronisation Disconnected"
title="Synchronisation Disconnected"></i>
</div>
<div t-if="sync.status === 'error'" class="js_error oe_icon oe_red">
<div t-if="sync.status === 'error'" class="js_error oe_icon text-danger">
<i class="fa fa-fw fa-warning" role="img" aria-label="Synchronisation Error"
title="Synchronisation Error"></i>
</div>
@@ -1,29 +0,0 @@
.o_pos_notification_manager {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
padding: 2rem;
justify-content: end;
align-items: center;
pointer-events: none;
}
.o_notification {
pointer-events: auto;
width: 350px;
background-color: #333333AA;
color: #fff;
text-align: center;
border-radius: 2px;
padding: 16px;
margin: .5rem;
font-size: 1rem;
span {
white-space: pre-line;
}
}
@@ -2,13 +2,13 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.Notification" owl="1">
<div class="o_notification" t-att-class="props.className" t-on-click="props.close">
<div class="o_notification o-fade o-fade-enter-active p-3 m-2 text-bg-dark bg-opacity-75 text-center pe-none" t-att-class="props.className" t-on-click="props.close">
<span t-esc="props.message"/>
</div>
</t>
<t t-name="point_of_sale.NotificationContainer" owl="1">
<div class="o_pos_notification_manager">
<div class="o_pos_notification_manager fixed-bottom d-flex flex-column align-items-center justify-content-end p-4 pe-none">
<t t-foreach="props.notifications" t-as="notification" t-key="notification">
<Transition name="'o-fade'" visible="notification_value.visible" leaveDuration="leaveDuration" t-slot-scope="transition" onLeave="notification_value.delete">
<Notification message="notification_value.message" close="notification_value.close" className="transition.className"/>
@@ -0,0 +1,13 @@
div.modal.in {
position: absolute;
background: white;
padding: 20px;
box-shadow: 0px 10px 20px black;
border-radius: 3px;
max-width: 600px;
max-height: 400px;
margin-top: -200px;
margin-left: -300px;
top: 50%;
left: 50%;
}
@@ -4,7 +4,8 @@
<t t-name="point_of_sale.PopupContainer" owl="1">
<div t-if="Object.keys(props.popups).length > 0" class="popups">
<t t-foreach="props.popups" t-as="popup" t-key="popup">
<div t-if="popup_last || popup_value.props.keepBehind" role="dialog" class="modal-dialog">
<div t-if="popup_last || popup_value.props.keepBehind" role="dialog"
class="modal-dialog position-absolute start-0 top-0 d-flex align-items-center justify-content-center h-100 w-100 mw-100 m-0 p-0 pe-auto bg-dark bg-opacity-50">
<t t-component="popup_value.component" t-props="popup_value.props" isActive="popup_last" />
</div>
</t>
@@ -0,0 +1,81 @@
.pos {
background: $gray-900;
--btn-height-size: 54px;
direction: ltr;
padding: 0;
margin: 0;
background-color: #f0eeee;
color: $gray-700;
/* Some elements inside .pos are allowed to be moved/resized. When
* this 'move/resize' event is performed in touch devices, .pos
* element also tries to scroll (as the default action of touchstart+
* touchmove events).
* Position is fixed to prevent the movement of .pos element during
* the described touch events.
*/
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
text-shadow: none;
}
/* ********* Generic element styling ********* */
.pos a {
text-decoration: none;
color: $gray-700;
}
.pos ul, .pos ol {
padding: 0;
margin: 0;
}
.pos li {
list-style-type: none;
}
.modal-footer {
justify-content: start;
}
/* --- Generic Restyling and Resets --- */
html {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-family: sans-serif;
}
table {
border-spacing: 0px;
border-collapse: collapse;
}
td {
padding: 0px;
}
.oe_hidden{
display: none !important;
}
.oe_invisible{
visibility: hidden !important;
}
.clearfix:after {
content:" ";
display: block;
visibility: hidden;
line-height: 0;
height: 0;
clear: both;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
@@ -2,25 +2,17 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.Chrome" owl="1">
<div class="pos" t-att-class="{ 'big-scrollbars': pos.hasBigScrollBars }">
<div class="pos vh-100 d-flex flex-column">
<div class="pos-receipt-print"></div>
<Navbar />
<div class="pos-content">
<div class="window">
<div class="subwindow">
<div class="subwindow-container">
<div class="subwindow-container-fix screens">
<!-- 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" />
</ErrorHandler>
</div>
</div>
</div>
</div>
<div class="pos-content w-100 h-100 bg-200 overflow-auto">
<!-- 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" />
</ErrorHandler>
</div>
</div>
<MainComponentsContainer/>
@@ -0,0 +1,23 @@
.partner-picture {
height: 64px;
width: 64px;
& > img {
position: absolute;
top: -9999px;
bottom: -9999px;
right: -9999px;
left: -9999px;
max-height: 64px;
margin: auto;
}
.image-uploader {
position: absolute;
z-index: 1000;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
cursor: pointer;
}
}
@@ -1,34 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PartnerDetailsEdit" owl="1">
<section class="partner-details edit">
<div class="partner-picture">
<t t-if="partnerImageUrl">
<img t-att-src="partnerImageUrl" alt="Partner" style="width: 64px; height: 64px; object-fit: cover;" />
</t>
<t t-else="">
<i class="fa fa-camera" role="img" aria-label="Picture" title="Picture"></i>
</t>
<input type="file" class="image-uploader" t-on-change="uploadImage" />
</div>
<input class="detail partner-name" name="name" placeholder="Name" t-model="changes.name" />
<div class="partner-details-box clearfix">
<div class="partner-details-left">
<div class="partner-detail">
<span class="label">Street</span>
<input class="detail" name="street" t-att-class="{'border-red': missingFields.includes('street')}" t-model="changes.street" placeholder="Street" />
<section class="partner-details edit d-flex flex-column m-3">
<div class="container">
<div class="partner-details-header d-flex pe-2 gap-2">
<div class="partner-picture position-relative rounded text-center overflow-hidden">
<t t-if="partnerImageUrl">
<img class="rounded" t-att-src="partnerImageUrl" alt="Partner" style="width: 64px; height: 64px; object-fit: cover;" />
</t>
<t t-else="">
<i class="fa fa-camera" role="img" aria-label="Picture" title="Picture"></i>
</t>
<input type="file" class="image-uploader" t-on-change="uploadImage" />
</div>
<div class="partner-detail">
<span class="label">City</span>
<input class="detail" name="city" t-att-class="{'border-red': missingFields.includes('city')}" t-model="changes.city" placeholder="City" />
</div>
<div class="partner-detail">
<span class="label">Postcode</span>
<input class="detail" name="zip" t-att-class="{'border-red': missingFields.includes('zip')}" t-model="changes.zip" placeholder="ZIP" />
</div>
<div class="partner-detail" t-if="pos.states.some((state) => state.country_id[0] == changes.country_id)">
<span class="label">State</span>
<select class="detail" name="state_id" t-att-class="{'border-red': missingFields.includes('state_id')}" t-model="changes.state_id">
<input class="detail partner-name form-control form-control-lg" name="name" t-att-value="props.partner.name or ''" placeholder="Name" t-on-change="captureChange" />
</div>
<div class="partner-details-box row row-cols-1 row-cols-sm-2 gy-3 mt-3">
<t t-foreach="['Street', 'City', 'Zip', 'Email', 'Phone', 'Mobile', 'Barcode' ]" t-as="item" t-key="item">
<div class="partner-detail col">
<label class="form-label label" t-attf-for="{{item}}" t-esc="item"/>
<input class="detail form-control" t-attf-id="{{item}}" t-attf-name="{{item}}" t-model="changes[item.toLowerCase()]" t-attf-placeholder="{{item}}" t-att-class="{'border-danger': missingFields.includes(item.toLowerCase())}" />
</div>
</t>
<div class="partner-detail col">
<label class="form-label label" for="state">State</label>
<select class="detail form-select" id="state" name="state_id" t-model="changes.state_id" t-att-class="{'border-danger': missingFields.includes('state_id')}">
<option value="">None</option>
<t t-foreach="pos.states" t-as="state" t-key="state.id">
<option t-if="changes.country_id == state.country_id[0]" t-att-value="state.id">
@@ -37,9 +34,9 @@
</t>
</select>
</div>
<div class="partner-detail">
<span class="label">Country</span>
<select class="detail" name="country_id" t-att-class="{'border-red': missingFields.includes('country_id')}" t-model="changes.country_id">
<div class="partner-detail col">
<label class="form-label label" for="country">Country</label>
<select class="detail form-select" id="country" name="country_id" t-model="changes.country_id" t-att-class="{'border-danger': missingFields.includes('country_id')}">
<option value="">None</option>
<t t-foreach="pos.countries" t-as="country" t-key="country.id">
<option t-att-value="country.id">
@@ -48,11 +45,9 @@
</t>
</select>
</div>
</div>
<div class="partner-details-right">
<div class="partner-detail">
<span class="label">Language</span>
<select class="detail" name="lang" t-att-class="{'border-red': missingFields.includes('lang')}" t-model="changes.lang">
<div class="partner-detail col">
<label class="form-label label" for="language">Language</label>
<select class="detail form-select" id="language" name="lang" t-model="changes.lang" t-att-class="{'border-danger': missingFields.includes('lang')}">
<t t-foreach="pos.langs" t-as="lang" t-key="lang.id">
<option t-att-value="lang.code" t-att-selected="changes.lang ? ((lang.code === changes.lang) ? true : undefined) : lang.code === pos.user.lang? true : undefined">
<t t-esc="lang.name" />
@@ -60,36 +55,20 @@
</t>
</select>
</div>
<div class="partner-detail">
<span class="label">Email</span>
<input class="detail" name="email" t-att-class="{'border-red': missingFields.includes('email')}" type="email" t-model="changes.email" />
</div>
<div class="partner-detail">
<span class="label">Phone</span>
<input class="detail" name="phone" t-att-class="{'border-red': missingFields.includes('phone')}" type="tel" t-model="changes.phone" />
</div>
<div class="partner-detail">
<span class="label">Mobile</span>
<input class="detail" name="mobile" t-att-class="{'border-red': missingFields.includes('mobile')}" type="tel" t-model="changes.mobile" />
</div>
<div class="partner-detail">
<span class="label">Barcode</span>
<input class="detail barcode" name="barcode" t-model="changes.barcode" t-att-class="{'border-red': missingFields.includes('barcode')}" />
</div>
<div class="partner-detail">
<span class="label">Tax ID</span>
<input class="detail vat" name="vat" t-model="changes.vat" t-att-class="{'border-red': missingFields.includes('vat')}" />
</div>
<div t-if="pos.pricelists.length gt 1" class="partner-detail">
<span class="label">Pricelist</span>
<select class="detail" name="property_product_pricelist" t-att-class="{'border-red': missingFields.includes('property_product_pricelist')}" t-model="changes.property_product_pricelist">
<div t-if="pos.pricelists.length gt 1" class="partner-detail col">
<label class="form-label label" for="pricelist">Pricelist</label>
<select class="detail form-select" id="pricelist" name="property_product_pricelist" t-model="changes.property_product_pricelist" t-att-class="{'border-danger': missingFields.includes('property_product_pricelist')}">
<t t-foreach="pos.pricelists" t-as="pricelist" t-key="pricelist.id">
<option t-att-value="pricelist.id" t-att-selected="changes.property_product_pricelist ? (pricelist.id === changes.property_product_pricelist[0] ? true : undefined) : pricelist.id === pos.default_pricelist.id ? true : undefined">
<option t-att-value="pricelist.id" t-att-selected="props.partner.property_product_pricelist ? (pricelist.id === props.partner.property_product_pricelist[0] ? true : undefined) : pricelist.id === pos.default_pricelist.id ? true : undefined">
<t t-esc="pricelist.display_name" />
</option>
</t>
</select>
</div>
<div class="partner-detail col">
<label class="form-label label" for="vat">Tax ID</label>
<input class="detail vat form-control" id="vat" name="vat" t-model="changes.vat" t-att-class="{'border-danger': missingFields.includes('vat')}" />
</div>
</div>
</div>
</section>
@@ -6,7 +6,7 @@ export class PartnerLine extends Component {
static template = "point_of_sale.PartnerLine";
get highlight() {
return this._isPartnerSelected ? "highlight" : "";
return this._isPartnerSelected ? "highlight active" : "";
}
get _isPartnerSelected() {
return this.props.partner === this.props.selectedPartner;
@@ -0,0 +1,34 @@
.partner-line.active {
background-color: $o-component-active-bg;
}
@include media-breakpoint-down(lg) {
.partner-line {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid $o-gray-300;
td {
border: 0;
flex-basis: 100%;
padding: 5px;
background-color: transparent;
}
}
.partner-line-balance{
order: 3;
}
.partner-line .pos-right-align{
text-align: left ;
}
.edit-partner-button-cell {
order: 4;
}
.partner-line-last-column-placeholder {
display: none;
}
}
@@ -2,43 +2,43 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PartnerLine" owl="1">
<tr t-attf-class="partner-line {{highlight}}" t-att-data-id="props.partner.id"
<tr t-attf-class="partner-line gap-2 gap-lg-0 align-top {{highlight}}" t-att-data-id="props.partner.id"
t-on-click="() => this.props.onClickPartner(props.partner)">
<td>
<div>
<b>
<t t-esc="props.partner.name or ''" />
</div>
<div class="company-field">
</b>
<div class="company-field text-bg-muted">
<t t-esc="props.partner.parent_name or ''" />
</div>
<div t-if="_isPartnerSelected" class="unselect-tag">
<i class="fa fa-times"></i>
<span> unselect </span>
</div>
<button t-if="_isPartnerSelected" class="unselect-tag d-lg-inline-block d-none btn btn-light mt-2">
<i class="fa fa-times me-1"></i>
<span> Unselect </span>
</button>
</td>
<td>
<div class="partner-line-adress" t-if="props.partner.address">
<t t-esc="props.partner.address" />
</div>
</td>
<td class="partner-line-email" >
<div t-if="props.partner.phone">
<i class="fa fa-fw fa-phone"/><t t-esc="props.partner.phone"/>
<td class="partner-line-email ">
<div class="mb-2" t-if="props.partner.phone">
<i class="fa fa-fw fa-phone me-2"/><t t-esc="props.partner.phone"/>
</div>
<div t-if="props.partner.mobile">
<i class="fa fa-fw fa-mobile fa-lg"/><t t-esc="props.partner.mobile"/>
<div class="mb-2" t-if="props.partner.mobile">
<i class="fa fa-fw fa-mobile me-2"/><t t-esc="props.partner.mobile"/>
</div>
<div t-if="props.partner.email" class="email-field">
<i class="fa fa-fw fa-paper-plane-o"/><t t-esc="props.partner.email" />
<div t-if="props.partner.email" class="email-field mb-2">
<i class="fa fa-fw fa-paper-plane-o me-2"/><t t-esc="props.partner.email" />
</div>
</td>
<td class="partner-line-balance" t-if="props.isBalanceDisplayed"></td>
<td class="edit-partner-button-cell">
<button class="edit-partner-button" t-on-click.stop="() => props.onClickEdit(props.partner)">DETAILS</button>
<div t-if="_isPartnerSelected" class="unselect-tag-mobile">
<button class="edit-partner-button btn btn-light border" t-on-click.stop="() => props.onClickEdit(props.partner)">DETAILS</button>
<button t-if="_isPartnerSelected" class="unselect-tag-mobile d-inline-block d-lg-none btn btn-light border ms-2">
<i class="fa fa-times"></i>
<span> unselect </span>
</div>
<span> UNSELECT </span>
</button>
</td>
<td class="partner-line-last-column-placeholder oe_invisible"></td>
</tr>
@@ -0,0 +1,32 @@
.partner-list th.partner-line-last-column-placeholder,
.partner-list td.partner-line-last-column-placeholder {
padding: 0;
}
.partnerlist-screen {
tr {
&.partner-line {
&:hover {
cursor: pointer;
background: $o-gray-200;
}
&:active {
background-color: $o-component-active-bg;
}
}
}
}
@include media-breakpoint-down(lg) {
.table {
border: transparent;
}
.partner-list {
thead {
display: none;
}
td {
padding: 0;
}
}
}
@@ -2,83 +2,78 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PartnerListScreen" owl="1">
<div class="partnerlist-screen screen">
<div class="screen-content">
<div class="top-content">
<div class="button highlight" t-if="state.detailIsShown" t-on-click="() => this.partnerEditor.save()">
<i t-if="ui.isSmall" class="fa fa-floppy-o"/>
<t t-else="">
<span> Save</span>
</t>
<div class="partnerlist-screen screen h-100 d-flex flex-column bg-100">
<div class="top-content d-flex align-items-center p-2 border-bottom text-center">
<div class="button highlight btn btn-lg btn-primary" t-if="state.detailIsShown" t-on-click="() => this.partnerEditor.save()">
<i t-if="ui.isSmall" class="fa fa-floppy-o"/>
<t t-else="">
<span> Save</span>
</t>
</div>
<button t-if="!state.detailIsShown" class="button new-customer highlight btn btn-lg btn-primary" role="img" aria-label="Add a customer"
t-on-click="createPartner"
title="Add a customer">
<i t-if="ui.isSmall" class="fa fa-plus"/>
<t t-else=""> Create</t>
</button>
<div class="button back btn btn-lg btn-secondary mx-2" t-on-click="back">
<i t-if="ui.isSmall" class="fa fa-angle-double-left"/>
<t t-else=""> Discard</t>
</div>
<div class="top-right-buttons d-flex gap-2 w-100 justify-content-end">
<div t-if="state.detailIsShown and state.editModeProps.partner.id" class="button more-info">
<a t-on-click="goToOrders" class="btn btn-lg btn-outline-secondary" target="_blank"> Orders </a>
</div>
<div t-if="!state.detailIsShown" class="button new-customer highlight" role="img" aria-label="Add a customer"
t-on-click="createPartner"
title="Add a customer">
<i t-if="ui.isSmall" class="fa fa-plus"/>
<t t-else=""> Create</t>
<div t-if="state.detailIsShown &amp;&amp; state.editModeProps.partner.id" class="button more-info">
<a t-att-href="partnerLink" class="btn btn-lg btn-outline-secondary" target="_blank"> More info</a>
</div>
<div class="button back" t-on-click="back">
<i t-if="ui.isSmall" class="fa fa-angle-double-left"/>
<t t-else=""> Discard</t>
</div>
<div class="top-right-buttons">
<div t-if="state.detailIsShown and state.editModeProps.partner.id" class="button more-info" t-on-click="goToOrders">
<span> Orders</span>
</div>
<a t-if="state.detailIsShown &amp;&amp; state.editModeProps.partner.id" class="button more-info" t-att-href="partnerLink" target="_blank"> More info</a>
<div class="search-bar-container sb-partner" t-if="!state.detailIsShown">
<div class="pos-search-bar">
<i class="fa fa-search" t-on-click="_onPressEnterKey"/>
<input t-ref="search-word-input-partner" placeholder="Search Customers..." size="1" t-on-keyup="updatePartnerList" />
<i class="fa fa-times" t-on-click="_clearSearch"/>
<div class="search-bar-container sb-partner d-flex align-items-center w-100 w-md-70 w-lg-50 h-100 h-100" t-if="!state.detailIsShown">
<div class="input-group h-100">
<div class="pos-search-bar form-control d-flex align-items-center bg-view">
<i class="oi oi-search me-2" t-on-click="_onPressEnterKey"/>
<input class="flex-grow-1 w-auto h-100 border-0 p-2 me-4" type="text" autofocus="autofocus" t-ref="search-word-input-partner" placeholder="Search Customers..." size="1" t-on-keyup="updatePartnerList" />
<i class="fa fa-times position-absolute end-0 me-2 pe-1 cursor-pointer" t-on-click="_clearSearch"/>
</div>
</div>
</div>
</div>
</div>
<section class="full-content">
<div class="partner-window">
<section class="subwindow collapsed">
<div class="subwindow-container collapsed">
<div t-if="state.detailIsShown" class="partner-details-contents subwindow-container-fix">
<PartnerDetailsEdit t-props="state.editModeProps" saveChanges.bind="saveChanges" imperativeHandle="partnerEditor" />
</div>
</div>
</section>
<section class="subwindow list">
<div class="subwindow-container">
<div t-if="!state.detailIsShown" class="subwindow-container-fix scrollable-y">
<table class="partner-list">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th class="partner-line-email">Contact</th>
<th class="pos-right-align" t-if="isBalanceDisplayed">Balance</th>
<th class="partner-line-details"></th>
<th class="partner-line-last-column-placeholder oe_invisible"></th>
</tr>
</thead>
<tbody class="partner-list-contents">
<t t-foreach="partners" t-as="partner"
t-key="partner.id">
<PartnerLine partner="partner"
selectedPartner="state.selectedPartner"
detailIsShown="state.detailIsShown"
isBalanceDisplayed="isBalanceDisplayed"
onClickEdit.bind="editPartner"
onClickPartner.bind="clickPartner"/>
</t>
</tbody>
</table>
<div t-if="state.query" class="search-more-button">
<div class="button" t-on-click="_onPressEnterKey">Search more</div>
</div>
</div>
</div>
</section>
</div>
</section>
</div>
<section class="overflow-auto">
<t t-if="state.detailIsShown">
<PartnerDetailsEdit
t-props="state.editModeProps"
saveChanges.bind="saveChanges"
imperativeHandle="partnerEditor" />
</t>
<t t-else="">
<table class="partner-list table table-striped w-100">
<thead>
<tr>
<th class="py-2">Name</th>
<th class="py-2">Address</th>
<th class="partner-line-email py-2">Contact</th>
<th class="pos-right-align py-2" t-if="isBalanceDisplayed">Balance</th>
<th class="partner-line-details py-2"></th>
<th class="partner-line-last-column-placeholder oe_invisible py-2"></th>
</tr>
</thead>
<tbody class="partner-list-contents">
<t t-foreach="partners" t-as="partner"
t-key="partner.id">
<PartnerLine partner="partner"
selectedPartner="state.selectedPartner"
detailIsShown="state.detailIsShown"
isBalanceDisplayed="isBalanceDisplayed"
onClickEdit.bind="editPartner"
onClickPartner.bind="clickPartner"/>
</t>
</tbody>
</table>
<div t-if="state.query" class="search-more-button d-flex justify-content-center my-2">
<button class="btn btn-lg btn-primary" t-on-click="_onPressEnterKey">Search more</button>
</div>
</t>
</section>
</div>
</t>
@@ -0,0 +1,15 @@
.payment-numpad .numpad button {
font-size: clamp(map-get($font-sizes, 6), 3vw, 36px);
}
.payment-numpad .numpad button:nth-child(16) img {
width: 35%;
max-width: 64px;
height: auto;
}
@include media-breakpoint-down(md) {
.payment-numpad .numpad button:nth-child(16) img {
max-width: 48px;
}
}
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PaymentScreenNumpad" owl="1">
<div class="numpad">
<div class="numpad d-grid text-center flex-grow-1 h-100 w-100">
<PSNumpadInputButton value="'1'" />
<PSNumpadInputButton value="'2'" />
<PSNumpadInputButton value="'3'" />
@@ -2,14 +2,14 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PSNumpadInputButton" owl="1">
<button t-attf-class="{{ _class }}"
<button t-attf-class="{{ _class }} btn btn-light w-100 rounded-0 py-3"
t-on-click="() => this.numberBuffer.sendKey(props.value)">
<t t-slot="default">
<t t-if="props.text">
<t t-esc="props.text" />
<h1 t-esc="props.text" />
</t>
<t t-else="">
<t t-esc="props.value" />
<h1 t-esc="props.value" />
</t>
</t>
</button>
@@ -0,0 +1,29 @@
.paymentlines .electronic_payment {
background: $gray-200;
border-collapse: unset;
font-size: 16px;
padding-right: 0;
}
.paymentline .electronic_status {
flex-grow: 1;
margin-left: 10px;
margin-right: 10px;
/* make the name of the payment line to be truncated with ellipsis */
white-space: nowrap;
overflow:hidden;
text-overflow:ellipsis;
padding: map-get($spacers, 3);
}
.paymentlines .electronic_payment div:first-child {
flex-grow: 2;
margin-left: 10px;
margin-right: 10px;
}
.paymentlines .electronic_payment div:last-child {
flex-grow: 1;
text-align: center;
padding: map-get($spacers, 3);
}
@@ -2,15 +2,15 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PaymentScreenPaymentLines" owl="1">
<div class="paymentlines">
<p class="title-category">Summary</p>
<div class="paymentlines d-flex flex-column">
<h4 class="title-category pt-3 pb-2 px-3 m-0 bg-view border-bottom border-2">Summary</h4>
<t t-foreach="props.paymentLines" t-as="line" t-key="line.cid">
<t t-if="line.selected">
<div class="paymentline selected"
<div class="paymentline selected d-flex align-items-center bg-200"
t-att-class="selectedLineClass(line)">
<div class="payment-infos" t-on-click="() => this.selectLine(line)">
<div class="payment-infos d-flex align-items-center justify-content-between flex-grow-1 px-3 py-4 my-2 text-truncate cursor-pointer" t-on-click="() => this.selectLine(line)">
<span class="payment-name"><t t-esc="line.payment_method.name"/></span>
<div class="payment-amount">
<div class="payment-amount px-3">
<t t-if="line and line.payment_status and ['done', 'waitingCard', 'waiting', 'reversing', 'reversed'].includes(line.payment_status)">
<t t-esc="env.utils.formatCurrency(line.get_amount(), false)" />
</t>
@@ -21,11 +21,11 @@
</div>
<t t-if="!line.payment_status or !['done', 'reversed', 'waitingCard', 'waitingCapture'].includes(line.payment_status)">
<div class="delete-button"
<button class="delete-button btn btn-link mx-2 px-3"
t-on-click="() => this.props.deleteLine(line.cid)"
aria-label="Delete" title="Delete">
<i class="fa fa-times-circle" />
</div>
<i class="fa fa-2x fa-times-circle" />
</button>
</t>
</div>
<t t-if="line and line.payment_status">
@@ -34,7 +34,7 @@
<div class="electronic_status">
Payment request pending
</div>
<div class="button send_payment_request highlight" title="Send Payment Request" t-on-click="() => this.props.sendPaymentRequest(line)">
<div class="button send_payment_request highlight text-bg-primary" title="Send Payment Request" t-on-click="() => this.props.sendPaymentRequest(line)">
Send
</div>
</t>
@@ -42,7 +42,7 @@
<div class="electronic_status">
Transaction cancelled
</div>
<div class="button send_payment_request highlight" title="Send Payment Request" t-on-click="() => this.props.sendPaymentRequest(line)">
<div class="button send_payment_request highlight text-bg-primary" title="Send Payment Request" t-on-click="() => this.props.sendPaymentRequest(line)">
Retry
</div>
</t>
@@ -101,11 +101,11 @@
</t>
</t>
<t t-else="">
<div class="paymentline"
<div class="paymentline d-flex align-items-center bg-view"
t-att-class="unselectedLineClass(line)">
<div class="payment-infos" t-on-click="() => this.selectLine(line)">
<div class="payment-infos d-flex align-items-center justify-content-between flex-grow-1 px-3 py-4 my-2 text-truncate cursor-pointer" t-on-click="() => this.selectLine(line)">
<t t-esc="line.payment_method.name" />
<div class="payment-amount">
<div class="payment-amount px-3">
<t t-if="line and line.payment_status and ['done', 'waitingCard', 'waiting', 'reversing', 'reversed'].includes(line.payment_status)">
<t t-esc="env.utils.formatCurrency(line.get_amount(), false)" />
</t>
@@ -115,10 +115,10 @@
</div>
</div>
<t t-if="!line.payment_status or !['done', 'reversed'].includes(line.payment_status)">
<div class="delete-button"
<div class="delete-button delete-button btn btn-link mx-2 px-3"
t-on-click="() => this.props.deleteLine(line.cid)"
aria-label="Delete" title="Delete">
<i class="fa fa-times-circle" />
<i class="fa fa-2x fa-times-circle" />
</div>
</t>
</div>
@@ -0,0 +1,5 @@
.payment-method-icon {
max-width: 50px;
max-height: 50px;
vertical-align: middle;
}
@@ -2,122 +2,121 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PaymentScreen" owl="1">
<div class="payment-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
<div class="screen-content">
<t t-if="!ui.isSmall">
<div class="top-content">
<div class="button back"
t-on-click="() => pos.showScreen('ProductScreen')">
<i class="fa fa-angle-double-left fa-fw"></i>
<span class="back_text">Back</span>
</div>
<div class="top-content-center"><h1>Payment</h1></div>
<t t-if="ui.isSmall">
<div class="payment-screen screen d-flex flex-column h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<PaymentScreenStatus order="currentOrder" />
<t t-call="point_of_sale.PaymentScreenMethods" />
<t t-call="point_of_sale.PaymentScreenButtons" />
<t t-call="point_of_sale.PaymentScreenValidate" />
</div>
</t>
<t t-else="">
<div class="payment-screen screen d-flex flex-column h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<t t-call="point_of_sale.PaymentScreenTop" />
<div class="main-content d-flex overflow-auto h-100">
<div class="left-content d-flex flex-column w-25 bg-400">
<t t-call="point_of_sale.PaymentScreenMethods" />
<t t-call="point_of_sale.PaymentScreenValidate" />
</div>
</t>
<div class="main-content">
<div class="left-content">
<t t-if="ui.isSmall">
<section class="paymentlines-container">
<PaymentScreenStatus order="currentOrder" />
</section>
</t>
<div class="paymentmethods-container">
<div class="paymentmethods">
<p class="title-category">Payment method</p>
<t t-foreach="payment_methods_from_config" t-as="paymentMethod"
t-key="paymentMethod.id">
<div class="button paymentmethod"
t-on-click="() => this.addNewPaymentLine(paymentMethod)">
<div class="payment-method-display">
<div><img class="payment-method-icon" t-att-src="paymentMethodImage(paymentMethod.id)"/></div>
<span class="payment-name"><t t-esc="paymentMethod.name" /></span>
</div>
</div>
</t>
</div>
<PaymentScreenPaymentLines
paymentLines="paymentLines"
deleteLine.bind="deletePaymentLine"
sendForceDone.bind="sendForceDone"
sendPaymentReverse.bind="sendPaymentReverse"
sendPaymentCancel.bind="sendPaymentCancel"
sendPaymentRequest.bind="sendPaymentRequest"
selectLine.bind="selectPaymentLine"
updateSelectedPaymentline.bind="updateSelectedPaymentline"/>
</div>
<div t-if="!ui.isSmall" class="button next validation" t-att-class="{ highlight: currentOrder.is_paid() and currentOrder._isValidEmptyOrder() }"
t-on-click="() => this.validateOrder()">
<div class="pay-circle">
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<span class="next_text">Validate</span>
<div class="center-content d-flex flex-column w-50 p-1 border-start border-end bg-300" >
<PaymentScreenStatus order="currentOrder" />
<div class="payment-numpad flex-grow-1 d-flex flex-wrap">
<PaymentScreenNumpad />
</div>
</div>
<div class="center-content" t-if="!ui.isSmall">
<section class="paymentlines-container">
<PaymentScreenStatus order="currentOrder" />
</section>
<div class="payment-buttons-container">
<section class="payment-numpad">
<PaymentScreenNumpad />
</section>
</div>
</div>
<div class="right-content">
<div class="payment-buttons">
<div class="partner-button">
<div class="button" t-att-class="{ highlight: currentOrder.get_partner() }" t-on-click="() => this.selectPartner(false, [])">
<i class="fa fa-user" role="img" title="Customer" />
<t t-set="partner" t-value="currentOrder.get_partner()" />
<span>
<t t-if="partner">
<t t-esc="partner.name" />
</t>
<t t-if="!partner">
Customer
</t>
</span>
</div>
</div>
<div class="payment-controls">
<div class="button js_invoice"
t-att-class="{ highlight: currentOrder.is_to_invoice() }"
t-on-click="toggleIsToInvoice">
<i class="fa fa-file-text-o" /> Invoice
</div>
<div t-if="pos.config.iface_tipproduct and pos.config.tip_product_id" class="button js_tip"
t-att-class="{ highlight: currentOrder.get_tip() }"
t-on-click="addTip">
<i class="fa fa-heart" /> Tip <t t-if="currentOrder.get_tip() != 0">(<t t-esc="env.utils.formatCurrency(currentOrder.get_tip())"/>)</t>
</div>
<div t-if="pos.config.iface_cashdrawer" class="button js_cashdrawer"
t-on-click="openCashbox">
<i class="fa fa-archive" /> Open Cashbox
</div>
<div t-if="pos.config.ship_later" class="button"
t-att-class="{ highlight: currentOrder.getShippingDate() }"
t-on-click="toggleShippingDatePicker">
<i class="fa fa-clock-o" /> Ship Later
<span t-if="currentOrder.getShippingDate()">
(<t t-esc="currentOrder.getShippingDate()"/>)
</span>
</div>
</div>
</div>
<div class="right-content w-25 bg-400">
<t t-call="point_of_sale.PaymentScreenButtons" />
</div>
</div>
<t t-if="ui.isSmall">
<div class="switchpane">
<button class="btn-switchpane validation-button"
t-att-class="{ secondary: !(currentOrder.is_paid() and currentOrder._isValidEmptyOrder()) }"
t-on-click="() => this.validateOrder()">
<span>Validate</span>
</button>
</div>
</t>
</div>
</t>
</t>
<t t-name="point_of_sale.PaymentScreenTop" owl="1">
<div class="top-content d-flex align-items-center p-2 border-bottom text-center">
<div class="button back btn btn-lg btn-secondary" t-on-click="() => pos.showScreen('ProductScreen')">
<i class="fa fa-angle-double-left me-2" />
<span class="back_text">Back</span>
</div>
<div class="top-content-center flex-grow-1">
<h2 class="mb-0">Payment</h2>
</div>
</div>
</t>
<t t-name="point_of_sale.PaymentScreenValidate" owl="1">
<t t-if="ui.isSmall">
<div class="switchpane d-flex">
<button class="btn-switchpane validation-button btn btn-primary flex-fill py-5 rounded-0 fw-bolder"
t-att-class="{ secondary: !(currentOrder.is_paid() and currentOrder._isValidEmptyOrder()) }"
t-on-click="() => this.validateOrder()">
<span>Validate</span>
</button>
</div>
</t>
<t t-else="">
<div class="button next validation btn btn-primary btn-lg py-5 rounded-0 d-flex flex-column align-items-center justify-content-center fw-bolder" t-attf-class="{{currentOrder.is_paid() and currentOrder._isValidEmptyOrder() ? 'highlight' : 'disabled'}}"
t-on-click="() => this.validateOrder()">
<div class="pay-circle d-flex align-items-center justify-content-center mb-2">
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<span class="next_text">Validate</span>
</div>
</t>
</t>
<t t-name="point_of_sale.PaymentScreenButtons" owl="1">
<div class="payment-buttons d-flex flex-column flex-wrap">
<button class="button partner-button btn btn-light py-3 text-start rounded-0 border-bottom"
t-att-class="{ 'highlight text-bg-primary': currentOrder.get_partner() }"
t-on-click="() => this.selectPartner(false, [])">
<i class="fa fa-user me-2" role="img" title="Customer" />
<span class="partner-name" t-esc="currentOrder.get_partner()?.name or 'Customer'" />
</button>
<button class="button js_invoice btn btn-light py-3 text-start rounded-0 border-bottom" t-att-class="{ 'highlight text-bg-primary': currentOrder.is_to_invoice() }"
t-on-click="toggleIsToInvoice">
<i class="fa fa-file-text-o me-2" />Invoice
</button>
<button t-if="pos.config.iface_tipproduct and pos.config.tip_product_id" class="button js_tip btn btn-light py-3 text-start rounded-0 border-bottom" t-att-class="{ 'highlight text-bg-primary': currentOrder.get_tip() }"
t-on-click="addTip">
<i class="fa fa-heart me-2" />Tip
<t t-if="currentOrder.get_tip() != 0">
(<t t-esc="env.utils.formatCurrency(currentOrder.get_tip())" />)
</t>
</button>
<button t-if="pos.config.iface_cashdrawer" class="button js_cashdrawer btn btn-light py-3 text-start rounded-0 border-bottom"
t-on-click="openCashbox">
<i class="fa fa-archive me-2" />Open Cashbox
</button>
<button t-if="pos.config.ship_later" class="button btn btn-light py-3 text-start rounded-0 border-bottom" t-att-class="{ 'highlight text-bg-primary': currentOrder.getShippingDate() }"
t-on-click="toggleShippingDatePicker">
<i class="fa fa-clock-o me-2" />Ship Later
<span t-if="currentOrder.getShippingDate()">
(<t t-esc="currentOrder.getShippingDate()" />)
</span>
</button>
</div>
</t>
<t t-name="point_of_sale.PaymentScreenMethods" owl="1">
<div class="paymentmethods-container overflow-y-auto flex-grow-1">
<div class="paymentmethods d-flex flex-column">
<h4 class="title-category pt-3 pb-2 px-3 m-0 bg-view border-bottom border-2">Payment method</h4>
<t t-foreach="payment_methods_from_config" t-as="paymentMethod" t-key="paymentMethod.id">
<div class="button paymentmethod btn btn-light rounded-0 border-bottom" t-on-click="() => this.addNewPaymentLine(paymentMethod)">
<div class="payment-method-display d-flex align-items-center flex-grow-1">
<div>
<img class="payment-method-icon" t-att-src="paymentMethodImage(paymentMethod.id)" />
</div>
<span class="payment-name" t-esc="paymentMethod.name" />
</div>
</div>
</t>
</div>
<PaymentScreenPaymentLines paymentLines="paymentLines" deleteLine.bind="deletePaymentLine" sendForceDone.bind="sendForceDone" sendPaymentReverse.bind="sendPaymentReverse" sendPaymentCancel.bind="sendPaymentCancel" sendPaymentRequest.bind="sendPaymentRequest" selectLine.bind="selectPaymentLine" updateSelectedPaymentline.bind="updateSelectedPaymentline" />
</div>
</t>
</templates>
@@ -0,0 +1,10 @@
.paymentlines-empty .total {
font-size: 3rem;
text-shadow: 0px 2px white, 0px 2px 2px rgba(0, 0, 0, 0.27);
}
@include media-breakpoint-down(md) {
.paymentlines-empty .message {
padding-bottom: map-get($spacers, 3);
}
}
@@ -1,43 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.PaymentScreenStatus" owl="1">
<div t-if="props.order.get_paymentlines().length === 0" class="paymentlines-empty">
<div class="total">
<t t-esc="totalDueText" />
</div>
<div class="message">
<t t-name="point_of_sale.PaymentScreenStatus" owl="1">
<section class="paymentlines-container bg-view p-3 mb-1 border-bottom">
<div t-if="props.order.get_paymentlines().length === 0" class="paymentlines-empty">
<div class="total text-center py-2 text-success">
<t t-esc="totalDueText" />
</div>
<div class="message text-center">
Please select a payment method.
</div>
</div>
</div>
</div>
<div t-else="">
<div class="payment-status-container">
<div>
<div class="payment-status-remaining">
<span class="label">Remaining</span>
<span class="amount"
t-att-class="{ highlight: props.order.get_due() > 0 }">
<t t-esc="remainingText" />
</span>
<t t-else="">
<div class="payment-status-container d-flex flex-column-reverse flex-lg-row justify-content-between fs-2">
<div>
<div class="payment-status-remaining">
<span class="label pe-2">Remaining</span>
<span class="amount" t-att-class="{ 'highlight text-primary fw-bolder': props.order.get_due() > 0 }">
<t t-esc="remainingText" />
</span>
</div>
<div class="payment-status-total-due py-2 fs-3 text-muted">
<span class="label pe-2">Total Due</span>
<span>
<t t-esc="totalDueText" />
</span>
</div>
</div>
<div>
<div class="payment-status-change">
<span class="label pe-2">Change</span>
<span class="amount" t-att-class="{ 'highlight text-primary fw-bolder': props.order.get_change() > 0 }">
<t t-esc="changeText" />
</span>
</div>
</div>
</div>
<div class="payment-status-total-due">
<span class="label">Total Due</span>
<span>
<t t-esc="totalDueText" />
</span>
</div>
</div>
<div>
<div class="payment-status-change">
<span class="label">Change</span>
<span class="amount"
t-att-class="{ highlight: props.order.get_change() > 0 }">
<t t-esc="changeText" />
</span>
</div>
</div>
</div>
</div>
</t>
</t>
</section>
</t>
</templates>
@@ -0,0 +1,9 @@
.set-partner span {
height: 32px;
width: 32px;
min-width: 32px;
}
.pay-order-button, .set-partner {
height: 56px;
}
@@ -2,25 +2,31 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ActionpadWidget" owl="1">
<div class="actionpad">
<button class="button mobile-more-button" t-if="ui.isSmall and props.onClickMore" t-on-click="props.onClickMore">
<div class="actionpad d-flex flex-column flex-grow-1 mw-50 p-0 border-end">
<button class="button mobile-more-button btn btn-secondary flex-fill border-bottom" t-if="ui.isSmall and props.onClickMore" t-on-click="props.onClickMore">
<span>More...</span>
</button>
<button class="button set-partner" t-att-class="{'decentered': isLongName}"
<button class="button set-partner btn btn-light rounded-0 py-2 flex-shrink-1 fw-bolder"
t-on-click="() => this.pos.selectPartner()">
<t t-if="!ui.isSmall"><i class="fa fa-user" role="img" aria-label="Customer" title="Customer" /></t>
<t t-if="props.partner">
<t t-esc="props.partner.name" />
</t>
<t t-else="">
Customer
</t>
<div class="d-flex justify-content-center align-items-center ">
<span class="d-flex justify-content-center align-items-center rounded-circle me-2 text-bg-dark" t-if="!ui.isSmall">
<i class="fa fa-user" role="img" aria-label="Customer" title="Customer" />
</span>
<div t-if="props.partner" class="text-truncate fw-bolder text-action">
<t t-esc="props.partner.name" />
</div>
<div t-else="fw-bolder">
Customer
</div>
</div>
</button>
<button class="button pay validation pay-order-button" t-att-class="{ 'with-more-button': props.onClickMore and ui.isSmall }" t-on-click="props.actionToTrigger ? this.props.actionToTrigger : () => pos.get_order().pay()">
<div class="pay-circle" t-att-class="{ 'highlight': this.highlightPay }">
<button class="button pay validation pay-order-button btn btn-primary btn-lg py-5 rounded-0 highlight d-flex flex-column flex-fill align-items-center justify-content-center fw-bolder"
t-att-class="{ 'with-more-button': props.onClickMore and ui.isSmall }"
t-on-click="props.actionToTrigger ? this.props.actionToTrigger : () => pos.get_order().pay()">
<div class="pay-circle d-flex align-items-center justify-content-center py-2 mb-2" t-att-class="{ 'highlight': this.highlightPay }">
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<t t-esc="props.actionName" />
<t class="text-white" t-esc="props.actionName" />
</button>
</div>
</t>
@@ -2,14 +2,14 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CategoryButton" owl="1">
<span class="category-button" t-on-click="() => this.props.switchCategory(props.category.id)">
<button class="category-button btn btn-light" t-on-click="() => this.props.switchCategory(props.category.id)">
<div class="category-img">
<img t-att-src="imageUrl" alt="Category" />
</div>
<div class="category-name">
<div class="category-name mt-2 fw-bolder">
<t t-esc="props.category.name" />
</div>
</span>
</button>
</t>
</templates>
@@ -0,0 +1,16 @@
.control-buttons {
white-space: nowrap;
gap: 1px;
}
.control-button {
flex: 1 1 29%;
padding: $o-horizontal-padding * 0.5;
}
.control-button.altlight,
.button.altlight {
background: #7F82AC !important;
border: solid 1px #756A99 !important;
color: white !important;
}
@@ -2,15 +2,15 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ControlButtonPopup" owl="1">
<div class="popup popup-control-buttons">
<div class="popup popup-control-buttons d-flex flex-column">
<!-- When one of the buttons is clicked, close this popup. -->
<div class="control-buttons" t-on-click="cancel">
<div class="control-buttons d-flex flex-column p-2" t-on-click="cancel">
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
<t t-component="cb.component" t-key="cb.name"/>
</t>
</div>
<footer class="footer">
<div class="button cancel" t-on-click="cancel">
<footer class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CustomerButton" owl="1">
<div class="control-button customer-button set-partner" t-on-click="() => this.pos.selectPartner()">
<i class="fa fa-user" role="img" aria-label="Customer" title="Customer" />
<div class="control-button customer-button btn rounded-0 fw-bolder" t-attf-class="{{this.partner ? 'btn-primary' : 'btn-light'}}"
t-on-click="() => this.pos.selectPartner()">
<i class="fa fa-user me-1" role="img" aria-label="Customer" title="Customer" />
<t t-esc="this.partner.name" t-if="this.partner" />
<t t-else="">
Customer
@@ -2,11 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.OrderlineCustomerNoteButton" owl="1">
<div class="control-button" t-on-click="() => this.onClick()">
<i class="fa fa-sticky-note" />
<button class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.onClick()">
<i class="fa fa-sticky-note me-1" />
<span> </span>
<span>Customer Note</span>
</div>
</button>
</t>
</templates>
@@ -2,8 +2,8 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.SetFiscalPositionButton" owl="1">
<div class="control-button o_fiscal_position_button" t-on-click="() => this.click()">
<i class="fa fa-book" role="img" aria-label="Set fiscal position"
<div class="control-button o_fiscal_position_button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()">
<i class="fa fa-book me-1" role="img" aria-label="Set fiscal position"
title="Set fiscal position" />
<t t-esc='currentFiscalPositionName' />
</div>
@@ -2,8 +2,8 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.SetPricelistButton" owl="1">
<div class="control-button o_pricelist_button" t-on-click="() => this.click()">
<i class="fa fa-th-list" role="img" aria-label="Price list" title="Price list" />
<div class="control-button o_pricelist_button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()">
<i class="fa fa-th-list me-2" role="img" aria-label="Price list" title="Price list" />
<t t-esc="currentPricelistName" />
</div>
</t>
@@ -2,10 +2,10 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductInfoButton" owl="1">
<div class="control-button" t-on-click="() => this.click()">
<i class="fa fa-info-circle" role="img" aria-label="Info" title="Info"/>
<button class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()">
<i class="fa fa-info-circle me-1" role="img" aria-label="Info" title="Info"/>
Info
</div>
</button>
</t>
</templates>
@@ -2,10 +2,10 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.RefundButton" owl="1">
<div class="control-button" t-on-click="() => this.click()">
<i class="fa fa-undo" role="img" aria-label="Refund" title="Refund" />
<button class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()">
<i class="fa fa-undo me-1" role="img" aria-label="Refund" title="Refund" />
Refund
</div>
</button>
</t>
</templates>
@@ -0,0 +1,11 @@
.numpad {
grid-template-columns: repeat(4, 1fr);
min-width: 216px;
gap: 1px;
}
.numpad .disabled-mode, .pos .numpad .disabled-mode:hover {
background: $gray-200;
color: $gray-500;
cursor: not-allowed;
}
@@ -2,35 +2,35 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.NumpadWidget" owl="1">
<div class="numpad">
<button class="input-button number-char" t-on-click="() => this.sendInput('1')">1</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('2')">2</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('3')">3</button>
<button class="mode-button" t-att-class="{'selected-mode': props.activeMode === 'quantity'}"
<div class="numpad d-grid text-center flex-grow-1 w-50 bg-300">
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('1')">1</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('2')">2</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('3')">3</button>
<button class="mode-button btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{'selected-mode active': props.activeMode === 'quantity'}"
t-on-click="() => this.changeMode('quantity')">Qty</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('4')">4</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('5')">5</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('6')">6</button>
<button class="mode-button" t-att-class="{
'selected-mode': props.activeMode === 'discount',
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('4')">4</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('5')">5</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('6')">6</button>
<button class="mode-button btn btn-light w-100 rounded-0 py-3 fw-bolder text-nowrap" t-att-class="{
'selected-mode active': props.activeMode === 'discount',
'disabled-mode': !hasManualDiscount
}"
t-att-disabled="!hasManualDiscount"
t-on-click="() => this.changeMode('discount')">% Disc</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('7')">7</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('8')">8</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('9')">9</button>
<button class="mode-button" t-att-class="{
'selected-mode': props.activeMode === 'price',
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('7')">7</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('8')">8</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('9')">9</button>
<button class="mode-button btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{
'selected-mode active': props.activeMode === 'price',
'disabled-mode': !hasPriceControlRights
}" t-att-disabled="!hasPriceControlRights"
t-on-click="() => this.changeMode('price')">Price</button>
<button class="input-button numpad-minus" t-att-class="{ 'disabled-mode': props.disableSign }" t-on-click="() => this.sendInput('-')">+/-</button>
<button class="input-button number-char" t-on-click="() => this.sendInput('0')">0</button>
<button class="input-button number-char" t-on-click="() => this.sendInput(decimalSeparator)">
<button class="input-button numpad-minus btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{ 'disabled-mode': props.disableSign }" t-on-click="() => this.sendInput('-')">+/-</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('0')">0</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput(decimalSeparator)">
<t t-esc="decimalSeparator" />
</button>
<button class="input-button numpad-backspace" t-on-click="() => this.sendInput('Backspace')">
<button class="input-button numpad-backspace btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('Backspace')">
<img style="pointer-events: none;" src="/point_of_sale/static/src/img/backspace.png"
width="24" height="21" alt="Backspace" />
</button>
@@ -2,17 +2,17 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.OrderWidget" owl="1">
<div class="order-container" t-ref="scrollable">
<div class="order">
<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'>
<i class='fa fa-shopping-cart' role="img" aria-label="Shopping cart"
<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"/>
<h1>This order is empty</h1>
<h3 class="mt-2">This order is empty</h3>
</div>
</t>
<t t-else="">
<ul class="orderlines">
<ul class="orderlines flex-grow-1 overflow-auto">
<t t-foreach="orderlinesArray" t-as="orderline" t-key="orderline.id">
<Orderline
editPackLotLines.bind="editPackLotLines"
@@ -2,16 +2,16 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.OrderSummary" owl="1">
<div class="summary clearfix">
<div class="summary clearfix sticky-top 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">
<span class="badge">Total: </span>
<div class="entry total fs-2 lh-sm">
Total:
<span class="value">
<t t-esc="_total" />
</span>
<div t-if="_tax.hasTax" class="subentry">
<div t-if="_tax.hasTax" class="subentry fs-6 text-muted">
Taxes:
<span class="value">
<t t-esc="_tax.displayAmount" />
@@ -36,9 +36,9 @@ export class Orderline extends Component {
get addedClasses() {
return {
selected: this.props.line.selected,
"has-change":
"has-change ps-2 text-success border-start border-success border-4":
this.props.line.hasChange && this.pos.config.module_pos_restaurant,
"skip-change":
"skip-change ps-2 text-secondary border-start border-secondary border-4":
this.props.line.skipChange && this.pos.config.module_pos_restaurant,
};
}
@@ -0,0 +1,10 @@
.orderline {
&:hover {
background-color: $o-gray-200;
}
&.selected {
background-color: $o-component-active-bg;
}
}
@@ -2,14 +2,13 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.Orderline" owl="1">
<li t-on-click="selectLine" class="orderline" t-att-class="addedClasses">
<span class="product-name">
<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()"/>
<span> </span>
<t t-if="props.line.get_product().tracking!=='none' &amp;&amp; (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 oe_green"
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"
@@ -17,7 +16,7 @@
</t>
<t t-else="">
<i t-on-click.stop="lotIconClicked"
class="oe_link_icon fa fa-list oe_icon line-lot-icon oe_red"
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"
@@ -25,13 +24,13 @@
</t>
</t>
</span>
<span class="price">
<span class="price fw-bolder float-end">
<t t-esc="env.utils.formatCurrency(props.line.get_display_price())"/>
</span>
<ul class="info-list">
<ul class="info-list text-dark">
<t t-if="props.line.get_quantity_str() !== '1' || props.line.selected ">
<li class="info">
<em>
<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" />
@@ -51,7 +50,7 @@
</li>
</t>
<t t-if="props.line.get_discount_str() !== '0'">
<li class="info">
<li class="info ms-2">
With a
<em>
<t t-esc="props.line.get_discount_str()" />%
@@ -60,14 +59,14 @@
</li>
</t>
<t t-if="customerNote">
<li class="info orderline-note">
<i class="fa fa-sticky-note" role="img" aria-label="Customer Note" title="Customer Note"/>
<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>
</ul>
<t t-if="props.line.pack_lot_lines">
<ul class="info-list">
<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"/>
@@ -0,0 +1,26 @@
.product:hover {
border: 1px solid $o-gray-100;
}
.product-img img {
height: 100px;
object-fit: cover;
}
.product-name {
box-sizing: border-box;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
&.no-image {
-webkit-line-clamp: 7;
}
}
@media screen and (max-width: 768px) {
.product:active {
box-shadow: 0 0 0 2px $primary;
}
}
@@ -2,18 +2,18 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductItem" owl="1">
<article t-attf-class="product {{this.pos.productListViewMode}}" tabindex="0" t-on-keypress="spaceClickProduct"
<article t-attf-class="product position-relative btn btn-light d-flex flex-column align-items-stretch p-0 m-0 text-start cursor-pointer overflow-hidden transition-base" tabindex="0" t-on-keypress="spaceClickProduct"
t-on-click="() => this.pos.addProductToCurrentOrder(props.product)"
t-att-data-product-id="props.product.id"
t-attf-aria-labelledby="article_product_{{props.product.id}}">
<div class="product-img">
<img t-if="props.product.image_128" t-att-src="imageUrl" t-att-alt="props.product.display_name" />
<img t-if="props.product.image_128" class="w-100" t-att-src="imageUrl" t-att-alt="props.product.display_name" />
</div>
<div class="product-content">
<div t-attf-class="#{props.product.image_128 ? 'product-name' : 'product-name no-image' }" t-attf-id="article_product_{{props.product.id}}">
<div class="product-content d-flex flex-column justify-content-between h-100 mx-2 py-1">
<div t-attf-class="overflow-hidden lh-sm fw-bolder #{props.product.image_128 ? 'product-name' : 'product-name no-image' }" t-attf-id="article_product_{{props.product.id}}">
<t t-esc="props.product.display_name" />
</div>
<span class="price-tag">
<span class="price-tag text-primary py-1 fw-bolder">
<t t-esc="price" />
</span>
</div>
@@ -24,8 +24,7 @@ export class ProductInfoPopup extends AbstractAwaitablePopup {
this.cancel();
}
_hasMarginsCostsAccessRights() {
const isAccessibleToEveryUser =
this.pos.config.is_margins_costs_accessible_to_every_user;
const isAccessibleToEveryUser = this.pos.config.is_margins_costs_accessible_to_every_user;
const isCashierManager = this.pos.get_cashier().role === "manager";
return isAccessibleToEveryUser || isCashierManager;
}
@@ -0,0 +1,7 @@
.product-info-popup {
max-width: 800px !important;
.modal-body {
max-height: 70vh;
}
}
@@ -2,29 +2,28 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductInfoPopup" owl="1">
<div class="popup product-info-popup">
<header class="title">
Product information
</header>
<main class="body">
<div class="section-product-info-title">
<div class="column flex-start">
<span t-esc="props.product.display_name" class="global-info-title product-name"/>
<span><t t-if="props.product.default_code" t-esc="props.product.default_code"/> <t t-if="props.product.default_code and props.product.barcode"> - </t> <t t-if="props.product.barcode" t-esc="props.product.barcode"/></span>
<div class="modal-header">
<h4 class="modal-title">Product information</h4>
</div>
<main class="body modal-body overflow-auto">
<div class="section-product-info-title d-flex flex-column flex-sm-row text-start gap-3 bg-info text-info bg-opacity-25 mx-n3 mt-n3 p-3">
<div class="d-flex flex-column flex-grow-1 text-start w-100 w-sm-25">
<span t-esc="props.product.display_name" class="global-info-title fs-2 fw-bolder text-truncate"/>
<span class="fs-3"><t t-if="props.product.default_code" t-esc="props.product.default_code"/> <t t-if="props.product.default_code and props.product.barcode"> - </t> <t t-if="props.product.barcode" t-esc="props.product.barcode"/></span>
</div>
<div class="column flex-end">
<span t-esc="env.utils.formatCurrency(productInfo.all_prices.price_with_tax)" class="global-info-title" />
<t t-foreach="productInfo.all_prices.tax_details" t-as="tax" t-key="tax.name">
<div><t t-esc="tax.name"/>: <t t-esc="env.utils.formatCurrency(tax.amount)"/></div>
</t>
<div class="d-flex flex-column justify-content-center text-start text-sm-end w-100 w-sm-25">
<span t-esc="env.utils.formatCurrency(productInfo.all_prices.price_with_tax)" class="global-info-title fs-2 fw-bolder" />
<span class="fs-3">
<t t-foreach="productInfo.all_prices.tax_details" t-as="tax" t-key="tax.name">
<div><t t-esc="tax.name"/>: <t t-esc="env.utils.formatCurrency(tax.amount)"/></div>
</t>
</span>
</div>
</div>
<div class="section-financials">
<div class="section-title">
<span>Financials</span>
<div class="section-title-line"></div>
</div>
<div class="section-financials-body">
<table>
<div class="section-financials mt-3 mb-4 pb-4 border-bottom text-start">
<h3 class="section-title">Financials</h3>
<div class="section-financials-body d-flex flex-column flex-sm-row">
<table class="table table-borderless mb-0">
<tr>
<td>Price excl. VAT:</td>
<td><t t-esc="env.utils.formatCurrency(productInfo.all_prices.price_without_tax)"/></td>
@@ -38,7 +37,7 @@
<td><t t-esc="marginCurrency"/> (<t t-esc="marginPercent"/>%) </td>
</tr>
</table>
<table>
<table class="table table-borderless">
<t t-foreach="productInfo.pricelists" t-as="pricelist" t-key="pricelist.name">
<tr>
<td t-esc="pricelist.name"/>
@@ -48,71 +47,75 @@
</table>
</div>
</div>
<div class="section-inventory" t-if="productInfo.warehouses.length > 0">
<div class="section-title">
<span>Inventory <t t-if="pos.pos_session.update_stock_at_closing">(as of opening)</t></span>
<div class="section-title-line"></div>
</div>
<div class="section-inventory mt-3 mb-4 pb-4 border-bottom text-start" t-if="productInfo.warehouses.length > 0">
<h3 class="section-title">
Inventory
<t t-if="pos.pos_session.update_stock_at_closing">(as of opening)</t>
</h3>
<div class="section-inventory-body">
<table class="mobile-table">
<t t-foreach="productInfo.warehouses" t-as="warehouse" t-key="warehouse.name">
<tr>
<td><span t-esc="warehouse.name" class="table-name"/>:</td>
<td><t t-esc="warehouse.available_quantity"/> <t t-esc="warehouse.uom"/> available,</td>
<td><t t-esc="warehouse.forecasted_quantity"/> forecasted</td>
</tr>
</t>
</table>
<t t-foreach="productInfo.warehouses" t-as="warehouse" t-key="warehouse.name">
<div class="d-flex flex-column flex-md-row gap-2">
<div>
<t t-esc="warehouse.name" class="table-name"/>
:
</div>
<div>
<span class="me-1 fw-bolder"><t t-esc="warehouse.available_quantity" class="table-name"/></span>
<t t-esc="warehouse.uom"/> available,
</div>
<div>
<span class="me-1 fw-bolder"><t t-esc="warehouse.forecasted_quantity"/></span>
forecasted
</div>
</div>
</t>
</div>
</div>
<div class="section-supplier" t-if="productInfo.suppliers.length > 0">
<div class="section-title">
<span>Replenishment</span>
<div class="section-title-line"></div>
</div>
<div class="section-supplier mt-3 mb-4 pb-4 border-bottom text-start" t-if="productInfo.suppliers.length > 0">
<h3 class="section-title">Replenishment</h3>
<div class="section-supplier-body">
<table class="mobile-table">
<t t-foreach="productInfo.suppliers" t-as="supplier" t-key="supplier.name">
<tr>
<td><span t-esc="supplier.name" class="table-name"/>:</td>
<div class="mobile-line">
<td><t t-esc="supplier.delay"/> Days</td>
<td t-if="_hasMarginsCostsAccessRights()"><t t-esc="env.utils.formatCurrency(supplier.price)"/></td>
</div>
</tr>
</t>
</table>
<t t-foreach="productInfo.suppliers" t-as="supplier" t-key="supplier.name">
<div class="d-flex flex-column flex-md-row gap-2">
<div>
<span t-esc="supplier.name" class="table-name"/>:
</div>
<span class="me-1 fw-bolder">
<t t-esc="supplier.delay"/>
Days
</span>
<span t-if="_hasMarginsCostsAccessRights()">
<t t-esc="env.utils.formatCurrency(supplier.price)"/>
</span>
</div>
</t>
</div>
</div>
<div class="extra">
<div class="section-variants" t-if="productInfo.variants.length > 0">
<div class="section-title">
<span>Attributes</span>
<div class="section-title-line"></div>
</div>
<div class="extra mt-3 mb-4 pb-4 border-bottom text-start" t-if="productInfo.variants.length > 0">
<div class="section-variants">
<h3 class="section-title">Attributes</h3>
<div class="section-variants-body">
<table class="mobile-table">
<t t-foreach="productInfo.variants" t-as="variant" t-key="variant.name">
<tr>
<td><span t-esc="variant.name" class="table-name"/>:</td>
<td class="table-value">
<t t-foreach="variant.values" t-as="attribute_value" t-key="attribute_value.name">
<span class="searchable" t-on-click="() => this.searchProduct(attribute_value.search)"><t t-esc="attribute_value.name"/></span> <t t-if="attribute_value_index lt variant.values.length - 1">, </t>
</t>
</td>
</tr>
</t>
</table>
<t t-foreach="productInfo.variants" t-as="variant" t-key="variant.name">
<div class="d-flex flex-column align-items-start gap-2 mb-2">
<div>
<span t-esc="variant.name" class="table-name"/>:
</div>
<div class="d-flex flex-wrap gap-1">
<t t-foreach="variant.values" t-as="attribute_value" t-key="attribute_value.name">
<span class="searchable btn btn-secondary" t-on-click="() => this.searchProduct(attribute_value.search)">
<t t-esc="attribute_value.name"/>
</span>
<t t-if="attribute_value_index lt variant.values.length - 1"> </t>
</t>
</div>
</div>
</t>
</div>
</div>
</div>
<div class="section-order">
<div class="section-title">
<span>Order</span>
<div class="section-title-line"></div>
</div>
<div class="section-order mt-3 mb-4 pb-4 border-bottom text-start">
<h3 class="section-title">Order</h3>
<div class="section-order-body">
<table>
<table class="table table-borderless w-100 w-sm-50 mb-0">
<tr>
<td>Total Price excl. VAT:</td>
<td t-esc="orderPriceWithoutTaxCurrency" class="table-value"/>
@@ -129,8 +132,8 @@
</div>
</div>
</main>
<footer class="footer">
<div class="button cancel" t-on-click="cancel">
<footer class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary" t-on-click="cancel">
Close
</div>
</footer>
@@ -0,0 +1,43 @@
.category-list-scroller{
max-height:40vh;
}
.category-button {
.category-img {
width: 120px;
height: 100px;
}
& img {
max-height: 100px;
max-width: 120px;
}
}
/* Search Bar (PartnerList & Product Screens*/
.pos-search-bar:focus-within {
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
color: $input-focus-color;
}
/* Product Screen Search Bar */
.info-load-product-message {
background: lightblue;
color: #017e84;
padding: 8px;
width: fit-content;
display: block;
margin : 3px auto 0 auto;
}
@media screen and (max-width: 768px) {
.pos .search-bar-container.sb-product {
max-width: none;
width: auto;
background-color: #FFFFFF;
}
}
@@ -2,57 +2,57 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductsWidgetControlPanel" owl="1">
<div class="products-widget-control" t-ref="products-widget-control">
<div class="rightpane-header">
<t t-if="this.pos.posHasValidProduct()">
<div class="search-bar-container sb-product">
<span t-if="state.isMobile" class="oe_status oe_icon icon" t-on-click="toggleMobileSearchBar">
<i t-attf-class="fa {{state.mobileSearchBarIsShown ? 'fa-arrow-right' : 'fa-search'}}"/>
</span>
<t t-if="!state.isMobile || state.mobileSearchBarIsShown">
<div class="pos-search-bar">
<i class="fa fa-search"/>
<input t-model="this.pos.searchProductWord" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="this.pos.searchProductWord" class="fa fa-times" t-on-click="_clearSearch"/>
</div>
</t>
</div>
</t>
<div class="products-widget-control d-grid shadow-sm z-index-1" t-ref="products-widget-control">
<div class="rightpane-header d-flex justify-content-between align-items-center text-center bg-view p-1 overflow-x-auto overflow-y-hidden">
<t t-if="!props.hasNoCategories">
<div class="categories-header">
<div class="categories-header d-flex overflow-x-auto">
<!-- Breadcrumbs -->
<div class="breadcrumbs">
<span class="breadcrumb">
<span class="breadcrumb-button breadcrumb-home" t-on-click="() => this.props.switchCategory(0)">
<i class="fa fa-home" role="img" aria-label="Home" title="Home"></i>
</span>
<div class="breadcrumbs d-flex">
<span class="breadcrumb float-start d-flex align-items-center">
<button class="breadcrumb-button breadcrumb-home btn btn-light fw-bolder" t-on-click="() => this.props.switchCategory(0)">
<i class="fa fa-home fa-2x py-1" role="img" aria-label="Home" title="Home"></i>
</button>
</span>
<t t-foreach="props.breadcrumbs" t-as="category" t-key="category.id">
<span class="breadcrumb">
<i class="breadcrumb-arrow fa fa-caret-right" aria-hidden="true"/>
<span class="breadcrumb-button"
<span class="breadcrumb float-start d-flex align-items-center">
<i class="breadcrumb-arrow fa fa-caret-right px-2" aria-hidden="true"/>
<button class="breadcrumb-button btn btn-light h-100 fw-bolder"
t-on-click="() => this.props.switchCategory(category.id)">
<t t-esc="category.name"></t>
</span>
</button>
</span>
</t>
</div>
<!-- Subcategories -->
<div t-if="props.subcategories.length > 0 and !displayCategImages" class="category-list simple">
<div t-if="props.subcategories.length > 0 and !displayCategImages" class="category-list simple d-flex align-items-center ms-1">
<t t-foreach="props.subcategories" t-as="category" t-key="category.id">
<span class="category-simple-button"
<button class="category-simple-button btn btn-light h-100 fw-bolder"
t-on-click="() => this.props.switchCategory(category.id)">
<t t-esc="category.name" />
</span>
</button>
</t>
</div>
</div>
</t>
<div class="search-bar-container sb-product d-flex align-items-center h-100">
<span t-if="state.isMobile" class="oe_status oe_icon icon d-flex align-items-center h-100 me-1 px-4 rounded text-bg-secondary cursor-pointer" t-on-click="toggleMobileSearchBar">
<i t-attf-class="fa {{state.mobileSearchBarIsShown ? 'fa-arrow-right' : 'fa-search'}}"/>
</span>
<t t-if="!state.isMobile || state.mobileSearchBarIsShown">
<div class="input-group h-100">
<div class="pos-search-bar form-control d-flex align-items-center py-1 bg-view">
<i class="oi oi-search me-2"/>
<input class="flex-grow-1 w-auto h-100 border-0 px-2 me-4" t-model="this.pos.searchProductWord" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="this.pos.searchProductWord" class="fa fa-times position-absolute end-0 me-2 pe-1 cursor-pointer" t-on-click="_clearSearch"/>
</div>
</div>
</t>
</div>
</div>
<t t-if="props.subcategories.length > 0 and displayCategImages">
<div class="categories">
<div class="category-list-scroller">
<div class="category-list">
<div class="categories border-bottom shadow-sm">
<div class="category-list-scroller w-100 h-100 overflow-hidden overflow-y-auto">
<div class="category-list d-flex flex-wrap px-1 py-2 gap-3">
<t t-foreach="props.subcategories" t-as="category" t-key="category.id">
<CategoryButton
switchCategory.bind="props.switchCategory"
@@ -0,0 +1,9 @@
.product-list {
grid-template-columns: repeat(auto-fill, minmax(122px, 1fr));
}
@media only screen and (max-width: 768px) {
.product-list {
grid-template-columns: repeat(3, 1fr);
}
}
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductsWidget" owl="1">
<div class="products-widget">
<div class="products-widget d-flex flex-column flex-grow-1 overflow-hidden">
<ProductsWidgetControlPanel
updateProductList.bind="updateProductList"
clearSearch.bind="clearSearch"
@@ -12,14 +12,14 @@
subcategories="subcategories"
hasNoCategories="hasNoCategories"
loadProductFromServer.bind="onPressEnterKey" />
<div class="product-list-container">
<div t-if="productsToDisplay.length != 0 and posHasValidProduct" t-attf-class="{{this.pos.productListViewMode}} product-list">
<div class="product-list-container flex-grow-1 overflow-hidden overflow-y-auto">
<div t-if="productsToDisplay.length != 0 and posHasValidProduct" t-attf-class="{{this.pos.productListViewMode}} product-list d-grid gap-1 p-1">
<t t-foreach="productsToDisplay" t-as="product" t-key="product.id">
<ProductItem
product="product" />
</t>
</div>
<div t-else="" class="product-list-empty no-results-message">
<div t-else="" class="product-list-empty no-results-message text-center mt-5">
<t t-if="searchWord">
<p>No products found for <b>"<t t-esc="searchWord"/>"</b> in this category.</p>
</t>
@@ -30,7 +30,7 @@
<t t-else="">
<div>
<p>No products here, but don't worry, we've got your back.</p>
<p>Play with <a role="button" class="button-no-demo" t-on-click="loadDemoDataProducts">our demo products</a> or <a role="button" class="button-no-demo" t-on-click="createNewProducts">create your own products</a></p>
<p>Play with <a role="button" class="button-no-demo btn btn-lg btn-primary" t-on-click="loadDemoDataProducts">our demo products</a> or <a role="button" class="button-no-demo" t-on-click="createNewProducts">create your own products</a></p>
<div t-if="state.showReloadMessage" class="info-load-product-message">
Please reload this page once products have been created
</div>
@@ -38,8 +38,8 @@
</t>
</t>
</div>
<div t-if="searchWord" class="search-more-button">
<div class="button" t-on-click="onPressEnterKey">Search more</div>
<div t-if="searchWord" class="search-more-button d-flex justify-content-center">
<button class="btn btn-primary btn-lg" t-on-click="onPressEnterKey">Search more</button>
</div>
<div class="portal search-database-button no-results-message" t-att-class="{ oe_hidden: !shouldShowButton }"></div>
</div>
@@ -2,19 +2,19 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductScreen" owl="1">
<div class="product-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
<div class="screen-full-width">
<div class="leftpane pane-border" t-if="!ui.isSmall || pos.mobile_pane === 'left'">
<div class="product-screen screen h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<div class="screen-full-width d-flex w-100 h-100">
<div class="leftpane pane-border d-flex flex-column flex-grow-1 w-100 h-100 border-end bg-200" t-if="!ui.isSmall || pos.mobile_pane === 'left'">
<OrderWidget/>
<div class="pads">
<div class="control-buttons">
<div class="pads border-top">
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300">
<t t-if="!ui.isSmall">
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
<t t-component="cb.component" t-key="cb.name"/>
</t>
</t>
</div>
<div class="subpads">
<div class="subpads d-flex">
<ActionpadWidget
partner="partner"
actionName="constructor.numpadActionName"
@@ -26,21 +26,20 @@
</div>
</div>
</div>
<div class="rightpane" t-if="!ui.isSmall || pos.mobile_pane === 'right'">
<div class="rightpane d-flex flex-column flex-grow-1 w-100 h-100 bg-300" t-if="!ui.isSmall || pos.mobile_pane === 'right'">
<ProductsWidget />
<t t-if="ui.isSmall">
<div class="product-reminder" t-if="showProductReminder" t-key="animationKey">
<span><t t-esc="selectedOrderlineQuantity"/> <t t-esc="selectedOrderlineDisplayName"/></span>
<span><t t-esc="selectedOrderlineTotal"/></span>
<div class="product-reminder d-flex justify-content-center align-items-center py-1 text-bg-warning bg-opacity-50 fw-bolder" t-if="showProductReminder" t-key="animationKey" >
<span><t t-esc="selectedOrderlineQuantity"/> <t t-esc="selectedOrderlineDisplayName"/> <t t-esc="selectedOrderlineTotal"/></span>
</div>
<div class="switchpane">
<button class="btn-switchpane pay-button" t-att-class="{'primary': primaryPayButton(), 'secondary': !primaryPayButton()}" t-on-click="() => currentOrder.pay()">
<h1>Pay</h1>
<div class="switchpane d-flex">
<button class="btn-switchpane pay-button btn btn-primary w-50 rounded-0 fw-bolder" t-att-class="{'primary': primaryPayButton(), 'secondary': !primaryPayButton()}" t-on-click="() => currentOrder.pay()">
<span class="fs-1 d-block">Pay</span>
<span><t t-esc="total" /></span>
</button>
<button class="btn-switchpane" t-att-class="{'primary': primaryReviewButton(), 'secondary': !primaryReviewButton()}" t-on-click="switchPane">
<h1>Review</h1>
<span><t t-esc="items_number"/> items</span>
<button class="btn-switchpane btn btn-secondary w-50 rounded-0 fw-bolder" t-att-class="{'primary': primaryReviewButton(), 'secondary': !primaryReviewButton()}" t-on-click="switchPane">
<span class="fs-1 d-block">Review</span>
<span><t t-esc="items"/> items</span>
</button>
</div>
</t>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.OrderReceipt" owl="1">
<div class="pos-receipt">
<div class="pos-receipt d-inline-block m-3 p-3 border rounded bg-view text-start overflow-hidden">
<t t-if="receipt.company.logo">
<img class="pos-receipt-logo" t-att-src="receipt.company.logo" alt="Logo"/>
<br/>
@@ -0,0 +1,48 @@
.pos-receipt-container {
direction: ltr;
}
.receipt-screen .default-view .pos-receipt-container {
flex: 0 1 400px;
}
@include media-breakpoint-down(md) {
.receipt-screen .default-view .pos-receipt-container {
flex: 1;
overflow: visible;
}
}
@page {
margin: 0;
}
@media print {
body {
background: white;
}
body * {
visibility: hidden;
}
.pos, .pos * {
position: static !important;
}
.pos .receipt-screen .pos-receipt-container {
position: absolute !important;
top: 0;
left: 0;
}
.pos .receipt-screen .pos-receipt-container, .pos .receipt-screen .pos-receipt-container * {
visibility: visible;
background: white !important;
color: black !important;
}
.pos .pos-receipt {
margin: 0 !important;
margin-left: auto !important;
margin-right: auto !important;
border: none !important;
font-size: 14px !important;
width: 266px !important;
}
}
@@ -2,81 +2,72 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ReceiptScreen" owl="1">
<div class="receipt-screen screen">
<div class="screen-content">
<div class="top-content" t-if="!ui.isSmall">
<div class="top-content-center">
<h1>
<div class="receipt-screen screen h-100 bg-100">
<div class="screen-content d-flex flex-column justify-content-between h-100">
<div class="top-content d-flex align-items-center py-3 border-bottom text-center" t-if="!ui.isSmall">
<div class="top-content-center flex-grow-1">
<h1 class="mb-0">
<t t-esc="orderAmountPlusTip" />
</h1>
</div>
</div>
<div class="default-view">
<div class="actions">
<div class="default-view d-flex flex-md-row flex-column overflow-hidden">
<div class="actions flex-md-grow-1 flex-grow-0 flex-shrink-1 flex-basis-0 m-4">
<h1>Payment Successful</h1>
<div class="buttons">
<div class="button print" t-on-click="printReceipt">
<i class="fa fa-print" t-ref="order-print-receipt-button"></i> Print Receipt
</div>
<div class="buttons my-3">
<button class="button print btn btn-lg btn-secondary w-100 py-3" t-on-click="printReceipt">
<i class="fa fa-print ms-2" t-ref="order-print-receipt-button"></i> Print Receipt
</button>
</div>
<form t-on-submit.prevent="onSendEmail" class="send-email">
<div class="input-email">
<input type="email" t-attf-placeholder="Email: {{currentOrder.getEmailItems().join(', ')}}" t-model="orderUiState.inputEmail" />
<button class="send" t-att-class="{ highlight: isValidEmail() }" type="submit">
<form t-on-submit.prevent="onSendEmail" class="send-email d-flex">
<div class="input-email input-group">
<input type="email" class="form-control py-3 bg-view" t-attf-placeholder="Email: {{currentOrder.getEmailItems().join(', ')}}" t-model="orderUiState.inputEmail" />
<button class="send btn px-5" t-att-class="isValidEmail() ? 'highlight btn btn-primary' : 'btn-secondary disabled'" type="submit">
<i class="fa fa-paper-plane" aria-hidden="true" t-ref="order-mail-receipt-button" />
</button>
</div>
</form>
<div class="notice">
<div t-if="orderUiState.emailSuccessful !== null" t-attf-class="{{ orderUiState.emailSuccessful ? 'successful' : 'failed' }}">
<div class="notice mt-2">
<div t-if="orderUiState.emailSuccessful !== null" t-attf-class="{{ orderUiState.emailSuccessful ? 'successful text-success' : 'failed text-danger' }}">
<t t-esc="orderUiState.emailNotice"></t>
</div>
</div>
</div>
<div class="pos-receipt-container" t-ref="order-receipt">
<div class="pos-receipt-container bg-200 text-center overflow-auto" t-ref="order-receipt">
<OrderReceipt order="currentOrder"/>
</div>
</div>
<t t-set="splittedOrder" t-value="this.currentOrder.originalSplittedOrder and !this.currentOrder.originalSplittedOrder.finalized"/>
<div t-if="!ui.isSmall" class="validation-buttons">
<div class="button next validation" t-att-class="{ highlight: !locked }"
<div t-if="!ui.isSmall" class="validation-buttons d-flex w-100 gap-1 sticky-bottom">
<button class="button next validation btn btn-primary w-100 py-5 rounded-0 fs-2" t-att-class="{ highlight: !locked }"
t-if="!splittedOrder"
t-on-click="orderDone" name="done">
<div class="pay-circle">
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
New Order
</div>
<div t-if="isResumeVisible() and !splittedOrder" class="button next validation" t-att-class="{ highlight: !locked }"
</button>
<button t-if="isResumeVisible() and !splittedOrder" class="button next validation btn btn-primary w-100 py-5 rounded-0 fs-2" t-att-class="{ highlight: !locked }"
t-on-click="resumeOrder" name="resume">
<div class="pay-circle">
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
Resume Order
</div>
<div t-if="splittedOrder" class="button next validation" t-att-class="{ highlight: !locked }"
</button>
<button t-if="splittedOrder" class="button next validation btn btn-primary w-100 py-5 rounded-0 fs-2" t-att-class="{ highlight: !locked }"
t-on-click="continueSplitting" name="resume">
<div class="pay-circle">
<i class="fa fa-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
Continue
</div>
</button>
</div>
<div t-else="" class="switchpane">
<div class="btn-switchpane validation-button" t-att-class="{ highlight: !locked }"
<div t-else="" class="switchpane d-flex">
<div class="btn-switchpane validation-button btn btn-primary flex-fill py-3 rounded-0 fw-bolder" t-att-class="{ highlight: !locked }"
t-if="!splittedOrder"
t-on-click="orderDone" name="done">
New Order
</div>
<div t-if="isResumeVisible() and !splittedOrder" class="btn-switchpane validation-button" t-att-class="{ highlight: !locked }"
<div t-if="isResumeVisible() and !splittedOrder" class="btn-switchpane validation-button btn btn-primary flex-fill py-3 rounded-0 fw-bolder" t-att-class="{ highlight: !locked }"
t-on-click="resumeOrder" name="resume">
Resume Order
</div>
<div t-if="splittedOrder" class="btn-switchpane validation-button" t-att-class="{ highlight: !locked }"
<div t-if="splittedOrder" class="btn-switchpane validation-button btn btn-primary flex-fill py-3 rounded-0 fw-bolder" t-att-class="{ highlight: !locked }"
t-on-click="continueSplitting" name="resume">
<div class="pay-circle">
<i class="fa fa-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
Continue
</div>
</div>
@@ -2,20 +2,20 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ReprintReceiptScreen" owl="1">
<div class="receipt-screen screen">
<div class="screen-content">
<div class="top-content">
<span class="button back" t-on-click="confirm">
<i class="fa fa-angle-double-left"></i>
<div class="receipt-screen screen h-100 bg-100">
<div class="screen-content d-flex flex-column h-100">
<div class="top-content d-flex align-items-center p-2 border-bottom text-center">
<button class="button back btn btn-lg btn-secondary" t-on-click="confirm">
<i class="fa fa-angle-double-left me-2"></i>
<span> </span>
<span>Back</span>
</span>
</button>
</div>
<div class="centered-content">
<div class="button print" t-on-click="tryReprint">
<i class="fa fa-print"></i> Print Receipt
</div>
<div class="pos-receipt-container" t-ref="order-receipt">
<div class="centered-content d-grid mx-auto mt-3 border-0 overflow-hidden">
<button class="button print btn btn-lg btn-primary" t-on-click="tryReprint">
<i class="fa fa-print ms-2"></i> Print Receipt
</button>
<div class="pos-receipt-container mt-2 bg-200 text-center overflow-y-auto" t-ref="order-receipt">
<OrderReceipt order="props.order"/>
</div>
</div>
@@ -0,0 +1,5 @@
.computed-price, .weight {
font-family: Inconsolata;
text-shadow: 0px 2px 0px rgb(210,210,210);
box-shadow: 0px 2px 0px rgb(225,225,225) inset;
}
@@ -3,34 +3,36 @@
<t t-name="point_of_sale.ScaleScreen" owl="1">
<div class="scale-screen screen">
<div class="screen-content">
<div class="top-content">
<span class="button back" t-on-click="back">
<i class="fa fa-angle-double-left"></i>
<div class="screen-content d-flex flex-column h-100">
<div class="top-content d-flex align-items-center p-2 border-bottom text-center">
<button class="button back btn btn-lg btn-secondary" t-on-click="back">
<i class="fa fa-angle-double-left me-2"></i>
Back
</span>
<h1 class="product-name">
</button>
<h1 class="product-name flex-grow-1 m-0">
<t t-esc="productName" />
</h1>
</div>
<div class="centered-content">
<div class="weight js-weight">
<div class="centered-content w-100 mx-auto mt-3 border-start border-end text-center overflow-x-hidden overflow-y-auto">
<div class="weight js-weight m-2 p-4 rounded bg-view text-center fs-1">
<t t-esc="productWeightString" />
</div>
<div class="product-price">
<t
t-esc="env.utils.formatCurrency(productPrice) + '/' + productUom" />
<div class="d-flex flex-row gap-2 m-2">
<div class="product-price d-flex align-items-center justify-content-center flex-grow-1 rounded text-bg-info bg-opacity-25 text-info fs-2">
<t
t-esc="env.utils.formatCurrency(productPrice) + '/' + productUom" />
</div>
<div class="computed-price flex-grow-1 p-3 bg-view rounded text-center fs-2 fw-bold">
<t t-esc="computedPriceString" />
</div>
</div>
<div class="computed-price">
<t t-esc="computedPriceString" />
</div>
<div class="buy-product" t-on-click="confirm">
<div class="buy-product btn btn-lg btn-primary d-flex align-items-center justify-content-center mx-2 mb-2 cursor-pointer" t-on-click="confirm">
Order
<i class="fa fa-angle-double-right"></i>
<i class="fa fa-angle-double-right ms-2"></i>
</div>
</div>
</div>
</div>
</t>
</templates>
</templates>
@@ -2,11 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.InvoiceButton" owl="1">
<div class="control-button" t-on-click="() => this.click()" t-ref="invoice-button">
<i class="fa fa-file-pdf-o"></i>
<button class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()" t-ref="invoice-button">
<i class="fa fa-file-pdf-o me-1"></i>
<span> </span>
<span><t t-esc="commandName"></t></span>
</div>
</button>
</t>
</templates>
@@ -2,28 +2,28 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.OrderDetails" owl="1">
<div class="order-container">
<div t-ref="scrollable" class="order-scroller touch-scrollable">
<div class="order">
<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="!props.order">
<div class="order-empty">
<i class="fa fa-shopping-cart" role="img" aria-label="Shopping cart"
<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" />
<h1>Select an order</h1>
<h3 class="mt-2">Select an order</h3>
</div>
</t>
<t t-elif="orderlines.length === 0">
<div class="order-empty">
<i class="fa fa-shopping-cart" role="img" aria-label="Shopping cart"
<i class="fa fa fa-4x fa-shopping-cart" role="img" aria-label="Shopping cart"
title="Shopping cart" />
<h1>Order is empty</h1>
<h3 class="mt-2">Order is empty</h3>
</div>
</t>
<t t-else="">
<div class="header-note" t-att-class="{ highlight: props.highlightHeaderNote }">
<div 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">
<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"
@@ -33,14 +33,14 @@
</t>
</ul>
<!-- Not using OrderSummary because we don't need it's overrides in this view. -->
<div class="summary clearfix">
<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">
<span class="badge">Total: </span>
<div class="entry total fs-2 lh-sm">
Total:
<span class="value">
<t t-esc="total" />
</span>
<div t-if="tax" class="subentry">
<div t-if="tax" class="subentry fs-6 text-muted">
Taxes:
<span class="value">
<t t-esc="tax" />
@@ -2,28 +2,28 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.OrderlineDetails" owl="1">
<li class="orderline" t-on-click="() => this.props.onClickOrderline(props.line)" t-att-class="{ selected: props.isSelected }">
<span class="product-name">
<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 }">
<span class="product-name d-inline-block flex-grow-1 fw-bolder text-truncate">
<t t-esc="productName" />
</span>
<span class="price">
<span class="price fw-bolder float-end">
<t t-esc="totalPrice" />
</span>
<ul class="info-list">
<li class="info">
<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">
<i class="fa fa-sticky-note" role="img" aria-label="Customer Note" title="Customer Note"/>
<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="hasRefundedQty()">
<li class="info refund-note">
<li class="info refund-note mt-1">
<strong>
<t t-esc="getFormattedRefundedQty()" />
</strong>
@@ -31,11 +31,11 @@
</li>
</t>
<t t-if="hasToRefundQty()">
<li class="info to-refund-highlight refund-note">
<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" t-on-click.stop="() => this.props.onClickRefundOrderUid(_destinationOrderUid)" />
<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()" />
@@ -2,11 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ReprintReceiptButton" owl="1">
<div class="control-button" t-on-click="() => this.click()">
<i class="fa fa-print"></i>
<button class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()">
<i class="fa fa-print me-1"></i>
<span> </span>
<span>Print Receipt</span>
</div>
</button>
</t>
</templates>
@@ -0,0 +1,34 @@
.search-icon {
z-index: 4;
}
.fields {
position: absolute;
top: calc(100% + 5px);
right: 2px;
left: 2px;
box-shadow: 1px 1px 3px grey;
overflow: auto;
li:hover, .highlight {
background: #DDD;
}
}
.options {
display: block;
position: absolute;
top: calc(100% + 5px);
right: 0;
z-index: 1;
.indented {
text-indent: 1em;
}
}
@media screen and (max-width: 768px) {
.search input {
padding-left: 10px;
}
}
@@ -2,44 +2,44 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.SearchBar" owl="1">
<div class="pos-search-bar">
<div class="search">
<span class="search-icon" t-if="!ui.isSmall"><i class="fa fa-search"></i></span>
<input class="radius-left" t-ref="autofocus" t-att-class="{ 'radius-right': !props.config.filter.show }"
<div class="pos-search-bar d-flex align-items-center flex-grow-1 h-100 ms-3 rounded">
<div class="search position-relative h-100 d-flex flex-grow-1">
<div class="input-group">
<input class="form-control bg-view border-light ps-5" t-ref="autofocus"
t-model="state.searchInput" t-on-keydown="onSearchInputKeydown" t-on-keyup="onSearchInputKeyup" type="text" t-att-placeholder="props.placeholder" />
<div t-if="state.showSearchFields and state.searchInput" class="fields">
<ul>
<t t-foreach="searchFieldsList" t-as="fieldName" t-key="fieldName">
<li t-att-class="{ highlight: fieldName == searchFieldsList[state.selectedSearchFieldId] }"
t-on-click="() => this._onClickSearchField(fieldName)">
<span class="field">
<t t-esc="props.config.searchFields.get(fieldName)"></t>
</span>
<span>: </span>
<span class="term">
<t t-esc="state.searchInput"></t>
</span>
</li>
</t>
</ul>
<div t-if="state.showSearchFields and state.searchInput" class="fields">
<ul class="py-1 bg-view small">
<t t-foreach="searchFieldsList" t-as="fieldName" t-key="fieldName">
<li class="ps-5 py-1 text-start"
t-att-class="{ highlight: fieldName == searchFieldsList[state.selectedSearchFieldId] }"
t-on-click="() => this._onClickSearchField(fieldName)">
<span class="field">
<t t-esc="props.config.searchFields.get(fieldName)"></t>
</span>
<span>: </span>
<span class="term text-primary fw-bolder">
<t t-esc="state.searchInput"></t>
</span>
</li>
</t>
</ul>
</div>
<div t-if="props.config.filter.show" class="filter radius-right btn btn-secondary py-2 dropdown-toggle h-100"
t-on-click.stop="() => { state.showFilterOptions = !state.showFilterOptions }">
<span>
<t t-esc="props.config.filter.options.get(state.selectedFilter).text" />
</span>
<ul t-if="state.showFilterOptions" class="options dropdown-menu dropdown-menu-end py-1">
<t t-foreach="filterOptionsList" t-as="key" t-key="key">
<t t-set="_option" t-value="props.config.filter.options.get(key)" />
<li class="dropdown-item" t-on-click="() => this._onSelectFilter(key)" t-att-class="{ 'indented': _option.indented }">
<t t-esc="_option.text"></t>
</li>
</t>
</ul>
</div>
</div>
</div>
<div t-if="props.config.filter.show" class="filter radius-right"
t-on-click.stop="() => { state.showFilterOptions = !state.showFilterOptions }">
<span class="down-icon">
<i class="oi oi-chevron-down" aria-hidden="true"></i>
</span>
<span>
<t t-esc="props.config.filter.options.get(state.selectedFilter).text" />
</span>
<ul t-if="state.showFilterOptions" class="options">
<t t-foreach="filterOptionsList" t-as="key" t-key="key">
<t t-set="_option" t-value="props.config.filter.options.get(key)" />
<li t-on-click="() => this._onSelectFilter(key)" t-att-class="{ indented: _option.indented }">
<t t-esc="_option.text"></t>
</li>
</t>
</ul>
<span class="search-icon position-absolute start-0 top-50 translate-middle ms-4 z-3" t-if="!ui.isSmall"><i class="oi oi-search"></i></span>
</div>
</div>
</t>
@@ -0,0 +1,95 @@
.left-pane {
max-width: 500px;
}
.orders .col {
flex: 1;
&.very-narrow {
flex: 0.2;
}
&.narrow {
flex: 0.5;
}
&.wide {
flex: 1.5;
}
}
.order-row:hover {
color: white;
background-color: $primary !important;
}
@media screen and (max-width: 640px) {
.screen-full-width {
flex-direction: column;
height: auto;
min-height: 100%;
}
.controls {
position: sticky;
top: 0;
background: $gray-300;
z-index: 100;
}
.ticket-screen .order,
.ticket-screen .leftpane {
max-width: inherit;
}
.ticket-screen .rightpane {
width: auto;
}
.pos .orders {
position: sticky;
}
}
@media screen and (min-width: 768px) {
.order-row {
display: flex;
cursor: pointer;
}
.order-row:nth-child(odd) {
background: $gray-200;
}
.order-row:nth-child(even) {
background: white;
}
}
@media screen and (max-width: 768px) {
.order-row {
margin: 5px;
border: 1px solid #C9CCD2; /* $border-color */
background: white; /* $o-view-background-color */
}
}
@media screen and (min-width: 768px) {
.pos .orders .col.end {
justify-content: flex-end;
}
}
@media screen and (max-width: 768px) {
.pos .orders .order-row > .col > div:first-child {
font-weight: bold;
}
.ticket-screen .order-row .delete-button {
margin-top: 5px;
justify-content: center;
background: #d23f3a /* o-text-color('danger') */ ;
color: white;
}
.ticket-screen .order-row .delete-button .fa-trash {
margin-right: 5px;
}
}
@@ -2,13 +2,13 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.TicketScreen" owl="1">
<div class="ticket-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
<div class="screen-full-width">
<div class="rightpane pane-border">
<div class="controls">
<div class="ticket-screen screen h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<div class="screen-full-width d-flex w-100 h-100">
<div class="rightpane pane-border d-flex flex-column flex-grow-1 w-100 h-50 h-md-100 bg-300 overflow-y-auto">
<div class="controls d-flex align-items-center justify-content-between mt-1 mt-md-0 p-2 bg-400">
<t t-if="!ui.isSmall || !state.showSearchBar">
<div class="buttons">
<button class="discard" t-on-click="() => this.pos.closeScreen()">
<div class="buttons d-flex gap-2">
<button class="discard btn btn-lg btn-light" t-on-click="() => this.pos.closeScreen()">
<span class="search-icon">
<i class="fa fa-angle-double-left"/>
</span>
@@ -16,19 +16,19 @@
Back
</t>
</button>
<button t-if="allowNewOrders" class="highlight" t-on-click="() => this.onCreateNewOrder()">New Order</button>
<button t-if="allowNewOrders" class="highlight btn btn-lg btn-primary" t-on-click="() => this.onCreateNewOrder()">New Order</button>
</div>
</t>
<t t-if="ui.isSmall">
<t t-if="state.showSearchBar">
<button class="arrow-left" t-on-click="() => { state.showSearchBar = !state.showSearchBar }">
<button class="arrow-left btn btn-lg btn-light" t-on-click="() => { state.showSearchBar = !state.showSearchBar }">
<span class="search-icon">
<i class="fa fa-angle-double-left"/>
</span>
</button>
</t>
<t t-else="">
<button class="search" t-on-click="() => { state.showSearchBar = !state.showSearchBar }">
<button class="search btn btn-lg btn-light" t-on-click="() => { state.showSearchBar = !state.showSearchBar }">
<span class="search-icon">
<i class="fa fa-search"/>
</span>
@@ -55,61 +55,61 @@
</div>
</div>
</div>
<div class="orders">
<div class="orders overflow-y-auto">
<t t-set="_filteredOrderList" t-value="getFilteredOrderList()" />
<t t-if="_filteredOrderList.length !== 0">
<div class="header-row" t-att-class="{ oe_hidden: ui.isSmall }">
<div class="col wide">Date</div>
<div class="col wide">Receipt Number</div>
<div class="col">Customer</div>
<div class="col wide" t-if="showCardholderName()">Cardholder Name</div>
<div class="col">Cashier</div>
<div class="col end">Total</div>
<div class="col narrow">Status</div>
<div class="col very-narrow" name="delete"></div>
<div class="header-row d-flex text-bg-700 fw-bolder" t-att-class="{ oe_hidden: ui.isSmall }">
<div class="col wide p-2">Date</div>
<div class="col wide p-2">Receipt Number</div>
<div class="col p-2">Customer</div>
<div class="col wide p-2" t-if="showCardholderName()">Cardholder Name</div>
<div class="col p-2">Cashier</div>
<div class="col end p-2">Total</div>
<div class="col narrow p-2">Status</div>
<div class="col very-narrow p-2" name="delete"></div>
</div>
<t t-foreach="_filteredOrderList" t-as="order" t-key="order.cid">
<div class="order-row" t-att-class="{ highlight: isHighlighted(order) }" t-on-click="() => this.onClickOrder(order)">
<div class="col wide">
<div class="order-row" t-att-class="{ 'highlight': isHighlighted(order) }" t-on-click="() => this.onClickOrder(order)">
<div class="col wide p-2 ">
<div><t t-esc="getDate(order)"></t></div>
<div t-if="ui.isSmall"><t t-esc="getTotal(order)"></t></div>
</div>
<div class="col wide">
<div class="col wide p-2">
<div><t t-esc="order.name"></t></div>
<div t-if="ui.isSmall"><t t-esc="getStatus(order)"></t></div>
</div>
<div class="col" t-if="!ui.isSmall">
<div class="col p-2" t-if="!ui.isSmall">
<div><t t-esc="getPartner(order)"></t></div>
</div>
<div t-if="showCardholderName() and !ui.isSmall" class="col">
<div t-if="showCardholderName() and !ui.isSmall" class="col p-2">
<div><t t-esc="getCardholderName(order)"></t></div>
</div>
<div class="col">
<div class="col p-2">
<div t-if="ui.isSmall"><t t-esc="getPartner(order)"></t></div>
<div t-att-class = "ui.isSmall ? 'cashier':''"><t t-esc="getCashier(order)"></t></div>
</div>
<div class="col end" t-if="!ui.isSmall">
<div class="col end p-2" t-if="!ui.isSmall">
<div><t t-esc="getTotal(order)"></t></div>
</div>
<div class="col narrow" t-if="!ui.isSmall">
<div class="col narrow p-2" t-if="!ui.isSmall">
<div><t t-esc="getStatus(order)"></t></div>
</div>
<div t-if="!shouldHideDeleteButton(order)" class="col very-narrow delete-button" name="delete" t-on-click.stop="() => this.onDeleteOrder(order)">
<div t-if="!shouldHideDeleteButton(order)" class="col very-narrow delete-button p-2" name="delete" t-on-click.stop="() => this.onDeleteOrder(order)">
<i class="fa fa-trash" aria-hidden="true"/><t t-if="ui.isSmall"> Delete</t>
</div>
<div t-else="" class="col very-narrow"></div>
<div t-else="" class="col very-narrow p-2"></div>
</div>
</t>
</t>
<t t-else="">
<div class="empty-order-list">
<i role="img" aria-label="Shopping cart" title="Shopping cart" class="fa fa-shopping-cart"></i>
<h1>No orders found</h1>
<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>
</div>
</div>
<div class="leftpane">
<div class="leftpane d-flex flex-column flex-grow-1 w-100 h-50 h-md-100 bg-200">
<t t-set="_selectedSyncedOrder" t-value="getSelectedSyncedOrder()" />
<t t-set="_selectedOrderlineId" t-value="getSelectedOrderlineId()" />
<OrderDetails
@@ -118,14 +118,14 @@
order="_selectedSyncedOrder"
selectedOrderlineId="_selectedOrderlineId"
highlightHeaderNote="_state.ui.highlightHeaderNote" />
<div class="pads">
<div class="control-buttons">
<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">
<div class="subpads d-flex">
<ActionpadWidget
partner="getSelectedPartner()"
actionName="constructor.numpadActionName"
@@ -2,25 +2,28 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CashOpeningPopup" owl="1">
<div class="popup opening-cash-control">
<header class="title drag-handle">
OPENING CASH CONTROL
</header>
<main class="body">
<div class="opening-cash-section">
<span class="info-title">Opening cash</span>
<div class="cash-input-sub-section" t-on-input="handleInputChange">
<input class="pos-input" t-ref="cash-input" t-on-focus="ev=>ev.target.select()"/>
<div class="button icon" t-on-click="openDetailsPopup">
<i class="fa fa-money" role="img" title="Open the money details popup"/>
<div class="modal-header drag-handle">
<h4 class="modal-title">Opening Cash Control</h4>
</div>
<main class="modal-body">
<div class="opening-cash-section mb-3">
<span class="info-title d-flex flex-grow-1 align-items-start">Opening cash</span>
<div class="cash-input-sub-section input-group" t-on-input="handleInputChange">
<input class="pos-input form-control" type="number" t-model.number="state.openingCash" t-ref="cash-input" t-on-focus="ev=>ev.target.select()"/>
<div class="button icon btn btn-secondary" t-on-click="openDetailsPopup">
<i class="fa fa-money fa-2x" role="img" title="Open the money details popup"/>
</div>
</div>
</div>
<textarea placeholder="Add an opening note..." class="opening-cash-notes" t-model="state.notes"/>
</main>
<footer class="footer">
<div class="button dynamic-size" t-on-click="confirm">
Open session <span t-esc="env.utils.formatCurrency(state.openingCash)"/>
<div class="opening-notes-container d-flex flex-column align-items-start">
<label class="form-label" for="openingNotes">Opening note</label>
<textarea class="opening-notes form-control" id="openingNotes" rows="4" t-model="state.notes" placeholder="Add an opening note...">
<t t-esc="openingNotes"/>
</textarea>
</div>
</main>
<footer class="footer modal-footer">
<div class="button btn btn-lg btn-primary" t-on-click="confirm">Open session <span t-esc="env.utils.formatCurrency(state.openingCash)"/></div>
</footer>
</div>
</t>
@@ -70,7 +70,6 @@ const getProductImage = memoize(function getProductImage(productId, writeDate) {
});
export class PosStore extends Reactive {
hasBigScrollBars = false;
loadingSkipButtonIsShown = false;
mainScreen = { name: null, component: null };
tempScreen = null;
@@ -205,7 +204,6 @@ export class PosStore extends Reactive {
);
const { start_category, iface_start_categ_id } = this.config;
this.selectedCategoryId = (start_category && iface_start_categ_id?.[0]) || 0;
this.hasBigScrollBars = this.config.iface_big_scrollbars;
// Push orders in background, do not await
this.push_orders();
this.markReady();
@@ -3,21 +3,22 @@
<t t-name="point_of_sale.RadioProductAttribute" owl="1">
<div class="configurator_radio" t-ref="root">
<div class="table">
<div class="d-flex flex-column gap-3">
<t t-foreach="values" t-as="value" t-key="value.id">
<div class="attribute-name-cell">
<input type="radio" t-model="state.selected_value" t-att-name="attribute.id"
<div class="attribute-name-cell form-check">
<input class="form-check-input" type="radio" t-model="state.selected_value" t-att-name="attribute.id"
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
<span t-esc="value.name"/>
<label t-att-name="value.name" t-attf-for="{{ attribute.id }}_{{ value.id }}"/>
</div>
<div t-if="value.price_extra" class="price-extra-cell">
<span class="price_extra">
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
</span>
<label class="form-check-label" t-att-name="value.name" t-attf-for="{{ attribute.id }}_{{ value.id }}">
<span t-esc="value.name"/>
</label>
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-3">
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
</span>
</div>
</div>
<div t-if="value.id == state.selected_value &amp;&amp; value.is_custom" class="custom-value-cell">
<input class="custom_value" type="text" t-model="state.custom_value"/>
<input class="custom_value form-control form-control-lg mt-2" type="text" t-model="state.custom_value"/>
</div>
</t>
</div>
@@ -28,7 +29,7 @@
<div>
<t t-set="is_custom" t-value="false"/>
<select class="configurator_select" t-model="state.selected_value">
<select class="configurator_select form-select form-select-lg" t-model="state.selected_value">
<option t-foreach="values" t-as="value" t-key="value.id" t-att-value="value.id">
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; value.id == state.selected_value)"/>
<t t-esc="value.name"/>
@@ -38,7 +39,7 @@
</option>
</select>
<input class="custom_value" t-if="is_custom" type="text" t-model="state.custom_value"/>
<input class="custom_value form-control form-control-lg mt-2" t-if="is_custom" type="text" t-model="state.custom_value"/>
</div>
</t>
@@ -46,41 +47,41 @@
<div>
<t t-set="is_custom" t-value="false"/>
<ul class="color_attribute_list">
<ul class="color_attribute_list d-flex gap-3">
<li t-foreach="values" t-as="value" t-key="value.id" class="color_attribute_list_item">
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; value.id == state.selected_value)"/>
<label t-attf-class="configurator_color {{ value.id == state.selected_value ? 'active' : '' }}"
<label t-attf-class="configurator_color rounded border {{ value.id == state.selected_value ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
t-attf-style="background-color: {{ value.html_color }};" t-att-data-color="value.name">
<input type="radio" t-model="state.selected_value" t-att-value="value.id" t-att-name="attribute.id"/>
<input class="m-4 opacity-0" type="radio" t-model="state.selected_value" t-att-value="value.id" t-att-name="attribute.id"/>
</label>
</li>
</ul>
<input class="custom_value" t-if="is_custom" type="text" t-model="state.custom_value"/>
<input class="custom_value form-control form-control-lg mt-2" t-if="is_custom" type="text" t-model="state.custom_value"/>
</div>
</t>
<t t-name="point_of_sale.ProductConfiguratorPopup" owl="1">
<t t-if="!ui.isSmall">
<div class="popup popup-text popup-med product-configurator-popup">
<header class="title">
<t t-esc="props.product.display_name" />
</header>
<div class="modal-header drag-handle">
<h4 class="modal-title"><t t-esc="props.product.display_name" /></h4>
</div>
<main class="body product_configurator_attributes col-lg-4 col-md-6 col-sm-12">
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute">
<div class="attribute_name" t-esc="attribute.name"/>
<main class="body modal-body product_configurator_attributes m-0 text-start">
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-3">
<div class="attribute_name mb-2 fw-bolder" t-esc="attribute.name"/>
<RadioProductAttribute t-if="attribute.display_type === 'radio' or attribute.display_type === 'pills'" attribute="attribute"/>
<SelectProductAttribute t-elif="attribute.display_type === 'select'" attribute="attribute"/>
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
</div>
</main>
<footer class="footer footer-flex">
<div class="button highlight confirm" t-on-click="confirm">
<footer class="footer footer-flex modal-footer">
<div class="button highlight confirm btn btn-lg btn-primary" t-on-click="confirm">
Add
</div>
<div class="button cancel" t-on-click="cancel">
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
Cancel
</div>
</footer>
@@ -88,40 +89,37 @@
</t>
<t t-else="">
<div class="popup mobile popup-text popup-med product-configurator-popup">
<main class="body mobile product_configurator_attributes col-lg-4 col-md-6 col-sm-12">
<div class="product-img">
<img t-att-src="imageUrl" t-att-alt="props.product.display_name" />
<div class="product-info">
<span><t t-esc="props.product.display_name" ></t></span>
<span><t t-esc="unitPrice" ></t></span>
</div>
<div class="modal-header">
<h4 class="modal-title"><t t-esc="props.product.display_name" /></h4>
<span><t t-esc="unitPrice" ></t></span>
</div>
<main class="body modal-body mobile product_configurator_attributes d-flex flex-column text-start">
<div class="product-img d-flex justify-content-center h-50 p-2" >
<img class="h-100" t-att-src="imageUrl" t-att-alt="props.product.display_name" />
</div>
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute">
<div class="attribute_name" t-esc="attribute.name"/>
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-3">
<div class="attribute_name mb-2 fw-bolder" t-esc="attribute.name"/>
<RadioProductAttribute t-if="attribute.display_type === 'radio' or attribute.display_type === 'pills'" attribute="attribute"/>
<SelectProductAttribute t-elif="attribute.display_type === 'select'" attribute="attribute"/>
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
</div>
</main>
<footer class="footer footer-flex mobile">
<div class="quantity-selector">
<button t-on-click="addOneQuantity">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
<span class="quantity"><t t-esc="state.quantity"></t></span>
<button t-on-click="removeOneQuantity">
<footer class="footer footer-flex mobile modal-footer">
<div class="button highlight confirm btn btn-lg btn-primary" t-on-click="confirm">
Add
</div>
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
Cancel
</div>
<div class="quantity-selector d-flex align-items-center justify-content-center w-100 py-2 fw-bolder">
<button class="btn btn-lg btn-secondary" t-on-click="removeOneQuantity">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</div>
<div class="confirm-line">
<div class="button highlight confirm" t-on-click="confirm">
Add
</div>
<div class="button cancel" t-on-click="cancel">
Cancel
</div>
<span class="quantity mx-3"><t t-esc="state.quantity"></t></span>
<button class="btn btn-lg btn-secondary" t-on-click="addOneQuantity">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
</div>
</footer>
</div>
@@ -2,11 +2,12 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.EditListInput" owl="1">
<div>
<input type="text" t-att-value="props.item.text" t-on-input="onInput" class="popup-input list-line-input"
placeholder="Serial/Lot Number" t-on-keyup="onKeyup" />
<i t-if="props.deletable" class="oe_link_icon fa fa-trash-o" role="img" aria-label="Remove" title="Remove"
t-on-click="props.removeItem"></i>
<div class="input-group mb-3">
<input type="text" t-att-value="props.item.text" t-on-input="onInput" class="popup-input list-line-input form-control form-control-lg w-75 mx-auto"
placeholder="Serial/Lot Number" t-on-keyup="onKeyup" />
<button t-if="props.deletable" class="btn btn-danger btn-lg" t-on-click="props.removeItem">
<i class="oe_link_icon fa fa-trash-o" role="img" aria-label="Remove" title="Remove" />
</button>
</div>
</t>
@@ -3,23 +3,23 @@
<t t-name="point_of_sale.EditListPopup" owl="1">
<div class="popup popup-text edit-list-popup" t-ref="root">
<header class="title">
<t t-esc="props.title" />
<div class="modal-header">
<h4 class="modal-title title">
<t t-esc="props.title" />
</h4>
<span class="sub-title" t-esc="props.name"/>
</header>
<main>
<div class="list-lines">
<t t-foreach="state.array" t-as="item" t-key="item._id">
<EditListInput item="item" createNewItem.bind="createNewItem" removeItem="() => this.removeItem(item._id)"
deletable="_hasMoreThanOneItem()" onInputChange.bind="onInputChange" />
</t>
</div>
</div>
<main class="modal-body">
<t t-foreach="state.array" t-as="item" t-key="item._id">
<EditListInput item="item" createNewItem.bind="createNewItem" removeItem="() => this.removeItem(item._id)"
deletable="_hasMoreThanOneItem()" onInputChange.bind="onInputChange" />
</t>
</main>
<footer class="footer footer-flex">
<div class="button confirm highlight" t-on-click="confirm">
<footer class="footer footer-flex modal-footer">
<div class="button confirm highlight btn btn-lg btn-primary" t-on-click="confirm">
Ok
</div>
<div class="button cancel" t-on-click="cancel">
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
Cancel
</div>
</footer>
@@ -2,14 +2,16 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.AlertPopup" owl="1">
<div class="popup">
<p class="title">
<t t-esc="props.title" />
</p>
<p class="body">
<div class="modal-header">
<h4 class="title modal-title">
<t t-esc="props.title" />
</h4>
</div>
<p class="modal-body">
<t t-esc="props.body" />
</p>
<div class="footer">
<div class="button cancel" t-on-click="confirm">
<div class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
</div>
@@ -3,17 +3,19 @@
<t t-name="point_of_sale.ConfirmPopup" owl="1">
<div class="popup popup-confirm">
<header class="title drag-handle">
<t t-esc="props.title" />
</header>
<main class="body">
<div class="modal-header">
<h4 class="modal-title title drag-handle">
<t t-esc="props.title" />
</h4>
</div>
<main class="modal-body">
<t t-esc=" props.body" />
</main>
<footer class="footer">
<div class="button confirm" t-on-click="confirm">
<footer class="footer modal-footer">
<div class="button confirm btn btn-lg btn-primary" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<div class="button cancel" t-on-click="cancel">
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
@@ -2,6 +2,7 @@
import { formatMonetary, formatFloat } from "@web/views/fields/formatters";
import { roundDecimals } from "@web/core/utils/numbers";
import { escapeRegExp } from '@web/core/utils/strings';
import { registry } from "@web/core/registry";
/**
@@ -9,10 +10,21 @@ import { registry } from "@web/core/registry";
* functions that are parameterized by the data in `pos` service.
*/
export const contextualUtilsService = {
dependencies: ["pos"],
start(env, { pos }) {
dependencies: ["pos", "localization"],
start(env, { pos, localization }) {
const currency = pos.currency;
const productUoMDecimals = pos.dp["Product Unit of Measure"];
const decimalPoint = localization.decimalPoint;
const thousandsSep = localization.thousandsSep;
// Replace the thousands separator and decimal point with regex-escaped versions
const escapedDecimalPoint = escapeRegExp(decimalPoint);
let floatRegex;
if (thousandsSep) {
const escapedThousandsSep = escapeRegExp(thousandsSep);
floatRegex = new RegExp(`^-?(?:\\d+(${escapedThousandsSep}\\d+)*)?(?:${escapedDecimalPoint}\\d*)?$`);
} else {
floatRegex = new RegExp(`^-?(?:\\d+)?(?:${escapedDecimalPoint}\\d*)?$`);
}
const formatProductQty = (qty) => {
return formatFloat(qty, { digits: [true, productUoMDecimals] });
@@ -29,10 +41,15 @@ export const contextualUtilsService = {
return roundDecimals(value, currency.decimal_places);
};
const isValidFloat = (inputValue) => {
return ![decimalPoint, "-"].includes(inputValue) && floatRegex.test(inputValue);
};
env.utils = {
formatCurrency,
roundCurrency,
formatProductQty,
isValidFloat,
};
},
};
@@ -3,17 +3,19 @@
<t t-name="point_of_sale.DatePickerPopup" owl="1">
<div class="popup">
<header class="title">
<t t-esc="props.title" />
</header>
<main>
<input type="date" t-model="state.shippingDate" t-ref="input" t-att-min="_today()"/>
<div class="modal-header">
<h4 class="modal-title title">
<t t-esc="props.title" />
</h4>
</div>
<main class="modal-body">
<input class="form-control form-control-lg w-75 mx-auto" type="date" t-model="state.shippingDate" t-ref="input" t-att-min="_today()"/>
</main>
<div class="footer footer-flex">
<div class="button confirm highlight" t-on-click="confirm">
<div class="footer footer-flex modal-footer">
<div class="button confirm highlight btn btn-lg btn-primary" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<div class="button cancel" t-on-click="cancel">
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</div>
@@ -5,7 +5,6 @@ import { ErrorTracebackPopup } from "@point_of_sale/app/errors/popups/error_trac
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { useEnv, onMounted, onPatched, onWillUnmount, useComponent, useRef } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { escapeRegExp } from '@web/core/utils/strings';
/**
* Introduce error handlers in the component.
@@ -100,24 +99,12 @@ export function useValidateCashInput(inputRef, startingValue) {
const cashInput = useRef(inputRef);
const localization = useService("localization");
const decimalPoint = localization.decimalPoint;
const thousandsSep = localization.thousandsSep;
// Replace the thousands separator and decimal point with regex-escaped versions
const escapedDecimalPoint = escapeRegExp(decimalPoint);
let floatRegex;
if (thousandsSep) {
const escapedThousandsSep = escapeRegExp(thousandsSep);
floatRegex = new RegExp(`^-?(?:\\d+(${escapedThousandsSep}\\d+)*)?(?:${escapedDecimalPoint}\\d*)?$`);
} else {
floatRegex = new RegExp(`^-?(?:\\d+)?(?:${escapedDecimalPoint}\\d*)?$`);
}
function isValidFloat(inputValue) {
return ![decimalPoint, '-'].includes(inputValue) && floatRegex.test(inputValue);
}
const env = useEnv();
function handleCashInputChange(event) {
let inputValue = (event.target.value || "").trim();
// Check if the current input value is a valid float
if (!isValidFloat(inputValue)) {
if (!env.utils.isValidFloat(inputValue)) {
event.target.classList.add('invalid-cash-input');
} else {
event.target.classList.remove('invalid-cash-input');
@@ -0,0 +1,5 @@
.popup-numpad .input-button,
.popup-numpad .mode-button {
height: 50px;
width: 50px;
}
@@ -2,67 +2,69 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.NumberPopup" owl="1">
<div class="popup popup-number" t-att-class="{ 'popup-password': props.isPassword }" t-if="!isMobile()">
<header class="title drag-handle">
<span><t t-esc="props.title"/></span>
<span t-if="props.subtitle" class="subtitle"><t t-esc="props.subtitle"/></span>
</header>
<div class="input-symbol">
<div class="popup-input value active">
<div class="popup popup-number w-auto" t-att-class="{ 'popup-password': props.isPassword }" t-if="!isMobile()">
<div class="modal-header drag-handle">
<h4 class="modal-title title"><t t-esc="props.title" /></h4>
<span t-if="props.subtitle" class="subtitle p-1"><t t-esc="props.subtitle"/></span>
</div>
<div class="input-symbol pt-3">
<div class="popup-input value active form-control form-control-lg w-75 mx-auto">
<span class="input-value" t-att-class="{ 'highlight': state.toStartOver }"><t t-esc="inputBuffer"/></span>
<span t-if="props.inputSuffix"><t t-esc="props.inputSuffix" /></span>
</div>
</div>
<div class="popup-numpad">
<!-- FIXME POSREF do we really need to hardcode all of these? -->
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('1')">1</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('2')">2</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('3')">3</button>
<t t-if="props.cheap">
<button class="mode-button add" t-on-mousedown.prevent="() => this.sendInput('+1')">+1</button>
</t>
<t t-if="!props.cheap">
<button class="mode-button add" t-on-mousedown.prevent="() => this.sendInput('+10')">+10</button>
</t>
<br />
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('4')">4</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('5')">5</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('6')">6</button>
<t t-if="props.cheap">
<button class="mode-button add" t-on-mousedown.prevent="() => this.sendInput('+2')">+2</button>
</t>
<t t-if="!props.cheap">
<button class="mode-button add" t-on-mousedown.prevent="() => this.sendInput('+20')">+20</button>
</t>
<br />
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('7')">7</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('8')">8</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('9')">9</button>
<button t-if="!props.isPassword" class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('-')">-</button>
<br />
<button class="input-button numpad-char" t-on-mousedown.prevent="() => this.sendInput('Delete')">C</button>
<button class="input-button number-char" t-on-mousedown.prevent="() => this.sendInput('0')">0</button>
<button class="input-button number-char dot" t-on-mousedown.prevent="() => this.sendInput(decimalSeparator)">
<t t-esc="decimalSeparator" /></button>
<button class="input-button numpad-backspace" t-on-mousedown.prevent="() => this.sendInput('Backspace')">
<img style="pointer-events: none;"
src="/point_of_sale/static/src/img/backspace.png" width="24"
height="21" alt="Backspace" />
</button>
<br />
<div class="popup-numpad px-5 my-3">
<!-- FIXME POSREF do we really need to hardcode all of these? -->
<div class="row">
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('1')">1</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('2')">2</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('3')">3</button>
<t t-if="!props.isPassword and props.cheap">
<button class="mode-button add btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('+1')">+1</button>
</t>
<t t-if="!props.isPassword and !props.cheap">
<button class="mode-button add btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('+10')">+10</button>
</t>
</div>
<div class="row">
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('4')">4</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('5')">5</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('6')">6</button>
<t t-if="!props.isPassword and props.cheap">
<button class="mode-button add btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('+2')">+2</button>
</t>
<t t-if="!props.isPassword and !props.cheap">
<button class="mode-button add btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('+20')">+20</button>
</t>
</div>
<div class="row">
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('7')">7</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('8')">8</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('9')">9</button>
<button t-if="!props.isPassword" class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('-')">-</button>
</div>
<div class="row">
<button class="col-3 input-button numpad-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('Delete')">C</button>
<button class="col-3 input-button number-char btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('0')">0</button>
<button t-if="!props.isPassword" class="col-3 input-button number-char dot btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput(decimalSeparator)">
<t t-esc="decimalSeparator" /></button>
<button class="col-3 input-button numpad-backspace btn btn-secondary p-0 rounded-circle m-1 fw-bolder" t-on-mousedown.prevent="() => this.sendInput('Backspace')">
<img style="pointer-events: none;"
src="/point_of_sale/static/src/img/backspace.png" width="24"
height="21" alt="Backspace" />
</button>
</div>
</div>
<footer class="footer centered">
<div class="button confirm highlight" t-on-mousedown.prevent="confirm">
<div class="flex-column">
<footer class="footer centered modal-footer justify-content-center">
<button class="button confirm highlight btn btn-lg btn-primary" t-on-mousedown.prevent="confirm">
<span><t t-esc="props.confirmText" /></span>
<span t-if="props.getInputBufferReminder(state.buffer)" class="input-buffer-reminder">
<span t-if="props.getInputBufferReminder(state.buffer)" class="input-buffer-reminder small">
(<t t-esc="props.getInputBufferReminder(state.buffer)" />)
</span>
</div>
</div>
<div class="button cancel" t-on-mousedown.prevent="cancel">
</button>
<button class="button cancel btn btn-lg btn-secondary" t-on-mousedown.prevent="cancel">
<t t-esc="props.cancelText" />
</div>
</button>
</footer>
</div>
<div class="popup" t-att-class="{ 'popup-password': props.isPassword }" t-else="">
@@ -72,13 +74,13 @@
<main>
<input type="text" t-model="state.payload" t-ref="input" class="value payment-input-number" inputmode="decimal">
</main>
<footer class="footer centered">
<div class="button confirm highlight" t-on-mousedown.prevent="confirm">
<footer class="footer centered modal-footer">
<button class="button confirm highlight btn btn-lg btn-primary" t-on-mousedown.prevent="confirm">
<t t-esc="props.confirmText" />
</div>
<div class="button cancel" t-on-mousedown.prevent="cancel">
</button>
<button class="button cancel btn btn-lg btn-secondary" t-on-mousedown.prevent="cancel">
<t t-esc="props.cancelText" />
</div>
</button>
</footer>
</div>
</t>
@@ -0,0 +1,3 @@
.popup-selection .selection {
max-height: 300px;
}
@@ -3,20 +3,23 @@
<t t-name="point_of_sale.SelectionPopup" owl="1">
<div class="popup popup-selection">
<header class="title drag-handle">
<t t-esc="props.title" />
</header>
<div class="selection scrollable-y">
<div class="modal-header drag-handle">
<h4 class="modal-title"><t t-esc="props.title" /></h4>
</div>
<div class="selection btn-group-vertical justify-content-start w-100 p-3 overflow-y-auto">
<t t-foreach="props.list" t-as="item" t-key="item.id">
<div class="selection-item" t-att-class="{ selected: item.isSelected }"
<button type="button" class="selection-item d-flex align-items-center justify-content-between btn btn-lg btn-outline-secondary w-100 p-3 text-start" t-att-class="{ 'selected active': item.isSelected }"
t-on-click="() => this.selectItem(item.id)">
<span t-esc="item.label" />
<span t-esc="item.description" t-if="item.description" />
</div>
<div class="d-flex flex-column">
<span t-esc="item.label" />
<span t-esc="item.description" t-if="item.description" />
</div>
<i class="oi oi-chevron-right"/>
</button>
</t>
</div>
<footer class="footer">
<div class="button cancel" t-on-click="cancel">
<footer class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
@@ -3,18 +3,17 @@
<t t-name="point_of_sale.TextInputPopup" owl="1">
<div class="popup popup-textinput">
<header class="title">
<t t-esc="props.title" />
</header>
<main>
<p t-esc="props.body" />
<input type="text" t-model="state.inputValue" t-ref="input" t-att-placeholder="props.placeholder"/>
</main>
<div class="footer footer-flex">
<div class="button confirm highlight" t-on-click="confirm">
<div class="modal-header">
<h4 class="modal-title"><t t-esc="props.title" /></h4>
</div>
<div class="modal-body">
<input class="form-control form-control-lg w-75 mx-auto" type="text" t-model="state.inputValue" t-ref="input" t-att-placeholder="props.placeholder"/>
</div>
<div class="footer footer-flex modal-footer">
<div class="button confirm highlight btn btn-lg btn-primary" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<div class="button cancel" t-on-click="cancel">
<div class="button cancel btn btn-lg btn-secondary" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</div>

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