[IMP] point_of_sale,pos_*: back to basics

Overall improvement and simplification (along with some minor re-alignment)
for the point of sale.

POS Dashboard:
- added a new check for companies that hasn't installed any chart of account,
this gives them a better understanding of the real problem.
- added a new `menuitem` in the configuration tab that list all the pos configs,
this gives the user a way to access the sub-config of a pos (and indirectly give
access to its whole settings)

Opening/closing control:
- the `MoneyDetailsPopup` is now opened through the `showPopup` method, we're
no longer using the css to show/hide.
- the details of the money (if it exists) is now given to the `MoneyDetailsPopup`
component, this allows to have a record data usable of the details and
the user doesn't have to start from scratch if he just wants to modify
one thing
- opening the `MoneyDetailsPopup` no longer resets the cash input, that way the
user can just quickly check if everything is correct again without having
to recount everything and this won't reset the outcome

Customer display:
- added "Powered by" before the Odoo logo in order to accentuate that
it's from the software used is from Odoo

Product screen:
- visual improvement of the `EditListInput` component which is mostly
used by the lot/serial number, it gives more information to the user
and better UX
- "Cash out" is now automatically selected upon opening the
`CashMovePopup`. No more minus sign (-) in the input amount. This
should give better intuitiveness and one less click to do
- no more clear search button shown if the search input is empty
- removed the hover and clickable effect on `CashierName` component
in the normal point_of_sale since nothing happens when we click on it
- increased the width of the click zone with a label in the
`ProductConfiguratorPopup` which makes it easier to select a radio input
- the `PrintBillButton`, `SplitBillButton`, `RewardButton` and
`ResetProgramsButton` are now disabled when nothing happens
- the `textarea` of the `TextAreaPopup` component has been fixed
and is no longer resizable

Partner list screen:
- fixed phone, mobile and mail icons the contact area
- increased details button size

Floor screen:
- when idle, no longer redirect to the floor screen if there's
an open error popup. An error popup is opened for a specific
reason, we shouldn't redirect while having this open or
automatically close it. Other kinds of popup will be
closed since those are issued by the user that went away.

task-2916197

closes odoo/odoo#97803

Signed-off-by: Masereel Pierre <pim@odoo.com>
This commit is contained in:
Jacky (trj)
2022-11-14 12:03:51 +01:00
parent c5c9d9d0c7
commit d0f89b3478
50 changed files with 489 additions and 417 deletions
+1
View File
@@ -116,6 +116,7 @@
'point_of_sale/static/src/css/popups/cash_opening_popup.css',
'point_of_sale/static/src/css/popups/closing_pos_popup.css',
'point_of_sale/static/src/css/popups/money_details_popup.css',
'point_of_sale/static/src/css/popups/text_area_popup.css',
'web/static/src/legacy/scss/fontawesome_overridden.scss',
# Here includes the lib and POS UI assets.
@@ -337,6 +337,12 @@ class PosConfig(models.Model):
if any(pricelist.company_id.id not in [False, config.company_id.id] for pricelist in config.available_pricelist_ids):
raise ValidationError(_("The selected pricelists must belong to no company or the company of the point of sale."))
def _check_company_has_template(self):
self.ensure_one()
if not self.company_has_template:
raise ValidationError(_("No chart of account configured, go to the \"configuration / settings\" menu, and "
"install one from the Invoicing tab."))
def name_get(self):
result = []
for config in self:
@@ -472,6 +478,7 @@ class PosConfig(models.Model):
}
def _check_before_creating_new_session(self):
self._check_company_has_template()
self._check_pricelists()
self._check_company_journal()
self._check_company_invoice_journal()
@@ -267,6 +267,13 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
background-repeat: no-repeat;
height: 1.5vw;
margin-top: 10%;
display: flex;
justify-content: end;
align-items: flex-end;
}
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-odoo_logo_container span {
margin-right: 5vw;
font-size: 1vw;
}
@media all and (orientation: portrait) {
body .pos-customer_facing_display {
@@ -1,6 +1,6 @@
/* Input style used in cash control popups */
.pos .popup .pos-input {
text-align: center;
text-align: right;
font-size: 18px;
color: #555555;
background: none;
@@ -19,3 +19,14 @@
border-color: blue;
font-weight: bold;
}
.pos .popup .footer-flex {
display: flex;
justify-content: space-between;
padding: 0 10px;
box-sizing: border-box;
}
.pos .popup .footer-flex .button {
margin-right: 0;
}
@@ -34,3 +34,7 @@
.pos .money-details .total-section {
font-weight: bold;
}
.pos .money-details .footer .button {
margin-right: unset;
}
@@ -0,0 +1,16 @@
.popup-textarea textarea {
resize: none;
height: 110px;
width: 450px;
}
.popup-textarea footer {
display: flex;
justify-content: space-between;
padding: 0 10px;
box-sizing: border-box;
}
.pos .popup.popup-textarea footer .button {
margin-right: 0;
}
+1 -1
View File
@@ -243,7 +243,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
openCashControl() {
if (this.shouldShowCashControl()) {
this.showPopup('CashOpeningPopup');
this.showPopup('CashOpeningPopup', { keepBehind: true });
}
}
@@ -15,6 +15,9 @@ odoo.define('point_of_sale.CashierName', function(require) {
const id = user_id ? user_id : -1;
return `/web/image/res.users/${id}/avatar_128`;
}
get cssClass() {
return {'not-clickable': true};
}
}
CashierName.template = 'CashierName';
@@ -3,67 +3,44 @@ odoo.define('point_of_sale.CashMovePopup', function (require) {
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _t } = require('web.core');
const { parse } = require('web.field_utils');
const { useRef, useState } = owl;
const { useRef, useState, onMounted } = owl;
class CashMovePopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.state = useState({
inputType: '', // '' | 'in' | 'out'
inputType: 'out', // '' | 'in' | 'out'
inputAmount: '',
inputReason: '',
inputHasError: false,
errorMessage: '',
});
this.inputAmountRef = useRef('input-amount-ref');
onMounted(() => this.inputAmountRef.el.focus());
}
confirm() {
try {
parse.float(this.state.inputAmount);
} catch {
this.state.inputHasError = true;
this.errorMessage = this.env._t('Invalid amount');
return;
}
if (this.state.inputType == '') {
this.state.inputHasError = true;
this.errorMessage = this.env._t('Select either Cash In or Cash Out before confirming.');
return;
}
if (this.state.inputType === 'out' && this.state.inputAmount > 0) {
this.state.inputHasError = true;
this.errorMessage = this.env._t('Insert a negative amount with the Cash Out option.');
return;
}
if (this.state.inputType === 'in' && this.state.inputAmount < 0) {
this.state.inputHasError = true;
this.errorMessage = this.env._t('Insert a positive amount with the Cash In option.');
this.state.errorMessage = this.env._t('Invalid amount');
return;
}
if (this.state.inputAmount < 0) {
this.state.inputAmount = this.state.inputAmount.substring(1);
this.state.errorMessage = this.env._t('Insert a positive amount');
return;
}
return super.confirm();
}
_onAmountKeypress(event) {
if (event.key === '-') {
if (['-', '+'].includes(event.key)) {
event.preventDefault();
this.state.inputAmount = this.state.inputType === 'out' ? this.state.inputAmount.substring(1) : `-${this.state.inputAmount}`;
this.state.inputType = this.state.inputType === 'out' ? 'in' : 'out';
}
}
onClickButton(type) {
let amount = this.state.inputAmount;
if (type === 'in') {
this.state.inputAmount = amount.charAt(0) === '-' ? amount.substring(1) : amount;
} else {
this.state.inputAmount = amount.charAt(0) === '-' ? amount : `-${amount}`;
}
this.state.inputType = type;
this.state.inputHasError = false;
this.inputAmountRef.el && this.inputAmountRef.el.focus();
this.state.errorMessage = '';
this.inputAmountRef.el.focus();
}
getPayload() {
return {
@@ -74,11 +51,6 @@ odoo.define('point_of_sale.CashMovePopup', function (require) {
}
}
CashMovePopup.template = 'point_of_sale.CashMovePopup';
CashMovePopup.defaultProps = {
cancelText: _t('Cancel'),
title: _t('Cash In/Out'),
};
Registries.Component.add(CashMovePopup);
return CashMovePopup;
@@ -10,10 +10,10 @@ odoo.define('point_of_sale.CashOpeningPopup', function(require) {
setup() {
super.setup();
this.manualInputCashCount = null;
this.moneyDetails = null;
this.state = useState({
notes: "",
openingCash: this.env.pos.pos_session.cash_register_balance_start || 0,
displayMoneyDetailsPopup: false,
});
}
//@override
@@ -27,25 +27,22 @@ odoo.define('point_of_sale.CashOpeningPopup', function(require) {
});
super.confirm();
}
openDetailsPopup() {
this.state.openingCash = 0;
this.state.notes = "";
this.state.displayMoneyDetailsPopup = true;
}
closeDetailsPopup() {
this.state.displayMoneyDetailsPopup = false;
}
updateCashOpening({ total, moneyDetailsNotes }) {
this.state.openingCash = total;
if (moneyDetailsNotes) {
this.state.notes = moneyDetailsNotes;
async openDetailsPopup() {
const { confirmed, payload } = await this.showPopup('MoneyDetailsPopup', {
moneyDetails: this.moneyDetails, total: this.manualInputCashCount ? 0 : this.state.openingCash });
if (confirmed) {
const { total, moneyDetails, moneyDetailsNotes } = payload;
this.state.openingCash = total;
if (moneyDetailsNotes) {
this.state.notes = moneyDetailsNotes;
}
this.manualInputCashCount = false;
this.moneyDetails = moneyDetails;
}
this.manualInputCashCount = false;
this.closeDetailsPopup();
}
handleInputChange() {
this.manualInputCashCount = true;
this.state.notes = "";
this.moneyDetails = null;
}
}
@@ -50,14 +50,22 @@ odoo.define('point_of_sale.ClosePosPopup', function(require) {
super.cancel();
}
}
openDetailsPopup() {
this.state.payments[this.defaultCashDetails.id].counted = 0;
this.state.payments[this.defaultCashDetails.id].difference = -this.defaultCashDetails.amount;
this.state.notes = "";
this.state.displayMoneyDetailsPopup = true;
}
closeDetailsPopup() {
this.state.displayMoneyDetailsPopup = false;
async openDetailsPopup() {
const { confirmed, payload } = await this.showPopup('MoneyDetailsPopup', {
moneyDetails: this.moneyDetails,
total: this.manualInputCashCount ? 0 : this.state.payments[this.defaultCashDetails.id].counted,
});
if (confirmed) {
const { total, moneyDetailsNotes, moneyDetails } = payload;
this.state.payments[this.defaultCashDetails.id].counted = total;
this.state.payments[this.defaultCashDetails.id].difference =
this.env.pos.round_decimals_currency(this.state.payments[[this.defaultCashDetails.id]].counted - this.defaultCashDetails.amount);
if (moneyDetailsNotes) {
this.state.notes = moneyDetailsNotes;
}
this.manualInputCashCount = false;
this.moneyDetails = moneyDetails;
}
}
async downloadSalesReport() {
await this.env.legacyActionManager.do_action('point_of_sale.sale_details_report', {
@@ -70,6 +78,7 @@ odoo.define('point_of_sale.ClosePosPopup', function(require) {
let expectedAmount;
if (paymentId === this.defaultCashDetails.id) {
this.manualInputCashCount = true;
this.moneyDetails = null;
this.state.notes = '';
expectedAmount = this.defaultCashDetails.amount;
} else {
@@ -78,17 +87,6 @@ odoo.define('point_of_sale.ClosePosPopup', function(require) {
this.state.payments[paymentId].difference =
this.env.pos.round_decimals_currency(this.state.payments[paymentId].counted - expectedAmount);
}
updateCountedCash({ total, moneyDetailsNotes, moneyDetails }) {
this.state.payments[this.defaultCashDetails.id].counted = total;
this.state.payments[this.defaultCashDetails.id].difference =
this.env.pos.round_decimals_currency(this.state.payments[[this.defaultCashDetails.id]].counted - this.defaultCashDetails.amount);
if (moneyDetailsNotes) {
this.state.notes = moneyDetailsNotes;
}
this.manualInputCashCount = false;
this.moneyDetails = moneyDetails;
this.closeDetailsPopup();
}
hasDifference() {
return Object.entries(this.state.payments).find(pm => pm[1].difference != 0);
}
@@ -4,12 +4,22 @@ odoo.define('point_of_sale.EditListInput', function(require) {
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
/**
* props {
* createNewItem: callback,
* removeItem: callback,
* item: object,
* }
*/
class EditListInput extends PosComponent {
onKeyup(event) {
if (event.key === "Enter" && event.target.value.trim() !== '') {
this.trigger('create-new-item');
this.props.createNewItem();
}
}
onInput(event) {
this.props.onInputChange(this.props.item._id, event.target.value);
}
}
EditListInput.template = 'EditListInput';
@@ -67,16 +67,18 @@ odoo.define('point_of_sale.EditListPopup', function(require) {
// Put _id for each item. It will serve as unique identifier of each item.
return array.map((item) => Object.assign({}, { _id: this._nextId() }, typeof item === 'object'? item: { 'text': item}));
}
removeItem(event) {
const itemToRemove = event.detail;
_hasMoreThanOneItem() {
return this.state.array.length > 1;
}
removeItem(itemId) {
this.state.array.splice(
this.state.array.findIndex(item => item._id == itemToRemove._id),
this.state.array.findIndex(item => item._id == itemId),
1
);
// We keep a minimum of one empty item in the popup.
if (this.state.array.length === 0) {
this.state.array.push(this._emptyItem());
}
}
onInputChange(itemId, text) {
const item = this.state.array.find(elem => elem._id === itemId);
item.text = text;
}
createNewItem() {
if (this.props.isSingleItem) return;
@@ -95,8 +97,8 @@ odoo.define('point_of_sale.EditListPopup', function(require) {
}
EditListPopup.template = 'EditListPopup';
EditListPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
confirmText: _lt('Add'),
cancelText: _lt('Discard'),
array: [],
isSingleItem: false,
};
@@ -1,27 +1,20 @@
odoo.define('point_of_sale.MoneyDetailsPopup', function(require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { useState } = owl;
/**
* Even if this component has a "confirm and cancel"-like buttons, this should not be an AbstractAwaitablePopup.
* We currently cannot show two popups at the same time, what we do is mount this component with its parent
* and hide it with some css. The confirm button will just trigger an event to the parent.
*/
class MoneyDetailsPopup extends PosComponent {
class MoneyDetailsPopup extends AbstractAwaitablePopup{
setup() {
super.setup();
this.currency = this.env.pos.currency;
this.state = useState({
moneyDetails: Object.fromEntries(this.env.pos.bills.map(bill => ([bill.value, 0]))),
total: 0,
moneyDetails: this.props.moneyDetails ? {...this.props.moneyDetails} :
Object.fromEntries(this.env.pos.bills.map(bill => ([bill.value, 0]))),
total: this.props.total ? this.props.total : 0,
});
if (this.props.manualInputCashCount) {
this.reset();
}
}
get firstHalfMoneyDetails() {
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
@@ -35,23 +28,15 @@ odoo.define('point_of_sale.MoneyDetailsPopup', function(require) {
let total = Object.entries(this.state.moneyDetails).reduce((total, money) => total + money[0] * money[1], 0);
this.state.total = this.env.pos.round_decimals_currency(total);
}
confirm() {
//@override
async getPayload() {
let moneyDetailsNotes = this.state.total ? 'Money details: \n' : null;
this.env.pos.bills.forEach(bill => {
if (this.state.moneyDetails[bill.value]) {
moneyDetailsNotes += ` - ${this.state.moneyDetails[bill.value]} x ${this.env.pos.format_currency(bill.value)}\n`;
}
})
const payload = { total: this.state.total, moneyDetailsNotes, moneyDetails: { ...this.state.moneyDetails } };
this.props.onConfirm(payload);
}
reset() {
for (let key in this.state.moneyDetails) { this.state.moneyDetails[key] = 0 }
this.state.total = 0;
}
discard() {
this.reset();
this.props.onDiscard();
return { total: this.state.total, moneyDetailsNotes, moneyDetails: { ...this.state.moneyDetails } };
}
}
@@ -65,8 +65,8 @@ odoo.define('point_of_sale.NumberPopup', function(require) {
}
NumberPopup.template = 'NumberPopup';
NumberPopup.defaultProps = {
confirmText: _t('Ok'),
cancelText: _t('Cancel'),
confirmText: _t('Confirm'),
cancelText: _t('Discard'),
title: _t('Confirm ?'),
body: '',
cheap: false,
@@ -30,8 +30,8 @@ odoo.define('point_of_sale.TextAreaPopup', function(require) {
}
TextAreaPopup.template = 'TextAreaPopup';
TextAreaPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
confirmText: _lt('Add'),
cancelText: _lt('Discard'),
title: '',
body: '',
};
@@ -24,8 +24,8 @@ odoo.define('point_of_sale.TextInputPopup', function(require) {
}
TextInputPopup.template = 'TextInputPopup';
TextInputPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
confirmText: _lt('Confirm'),
cancelText: _lt('Discard'),
title: '',
body: '',
startingValue: '',
@@ -46,6 +46,7 @@ odoo.define('point_of_sale.OrderWidget', function(require) {
const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot);
const { confirmed, payload } = await this.showPopup('EditListPopup', {
title: this.env._t('Lot/Serial Number(s) Required'),
name: orderline.product.display_name,
isSingleItem: isAllowOnlyOneLot,
array: packLotLinesToEdit,
});
@@ -3,8 +3,6 @@ odoo.define('point_of_sale.ProductItem', function(require) {
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service')
const { identifyError } = require('point_of_sale.utils');
class ProductItem extends PosComponent {
/**
@@ -42,24 +40,6 @@ odoo.define('point_of_sale.ProductItem', function(require) {
return formattedUnitPrice;
}
}
async onProductInfoClick() {
try {
const info = await this.env.pos.getProductInfo(this.props.product, 1);
this.showPopup('ProductInfoPopup', { info: info , product: this.props.product });
} catch (e) {
if (identifyError(e) instanceof ConnectionLostError||ConnectionAbortedError) {
this.showPopup('ErrorPopup', {
title: this.env._t('OfflineErrorPopup'),
body: this.env._t('Cannot access product information screen if offline.'),
});
} else {
this.showPopup('ErrorPopup', {
title: this.env._t('Unknown error'),
body: this.env._t('An unknown error prevents us from loading product information.'),
});
}
}
}
}
ProductItem.template = 'ProductItem';
@@ -98,6 +98,7 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
}
const { confirmed, payload } = await this.showPopup('EditListPopup', {
title: this.env._t('Lot/Serial Number(s) Required'),
name: product.display_name,
isSingleItem: isAllowOnlyOneLot,
array: packLotLinesToEdit,
});
@@ -7,13 +7,13 @@ odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
const Registries = require('point_of_sale.Registries');
const { debounce } = require("@web/core/utils/timing");
const { onMounted, onWillUnmount, useRef } = owl;
const { onMounted, onWillUnmount } = owl;
class ProductsWidgetControlPanel extends PosComponent {
setup() {
super.setup();
this.searchWordInput = useRef('search-word-input-product');
this.updateSearch = debounce(this.updateSearch, 100);
this.state = { searchInput: '' };
onMounted(() => {
this.env.posbus.on('search-product-from-info-popup', this, this.searchProductFromInfo)
@@ -26,32 +26,32 @@ odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
});
}
_clearSearch() {
this.searchWordInput.el.value = '';
this.state.searchInput = '';
this.trigger('clear-search');
}
get displayCategImages() {
return Object.values(this.env.pos.db.category_by_id).some(categ => categ.has_image) && !this.env.isMobile;
}
updateSearch(event) {
this.trigger('update-search', event.target.value);
this.trigger('update-search', this.state.searchInput);
if (event.key === 'Enter') {
this._onPressEnterKey()
}
}
async _onPressEnterKey() {
if (!this.searchWordInput.el.value) return;
if (!this.state.searchInput) return;
if (!this.env.pos.isEveryProductLoaded) {
const result = await this.loadProductFromDB();
this.showNotification(
_.str.sprintf(this.env._t('%s product(s) found for "%s".'),
result.length,
this.searchWordInput.el.value)
this.state.searchInput)
, 3000);
if (!result.length) this._clearSearch();
}
}
searchProductFromInfo(productName) {
this.searchWordInput.el.value = productName;
this.state.searchInput = productName;
this.trigger('switch-category', 0);
this.trigger('update-search', productName);
}
@@ -59,14 +59,14 @@ odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
this.trigger('toggle-mobile-searchbar');
}
async loadProductFromDB() {
if(!this.searchWordInput.el.value)
if(!this.state.searchInput)
return;
try {
let ProductIds = await this.rpc({
model: 'product.product',
method: 'search',
args: [['&', ['name', 'ilike', this.searchWordInput.el.value + "%"], ['available_in_pos', '=', true]]],
args: [['&', ['name', 'ilike', this.state.searchInput + "%"], ['available_in_pos', '=', true]]],
context: this.env.session.user_context,
});
if(ProductIds.length) {
+57 -20
View File
@@ -673,6 +673,7 @@ td {
opacity: 0.5;
cursor: default;
color: inherit;
pointer-events: none;
}
/* ********* The actionpad (payment, set customer) ********* */
@@ -1906,12 +1907,12 @@ td {
}
.pos .partnerlist-screen .partner-list th,
.pos .partnerlist-screen .partner-list td {
padding: 12px;
padding: 15px 8px;
}
.pos .partnerlist-screen .fa.fa.fa-phone,
.pos .partnerlist-screen .fa.fa-mobile,
.pos .partnerlist-screen .fa.fa-paper-plane-o {
.pos .partnerlist-screen .fa.fa-paper-plane-o,
.pos .partnerlist-screen .fa.fa-mobile{
margin-right: 4px;
}
@@ -1977,9 +1978,11 @@ td {
width: 30px;
opacity: .75;
}
.pos .edit-partner-button-cell {
vertical-align: middle;
}
.pos .edit-partner-button {
margin-top: 6px;
color: black;
height: 30px;
font-weight: bold;
}
.pos .partner-list tr.partner-line.highlight{
@@ -2638,13 +2641,6 @@ td {
color: $primary;
}
.pos .popup-textarea textarea {
width: 100%;
box-sizing: border-box;
}
.pos .popup-textarea .popup-textarea-wrap {
padding: map-get($spacers, 2);
}
.pos .popup .button.dont-show-again {
width: 130px;
}
@@ -2698,13 +2694,35 @@ td {
box-sizing: border-box;
width: 80%;
}
.pos .popup .list-lines{
overflow: auto;
height: 250px;
margin: 10px;
.pos .edit-list-popup header {
display: flex;
flex-direction: column;
}
.pos .popup .list-line-input {
margin: 3px;
.pos .edit-list-popup header .sub-title {
font-weight: normal;
margin-top: 5px
}
.pos .edit-list-popup main {
margin: 10px;
padding-left: 40px;
text-align: left;
}
.pos .edit-list-popup .list-lines {
overflow: auto;
display: flex;
flex-direction: column;
height: 250px;
}
.pos .edit-list-popup .list-lines span {
font-size: 16px;
padding-right: 40px;
margin-left: 3px;
}
.pos .edit-list-popup .list-line-input {
margin: 3px 10px 3px 3px;
width: 90%
}
.pos .popup-number .popup-input {
@@ -2774,6 +2792,9 @@ td {
{
margin-bottom: 20px;
}
.pos .popup-textinput main {
margin-bottom: 20px;
}
.pos .popup-numpad .input-button,
.pos .popup-numpad .mode-button {
background: none;
@@ -3010,6 +3031,20 @@ td {
grid-column: 1;
word-break: break-word;
max-width: 300px;
position: relative
}
.pos .product_configurator_attributes .configurator_radio .attribute-name-cell label {
position: absolute;
top: 0;
left: 0;
width: 80vw;
max-width: 600px;
height: 100%;
}
.pos .product_configurator_attributes .configurator_radio .attribute-name-cell label:hover {
cursor: pointer;
}
.pos .product_configurator_attributes .configurator_radio .price-extra-cell {
@@ -3019,6 +3054,7 @@ td {
.pos .product_configurator_attributes .configurator_radio input[type='radio'] {
box-shadow: none;
margin-right: 0.5em;
cursor: pointer;
}
.pos .product_configurator_attributes .configurator_radio .price_extra {
@@ -3032,7 +3068,8 @@ td {
.pos .product_configurator_attributes .configurator_radio .custom_value {
margin-left: 0.5em;
height: 1.3em;
position: relative;
z-index: 1;
}
/** Selector attribute **/
@@ -3727,7 +3764,7 @@ td {
font-size: 17px;
}
.pos .pos-topheader .status-buttons > div:hover {
.pos .pos-topheader .status-buttons > div:not(.not-clickable):hover {
background: rgba(0, 0, 0, .08);
}
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="CashierName" owl="1">
<div class="oe_status">
<div t-att-class="cssClass">
<span><img t-att-src="avatar" t-att-alt="username" class="avatar"/> <span t-if="!env.isMobile" t-esc="username" class="username"/></span>
</div>
</t>
@@ -41,7 +41,9 @@
<t t-call="CustomerFacingDisplayPaymentLines"/>
<!-- Odoo Logo -->
<div class="pos-odoo_logo_container"/>
<div class="pos-odoo_logo_container">
<span>powered by </span>
</div>
</div>
</div>
</div>
@@ -2,36 +2,36 @@
<templates id="template" xml:space="preserve">
<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.inputType == 'in' }">
Cash In
</span>
<span t-on-click="() => this.onClickButton('out')" class="input-type" t-att-class="{ 'red-highlight': state.inputType == 'out' }">
Cash Out
</span>
<div class="input-field">
<input type="text" name="amount" t-model="state.inputAmount" t-ref="input-amount-ref" t-on-keypress="_onAmountKeypress"/>
<span class="currency" t-esc="env.pos.currency.symbol" />
</div>
</div>
<textarea name="reason" t-model="state.inputReason" placeholder="Reason"></textarea>
<span t-if="state.inputHasError" class="error-message">
<t t-esc="errorMessage" />
</span>
<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.inputType == 'in' }">
Cash In
</span>
<span t-on-click="() => this.onClickButton('out')" class="input-type" t-att-class="{ 'red-highlight': state.inputType == 'out' }">
Cash Out
</span>
<div class="input-field">
<input type="number" name="amount" t-model="state.inputAmount" t-ref="input-amount-ref" t-on-keypress="_onAmountKeypress"/>
<span class="currency" t-esc="env.pos.currency.symbol" />
</div>
</main>
<footer class="footer cash-move">
<div class="button confirm disable highlight" t-on-click="confirm">
Confirm
</div>
<div class="button cancel" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
</div>
<textarea name="reason" t-model="state.inputReason" placeholder="Reason"></textarea>
<span t-if="state.errorMessage" class="error-message">
<t t-esc="state.errorMessage" />
</span>
</div>
</main>
<footer class="footer cash-move">
<div class="button confirm disable highlight" t-on-click="confirm">
Confirm
</div>
<div class="button cancel" t-on-click="cancel">
Discard
</div>
</footer>
</div>
</t>
</templates>
@@ -1,31 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="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" type="number" t-model.number="state.openingCash"/>
<div class="button icon" t-on-click="openDetailsPopup">
<i class="fa fa-calculator" role="img" title="Open the money details popup"/>
</div>
<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" type="number" t-model.number="state.openingCash"/>
<div class="button icon" t-on-click="openDetailsPopup">
<i class="fa fa-calculator" role="img" title="Open the money details popup"/>
</div>
</div>
<textarea placeholder="Add an opening note..." class="opening-cash-notes" t-model="state.notes"/>
</main>
<footer class="footer">
<div class="button" t-on-click="confirm">Open session</div>
</footer>
</div>
<MoneyDetailsPopup
t-if="state.displayMoneyDetailsPopup"
manualInputCashCount="manualInputCashCount"
onConfirm="(payload) => this.updateCashOpening(payload)"
onDiscard.bind="closeDetailsPopup"
/>
</div>
<textarea placeholder="Add an opening note..." class="opening-cash-notes" t-model="state.notes"/>
</main>
<footer class="footer">
<div class="button" t-on-click="confirm">Open session</div>
</footer>
</div>
</t>
</templates>
@@ -2,104 +2,98 @@
<templates id="template" xml:space="preserve">
<t t-name="ClosePosPopup" owl="1">
<div class="popup close-pos-popup">
<header>
<div class="title">Closing Session</div>
<div class="total-orders">
Total <t t-esc="ordersDetails.quantity"/> orders:
<span class="amount" t-esc="env.pos.format_currency(ordersDetails.amount)"/>
</div>
</header>
<main class="body">
<div class="payment-methods-overview">
<table>
<thead>
<div class="popup close-pos-popup">
<header>
<div class="title">Closing Session</div>
<div class="total-orders">
Total <t t-esc="ordersDetails.quantity"/> orders:
<span class="amount" t-esc="env.pos.format_currency(ordersDetails.amount)"/>
</div>
</header>
<main class="body">
<div class="payment-methods-overview">
<table>
<thead>
<tr>
<th>Payment Method</th>
<th>Expected</th>
<th style="width: 25%">Counted</th>
<th style="width: 20%">Difference</th>
</tr>
</thead>
<t t-if="cashControl">
<tbody>
<tr>
<th>Payment Method</th>
<th>Expected</th>
<th style="width: 25%">Counted</th>
<th style="width: 20%">Difference</th>
</tr>
</thead>
<t t-if="cashControl">
<tbody>
<tr>
<td t-esc="defaultCashDetails.name"/>
<td t-esc="env.pos.format_currency(defaultCashDetails.amount)"/>
<td class="flex" t-on-input="() => this.handleInputChange(defaultCashDetails.id)">
<input class="pos-input" type="number" t-model.number="state.payments[defaultCashDetails.id].counted"/>
<div class="button icon" t-on-click="openDetailsPopup">
<i class="fa fa-calculator" role="img" title="Open the money details popup"/>
</div>
</td>
<td t-esc="env.pos.format_currency(state.payments[defaultCashDetails.id].difference)" t-att-class="{'warning': state.payments[defaultCashDetails.id].difference}"/>
</tr>
</tbody>
<tbody class="cash-overview">
<tr>
<td>Opening</td>
<td t-esc="env.pos.format_currency(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 ? '-' : '+'"/>
<t t-esc="move.name"/>
</div>
</td>
<td t-esc="env.pos.format_currency(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 ? '-' : '+'"/>
Payments in <t t-esc="defaultCashDetails.name"/>
</div>
</td>
<td t-esc="env.pos.format_currency(Math.abs(defaultCashDetails.payment_amount))"/>
</tr>
</tbody>
</t>
<tbody t-if="otherPaymentMethods.length &gt; 0">
<tr t-foreach="otherPaymentMethods" t-as="pm" t-key="pm.id">
<td t-esc="pm.name"/>
<td t-esc="env.pos.format_currency(pm.amount)"/>
<t t-set="_showDiff" t-value="_getShowDiff(pm)" />
<td t-if="_showDiff" t-on-input="() => this.handleInputChange(pm.id)"><input class="pos-input" type="number" t-model.number="state.payments[pm.id].counted"/></td>
<td t-if="_showDiff" t-esc="env.pos.format_currency(state.payments[pm.id].difference)" t-att-class="{'warning': state.payments[pm.id].difference}"/>
<td t-esc="defaultCashDetails.name"/>
<td t-esc="env.pos.format_currency(defaultCashDetails.amount)"/>
<td class="flex" t-on-input="() => this.handleInputChange(defaultCashDetails.id)">
<input class="pos-input" type="number" t-model.number="state.payments[defaultCashDetails.id].counted"/>
<div class="button icon" t-on-click="openDetailsPopup">
<i class="fa fa-calculator" role="img" title="Open the money details popup"/>
</div>
</td>
<td t-esc="env.pos.format_currency(state.payments[defaultCashDetails.id].difference)" t-att-class="{'warning': state.payments[defaultCashDetails.id].difference}"/>
</tr>
</tbody>
</table>
<tbody class="cash-overview">
<tr>
<td>Opening</td>
<td t-esc="env.pos.format_currency(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 ? '-' : '+'"/>
<t t-esc="move.name"/>
</div>
</td>
<td t-esc="env.pos.format_currency(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 ? '-' : '+'"/>
Payments in <t t-esc="defaultCashDetails.name"/>
</div>
</td>
<td t-esc="env.pos.format_currency(Math.abs(defaultCashDetails.payment_amount))"/>
</tr>
</tbody>
</t>
<tbody t-if="otherPaymentMethods.length &gt; 0">
<tr t-foreach="otherPaymentMethods" t-as="pm" t-key="pm.id">
<td t-esc="pm.name"/>
<td t-esc="env.pos.format_currency(pm.amount)"/>
<t t-set="_showDiff" t-value="_getShowDiff(pm)" />
<td t-if="_showDiff" t-on-input="() => this.handleInputChange(pm.id)"><input class="pos-input" type="number" t-model.number="state.payments[pm.id].counted"/></td>
<td t-if="_showDiff" t-esc="env.pos.format_currency(state.payments[pm.id].difference)" t-att-class="{'warning': state.payments[pm.id].difference}"/>
</tr>
</tbody>
</table>
</div>
<div class="notes-container">
<div class="opening-notes" t-if="openingNotes" >
<t t-esc="openingNotes"/>
</div>
<div class="notes-container">
<div class="opening-notes" t-if="openingNotes" >
<t t-esc="openingNotes"/>
</div>
<textarea class="closing-notes" placeholder="Add a closing note..." t-model="state.notes"/>
</div>
</main>
<footer class="footer">
<div class="button highlight" t-on-click="confirm">Close Session</div>
<div class="button" t-on-click="closePos" title="Visit the Backend but keep session open">Backend</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">
<i class="fa fa-download" role="img"/>
</div>
<!-- Print Sale Details -->
<div t-if="env.proxy.printer" class="small button icon">
<SaleDetailsButton/>
</div>
</footer>
</div>
<MoneyDetailsPopup
t-if="state.displayMoneyDetailsPopup"
manualInputCashCount="manualInputCashCount"
onConfirm.bind="updateCountedCash"
onDiscard.bind="closeDetailsPopup"
/>
<textarea class="closing-notes" placeholder="Add a closing note..." t-model="state.notes"/>
</div>
</main>
<footer class="footer">
<div class="button highlight" t-on-click="confirm">Close Session</div>
<div class="button" t-on-click="closePos" title="Visit the Backend but keep session open">Backend</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">
<i class="fa fa-download" role="img"/>
</div>
<!-- Print Sale Details -->
<div t-if="env.proxy.printer" class="small button icon">
<SaleDetailsButton/>
</div>
</footer>
</div>
</t>
</templates>
@@ -3,10 +3,10 @@
<t t-name="EditListInput" owl="1">
<div>
<input type="text" t-model="props.item.text" class="popup-input list-line-input"
<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 class="oe_link_icon fa fa-trash-o" role="img" aria-label="Remove" title="Remove"
t-on-click="() => this.trigger('remove-item', props.item)"></i>
<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>
</t>
@@ -2,25 +2,28 @@
<templates id="template" xml:space="preserve">
<t t-name="EditListPopup" owl="1">
<div class="popup popup-text">
<header class="title">
<t t-esc="props.title" />
</header>
<main class="list-lines" t-on-remove-item="removeItem"
t-on-create-new-item="createNewItem">
<div class="popup popup-text edit-list-popup">
<header class="title">
<t t-esc="props.title" />
<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" />
<EditListInput item="item" createNewItem.bind="createNewItem" removeItem="() => this.removeItem(item._id)"
deletable="_hasMoreThanOneItem()" onInputChange.bind="onInputChange" />
</t>
</main>
<footer class="footer">
<div class="button confirm highlight" t-on-click="confirm">
Ok
</div>
<div class="button cancel" t-on-click="cancel">
Cancel
</div>
</footer>
</div>
</div>
</main>
<footer class="footer footer-flex">
<div class="button confirm highlight" t-on-click="confirm">
Ok
</div>
<div class="button cancel" t-on-click="cancel">
Cancel
</div>
</footer>
</div>
</t>
</templates>
@@ -21,9 +21,9 @@
<CurrencyAmount currency="currency" amount="env.pos.format_currency_no_symbol(state.total)"/>
</div>
</main>
<footer class="footer">
<div class="button" t-on-click="discard">Discard</div>
<div class="button" t-on-click="confirm">Confirm</div>
<footer class="footer footer-flex">
<div class="button highlight" t-on-click="confirm">Confirm</div>
<div class="button" t-on-click="cancel">Discard</div>
</footer>
</div>
</t>
@@ -48,12 +48,12 @@
<br />
</div>
<footer class="footer centered">
<div class="button cancel" t-on-mousedown.prevent="cancel">
<t t-esc="props.cancelText" />
</div>
<div class="button confirm highlight" t-on-mousedown.prevent="confirm">
<t t-esc="props.confirmText" />
</div>
<div class="button cancel" t-on-mousedown.prevent="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
</div>
</Draggable>
@@ -16,7 +16,7 @@
</div>
</main>
<footer class="footer">
<footer class="footer footer-flex">
<div class="button highlight confirm" t-on-click="confirm">
Add
</div>
@@ -34,21 +34,16 @@
<div class="attribute-name-cell">
<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"/>
<label t-attf-for="{{ attribute.id }}_{{ value.id }}">
<t t-esc="value.name"/>
</label>
<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">
<label t-attf-for="{{ attribute.id }}_{{ value.id }}">
<span class="price_extra">
+ <t t-esc="env.pos.format_currency(value.price_extra)"/>
</span>
</label>
<span class="price_extra">
+ <t t-esc="env.pos.format_currency(value.price_extra)"/>
</span>
</div>
<div t-if="value.id == state.selected_value &amp;&amp; value.is_custom" class="custom-value-cell">
<label t-attf-for="{{ attribute.id }}_{{ value.id }}">
<input class="custom_value" type="text" t-model="state.custom_value"/>
</label>
<input class="custom_value" type="text" t-model="state.custom_value"/>
</div>
</t>
</div>
@@ -2,24 +2,22 @@
<templates id="template" xml:space="preserve">
<t t-name="TextAreaPopup" owl="1">
<Draggable>
<div class="popup popup-textarea">
<header class="title drag-handle">
<t t-esc="props.title" />
</header>
<div class="popup-textarea-wrap">
<textarea t-model="state.inputValue" t-ref="input"></textarea>
<Draggable>
<div class="popup popup-textarea">
<header class="title drag-handle">
<t t-esc="props.title" />
</header>
<textarea t-model="state.inputValue" t-ref="input"></textarea>
<footer class="footer footer-flex">
<div class="button confirm highlight" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<footer class="footer">
<div class="button confirm highlight" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
<div class="button cancel" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
</div>
</Draggable>
<div class="button cancel" t-on-click="cancel">
<t t-esc="props.cancelText" />
</div>
</footer>
</div>
</Draggable>
</t>
</templates>
@@ -6,13 +6,11 @@
<header class="title">
<t t-esc="props.title" />
</header>
<div class="body">
<p>
<t t-esc="props.body" />
</p>
<main>
<p t-esc="props.body" />
<input type="text" t-model="state.inputValue" t-ref="input" t-att-placeholder="props.placeholder"/>
</div>
<div class="footer">
</main>
<div class="footer footer-flex">
<div class="button confirm highlight" t-on-click="confirm">
<t t-esc="props.confirmText" />
</div>
@@ -21,13 +21,13 @@
</td>
<td class="partner-line-email" t-if="!env.isMobile">
<div t-if="props.partner.phone">
<i class="fa fa-phone fa-fw"/><t t-esc="props.partner.phone"/>
<i class="fa fa-fw fa-phone"/><t t-esc="props.partner.phone"/>
</div>
<div t-if="props.partner.mobile">
<i class="fa fa-mobile fa-lg fa-fw"/><t t-esc="props.partner.mobile"/>
<i class="fa fa-fw fa-mobile fa-lg"/><t t-esc="props.partner.mobile"/>
</div>
<div t-if="props.partner.email" class="email-field">
<i class="fa fa-paper-plane-o fa-fw"/><t t-esc="props.partner.email" />
<i class="fa fa-fw fa-paper-plane-o"/><t t-esc="props.partner.email" />
</div>
</td>
<td t-if="env.isMobile">
@@ -35,7 +35,7 @@
<span t-if="highlight"><br/></span>
</td>
<td class="partner-line-balance" t-if="props.isBalanceDisplayed"></td>
<td>
<td class="edit-partner-button-cell">
<button class="edit-partner-button" t-on-click.stop="() => props.onClickEdit(props.partner)">DETAILS</button>
</td>
<td class="partner-line-last-column-placeholder oe_invisible"></td>
@@ -11,8 +11,8 @@
<t t-if="!env.isMobile || props.mobileSearchBarIsShown">
<div class="pos-search-bar">
<i class="fa fa-search" t-on-click="_onPressEnterKey"/>
<input t-ref="search-word-input-product" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i class="fa fa-times search-clear-partner" t-on-click="_clearSearch"/>
<input t-model="state.searchInput" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="state.searchInput" class="fa fa-times" t-on-click="_clearSearch"/>
</div>
</t>
</div>
@@ -8,7 +8,7 @@ odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (requi
return [
{
content: `picking radio attribute with name ${name}`,
trigger: `.product-configurator-popup .attribute-name-cell label:contains('${name}')`,
trigger: `.product-configurator-popup .attribute-name-cell label[name='${name}']`,
},
];
}
@@ -165,6 +165,22 @@
</field>
</record>
<record id="action_pos_config_tree" model="ir.actions.act_window">
<field name="name">Point of Sale List</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos.config</field>
<field name="view_mode">tree,form</field>
<field name="domain"></field>
<field name="search_view_id" ref="view_pos_config_search" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new PoS
</p><p>
Configure at least one Point of Sale.
</p>
</field>
</record>
<!-- Products sub Category -->
<menuitem id="menu_products_pos_category"
action="point_of_sale.product_pos_category_action"
@@ -175,4 +191,7 @@
parent="point_of_sale.pos_menu_products_configuration" groups="product.group_product_variant" sequence="2"/>
<menuitem id="menu_pos_dashboard" action="action_pos_config_kanban" parent="menu_point_root" name="Dashboard" sequence="1"/>
<menuitem id="menu_point_of_sale_list" name="Point of Sales" parent="menu_point_config_product"
action="action_pos_config_tree" sequence="10"/>
</odoo>
@@ -20,6 +20,14 @@ odoo.define('pos_hr.CashierName', function (require) {
}
return super.avatar;
}
//@Override
get cssClass() {
if (this.env.pos.config.module_pos_hr) {
return {'oe_status': true};
}
return super.cssClass;
}
};
Registries.Component.extend(CashierName, PosHrCashierName);
@@ -10,8 +10,10 @@ export class ResetProgramsButton extends PosComponent {
super.setup();
useListener('click', this.onClick);
}
async onClick() {
_isDisabled() {
return !this.env.pos.get_order().isProgramsResettable();
}
onClick() {
this.env.pos.get_order()._resetPrograms();
}
}
@@ -41,6 +41,8 @@ export class RewardButton extends PosComponent {
return discountRewards.concat(this._mergeFreeProductRewards(freeProductRewards, potentialFreeProductRewards));
}
_isDisabled() {}
hasClaimableRewards() {
return this._getPotentialRewards().length > 0;
}
@@ -94,13 +96,7 @@ export class RewardButton extends PosComponent {
async onClick() {
const rewards = this._getPotentialRewards();
if (rewards.length === 0) {
await this.showPopup('ErrorPopup', {
title: this.env._t('No rewards available.'),
body: this.env._t('There are no rewards claimable for this customer.')
});
return false;
} else if (rewards.length === 1) {
if (rewards.length === 1) {
return this._applyReward(rewards[0].reward, rewards[0].coupon_id, rewards[0].potentialQty);
} else {
const rewardsList = rewards.map((reward) => ({
@@ -1445,5 +1445,10 @@ const PosLoyaltyOrder = (Order) => class PosLoyaltyOrder extends Order {
Gui.showNotification(res);
}
}
isProgramsResettable() {
const array = [this.disabledRewards, this.codeActivatedProgramRules, this.codeActivatedCoupons,
this.couponPointChanges, this._get_reward_lines()];
return array.some(elem => elem.length > 0);
}
}
Registries.Model.extend(Order, PosLoyaltyOrder);
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="ResetProgramsButton" owl="1">
<span class="control-button">
<span class="control-button" t-att-class="{'disabled': _isDisabled()}">
<i class="fa fa-star"></i>
<span> </span>
<span>Reset Programs</span>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="RewardButton" owl="1">
<span class="control-button" t-att-class="hasClaimableRewards() ? 'highlight' : ''">
<span class="control-button" t-att-class="hasClaimableRewards() ? 'highlight' : 'disabled'">
<i class="fa fa-star"></i>
<span> </span>
<span>Reward</span>
+16 -12
View File
@@ -33,7 +33,7 @@ odoo.define('pos_restaurant.chrome', function (require) {
get startScreen() {
if (this.env.pos.config.iface_floorplan) {
const table = this.env.pos.table;
return { name: 'FloorScreen', props: { floor: table ? table.floor : null } };
return {name: 'FloorScreen', props: {floor: table ? table.floor : null}};
} else {
return super.startScreen;
}
@@ -52,18 +52,22 @@ odoo.define('pos_restaurant.chrome', function (require) {
}, 60000);
}
}
_actionAfterIdle() {
if (this.tempScreen.isShown) {
this.trigger('close-temp-screen');
async _actionAfterIdle() {
const isPopupClosed = await new Promise(resolve => this.env.posbus
.trigger('close-popups-but-error', {resolve}));
if (isPopupClosed) {
if (this.tempScreen.isShown) {
this.trigger('close-temp-screen');
}
const table = this.env.pos.table;
const order = this.env.pos.get_order();
if (order && order.get_screen_data().name === 'ReceiptScreen') {
// When the order is finalized, we can safely remove it from the memory
// We check that it's in ReceiptScreen because we want to keep the order if it's in a tipping state
this.env.pos.removeOrder(order);
}
this.showScreen('FloorScreen', {floor: table ? table.floor : null});
}
const table = this.env.pos.table;
const order = this.env.pos.get_order();
if (order && order.get_screen_data().name === 'ReceiptScreen') {
// When the order is finalized, we can safely remove it from the memory
// We check that it's in ReceiptScreen because we want to keep the order if it's in a tipping state
this.env.pos.removeOrder(order);
}
this.showScreen('FloorScreen', { floor: table ? table.floor : null });
}
_shouldResetIdleTimer() {
const stayPaymentScreen = this.mainScreen.name === 'PaymentScreen' && this.env.pos.get_order().paymentlines.length > 0;
@@ -0,0 +1,25 @@
/* @odoo-module alias=pos_restaurant.PosPopupController */
import PosPopupController from 'point_of_sale.PosPopupController';
import Registries from 'point_of_sale.Registries';
import { useBus } from '@web/core/utils/hooks';
export const PosResPopupController = (PosPopupController) => class extends PosPopupController {
setup() {
super.setup();
useBus(this.env.posbus, 'close-popups-but-error', this._closePopupsButError);
}
_closePopupsButError(event) {
const { resolve } = event.detail;
const isErrorPopupOpen = this.popups.some(popup => popup.name.toLowerCase().includes('error'));
if (!isErrorPopupOpen) {
for (const popup of this.popups) {
popup.props.resolve(false);
}
this.popups.length = 0; // clearing the array but keep the useState
}
resolve(!isErrorPopupOpen);
}
};
Registries.Component.extend(PosPopupController, PosResPopupController);
@@ -11,16 +11,12 @@ odoo.define('pos_restaurant.PrintBillButton', function(require) {
super.setup();
useListener('click', this.onClick);
}
async onClick() {
_isDisabled() {
const order = this.env.pos.get_order();
if (order.get_orderlines().length > 0) {
await this.showTempScreen('BillScreen');
} else {
await this.showPopup('ErrorPopup', {
title: this.env._t('Nothing to Print'),
body: this.env._t('There are no order lines'),
});
}
return order.get_orderlines().length === 0;
}
onClick() {
this.showTempScreen('BillScreen');
}
}
PrintBillButton.template = 'PrintBillButton';
@@ -11,11 +11,12 @@ odoo.define('pos_restaurant.SplitBillButton', function(require) {
super.setup();
useListener('click', this.onClick);
}
async onClick() {
_isDisabled() {
const order = this.env.pos.get_order();
if (order.get_orderlines().length > 0) {
this.showScreen('SplitBillScreen');
}
return order.get_orderlines().reduce((totalProduct, orderline) => totalProduct + orderline.quantity, 0) < 2;
}
async onClick() {
this.showScreen('SplitBillScreen');
}
}
SplitBillButton.template = 'SplitBillButton';
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="PrintBillButton" owl="1">
<span class="control-button order-printbill">
<span class="control-button order-printbill" t-att-class="{'disabled': _isDisabled()}">
<i class="fa fa-print"></i>
<span> </span>
<span>Bill</span>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="SplitBillButton" owl="1">
<span class="control-button order-split">
<span class="control-button order-split" t-att-class="{'disabled': _isDisabled()}">
<i class="fa fa-files-o"></i>
<span> </span>
<span>Split</span>