[REF] point_of_sale, *: convert legacy modules to ES odoo modules

*: l10n_co_pos, l10n_fr_pos_cert, l10n_gcc_pos, l10n_in_pos,
l10n_sa_pos, pos_adyen, pos_discount, pos_epson_printer,
pos_epson_printer_restaurant, pos_hr, pos_loyalty, pos_mercury,
pos_restaurant, pos_restaurant_adyen, pos_restaurant_stripe, pos_sale,
pos_sale_product_configurator, pos_six, pos_stripe

We are about to refactor most of the Javasript code base of the point of
sale and related modules. In doing so, we will move a lot of files and
modernize the entire code-base. In order to avoid diffcult rebases,
their conversion to odoo-modules is done as a first step to avoid
getting lots of conflicts on files that were unindented, which marks the
entire file as being in conflict. Conflicts will occur during forward
ports but they will be easier to manage as the changes will be much
smaller in scope, and the author will have the context of the change
that causes the conflict in mind when dealing with it.

closes odoo/odoo#107621

Related: odoo/enterprise#34910
Signed-off-by: Trinh Jacky (trj) <trj@odoo.com>
This commit is contained in:
Samuel Degueldre
2022-12-14 11:41:52 +01:00
parent c321f825f9
commit 990b10aece
229 changed files with 19802 additions and 18628 deletions
@@ -1,31 +1,30 @@
odoo.define('l10n_co_pos.PaymentScreen', function(require) {
'use strict';
/** @odoo-module */
const PaymentScreen = require('point_of_sale.PaymentScreen');
const Registries = require('point_of_sale.Registries');
const session = require('web.session');
import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen";
import Registries from "@point_of_sale/js/Registries";
import session from "web.session";
const L10nCoPosPaymentScreen = PaymentScreen =>
class extends PaymentScreen {
async _postPushOrderResolve(order, order_server_ids) {
try {
if (this.env.pos.is_colombian_country()) {
const result = await this.rpc({
model: 'pos.order',
method: 'search_read',
domain: [['id', 'in', order_server_ids]],
fields: ['name'],
context: session.user_context,
});
order.set_l10n_co_dian(result[0].name || false);
}
} finally {
return super._postPushOrderResolve(...arguments);
const L10nCoPosPaymentScreen = (PaymentScreen) =>
class extends PaymentScreen {
async _postPushOrderResolve(order, order_server_ids) {
try {
if (this.env.pos.is_colombian_country()) {
const result = await this.rpc({
model: "pos.order",
method: "search_read",
domain: [["id", "in", order_server_ids]],
fields: ["name"],
context: session.user_context,
});
order.set_l10n_co_dian(result[0].name || false);
}
} catch {
// FIXME this doesn't seem correct but is equivalent to return in finally which we had before.
}
};
return super._postPushOrderResolve(...arguments);
}
};
Registries.Component.extend(PaymentScreen, L10nCoPosPaymentScreen);
Registries.Component.extend(PaymentScreen, L10nCoPosPaymentScreen);
return PaymentScreen;
});
export default PaymentScreen;
+28 -29
View File
@@ -1,34 +1,33 @@
odoo.define('l10n_co_pos.pos', function (require) {
"use strict";
/** @odoo-module */
var { PosGlobalState, Order } = require('point_of_sale.models');
const Registries = require('point_of_sale.Registries');
import { PosGlobalState, Order } from "@point_of_sale/js/models";
import Registries from "@point_of_sale/js/Registries";
const L10nCoPosGlobalState = (PosGlobalState) => class L10nCoPosGlobalState extends PosGlobalState {
is_colombian_country() {
return this.company.country.code === 'CO';
}
}
const L10nCoPosGlobalState = (PosGlobalState) =>
class L10nCoPosGlobalState extends PosGlobalState {
is_colombian_country() {
return this.company.country.code === "CO";
}
};
Registries.Model.extend(PosGlobalState, L10nCoPosGlobalState);
const L10nCoPosOrder = (Order) => class L10nCoPosOrder extends Order {
export_for_printing() {
var result = super.export_for_printing(...arguments);
result.l10n_co_dian = this.get_l10n_co_dian();
return result;
}
set_l10n_co_dian(l10n_co_dian) {
this.l10n_co_dian = l10n_co_dian;
}
get_l10n_co_dian() {
return this.l10n_co_dian;
}
wait_for_push_order() {
var result = super.wait_for_push_order(...arguments);
result = Boolean(result || this.pos.is_colombian_country());
return result;
}
}
const L10nCoPosOrder = (Order) =>
class L10nCoPosOrder extends Order {
export_for_printing() {
var result = super.export_for_printing(...arguments);
result.l10n_co_dian = this.get_l10n_co_dian();
return result;
}
set_l10n_co_dian(l10n_co_dian) {
this.l10n_co_dian = l10n_co_dian;
}
get_l10n_co_dian() {
return this.l10n_co_dian;
}
wait_for_push_order() {
var result = super.wait_for_push_order(...arguments);
result = Boolean(result || this.pos.is_colombian_country());
return result;
}
};
Registries.Model.extend(Order, L10nCoPosOrder);
});
+39 -34
View File
@@ -1,42 +1,47 @@
odoo.define('l10n_fr_pos_cert.Chrome', function (require) {
'use strict';
/** @odoo-module */
const Chrome = require('point_of_sale.Chrome');
const Registries = require('point_of_sale.Registries');
const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service')
const { identifyError } = require('point_of_sale.utils');
import Chrome from "@point_of_sale/js/Chrome";
import Registries from "@point_of_sale/js/Registries";
import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service";
import { identifyError } from "@point_of_sale/js/utils";
const PosFrCertChrome = (Chrome) =>
class extends Chrome {
async start() {
await super.start();
if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) {
const now = Date.now();
let limitDate = new Date(this.env.pos.pos_session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
if (limitDate < now) {
try {
const info = await this.env.pos.getClosePosInfo();
this.showPopup('ClosePosPopup', { info: info });
} catch (e) {
if (identifyError(e) instanceof ConnectionLostError||ConnectionAbortedError) {
this.showPopup('OfflineErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Please check your internet connection and try again.'),
});
} else {
this.showPopup('ErrorPopup', {
title: this.env._t('Unknown Error'),
body: this.env._t('An unknown error prevents us from getting closing information.'),
});
}
const PosFrCertChrome = (Chrome) =>
class extends Chrome {
async start() {
await super.start();
if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) {
const now = Date.now();
const limitDate = new Date(this.env.pos.pos_session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
if (limitDate < now) {
try {
const info = await this.env.pos.getClosePosInfo();
this.showPopup("ClosePosPopup", { info: info });
} catch (e) {
if (
identifyError(e) instanceof ConnectionLostError ||
ConnectionAbortedError
) {
this.showPopup("OfflineErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t(
"Please check your internet connection and try again."
),
});
} else {
this.showPopup("ErrorPopup", {
title: this.env._t("Unknown Error"),
body: this.env._t(
"An unknown error prevents us from getting closing information."
),
});
}
}
}
}
};
}
};
Registries.Component.extend(Chrome, PosFrCertChrome);
Registries.Component.extend(Chrome, PosFrCertChrome);
return Chrome;
});
export default Chrome;
@@ -1,29 +1,30 @@
odoo.define('l10n_fr_pos_cert.PaymentScreen', function(require) {
/** @odoo-module */
const PaymentScreen = require('point_of_sale.PaymentScreen');
const Registries = require('point_of_sale.Registries');
const session = require('web.session');
import PaymentScreen from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen";
import Registries from "@point_of_sale/js/Registries";
import session from "web.session";
const PosFrPaymentScreen = PaymentScreen => class extends PaymentScreen {
const PosFrPaymentScreen = (PaymentScreen) =>
class extends PaymentScreen {
async _postPushOrderResolve(order, order_server_ids) {
try {
if(this.env.pos.is_french_country()) {
let result = await this.rpc({
model: 'pos.order',
method: 'search_read',
domain: [['id', 'in', order_server_ids]],
fields: ['l10n_fr_hash'],
if (this.env.pos.is_french_country()) {
const result = await this.rpc({
model: "pos.order",
method: "search_read",
domain: [["id", "in", order_server_ids]],
fields: ["l10n_fr_hash"],
context: session.user_context,
});
order.set_l10n_fr_hash(result[0].l10n_fr_hash || false);
}
} finally {
return super._postPushOrderResolve(...arguments);
} catch {
// FIXME this doesn't seem correct but is equivalent to return in finally which we had before.
}
return super._postPushOrderResolve(...arguments);
}
};
Registries.Component.extend(PaymentScreen, PosFrPaymentScreen);
Registries.Component.extend(PaymentScreen, PosFrPaymentScreen);
return PaymentScreen;
});
export default PaymentScreen;
@@ -1,27 +1,25 @@
odoo.define('l10n_fr_pos_cert.ClosePosPopup', function (require) {
'use strict';
/** @odoo-module */
const ClosePosPopup = require('point_of_sale.ClosePosPopup');
const Registries = require('point_of_sale.Registries');
import ClosePosPopup from "@point_of_sale/js/Popups/ClosePosPopup";
import Registries from "@point_of_sale/js/Registries";
const PosFrCertClosePopup = (ClosePosPopup) =>
class extends ClosePosPopup {
sessionIsOutdated() {
let isOutdated = false;
if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) {
const now = Date.now();
let limitDate = new Date(this.env.pos.pos_session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
isOutdated = limitDate < now;
}
return isOutdated;
const PosFrCertClosePopup = (ClosePosPopup) =>
class extends ClosePosPopup {
sessionIsOutdated() {
let isOutdated = false;
if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) {
const now = Date.now();
const limitDate = new Date(this.env.pos.pos_session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
isOutdated = limitDate < now;
}
canCancel() {
return super.canCancel() && !this.sessionIsOutdated();
}
};
return isOutdated;
}
canCancel() {
return super.canCancel() && !this.sessionIsOutdated();
}
};
Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup);
Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup);
return ClosePosPopup;
});
export default ClosePosPopup;
+81 -72
View File
@@ -1,84 +1,93 @@
odoo.define('l10n_fr_pos_cert.pos', function (require) {
"use strict";
/** @odoo-module */
const { Gui } = require('point_of_sale.Gui');
var { PosGlobalState, Order, Orderline } = require('point_of_sale.models');
var core = require('web.core');
const Registries = require('point_of_sale.Registries');
import { Gui } from "@point_of_sale/js/Gui";
import { PosGlobalState, Order, Orderline } from "@point_of_sale/js/models";
import core from "web.core";
import Registries from "@point_of_sale/js/Registries";
var _t = core._t;
const L10nFrPosGlobalState = (PosGlobalState) => class L10nFrPosGlobalState extends PosGlobalState {
is_french_country(){
var french_countries = ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF'];
if (!this.company.country) {
Gui.showPopup("ErrorPopup", {
'title': _t("Missing Country"),
'body': _.str.sprintf(_t('The company %s doesn\'t have a country set.'), this.company.name),
});
return false;
}
return _.contains(french_countries, this.company.country.code);
}
disallowLineQuantityChange() {
let result = super.disallowLineQuantityChange(...arguments);
return this.is_french_country() || result;
}
}
const L10nFrPosGlobalState = (PosGlobalState) =>
class L10nFrPosGlobalState extends PosGlobalState {
is_french_country() {
var french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"];
if (!this.company.country) {
Gui.showPopup("ErrorPopup", {
title: _t("Missing Country"),
body: _.str.sprintf(
_t("The company %s doesn't have a country set."),
this.company.name
),
});
return false;
}
return _.contains(french_countries, this.company.country.code);
}
disallowLineQuantityChange() {
const result = super.disallowLineQuantityChange(...arguments);
return this.is_french_country() || result;
}
};
Registries.Model.extend(PosGlobalState, L10nFrPosGlobalState);
const L10nFrOrder = (Order) => class L10nFrOrder extends Order {
constructor() {
super(...arguments);
this.l10n_fr_hash = this.l10n_fr_hash || false;
this.save_to_db();
}
export_for_printing() {
var result = super.export_for_printing(...arguments);
result.l10n_fr_hash = this.get_l10n_fr_hash();
return result;
}
set_l10n_fr_hash (l10n_fr_hash){
this.l10n_fr_hash = l10n_fr_hash;
}
get_l10n_fr_hash() {
return this.l10n_fr_hash;
}
wait_for_push_order() {
var result = super.wait_for_push_order(...arguments);
result = Boolean(result || this.pos.is_french_country());
return result;
}
destroy (option) {
// SUGGESTION: It's probably more appropriate to apply this restriction
// in the TicketScreen.
if (option && option.reason == 'abandon' && this.pos.is_french_country() && this.get_orderlines().length) {
Gui.showPopup("ErrorPopup", {
'title': _t("Fiscal Data Module error"),
'body': _t("Deleting of orders is not allowed."),
});
} else {
super.destroy(...arguments);
const L10nFrOrder = (Order) =>
class L10nFrOrder extends Order {
constructor() {
super(...arguments);
this.l10n_fr_hash = this.l10n_fr_hash || false;
this.save_to_db();
}
}
}
export_for_printing() {
var result = super.export_for_printing(...arguments);
result.l10n_fr_hash = this.get_l10n_fr_hash();
return result;
}
set_l10n_fr_hash(l10n_fr_hash) {
this.l10n_fr_hash = l10n_fr_hash;
}
get_l10n_fr_hash() {
return this.l10n_fr_hash;
}
wait_for_push_order() {
var result = super.wait_for_push_order(...arguments);
result = Boolean(result || this.pos.is_french_country());
return result;
}
destroy(option) {
// SUGGESTION: It's probably more appropriate to apply this restriction
// in the TicketScreen.
if (
option &&
option.reason == "abandon" &&
this.pos.is_french_country() &&
this.get_orderlines().length
) {
Gui.showPopup("ErrorPopup", {
title: _t("Fiscal Data Module error"),
body: _t("Deleting of orders is not allowed."),
});
} else {
super.destroy(...arguments);
}
}
};
Registries.Model.extend(Order, L10nFrOrder);
const L10nFrOrderline = (Orderline) =>
class L10nFrOrderline extends Orderline {
can_be_merged_with(orderline) {
const order = this.pos.get_order();
const orderlines = order.orderlines;
const lastOrderline = order.orderlines.at(orderlines.length - 1);
const L10nFrOrderline = (Orderline) => class L10nFrOrderline extends Orderline {
can_be_merged_with(orderline) {
let order = this.pos.get_order();
let orderlines = order.orderlines;
let lastOrderline = order.orderlines.at(orderlines.length - 1);
if(this.pos.is_french_country() && (lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0)) {
return false;
} else {
return super.can_be_merged_with(...arguments);
if (
this.pos.is_french_country() &&
(lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0)
) {
return false;
} else {
return super.can_be_merged_with(...arguments);
}
}
}
}
};
Registries.Model.extend(Orderline, L10nFrOrderline);
});
@@ -1,20 +1,19 @@
odoo.define('l10n_gcc_pos.OrderReceipt', function (require) {
'use strict';
/** @odoo-module */
const OrderReceipt = require('point_of_sale.OrderReceipt')
const Registries = require('point_of_sale.Registries');
import OrderReceipt from "@point_of_sale/js/Screens/ReceiptScreen/OrderReceipt";
import Registries from "@point_of_sale/js/Registries";
const OrderReceiptGCC = OrderReceipt =>
class extends OrderReceipt {
get receiptEnv() {
let receipt_render_env = super.receiptEnv;
let receipt = receipt_render_env.receipt;
const country = receipt_render_env.order.pos.company.country;
receipt.is_gcc_country = ['SA', 'AE', 'BH', 'OM', 'QA', 'KW'].includes(country && country.code);
return receipt_render_env;
}
const OrderReceiptGCC = (OrderReceipt) =>
class extends OrderReceipt {
get receiptEnv() {
const receipt_render_env = super.receiptEnv;
const receipt = receipt_render_env.receipt;
const country = receipt_render_env.order.pos.company.country;
receipt.is_gcc_country = ["SA", "AE", "BH", "OM", "QA", "KW"].includes(
country && country.code
);
return receipt_render_env;
}
Registries.Component.extend(OrderReceipt, OrderReceiptGCC)
return OrderReceiptGCC
});
};
Registries.Component.extend(OrderReceipt, OrderReceiptGCC);
export default OrderReceiptGCC;
+11 -14
View File
@@ -1,17 +1,14 @@
odoo.define('l10n_in_pos.receipt', function (require) {
"use strict";
/** @odoo-module */
var { Orderline } = require('point_of_sale.models');
const Registries = require('point_of_sale.Registries');
import { Orderline } from "@point_of_sale/js/models";
import Registries from "@point_of_sale/js/Registries";
const L10nInOrderline = (Orderline) => class L10nInOrderline extends Orderline {
export_for_printing() {
var line = super.export_for_printing(...arguments);
line.l10n_in_hsn_code = this.get_product().l10n_in_hsn_code;
return line;
}
}
const L10nInOrderline = (Orderline) =>
class L10nInOrderline extends Orderline {
export_for_printing() {
var line = super.export_for_printing(...arguments);
line.l10n_in_hsn_code = this.get_product().l10n_in_hsn_code;
return line;
}
};
Registries.Model.extend(Orderline, L10nInOrderline);
});
+67 -52
View File
@@ -1,64 +1,79 @@
odoo.define('l10n_sa_pos.pos', function (require) {
"use strict";
/** @odoo-module */
var { Order } = require('point_of_sale.models');
var Registries = require('point_of_sale.Registries');
import { Order } from "@point_of_sale/js/models";
import Registries from "@point_of_sale/js/Registries";
const PosL10nSAOrder = (Order) => class PosL10nSAOrder extends Order {
export_for_printing() {
var result = super.export_for_printing(...arguments);
if (this.pos.company.country.code === 'SA') {
result.is_settlement = this.is_settlement();
if (!result.is_settlement) {
const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter()
let qr_values = this.compute_sa_qr_code(result.company.name, result.company.vat, result.date.isostring, result.total_with_tax, result.total_tax);
let qr_code_svg = new XMLSerializer().serializeToString(codeWriter.write(qr_values, 150, 150));
result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg);
const PosL10nSAOrder = (Order) =>
class PosL10nSAOrder extends Order {
export_for_printing() {
var result = super.export_for_printing(...arguments);
if (this.pos.company.country.code === "SA") {
result.is_settlement = this.is_settlement();
if (!result.is_settlement) {
const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter();
const qr_values = this.compute_sa_qr_code(
result.company.name,
result.company.vat,
result.date.isostring,
result.total_with_tax,
result.total_tax
);
const qr_code_svg = new XMLSerializer().serializeToString(
codeWriter.write(qr_values, 150, 150)
);
result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg);
}
}
return result;
}
/**
* If the order is empty (there are no products)
* and all "pay_later" payments are negative,
* we are settling a customer's account.
* If the module pos_settle_due is not installed,
* the function always returns false (since "pay_later" doesn't exist)
* @returns {boolean} true if the current order is a settlement, else false
*/
is_settlement() {
return (
this.is_empty() &&
!!this.paymentlines.filter(
(paymentline) =>
paymentline.payment_method.type === "pay_later" && paymentline.amount < 0
).length
);
}
return result;
}
/**
* If the order is empty (there are no products)
* and all "pay_later" payments are negative,
* we are settling a customer's account.
* If the module pos_settle_due is not installed,
* the function always returns false (since "pay_later" doesn't exist)
* @returns {boolean} true if the current order is a settlement, else false
*/
is_settlement() {
return this.is_empty() &&
!!this.paymentlines.filter(paymentline => paymentline.payment_method.type === "pay_later" && paymentline.amount < 0).length;
}
compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) {
/* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23
compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) {
/* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23
https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf
*/
const seller_name_enc = this._compute_qr_code_field(1, name);
const company_vat_enc = this._compute_qr_code_field(2, vat);
const timestamp_enc = this._compute_qr_code_field(3, date_isostring);
const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString());
const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString());
const seller_name_enc = this._compute_qr_code_field(1, name);
const company_vat_enc = this._compute_qr_code_field(2, vat);
const timestamp_enc = this._compute_qr_code_field(3, date_isostring);
const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString());
const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString());
const str_to_encode = seller_name_enc.concat(company_vat_enc, timestamp_enc, invoice_total_enc, total_vat_enc);
const str_to_encode = seller_name_enc.concat(
company_vat_enc,
timestamp_enc,
invoice_total_enc,
total_vat_enc
);
let binary = '';
for (let i = 0; i < str_to_encode.length; i++) {
binary += String.fromCharCode(str_to_encode[i]);
let binary = "";
for (let i = 0; i < str_to_encode.length; i++) {
binary += String.fromCharCode(str_to_encode[i]);
}
return btoa(binary);
}
return btoa(binary);
}
_compute_qr_code_field(tag, field) {
const textEncoder = new TextEncoder();
const name_byte_array = Array.from(textEncoder.encode(field));
const name_tag_encoding = [tag];
const name_length_encoding = [name_byte_array.length];
return name_tag_encoding.concat(name_length_encoding, name_byte_array);
}
}
_compute_qr_code_field(tag, field) {
const textEncoder = new TextEncoder();
const name_byte_array = Array.from(textEncoder.encode(field));
const name_tag_encoding = [tag];
const name_length_encoding = [name_byte_array.length];
return name_tag_encoding.concat(name_length_encoding, name_byte_array);
}
};
Registries.Model.extend(Order, PosL10nSAOrder);
});
@@ -1,30 +1,40 @@
odoo.define('point_of_sale.tour', function(require) {
"use strict";
/** @odoo-module */
const {_t} = require('web.core');
const {Markup} = require('web.utils');
var tour = require('web_tour.tour');
import { _t } from "web.core";
import { Markup } from "web.utils";
import tour from "web_tour.tour";
tour.register('point_of_sale_tour', {
url: "/web",
rainbowMan: false,
sequence: 45,
}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
width: 215,
position: 'right',
edition: 'community'
}, {
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
width: 215,
position: 'bottom',
edition: 'enterprise'
}, {
trigger: ".o_pos_kanban button.oe_kanban_action_button",
content: Markup(_t("<p>Ready to have a look at the <b>POS Interface</b>? Let's start our first session.</p>")),
position: "bottom"
}]);
});
tour.register(
"point_of_sale_tour",
{
url: "/web",
rainbowMan: false,
sequence: 45,
},
[
tour.stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
width: 215,
position: "right",
edition: "community",
},
{
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
width: 215,
position: "bottom",
edition: "enterprise",
},
{
trigger: ".o_pos_kanban button.oe_kanban_action_button",
content: Markup(
_t(
"<p>Ready to have a look at the <b>POS Interface</b>? Let's start our first session.</p>"
)
),
position: "bottom",
},
]
);
@@ -2,15 +2,15 @@
import { useService } from "@web/core/utils/hooks";
import Chrome from "point_of_sale.Chrome";
import ProductScreen from "point_of_sale.ProductScreen";
import Registries from "point_of_sale.Registries";
import { PosGlobalState } from "point_of_sale.models";
import { configureGui } from "point_of_sale.Gui";
import Chrome from "@point_of_sale/js/Chrome";
import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import Registries from "@point_of_sale/js/Registries";
import { PosGlobalState } from "@point_of_sale/js/models";
import { configureGui } from "@point_of_sale/js/Gui";
import { registry } from "@web/core/registry";
import env from "point_of_sale.env";
import env from "@point_of_sale/js/pos_env";
import { debounce } from "@web/core/utils/timing";
import { batched } from "point_of_sale.utils";
import { batched } from "@point_of_sale/js/utils";
const { Component, reactive, markRaw, useExternalListener, useSubEnv, onWillUnmount, xml } = owl;
@@ -58,8 +58,8 @@ export class ChromeAdapter extends Component {
async configureAndStart(chrome) {
// Add the pos error handler when the chrome component is available.
registry.category('error_handlers').add(
'posErrorHandler',
registry.category("error_handlers").add(
"posErrorHandler",
(env, ...noEnvArgs) => {
if (chrome) {
return chrome.errorHandler(this.env, ...noEnvArgs);
@@ -3,7 +3,7 @@
import { startWebClient } from "@web/start";
import { ChromeAdapter } from "@point_of_sale/entry/chrome_adapter";
import Registries from "point_of_sale.Registries";
import Registries from "@point_of_sale/js/Registries";
import { registry } from "@web/core/registry";
const { Component, xml } = owl;
File diff suppressed because it is too large Load Diff
@@ -1,59 +1,62 @@
odoo.define('point_of_sale.CashMoveButton', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { _t } = require('web.core');
const { renderToString } = require('@web/core/utils/render');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { _t } from "web.core";
import { renderToString } from "@web/core/utils/render";
const TRANSLATED_CASH_MOVE_TYPE = {
in: _t('in'),
out: _t('out'),
};
const TRANSLATED_CASH_MOVE_TYPE = {
in: _t("in"),
out: _t("out"),
};
class CashMoveButton extends PosComponent {
async onClick() {
const { confirmed, payload } = await this.showPopup('CashMovePopup');
if (!confirmed) return;
const { type, amount, reason } = payload;
const translatedType = TRANSLATED_CASH_MOVE_TYPE[type];
const formattedAmount = this.env.pos.format_currency(amount);
if (!amount) {
return this.showNotification(
_.str.sprintf(this.env._t('Cash in/out of %s is ignored.'), formattedAmount),
3000
);
}
const extras = { formattedAmount, translatedType };
await this.rpc({
model: 'pos.session',
method: 'try_cash_in_out',
args: [[this.env.pos.pos_session.id], type, amount, reason, extras],
});
if (this.env.proxy.printer) {
const renderedReceipt = renderToString('point_of_sale.CashMoveReceipt', {
_receipt: this._getReceiptInfo({ ...payload, translatedType, formattedAmount }),
});
const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt);
if (!printResult.successful) {
this.showPopup('ErrorPopup', { title: printResult.message.title, body: printResult.message.body });
}
}
this.showNotification(
_.str.sprintf(this.env._t('Successfully made a cash %s of %s.'), type, formattedAmount),
class CashMoveButton extends PosComponent {
async onClick() {
const { confirmed, payload } = await this.showPopup("CashMovePopup");
if (!confirmed) {
return;
}
const { type, amount, reason } = payload;
const translatedType = TRANSLATED_CASH_MOVE_TYPE[type];
const formattedAmount = this.env.pos.format_currency(amount);
if (!amount) {
return this.showNotification(
_.str.sprintf(this.env._t("Cash in/out of %s is ignored."), formattedAmount),
3000
);
}
_getReceiptInfo(payload) {
const result = { ...payload };
result.cashier = this.env.pos.get_cashier();
result.company = this.env.pos.company;
return result;
const extras = { formattedAmount, translatedType };
await this.rpc({
model: "pos.session",
method: "try_cash_in_out",
args: [[this.env.pos.pos_session.id], type, amount, reason, extras],
});
if (this.env.proxy.printer) {
const renderedReceipt = renderToString("point_of_sale.CashMoveReceipt", {
_receipt: this._getReceiptInfo({ ...payload, translatedType, formattedAmount }),
});
const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt);
if (!printResult.successful) {
this.showPopup("ErrorPopup", {
title: printResult.message.title,
body: printResult.message.body,
});
}
}
this.showNotification(
_.str.sprintf(this.env._t("Successfully made a cash %s of %s."), type, formattedAmount),
3000
);
}
CashMoveButton.template = 'point_of_sale.CashMoveButton';
_getReceiptInfo(payload) {
const result = { ...payload };
result.cashier = this.env.pos.get_cashier();
result.company = this.env.pos.company;
return result;
}
}
CashMoveButton.template = "point_of_sale.CashMoveButton";
Registries.Component.add(CashMoveButton);
Registries.Component.add(CashMoveButton);
return CashMoveButton;
});
export default CashMoveButton;
@@ -1,27 +1,25 @@
odoo.define('point_of_sale.CashierName', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
// Previously UsernameWidget
class CashierName extends PosComponent {
get username() {
const { name } = this.env.pos.get_cashier();
return name ? name : '';
}
get avatar() {
const user_id = this.env.pos.get_cashier_user_id();
const id = user_id ? user_id : -1;
return `/web/image/res.users/${id}/avatar_128`;
}
get cssClass() {
return {'not-clickable': true};
}
// Previously UsernameWidget
class CashierName extends PosComponent {
get username() {
const { name } = this.env.pos.get_cashier();
return name ? name : "";
}
CashierName.template = 'CashierName';
get avatar() {
const user_id = this.env.pos.get_cashier_user_id();
const id = user_id ? user_id : -1;
return `/web/image/res.users/${id}/avatar_128`;
}
get cssClass() {
return { "not-clickable": true };
}
}
CashierName.template = "CashierName";
Registries.Component.add(CashierName);
Registries.Component.add(CashierName);
return CashierName;
});
export default CashierName;
@@ -1,106 +1,108 @@
odoo.define('point_of_sale.CustomerFacingDisplayButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { useState } = owl;
const { useState } = owl;
class CustomerFacingDisplayButton extends PosComponent {
setup() {
super.setup();
this.local = this.env.pos.config.iface_customer_facing_display_local && !this.env.pos.config.iface_customer_facing_display_via_proxy;
this.state = useState({ status: this.local ? 'success' : 'failure' });
this._start();
}
get message() {
return {
success: '',
warning: this.env._t('Connected, Not Owned'),
failure: this.env._t('Disconnected'),
not_found: this.env._t('Customer Screen Unsupported. Please upgrade the IoT Box'),
}[this.state.status];
}
onClick() {
if (this.local) {
return this.onClickLocal();
} else {
return this.onClickProxy();
}
}
async onClickLocal() {
this.env.pos.customer_display = window.open('', 'Customer Display', 'height=600,width=900');
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
var $renderedHtml = $('<div>').html(renderedHtml);
$(this.env.pos.customer_display.document.body).html($renderedHtml.find('.pos-customer_facing_display'));
$(this.env.pos.customer_display.document.head).html($renderedHtml.find('.resources').html());
}
async onClickProxy() {
try {
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
let ownership = await this.env.proxy.take_ownership_over_customer_screen(
renderedHtml
);
if (typeof ownership === 'string') {
ownership = JSON.parse(ownership);
}
if (ownership.status === 'success') {
this.state.status = 'success';
} else {
this.state.status = 'warning';
}
if (!this.env.proxy.posbox_supports_display) {
this.env.proxy.posbox_supports_display = true;
this._start();
}
} catch (error) {
if (typeof error == 'undefined') {
this.state.status = 'failure';
} else {
this.state.status = 'not_found';
}
}
}
_start() {
if (this.local) {
return;
}
const self = this;
async function loop() {
if (self.env.proxy.posbox_supports_display) {
try {
let ownership = await self.env.proxy.test_ownership_of_customer_screen();
if (typeof ownership === 'string') {
ownership = JSON.parse(ownership);
}
if (ownership.status === 'OWNER') {
self.state.status = 'success';
} else {
self.state.status = 'warning';
}
setTimeout(loop, 3000);
} catch (error) {
if (error.abort) {
// Stop the loop
return;
}
if (typeof error == 'undefined') {
self.state.status = 'failure';
} else {
self.state.status = 'not_found';
self.env.proxy.posbox_supports_display = false;
}
setTimeout(loop, 3000);
}
}
}
loop();
class CustomerFacingDisplayButton extends PosComponent {
setup() {
super.setup();
this.local =
this.env.pos.config.iface_customer_facing_display_local &&
!this.env.pos.config.iface_customer_facing_display_via_proxy;
this.state = useState({ status: this.local ? "success" : "failure" });
this._start();
}
get message() {
return {
success: "",
warning: this.env._t("Connected, Not Owned"),
failure: this.env._t("Disconnected"),
not_found: this.env._t("Customer Screen Unsupported. Please upgrade the IoT Box"),
}[this.state.status];
}
onClick() {
if (this.local) {
return this.onClickLocal();
} else {
return this.onClickProxy();
}
}
CustomerFacingDisplayButton.template = 'CustomerFacingDisplayButton';
async onClickLocal() {
this.env.pos.customer_display = window.open("", "Customer Display", "height=600,width=900");
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
var $renderedHtml = $("<div>").html(renderedHtml);
$(this.env.pos.customer_display.document.body).html(
$renderedHtml.find(".pos-customer_facing_display")
);
$(this.env.pos.customer_display.document.head).html(
$renderedHtml.find(".resources").html()
);
}
async onClickProxy() {
try {
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
let ownership = await this.env.proxy.take_ownership_over_customer_screen(renderedHtml);
if (typeof ownership === "string") {
ownership = JSON.parse(ownership);
}
if (ownership.status === "success") {
this.state.status = "success";
} else {
this.state.status = "warning";
}
if (!this.env.proxy.posbox_supports_display) {
this.env.proxy.posbox_supports_display = true;
this._start();
}
} catch (error) {
if (typeof error == "undefined") {
this.state.status = "failure";
} else {
this.state.status = "not_found";
}
}
}
_start() {
if (this.local) {
return;
}
Registries.Component.add(CustomerFacingDisplayButton);
const self = this;
async function loop() {
if (self.env.proxy.posbox_supports_display) {
try {
let ownership = await self.env.proxy.test_ownership_of_customer_screen();
if (typeof ownership === "string") {
ownership = JSON.parse(ownership);
}
if (ownership.status === "OWNER") {
self.state.status = "success";
} else {
self.state.status = "warning";
}
setTimeout(loop, 3000);
} catch (error) {
if (error.abort) {
// Stop the loop
return;
}
if (typeof error == "undefined") {
self.state.status = "failure";
} else {
self.state.status = "not_found";
self.env.proxy.posbox_supports_display = false;
}
setTimeout(loop, 3000);
}
}
}
loop();
}
}
CustomerFacingDisplayButton.template = "CustomerFacingDisplayButton";
return CustomerFacingDisplayButton;
});
Registries.Component.add(CustomerFacingDisplayButton);
export default CustomerFacingDisplayButton;
@@ -1,163 +1,161 @@
odoo.define('point_of_sale.DebugWidget', function (require) {
'use strict';
/** @odoo-module */
const { getFileAsText } = require('point_of_sale.utils');
const { parse } = require('web.field_utils');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { getFileAsText } from "@point_of_sale/js/utils";
import { parse } from "web.field_utils";
import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onMounted, onWillUnmount, useRef, useState } = owl;
const { onMounted, onWillUnmount, useRef, useState } = owl;
class DebugWidget extends PosComponent {
setup() {
super.setup();
this.state = useState({
barcodeInput: '',
weightInput: '',
isPaidOrdersReady: false,
isUnpaidOrdersReady: false,
buffer: NumberBuffer.get(),
});
class DebugWidget extends PosComponent {
setup() {
super.setup();
this.state = useState({
barcodeInput: "",
weightInput: "",
isPaidOrdersReady: false,
isUnpaidOrdersReady: false,
buffer: NumberBuffer.get(),
});
// NOTE: Perhaps this can still be improved.
// What we do here is loop thru the `event` elements
// then we assign animation that happens when the event is triggered
// in the proxy. E.g. if open_cashbox is sent, the open_cashbox element
// changes color from '#6CD11D' to '#1E1E1E' for a duration of 2sec.
this.eventElementsRef = {};
this.animations = {};
for (let eventName of ['open_cashbox', 'print_receipt', 'scale_read']) {
this.eventElementsRef[eventName] = useRef(eventName);
this.env.proxy.add_notification(
eventName,
(() => {
if (this.animations[eventName]) {
this.animations[eventName].cancel();
}
const eventElement = this.eventElementsRef[eventName].el;
eventElement.style.backgroundColor = '#6CD11D';
this.animations[eventName] = eventElement.animate(
{ backgroundColor: ['#6CD11D', '#1E1E1E'] },
2000
);
}).bind(this)
);
}
onMounted(() => {
NumberBuffer.on('buffer-update', this, this._onBufferUpdate);
});
onWillUnmount(() => {
NumberBuffer.off('buffer-update', this, this._onBufferUpdate);
});
}
toggleWidget() {
this.state.isShown = !this.state.isShown;
}
setWeight() {
var weightInKg = parse.float(this.state.weightInput);
if (!isNaN(weightInKg)) {
this.env.proxy.debug_set_weight(weightInKg);
}
}
resetWeight() {
this.state.weightInput = '';
this.env.proxy.debug_reset_weight();
}
async barcodeScan() {
await this.env.barcode_reader.scan(this.state.barcodeInput);
}
async barcodeScanEAN() {
const ean = this.env.barcode_reader.barcode_parser.sanitize_ean(
this.state.barcodeInput || '0'
// NOTE: Perhaps this can still be improved.
// What we do here is loop thru the `event` elements
// then we assign animation that happens when the event is triggered
// in the proxy. E.g. if open_cashbox is sent, the open_cashbox element
// changes color from '#6CD11D' to '#1E1E1E' for a duration of 2sec.
this.eventElementsRef = {};
this.animations = {};
for (const eventName of ["open_cashbox", "print_receipt", "scale_read"]) {
this.eventElementsRef[eventName] = useRef(eventName);
this.env.proxy.add_notification(
eventName,
(() => {
if (this.animations[eventName]) {
this.animations[eventName].cancel();
}
const eventElement = this.eventElementsRef[eventName].el;
eventElement.style.backgroundColor = "#6CD11D";
this.animations[eventName] = eventElement.animate(
{ backgroundColor: ["#6CD11D", "#1E1E1E"] },
2000
);
}).bind(this)
);
this.state.barcodeInput = ean;
await this.env.barcode_reader.scan(ean);
}
async deleteOrders() {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Delete Paid Orders ?'),
body: this.env._t(
'This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone.'
),
});
if (confirmed) {
this.env.pos.db.remove_all_orders();
this.env.pos.set_synch('connected', 0);
}
}
async deleteUnpaidOrders() {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Delete Unpaid Orders ?'),
body: this.env._t(
'This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone.'
),
});
if (confirmed) {
this.env.pos.db.remove_all_unpaid_orders();
window.location = '/';
}
}
_createBlob(contents) {
if (typeof contents !== 'string') {
contents = JSON.stringify(contents, null, 2);
}
return new Blob([contents]);
}
// IMPROVEMENT: Duplicated codes for downloading paid and unpaid orders.
// The implementation can be better.
preparePaidOrders() {
try {
this.paidOrdersBlob = this._createBlob(this.env.pos.export_paid_orders());
this.state.isPaidOrdersReady = true;
} catch (error) {
console.warn(error);
}
}
get paidOrdersFilename() {
return `${this.env._t('paid orders')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`;
}
get paidOrdersURL() {
var URL = window.URL || window.webkitURL;
return URL.createObjectURL(this.paidOrdersBlob);
}
prepareUnpaidOrders() {
try {
this.unpaidOrdersBlob = this._createBlob(this.env.pos.export_unpaid_orders());
this.state.isUnpaidOrdersReady = true;
} catch (error) {
console.warn(error);
}
}
get unpaidOrdersFilename() {
return `${this.env._t('unpaid orders')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`;
}
get unpaidOrdersURL() {
var URL = window.URL || window.webkitURL;
return URL.createObjectURL(this.unpaidOrdersBlob);
}
async importOrders(event) {
const file = event.target.files[0];
if (file) {
const report = this.env.pos.import_orders(await getFileAsText(file));
await this.showPopup('OrderImportPopup', { report });
}
}
refreshDisplay() {
this.env.proxy.message('display_refresh', {});
}
_onBufferUpdate(buffer) {
this.state.buffer = buffer;
}
get bufferRepr() {
return `"${this.state.buffer}"`;
onMounted(() => {
NumberBuffer.on("buffer-update", this, this._onBufferUpdate);
});
onWillUnmount(() => {
NumberBuffer.off("buffer-update", this, this._onBufferUpdate);
});
}
toggleWidget() {
this.state.isShown = !this.state.isShown;
}
setWeight() {
var weightInKg = parse.float(this.state.weightInput);
if (!isNaN(weightInKg)) {
this.env.proxy.debug_set_weight(weightInKg);
}
}
DebugWidget.template = 'DebugWidget';
resetWeight() {
this.state.weightInput = "";
this.env.proxy.debug_reset_weight();
}
async barcodeScan() {
await this.env.barcode_reader.scan(this.state.barcodeInput);
}
async barcodeScanEAN() {
const ean = this.env.barcode_reader.barcode_parser.sanitize_ean(
this.state.barcodeInput || "0"
);
this.state.barcodeInput = ean;
await this.env.barcode_reader.scan(ean);
}
async deleteOrders() {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Delete Paid Orders ?"),
body: this.env._t(
"This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone."
),
});
if (confirmed) {
this.env.pos.db.remove_all_orders();
this.env.pos.set_synch("connected", 0);
}
}
async deleteUnpaidOrders() {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Delete Unpaid Orders ?"),
body: this.env._t(
"This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone."
),
});
if (confirmed) {
this.env.pos.db.remove_all_unpaid_orders();
window.location = "/";
}
}
_createBlob(contents) {
if (typeof contents !== "string") {
contents = JSON.stringify(contents, null, 2);
}
return new Blob([contents]);
}
// IMPROVEMENT: Duplicated codes for downloading paid and unpaid orders.
// The implementation can be better.
preparePaidOrders() {
try {
this.paidOrdersBlob = this._createBlob(this.env.pos.export_paid_orders());
this.state.isPaidOrdersReady = true;
} catch (error) {
console.warn(error);
}
}
get paidOrdersFilename() {
return `${this.env._t("paid orders")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.json`;
}
get paidOrdersURL() {
var URL = window.URL || window.webkitURL;
return URL.createObjectURL(this.paidOrdersBlob);
}
prepareUnpaidOrders() {
try {
this.unpaidOrdersBlob = this._createBlob(this.env.pos.export_unpaid_orders());
this.state.isUnpaidOrdersReady = true;
} catch (error) {
console.warn(error);
}
}
get unpaidOrdersFilename() {
return `${this.env._t("unpaid orders")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.json`;
}
get unpaidOrdersURL() {
var URL = window.URL || window.webkitURL;
return URL.createObjectURL(this.unpaidOrdersBlob);
}
async importOrders(event) {
const file = event.target.files[0];
if (file) {
const report = this.env.pos.import_orders(await getFileAsText(file));
await this.showPopup("OrderImportPopup", { report });
}
}
refreshDisplay() {
this.env.proxy.message("display_refresh", {});
}
_onBufferUpdate(buffer) {
this.state.buffer = buffer;
}
get bufferRepr() {
return `"${this.state.buffer}"`;
}
}
DebugWidget.template = "DebugWidget";
Registries.Component.add(DebugWidget);
Registries.Component.add(DebugWidget);
return DebugWidget;
});
export default DebugWidget;
@@ -1,36 +1,36 @@
odoo.define('point_of_sale.HeaderButton', function(require) {
'use strict';
/** @odoo-module */
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');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service";
import { identifyError } from "@point_of_sale/js/utils";
// Previously HeaderButtonWidget
// This is the close session button
class HeaderButton extends PosComponent {
async onClick() {
try {
const info = await this.env.pos.getClosePosInfo();
this.showPopup('ClosePosPopup', { info: info, keepBehind: true });
} catch (e) {
if (identifyError(e) instanceof ConnectionAbortedError||ConnectionLostError) {
this.showPopup('OfflineErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Please check your internet connection and try again.'),
});
} else {
this.showPopup('ErrorPopup', {
title: this.env._t('Unknown Error'),
body: this.env._t('An unknown error prevents us from getting closing information.'),
});
}
// Previously HeaderButtonWidget
// This is the close session button
class HeaderButton extends PosComponent {
async onClick() {
try {
const info = await this.env.pos.getClosePosInfo();
this.showPopup("ClosePosPopup", { info: info, keepBehind: true });
} catch (e) {
if (identifyError(e) instanceof ConnectionAbortedError || ConnectionLostError) {
this.showPopup("OfflineErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t("Please check your internet connection and try again."),
});
} else {
this.showPopup("ErrorPopup", {
title: this.env._t("Unknown Error"),
body: this.env._t(
"An unknown error prevents us from getting closing information."
),
});
}
}
}
HeaderButton.template = 'HeaderButton';
}
HeaderButton.template = "HeaderButton";
Registries.Component.add(HeaderButton);
Registries.Component.add(HeaderButton);
return HeaderButton;
});
export default HeaderButton;
@@ -1,83 +1,76 @@
odoo.define('point_of_sale.ProxyStatus', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onMounted, onWillUnmount, useState } = owl;
const { onMounted, onWillUnmount, useState } = owl;
// Previously ProxyStatusWidget
class ProxyStatus extends PosComponent {
setup() {
super.setup();
const initialProxyStatus = this.env.proxy.get('status');
this.state = useState({
status: initialProxyStatus.status,
msg: initialProxyStatus.msg,
});
this.statuses = ['connected', 'connecting', 'disconnected', 'warning'];
this.index = 0;
// Previously ProxyStatusWidget
class ProxyStatus extends PosComponent {
setup() {
super.setup();
const initialProxyStatus = this.env.proxy.get("status");
this.state = useState({
status: initialProxyStatus.status,
msg: initialProxyStatus.msg,
});
this.statuses = ["connected", "connecting", "disconnected", "warning"];
this.index = 0;
onMounted(() => {
this.env.proxy.on('change:status', this, this._onChangeStatus);
});
onMounted(() => {
this.env.proxy.on("change:status", this, this._onChangeStatus);
});
onWillUnmount(() => {
this.env.proxy.off('change:status', this, this._onChangeStatus);
});
}
_onChangeStatus(posProxy, statusChange) {
this._setStatus(statusChange.newValue);
}
_setStatus(newStatus) {
if (newStatus.status === 'connected') {
var warning = false;
var msg = '';
if (this.env.pos.config.iface_scan_via_proxy) {
var scannerStatus = newStatus.drivers.scanner
? newStatus.drivers.scanner.status
: false;
if (scannerStatus != 'connected' && scannerStatus != 'connecting') {
warning = true;
msg += this.env._t('Scanner');
}
onWillUnmount(() => {
this.env.proxy.off("change:status", this, this._onChangeStatus);
});
}
_onChangeStatus(posProxy, statusChange) {
this._setStatus(statusChange.newValue);
}
_setStatus(newStatus) {
if (newStatus.status === "connected") {
var warning = false;
var msg = "";
if (this.env.pos.config.iface_scan_via_proxy) {
var scannerStatus = newStatus.drivers.scanner
? newStatus.drivers.scanner.status
: false;
if (scannerStatus != "connected" && scannerStatus != "connecting") {
warning = true;
msg += this.env._t("Scanner");
}
if (
this.env.pos.config.iface_print_via_proxy ||
this.env.pos.config.iface_cashdrawer
) {
var printerStatus = newStatus.drivers.printer
? newStatus.drivers.printer.status
: false;
if (printerStatus != 'connected' && printerStatus != 'connecting') {
warning = true;
msg = msg ? msg + ' & ' : msg;
msg += this.env._t('Printer');
}
}
if (this.env.pos.config.iface_electronic_scale) {
var scaleStatus = newStatus.drivers.scale
? newStatus.drivers.scale.status
: false;
if (scaleStatus != 'connected' && scaleStatus != 'connecting') {
warning = true;
msg = msg ? msg + ' & ' : msg;
msg += this.env._t('Scale');
}
}
msg = msg ? msg + ' ' + this.env._t('Offline') : msg;
this.state.status = warning ? 'warning' : 'connected';
this.state.msg = msg;
} else {
this.state.status = newStatus.status;
this.state.msg = newStatus.msg || '';
}
if (this.env.pos.config.iface_print_via_proxy || this.env.pos.config.iface_cashdrawer) {
var printerStatus = newStatus.drivers.printer
? newStatus.drivers.printer.status
: false;
if (printerStatus != "connected" && printerStatus != "connecting") {
warning = true;
msg = msg ? msg + " & " : msg;
msg += this.env._t("Printer");
}
}
if (this.env.pos.config.iface_electronic_scale) {
var scaleStatus = newStatus.drivers.scale ? newStatus.drivers.scale.status : false;
if (scaleStatus != "connected" && scaleStatus != "connecting") {
warning = true;
msg = msg ? msg + " & " : msg;
msg += this.env._t("Scale");
}
}
msg = msg ? msg + " " + this.env._t("Offline") : msg;
this.state.status = warning ? "warning" : "connected";
this.state.msg = msg;
} else {
this.state.status = newStatus.status;
this.state.msg = newStatus.msg || "";
}
}
ProxyStatus.template = 'ProxyStatus';
}
ProxyStatus.template = "ProxyStatus";
Registries.Component.add(ProxyStatus);
Registries.Component.add(ProxyStatus);
return ProxyStatus;
});
export default ProxyStatus;
@@ -1,39 +1,37 @@
odoo.define('point_of_sale.SaleDetailsButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { renderToString } = require('@web/core/utils/render');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { renderToString } from "@web/core/utils/render";
class SaleDetailsButton extends PosComponent {
async onClick() {
// IMPROVEMENT: Perhaps put this logic in a parent component
// so that for unit testing, we can check if this simple
// component correctly triggers an event.
const saleDetails = await this.rpc({
model: 'report.point_of_sale.report_saledetails',
method: 'get_sale_details',
args: [false, false, false, [this.env.pos.pos_session.id]],
class SaleDetailsButton extends PosComponent {
async onClick() {
// IMPROVEMENT: Perhaps put this logic in a parent component
// so that for unit testing, we can check if this simple
// component correctly triggers an event.
const saleDetails = await this.rpc({
model: "report.point_of_sale.report_saledetails",
method: "get_sale_details",
args: [false, false, false, [this.env.pos.pos_session.id]],
});
const report = renderToString(
"SaleDetailsReport",
Object.assign({}, saleDetails, {
date: new Date().toLocaleString(),
pos: this.env.pos,
})
);
const printResult = await this.env.proxy.printer.print_receipt(report);
if (!printResult.successful) {
await this.showPopup("ErrorPopup", {
title: printResult.message.title,
body: printResult.message.body,
});
const report = renderToString(
'SaleDetailsReport',
Object.assign({}, saleDetails, {
date: new Date().toLocaleString(),
pos: this.env.pos,
})
);
const printResult = await this.env.proxy.printer.print_receipt(report);
if (!printResult.successful) {
await this.showPopup('ErrorPopup', {
title: printResult.message.title,
body: printResult.message.body,
});
}
}
}
SaleDetailsButton.template = 'SaleDetailsButton';
}
SaleDetailsButton.template = "SaleDetailsButton";
Registries.Component.add(SaleDetailsButton);
Registries.Component.add(SaleDetailsButton);
return SaleDetailsButton;
});
export default SaleDetailsButton;
@@ -1,17 +1,15 @@
odoo.define('point_of_sale.SyncNotification', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class SyncNotification extends PosComponent {
onClick() {
this.env.pos.push_orders(null, { show_error: true });
}
class SyncNotification extends PosComponent {
onClick() {
this.env.pos.push_orders(null, { show_error: true });
}
SyncNotification.template = 'SyncNotification';
}
SyncNotification.template = "SyncNotification";
Registries.Component.add(SyncNotification);
Registries.Component.add(SyncNotification);
return SyncNotification;
});
export default SyncNotification;
@@ -1,28 +1,26 @@
odoo.define('point_of_sale.TicketButton', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class TicketButton extends PosComponent {
onClick() {
if (this.props.isTicketScreenShown) {
this.env.posbus.trigger('ticket-button-clicked');
} else {
this.showScreen('TicketScreen');
}
}
get count() {
if (this.env.pos) {
return this.env.pos.get_order_list().length;
} else {
return 0;
}
class TicketButton extends PosComponent {
onClick() {
if (this.props.isTicketScreenShown) {
this.env.posbus.trigger("ticket-button-clicked");
} else {
this.showScreen("TicketScreen");
}
}
TicketButton.template = 'TicketButton';
get count() {
if (this.env.pos) {
return this.env.pos.get_order_list().length;
} else {
return 0;
}
}
}
TicketButton.template = "TicketButton";
Registries.Component.add(TicketButton);
Registries.Component.add(TicketButton);
return TicketButton;
});
export default TicketButton;
@@ -1,262 +1,260 @@
odoo.define('point_of_sale.ClassRegistry', function (require) {
'use strict';
/** @odoo-module */
/**
* **Usage:**
* ```
* const Registry = new ClassRegistry();
*
* class A {}
* Registry.add(A);
*
* const AExt1 = A => class extends A {}
* Registry.extend(A, AExt1)
*
* const B = A => class extends A {}
* Registry.addByExtending(B, A)
*
* const AExt2 = A => class extends A {}
* Registry.extend(A, AExt2)
*
* Registry.get(A)
* // above returns: AExt2 -> AExt1 -> A
* // Basically, 'A' in the registry points to
* // the inheritance chain above.
*
* Registry.get(B)
* // above returns: B -> AExt2 -> AExt1 -> A
* // Even though B extends A before applying all
* // the extensions of A, when getting it from the
* // registry, the return points to a class with
* // inheritance chain that includes all the extensions
* // of 'A'.
*
* Registry.freeze()
* // Example 'B' above is lazy. Basically, it is only
* // computed when we call `get` from the registry.
* // If we know that no more dynamic inheritances will happen,
* // we can freeze the registry and cache the final form
* // of each class in the registry.
* ```
*
* IMPROVEMENT:
* * So far, mixin can be accomplished by creating a method
* the takes a class and returns a class expression. This is then
* used before the extends keyword like so:
*
* ```js
* class A {}
* Registry.add(A)
* const Mixin = x => class extends x {}
* // apply mixin
* // |
* // v
* const B = x => class extends Mixin(x) {}
* Registry.addByExtending(B, A)
* ```
*
* In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient
* already. However, this can still be improved since classes are only
* compiled after `Registry.freeze()`. Perhaps, we can make the
* Mixins extensible as well, such as so:
*
* ```
* class A {}
* Registry.add(A)
* const Mixin = x => class extends x {}
* Registry.add(Mixin)
* const OtherMixin = x => class extends x {}
* Registry.add(OtherMixin)
* const B = x => class extends x {}
* Registry.addByExtending(B, A, [Mixin, OtherMixin])
* const ExtendMixin = x => class extends x {}
* Registry.extend(Mixin, ExtendMixin)
* ```
*
* In the above, after `Registry.freeze()`,
* `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A`
*/
class ClassRegistry {
constructor() {
// base name map
this.baseNameMap = {};
// Object that maps `baseClass` to the class implementation extended in-place.
this.includedMap = new Map();
// Object that maps `baseClassCB` to the array of callbacks to generate the extended class.
this.extendedCBMap = new Map();
// Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap.
this.extendedSuperMap = new Map();
// For faster access, we can `freeze` the registry so that instead of dynamically generating
// the extended classes, it is taken from the cache instead.
this.cache = new Map();
}
/**
* Add a new class in the Registry.
* @param {Function} baseClass `class`
*/
add(baseClass) {
this.includedMap.set(baseClass, []);
this.baseNameMap[baseClass.name] = baseClass;
}
/**
* Add a new class in the Registry based on other class
* in the registry.
* @param {Function} baseClassCB `class -> class`
* @param {Function} base `class | class -> class`
*/
addByExtending(baseClassCB, base) {
this.extendedCBMap.set(baseClassCB, [baseClassCB]);
this.extendedSuperMap.set(baseClassCB, base);
this.baseNameMap[baseClassCB.name] = baseClassCB;
}
/**
* Extend in-place a class in the registry. E.g.
* ```
* // Using the following notation:
* // * |A| - compiled class in the registry
* // * A - class or an extension callback
* // * |A| => A2 -> A1 -> A
* // - the above means, compiled class A
* // points to the class inheritance derived from
* // A2(A1(A))
*
* class A {};
* Registry.add(A);
* // |A| => A
*
* let A1 = x => class extends x {};
* Registry.extend(A, A1);
* // |A| => A1 -> A
*
* let B = x => class extends x {};
* Registry.addByExtending(B, A);
* // |B| => B -> |A|
* // |B| => B -> A1 -> A
*
* let B1 = x => class extends x {};
* Registry.extend(B, B1);
* // |B| => B1 -> B -> |A|
*
* let C = x => class extends x {};
* Registry.addByExtending(C, B);
* // |C| => C -> |B|
*
* let B2 = x => class extends x {};
* Registry.extend(B, B2);
* // |B| => B2 -> B1 -> B -> |A|
*
* // Overall:
* // |A| => A1 -> A
* // |B| => B2 -> B1 -> B -> A1 -> A
* // |C| => C -> B2 -> B1 -> B -> A1 -> A
* ```
* @param {Function} base `class | class -> class`
* @param {Function} extensionCB `class -> class`
*/
extend(base, extensionCB) {
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
let extensionArray;
if (this.includedMap.get(base)) {
extensionArray = this.includedMap.get(base);
} else if (this.extendedCBMap.get(base)) {
extensionArray = this.extendedCBMap.get(base);
} else {
throw new Error(
`'${base.name}' is not in the Registry. Add it to Registry before extending.`
);
}
extensionArray.push(extensionCB);
const locOfNewExtension = extensionArray.length - 1;
const self = this;
const oldCompiled = this.isFrozen ? this.cache.get(base) : null;
return {
remove() {
extensionArray.splice(locOfNewExtension, 1);
self._recompute(base, oldCompiled);
},
compile() {
self._recompute(base);
}
};
}
_compile(base) {
let res;
if (this.includedMap.has(base)) {
res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base);
} else {
const superClass = this.extendedSuperMap.get(base);
const extensionCBs = this.extendedCBMap.get(base);
res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass));
}
Object.defineProperty(res, 'name', { value: base.name });
return res;
}
/**
* Return the compiled class (containing all the extensions) of the base class.
* @param {Function} base `class | class -> class` function used in adding the class
*/
get(base) {
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
if (this.isFrozen) {
return this.cache.get(base);
}
return this._compile(base);
}
/**
* Uses the callbacks registered in the registry to compile the classes.
*/
freeze() {
// Step: Compile the `included classes`.
for (let [baseClass, extensionCBs] of this.includedMap.entries()) {
const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass);
this.cache.set(baseClass, compiled);
}
// Step: Compile the `extended classes` based on `included classes`.
// Also gather those the are based on `extended classes`.
const remaining = [];
for (let [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) {
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
if (!compiled) {
remaining.push([baseClassCB, extensionCBArray]);
continue;
}
const extendedClass = extensionCBArray.reduce(
(acc, extensionCB) => extensionCB(acc),
compiled
);
this.cache.set(baseClassCB, extendedClass);
}
// Step: Compile the `extended classes` based on `extended classes`.
for (let [baseClassCB, extensionCBArray] of remaining) {
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
const extendedClass = extensionCBArray.reduce(
(acc, extensionCB) => extensionCB(acc),
compiled
);
this.cache.set(baseClassCB, extendedClass);
}
// Step: Set the name of the compiled classess
for (let [base, compiledClass] of this.cache.entries()) {
Object.defineProperty(compiledClass, 'name', { value: base.name });
}
// Step: Set the flag to true;
this.isFrozen = true;
}
_recompute(base, old) {
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
return old ? old : this._compile(base);
}
/**
* **Usage:**
* ```
* const Registry = new ClassRegistry();
*
* class A {}
* Registry.add(A);
*
* const AExt1 = A => class extends A {}
* Registry.extend(A, AExt1)
*
* const B = A => class extends A {}
* Registry.addByExtending(B, A)
*
* const AExt2 = A => class extends A {}
* Registry.extend(A, AExt2)
*
* Registry.get(A)
* // above returns: AExt2 -> AExt1 -> A
* // Basically, 'A' in the registry points to
* // the inheritance chain above.
*
* Registry.get(B)
* // above returns: B -> AExt2 -> AExt1 -> A
* // Even though B extends A before applying all
* // the extensions of A, when getting it from the
* // registry, the return points to a class with
* // inheritance chain that includes all the extensions
* // of 'A'.
*
* Registry.freeze()
* // Example 'B' above is lazy. Basically, it is only
* // computed when we call `get` from the registry.
* // If we know that no more dynamic inheritances will happen,
* // we can freeze the registry and cache the final form
* // of each class in the registry.
* ```
*
* IMPROVEMENT:
* * So far, mixin can be accomplished by creating a method
* the takes a class and returns a class expression. This is then
* used before the extends keyword like so:
*
* ```js
* class A {}
* Registry.add(A)
* const Mixin = x => class extends x {}
* // apply mixin
* // |
* // v
* const B = x => class extends Mixin(x) {}
* Registry.addByExtending(B, A)
* ```
*
* In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient
* already. However, this can still be improved since classes are only
* compiled after `Registry.freeze()`. Perhaps, we can make the
* Mixins extensible as well, such as so:
*
* ```
* class A {}
* Registry.add(A)
* const Mixin = x => class extends x {}
* Registry.add(Mixin)
* const OtherMixin = x => class extends x {}
* Registry.add(OtherMixin)
* const B = x => class extends x {}
* Registry.addByExtending(B, A, [Mixin, OtherMixin])
* const ExtendMixin = x => class extends x {}
* Registry.extend(Mixin, ExtendMixin)
* ```
*
* In the above, after `Registry.freeze()`,
* `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A`
*/
class ClassRegistry {
constructor() {
// base name map
this.baseNameMap = {};
// Object that maps `baseClass` to the class implementation extended in-place.
this.includedMap = new Map();
// Object that maps `baseClassCB` to the array of callbacks to generate the extended class.
this.extendedCBMap = new Map();
// Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap.
this.extendedSuperMap = new Map();
// For faster access, we can `freeze` the registry so that instead of dynamically generating
// the extended classes, it is taken from the cache instead.
this.cache = new Map();
}
/**
* Add a new class in the Registry.
* @param {Function} baseClass `class`
*/
add(baseClass) {
this.includedMap.set(baseClass, []);
this.baseNameMap[baseClass.name] = baseClass;
}
/**
* Add a new class in the Registry based on other class
* in the registry.
* @param {Function} baseClassCB `class -> class`
* @param {Function} base `class | class -> class`
*/
addByExtending(baseClassCB, base) {
this.extendedCBMap.set(baseClassCB, [baseClassCB]);
this.extendedSuperMap.set(baseClassCB, base);
this.baseNameMap[baseClassCB.name] = baseClassCB;
}
/**
* Extend in-place a class in the registry. E.g.
* ```
* // Using the following notation:
* // * |A| - compiled class in the registry
* // * A - class or an extension callback
* // * |A| => A2 -> A1 -> A
* // - the above means, compiled class A
* // points to the class inheritance derived from
* // A2(A1(A))
*
* class A {};
* Registry.add(A);
* // |A| => A
*
* let A1 = x => class extends x {};
* Registry.extend(A, A1);
* // |A| => A1 -> A
*
* let B = x => class extends x {};
* Registry.addByExtending(B, A);
* // |B| => B -> |A|
* // |B| => B -> A1 -> A
*
* let B1 = x => class extends x {};
* Registry.extend(B, B1);
* // |B| => B1 -> B -> |A|
*
* let C = x => class extends x {};
* Registry.addByExtending(C, B);
* // |C| => C -> |B|
*
* let B2 = x => class extends x {};
* Registry.extend(B, B2);
* // |B| => B2 -> B1 -> B -> |A|
*
* // Overall:
* // |A| => A1 -> A
* // |B| => B2 -> B1 -> B -> A1 -> A
* // |C| => C -> B2 -> B1 -> B -> A1 -> A
* ```
* @param {Function} base `class | class -> class`
* @param {Function} extensionCB `class -> class`
*/
extend(base, extensionCB) {
if (typeof base === "string") {
base = this.baseNameMap[base];
}
let extensionArray;
if (this.includedMap.get(base)) {
extensionArray = this.includedMap.get(base);
} else if (this.extendedCBMap.get(base)) {
extensionArray = this.extendedCBMap.get(base);
} else {
throw new Error(
`'${base.name}' is not in the Registry. Add it to Registry before extending.`
);
}
extensionArray.push(extensionCB);
const locOfNewExtension = extensionArray.length - 1;
const self = this;
const oldCompiled = this.isFrozen ? this.cache.get(base) : null;
return {
remove() {
extensionArray.splice(locOfNewExtension, 1);
self._recompute(base, oldCompiled);
},
compile() {
self._recompute(base);
},
};
}
_compile(base) {
let res;
if (this.includedMap.has(base)) {
res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base);
} else {
const superClass = this.extendedSuperMap.get(base);
const extensionCBs = this.extendedCBMap.get(base);
res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass));
}
Object.defineProperty(res, "name", { value: base.name });
return res;
}
/**
* Return the compiled class (containing all the extensions) of the base class.
* @param {Function} base `class | class -> class` function used in adding the class
*/
get(base) {
if (typeof base === "string") {
base = this.baseNameMap[base];
}
if (this.isFrozen) {
return this.cache.get(base);
}
return this._compile(base);
}
/**
* Uses the callbacks registered in the registry to compile the classes.
*/
freeze() {
// Step: Compile the `included classes`.
for (const [baseClass, extensionCBs] of this.includedMap.entries()) {
const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass);
this.cache.set(baseClass, compiled);
}
return ClassRegistry;
});
// Step: Compile the `extended classes` based on `included classes`.
// Also gather those the are based on `extended classes`.
const remaining = [];
for (const [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) {
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
if (!compiled) {
remaining.push([baseClassCB, extensionCBArray]);
continue;
}
const extendedClass = extensionCBArray.reduce(
(acc, extensionCB) => extensionCB(acc),
compiled
);
this.cache.set(baseClassCB, extendedClass);
}
// Step: Compile the `extended classes` based on `extended classes`.
for (const [baseClassCB, extensionCBArray] of remaining) {
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
const extendedClass = extensionCBArray.reduce(
(acc, extensionCB) => extensionCB(acc),
compiled
);
this.cache.set(baseClassCB, extendedClass);
}
// Step: Set the name of the compiled classess
for (const [base, compiledClass] of this.cache.entries()) {
Object.defineProperty(compiledClass, "name", { value: base.name });
}
// Step: Set the flag to true;
this.isFrozen = true;
}
_recompute(base, old) {
if (typeof base === "string") {
base = this.baseNameMap[base];
}
return old ? old : this._compile(base);
}
}
export default ClassRegistry;
@@ -1,29 +1,27 @@
odoo.define('point_of_sale.ComponentRegistry', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ClassRegistry = require('point_of_sale.ClassRegistry');
import PosComponent from "@point_of_sale/js/PosComponent";
import ClassRegistry from "@point_of_sale/js/ClassRegistry";
class ComponentRegistry extends ClassRegistry {
freeze() {
super.freeze();
// Make sure PosComponent has the compiled classes.
// This way, we don't need to explicitly declare that
// a set of components is children of another.
PosComponent.components = {};
for (let [base, compiledClass] of this.cache.entries()) {
PosComponent.components[base.name] = compiledClass;
}
}
_recompute(base, old) {
const res = super._recompute(base, old);
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
PosComponent.components[base.name] = res;
return res;
class ComponentRegistry extends ClassRegistry {
freeze() {
super.freeze();
// Make sure PosComponent has the compiled classes.
// This way, we don't need to explicitly declare that
// a set of components is children of another.
PosComponent.components = {};
for (const [base, compiledClass] of this.cache.entries()) {
PosComponent.components[base.name] = compiledClass;
}
}
_recompute(base, old) {
const res = super._recompute(base, old);
if (typeof base === "string") {
base = this.baseNameMap[base];
}
PosComponent.components[base.name] = res;
return res;
}
}
return ComponentRegistry;
});
export default ComponentRegistry;
@@ -1,107 +1,107 @@
odoo.define('point_of_sale.ControlButtonsMixin', function (require) {
'use strict';
/** @odoo-module */
const Registries = require('point_of_sale.Registries');
import Registries from "@point_of_sale/js/Registries";
/**
* Component that has this mixin allows the use of `addControlButton`.
* All added control buttons that satisfies the condition can be accessed
* thru the `controlButtons` field of the Component's instance. These
* control buttons can then be rendered in the Component.
* @param {Function} x superclass
*/
const ControlButtonsMixin = (x) => {
const controlButtonsToPosition = [];
const sortedControlButtons = [];
class Extended extends x {
get controlButtons() {
return sortedControlButtons
.filter((cb) => {
return cb.condition ? cb.condition.bind(this)() : true;
})
.map((cb) =>
Object.assign({}, cb, { component: Registries.Component.get(cb.component) })
);
}
}
/**
* Component that has this mixin allows the use of `addControlButton`.
* All added control buttons that satisfies the condition can be accessed
* thru the `controlButtons` field of the Component's instance. These
* control buttons can then be rendered in the Component.
* @param {Function} x superclass
* @param {Object} controlButton
* @param {Function} controlButton.component
* Base class that is added in the Registries.Component.
* @param {Function} controlButton.condition zero argument function that is bound
* to the instance of ProductScreen, such that `this.env.pos` can be used
* inside the function.
* @param {Array} [controlButton.position] array of two elements
* [locator, relativeTo]
* locator: string -> any of ('before', 'after', 'replace')
* relativeTo: string -> other controlButtons component name
*/
const ControlButtonsMixin = (x) => {
const controlButtonsToPosition = [];
const sortedControlButtons = [];
class Extended extends x {
get controlButtons() {
return sortedControlButtons
.filter((cb) => {
return cb.condition ? cb.condition.bind(this)() : true;
})
.map((cb) =>
Object.assign({}, cb, { component: Registries.Component.get(cb.component) })
);
}
Extended.addControlButton = function (controlButton) {
// We set the name first.
if (!controlButton.name) {
controlButton.name = controlButton.component.name;
}
/**
* @param {Object} controlButton
* @param {Function} controlButton.component
* Base class that is added in the Registries.Component.
* @param {Function} controlButton.condition zero argument function that is bound
* to the instance of ProductScreen, such that `this.env.pos` can be used
* inside the function.
* @param {Array} [controlButton.position] array of two elements
* [locator, relativeTo]
* locator: string -> any of ('before', 'after', 'replace')
* relativeTo: string -> other controlButtons component name
*/
Extended.addControlButton = function (controlButton) {
// We set the name first.
if (!controlButton.name) {
controlButton.name = controlButton.component.name;
}
// If no position is set, we just push it to the array.
if (!controlButton.position) {
sortedControlButtons.push(controlButton);
} else {
controlButtonsToPosition.push(controlButton);
}
};
/**
* Call this static method to make the added control buttons in proper
* order.
* NOTE: This isn't necessarily a fast algorithm. I doubt that the number
* of control buttons will exceed an order of hundreds, so for practical
* purposes, it is enough.
*/
Extended.sortControlButtons = function () {
function setControlButton(locator, index, cb) {
if (locator == 'replace') {
sortedControlButtons[index] = cb;
} else if (locator == 'before') {
sortedControlButtons.splice(index, 0, cb);
} else if (locator == 'after') {
sortedControlButtons.splice(index + 1, 0, cb);
}
}
function locate(cb) {
const [locator, reference] = cb.position;
const index = sortedControlButtons.findIndex((cb) => cb.name == reference);
return [locator, index, reference];
}
const cbMissingReference = [];
// 1. First pass. If the reference control button isn't there, collect it for second pass.
for (let cb of controlButtonsToPosition) {
const [locator, index] = locate(cb);
if (index == -1) {
cbMissingReference.push(cb);
continue;
}
setControlButton(locator, index, cb);
}
// 2. Second pass.
// If during the first pass, 1 -> 2, 2 -> 3, 3 -> 4, 4 -> 5 and 5 is already
// in the sorted control buttons, then 1, 2, 3 & 4 are put in `cbMissingReference`.
// This only means 2 things about the objects in `cbMissingReference`:
// i) They are referencing the cb after them
// ii) They really have missing reference.
// Thus, we have to iterate the cb with missing reference in reverse.
for (let cb of cbMissingReference.reverse()) {
const [locator, index, reference] = locate(cb);
if (index == -1) {
console.warn(`'${cb.name}' is not properly position because '${reference}' is not found. Is '${reference}' spelled correctly?`);
sortedControlButtons.push(cb);
} else {
setControlButton(locator, index, cb);
}
}
// If no position is set, we just push it to the array.
if (!controlButton.position) {
sortedControlButtons.push(controlButton);
} else {
controlButtonsToPosition.push(controlButton);
}
return Extended;
};
return ControlButtonsMixin;
});
/**
* Call this static method to make the added control buttons in proper
* order.
* NOTE: This isn't necessarily a fast algorithm. I doubt that the number
* of control buttons will exceed an order of hundreds, so for practical
* purposes, it is enough.
*/
Extended.sortControlButtons = function () {
function setControlButton(locator, index, cb) {
if (locator == "replace") {
sortedControlButtons[index] = cb;
} else if (locator == "before") {
sortedControlButtons.splice(index, 0, cb);
} else if (locator == "after") {
sortedControlButtons.splice(index + 1, 0, cb);
}
}
function locate(cb) {
const [locator, reference] = cb.position;
const index = sortedControlButtons.findIndex((cb) => cb.name == reference);
return [locator, index, reference];
}
const cbMissingReference = [];
// 1. First pass. If the reference control button isn't there, collect it for second pass.
for (const cb of controlButtonsToPosition) {
const [locator, index] = locate(cb);
if (index == -1) {
cbMissingReference.push(cb);
continue;
}
setControlButton(locator, index, cb);
}
// 2. Second pass.
// If during the first pass, 1 -> 2, 2 -> 3, 3 -> 4, 4 -> 5 and 5 is already
// in the sorted control buttons, then 1, 2, 3 & 4 are put in `cbMissingReference`.
// This only means 2 things about the objects in `cbMissingReference`:
// i) They are referencing the cb after them
// ii) They really have missing reference.
// Thus, we have to iterate the cb with missing reference in reverse.
for (const cb of cbMissingReference.reverse()) {
const [locator, index, reference] = locate(cb);
if (index == -1) {
console.warn(
`'${cb.name}' is not properly position because '${reference}' is not found. Is '${reference}' spelled correctly?`
);
sortedControlButtons.push(cb);
} else {
setControlButton(locator, index, cb);
}
}
};
return Extended;
};
export default ControlButtonsMixin;
+56 -59
View File
@@ -1,64 +1,61 @@
odoo.define('point_of_sale.Gui', function (require) {
'use strict';
/** @odoo-module */
const { status } = owl;
const { status } = owl;
/**
* This module bridges the data classes (such as those defined in
* models.js) to the view (Component) but not vice versa.
*
* The idea is to be able to perform side-effects to the user interface
* during calculation. Think of console.log during times we want to see
* the result of calculations. This is no different, except that instead
* of printing something in the console, we access a method in the user
* interface then the user interface reacts, e.g. calling `showPopup`.
*
* This however can be dangerous to the user interface as it can be possible
* that a rendered component is destroyed during the calculation. Because of
* this, we are going to limit external ui controls to those safe ones to
* use such as:
* - `showPopup`
* - `showTempScreen`
*
* IMPROVEMENT: After all, this Gui layer seems to be a good abstraction because
* there is a complete decoupling between data and view despite the data being
* able to use selected functionalities in the view layer. More formalized
* implementation is welcome.
*/
/**
* This module bridges the data classes (such as those defined in
* models.js) to the view (Component) but not vice versa.
*
* The idea is to be able to perform side-effects to the user interface
* during calculation. Think of console.log during times we want to see
* the result of calculations. This is no different, except that instead
* of printing something in the console, we access a method in the user
* interface then the user interface reacts, e.g. calling `showPopup`.
*
* This however can be dangerous to the user interface as it can be possible
* that a rendered component is destroyed during the calculation. Because of
* this, we are going to limit external ui controls to those safe ones to
* use such as:
* - `showPopup`
* - `showTempScreen`
*
* IMPROVEMENT: After all, this Gui layer seems to be a good abstraction because
* there is a complete decoupling between data and view despite the data being
* able to use selected functionalities in the view layer. More formalized
* implementation is welcome.
*/
const config = {};
const config = {};
/**
* Call this when the user interface is ready. Provide the component
* that will be used to control the ui.
* @param {component} component component having the ui methods.
*/
export const configureGui = ({ component }) => {
config.component = component;
config.availableMethods = new Set([
"showScreen",
"showPopup",
"showTempScreen",
"playSound",
"setSyncStatus",
"showNotification",
]);
};
/**
* Call this when the user interface is ready. Provide the component
* that will be used to control the ui.
* @param {component} component component having the ui methods.
*/
const configureGui = ({ component }) => {
config.component = component;
config.availableMethods = new Set([
'showScreen',
'showPopup',
'showTempScreen',
'playSound',
'setSyncStatus',
'showNotification',
]);
};
/**
* Import this and consume like so: `Gui.showPopup(<PopupName>, <props>)`.
* Like you would call `showPopup` in a component.
*/
const Gui = new Proxy(config, {
get(target, key) {
const { component, availableMethods } = target;
if (!component) throw new Error(`Call 'configureGui' before using Gui.`);
const isMounted = status(component) === 'mounted';
if (availableMethods.has(key) && isMounted) {
return component[key].bind(component);
}
},
});
return { configureGui, Gui };
/**
* Import this and consume like so: `Gui.showPopup(<PopupName>, <props>)`.
* Like you would call `showPopup` in a component.
*/
export const Gui = new Proxy(config, {
get(target, key) {
const { component, availableMethods } = target;
if (!component) {
throw new Error(`Call 'configureGui' before using Gui.`);
}
const isMounted = status(component) === "mounted";
if (availableMethods.has(key) && isMounted) {
return component[key].bind(component);
}
},
});
@@ -1,63 +1,63 @@
odoo.define('point_of_sale.AbstractReceiptScreen', function (require) {
'use strict';
/** @odoo-module */
const { nextFrame } = require('point_of_sale.utils');
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { nextFrame } from "@point_of_sale/js/utils";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { useRef } = owl;
const { useRef } = owl;
/**
* This relies on the assumption that there is a reference to
* `order-receipt` so it is important to declare a `t-ref` to
* `order-receipt` in the template of the Component that extends
* this abstract component.
*/
class AbstractReceiptScreen extends PosComponent {
setup() {
super.setup();
this.orderReceipt = useRef('order-receipt');
}
async _printReceipt() {
if (this.env.proxy.printer) {
const printResult = await this.env.proxy.printer.print_receipt(this.orderReceipt.el.innerHTML);
if (printResult.successful) {
return true;
} else {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: printResult.message.title,
body: 'Do you want to print using the web printer?',
});
if (confirmed) {
// We want to call the _printWeb when the popup is fully gone
// from the screen which happens after the next animation frame.
await nextFrame();
return await this._printWeb();
}
return false;
}
} else {
return await this._printWeb();
}
}
async _printWeb() {
try {
window.print();
/**
* This relies on the assumption that there is a reference to
* `order-receipt` so it is important to declare a `t-ref` to
* `order-receipt` in the template of the Component that extends
* this abstract component.
*/
class AbstractReceiptScreen extends PosComponent {
setup() {
super.setup();
this.orderReceipt = useRef("order-receipt");
}
async _printReceipt() {
if (this.env.proxy.printer) {
const printResult = await this.env.proxy.printer.print_receipt(
this.orderReceipt.el.innerHTML
);
if (printResult.successful) {
return true;
} catch {
await this.showPopup('ErrorPopup', {
title: this.env._t('Printing is not supported on some browsers'),
body: this.env._t(
'Printing is not supported on some browsers due to no default printing protocol ' +
'is available. It is possible to print your tickets by making use of an IoT Box.'
),
} else {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: printResult.message.title,
body: "Do you want to print using the web printer?",
});
if (confirmed) {
// We want to call the _printWeb when the popup is fully gone
// from the screen which happens after the next animation frame.
await nextFrame();
return await this._printWeb();
}
return false;
}
} else {
return await this._printWeb();
}
}
async _printWeb() {
try {
window.print();
return true;
} catch {
await this.showPopup("ErrorPopup", {
title: this.env._t("Printing is not supported on some browsers"),
body: this.env._t(
"Printing is not supported on some browsers due to no default printing protocol " +
"is available. It is possible to print your tickets by making use of an IoT Box."
),
});
return false;
}
}
}
Registries.Component.add(AbstractReceiptScreen);
Registries.Component.add(AbstractReceiptScreen);
return AbstractReceiptScreen;
});
export default AbstractReceiptScreen;
@@ -1,13 +1,11 @@
odoo.define('point_of_sale.CurrencyAmount', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class CurrencyAmount extends PosComponent {}
CurrencyAmount.template = 'CurrencyAmount';
class CurrencyAmount extends PosComponent {}
CurrencyAmount.template = "CurrencyAmount";
Registries.Component.add(CurrencyAmount);
Registries.Component.add(CurrencyAmount);
return CurrencyAmount;
});
export default CurrencyAmount;
@@ -1,145 +1,145 @@
odoo.define('point_of_sale.Draggable', function(require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useListener } from "@web/core/utils/hooks";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onMounted, useExternalListener } = owl;
const { onMounted, useExternalListener } = owl;
/**
* Wrap an element or a component with { position: absolute } to make it
* draggable around the limitArea or the nearest positioned ancestor.
*
* e.g.
* ```
* <div class="limit-area">
* <Draggable limitArea="'.limit-area'">
* <div class="popup">
* <header class="drag-handle"></header>
* </div>
* <div class="popup body"></div>
* </Draggable>
* </div>
* ```
*
* In the above snippet, if the popup div is { position: absolute },
* then it becomes draggable around the .limit-area element if it is dragged
* thru its Header -- because of the .drag-handle element.
*
* @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }`
*/
class Draggable extends PosComponent {
setup() {
super.setup();
this.isDragging = false;
this.dx = 0;
this.dy = 0;
// drag with mouse
useExternalListener(document, 'mousemove', this.move);
useExternalListener(document, 'mouseup', this.endDrag);
// drag with touch
useExternalListener(document, 'touchmove', this.move);
useExternalListener(document, 'touchend', this.endDrag);
/**
* Wrap an element or a component with { position: absolute } to make it
* draggable around the limitArea or the nearest positioned ancestor.
*
* e.g.
* ```
* <div class="limit-area">
* <Draggable limitArea="'.limit-area'">
* <div class="popup">
* <header class="drag-handle"></header>
* </div>
* <div class="popup body"></div>
* </Draggable>
* </div>
* ```
*
* In the above snippet, if the popup div is { position: absolute },
* then it becomes draggable around the .limit-area element if it is dragged
* thru its Header -- because of the .drag-handle element.
*
* @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }`
*/
class Draggable extends PosComponent {
setup() {
super.setup();
this.isDragging = false;
this.dx = 0;
this.dy = 0;
// drag with mouse
useExternalListener(document, "mousemove", this.move);
useExternalListener(document, "mouseup", this.endDrag);
// drag with touch
useExternalListener(document, "touchmove", this.move);
useExternalListener(document, "touchend", this.endDrag);
useListener('mousedown', '.drag-handle', this.startDrag);
useListener('touchstart', '.drag-handle', this.startDrag);
useListener("mousedown", ".drag-handle", this.startDrag);
useListener("touchstart", ".drag-handle", this.startDrag);
onMounted(() => {
this.limitArea = this.props.limitArea
? document.querySelector(this.props.limitArea)
: this.el.offsetParent;
if (!this.limitArea) return;
this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect();
if (this.limitArea === this.el.offsetParent) {
this.limitLeft = 0;
this.limitTop = 0;
this.limitRight = this.limitAreaBoundingRect.width;
this.limitBottom = this.limitAreaBoundingRect.height;
} else {
this.limitLeft = -this.el.offsetParent.offsetLeft;
this.limitTop = -this.el.offsetParent.offsetTop;
this.limitRight =
this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft;
this.limitBottom =
this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop;
}
this.limitAreaWidth = this.limitAreaBoundingRect.width;
this.limitAreaHeight = this.limitAreaBoundingRect.height;
// absolutely position the element then remove the transform.
const elBoundingRect = this.el.getBoundingClientRect();
this.el.style.top = `${elBoundingRect.top}px`;
this.el.style.left = `${elBoundingRect.left}px`;
this.el.style.transform = 'none';
});
}
startDrag(event) {
let realEvent;
if (event instanceof CustomEvent) {
realEvent = event.detail;
onMounted(() => {
this.limitArea = this.props.limitArea
? document.querySelector(this.props.limitArea)
: this.el.offsetParent;
if (!this.limitArea) {
return;
}
this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect();
if (this.limitArea === this.el.offsetParent) {
this.limitLeft = 0;
this.limitTop = 0;
this.limitRight = this.limitAreaBoundingRect.width;
this.limitBottom = this.limitAreaBoundingRect.height;
} else {
realEvent = event;
this.limitLeft = -this.el.offsetParent.offsetLeft;
this.limitTop = -this.el.offsetParent.offsetTop;
this.limitRight =
this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft;
this.limitBottom =
this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop;
}
const { x, y } = this._getEventLoc(realEvent);
this.isDragging = true;
this.dx = this.el.offsetLeft - x;
this.dy = this.el.offsetTop - y;
event.stopPropagation();
this.limitAreaWidth = this.limitAreaBoundingRect.width;
this.limitAreaHeight = this.limitAreaBoundingRect.height;
// absolutely position the element then remove the transform.
const elBoundingRect = this.el.getBoundingClientRect();
this.el.style.top = `${elBoundingRect.top}px`;
this.el.style.left = `${elBoundingRect.left}px`;
this.el.style.transform = "none";
});
}
startDrag(event) {
let realEvent;
if (event instanceof CustomEvent) {
realEvent = event.detail;
} else {
realEvent = event;
}
move(event) {
if (this.isDragging) {
const { x: pointerX, y: pointerY } = this._getEventLoc(event);
const posLeft = this._getPosLeft(pointerX, this.dx);
const posTop = this._getPosTop(pointerY, this.dy);
this.el.style.left = `${posLeft}px`;
this.el.style.top = `${posTop}px`;
}
}
endDrag() {
if (this.isDragging) {
this.isDragging = false;
this.trigger('drag-end', {
loc: { top: this.el.offsetTop, left: this.el.offsetLeft },
});
}
}
_getEventLoc(event) {
let coordX, coordY;
if (event.touches && event.touches[0]) {
coordX = event.touches[0].clientX;
coordY = event.touches[0].clientY;
} else {
coordX = event.clientX;
coordY = event.clientY;
}
return {
x: coordX,
y: coordY,
};
}
_getPosLeft(pointerX, dx) {
const posLeft = pointerX + dx;
if (posLeft < this.limitLeft) {
return this.limitLeft;
} else if (posLeft > this.limitRight - this.el.offsetWidth) {
return this.limitRight - this.el.offsetWidth;
}
return posLeft;
}
_getPosTop(pointerY, dy) {
const posTop = pointerY + dy;
if (posTop < this.limitTop) {
return this.limitTop;
} else if (posTop > this.limitBottom - this.el.offsetHeight) {
return this.limitBottom - this.el.offsetHeight;
}
return posTop;
const { x, y } = this._getEventLoc(realEvent);
this.isDragging = true;
this.dx = this.el.offsetLeft - x;
this.dy = this.el.offsetTop - y;
event.stopPropagation();
}
move(event) {
if (this.isDragging) {
const { x: pointerX, y: pointerY } = this._getEventLoc(event);
const posLeft = this._getPosLeft(pointerX, this.dx);
const posTop = this._getPosTop(pointerY, this.dy);
this.el.style.left = `${posLeft}px`;
this.el.style.top = `${posTop}px`;
}
}
Draggable.template = 'Draggable';
endDrag() {
if (this.isDragging) {
this.isDragging = false;
this.trigger("drag-end", {
loc: { top: this.el.offsetTop, left: this.el.offsetLeft },
});
}
}
_getEventLoc(event) {
let coordX, coordY;
if (event.touches && event.touches[0]) {
coordX = event.touches[0].clientX;
coordY = event.touches[0].clientY;
} else {
coordX = event.clientX;
coordY = event.clientY;
}
return {
x: coordX,
y: coordY,
};
}
_getPosLeft(pointerX, dx) {
const posLeft = pointerX + dx;
if (posLeft < this.limitLeft) {
return this.limitLeft;
} else if (posLeft > this.limitRight - this.el.offsetWidth) {
return this.limitRight - this.el.offsetWidth;
}
return posLeft;
}
_getPosTop(pointerY, dy) {
const posTop = pointerY + dy;
if (posTop < this.limitTop) {
return this.limitTop;
} else if (posTop > this.limitBottom - this.el.offsetHeight) {
return this.limitBottom - this.el.offsetHeight;
}
return posTop;
}
}
Draggable.template = "Draggable";
Registries.Component.add(Draggable);
Registries.Component.add(Draggable);
return Draggable;
});
export default Draggable;
@@ -1,15 +1,13 @@
odoo.define('point_of_sale.IndependentToOrderScreen', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
import PosComponent from "@point_of_sale/js/PosComponent";
class IndependentToOrderScreen extends PosComponent {
close() {
const order = this.env.pos.get_order();
const { name: screenName } = order.get_screen_data();
this.showScreen(screenName);
}
class IndependentToOrderScreen extends PosComponent {
close() {
const order = this.env.pos.get_order();
const { name: screenName } = order.get_screen_data();
this.showScreen(screenName);
}
}
return IndependentToOrderScreen;
});
export default IndependentToOrderScreen;
@@ -1,25 +1,25 @@
odoo.define('point_of_sale.MobileOrderWidget', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class MobileOrderWidget extends PosComponent {
get order() {
return this.env.pos.get_order();
}
get total() {
const _total = this.order ? this.order.get_total_with_tax() : 0;
return this.env.pos.format_currency(_total);
}
get items_number() {
return this.order ? this.order.orderlines.reduce((items_number,line) => items_number + line.quantity, 0) : 0;
}
class MobileOrderWidget extends PosComponent {
get order() {
return this.env.pos.get_order();
}
get total() {
const _total = this.order ? this.order.get_total_with_tax() : 0;
return this.env.pos.format_currency(_total);
}
get items_number() {
return this.order
? this.order.orderlines.reduce((items_number, line) => items_number + line.quantity, 0)
: 0;
}
}
MobileOrderWidget.template = 'MobileOrderWidget';
MobileOrderWidget.template = "MobileOrderWidget";
Registries.Component.add(MobileOrderWidget);
Registries.Component.add(MobileOrderWidget);
return MobileOrderWidget;
});
export default MobileOrderWidget;
@@ -1,19 +1,17 @@
odoo.define('point_of_sale.NotificationSound', function (require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useListener } from "@web/core/utils/hooks";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class NotificationSound extends PosComponent {
setup() {
super.setup();
useListener('ended', () => (this.props.sound.src = null));
}
class NotificationSound extends PosComponent {
setup() {
super.setup();
useListener("ended", () => (this.props.sound.src = null));
}
NotificationSound.template = 'NotificationSound';
}
NotificationSound.template = "NotificationSound";
Registries.Component.add(NotificationSound);
Registries.Component.add(NotificationSound);
return NotificationSound;
});
export default NotificationSound;
@@ -1,311 +1,320 @@
odoo.define('point_of_sale.NumberBuffer', function(require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const { parse } = require('web.field_utils');
const { barcodeService } = require('@barcodes/barcode_service');
const { _t } = require('web.core');
const { Gui } = require('point_of_sale.Gui');
import { useListener } from "@web/core/utils/hooks";
import { parse } from "web.field_utils";
import { barcodeService } from "@barcodes/barcode_service";
import { _t } from "web.core";
import { Gui } from "@point_of_sale/js/Gui";
const { EventBus, onMounted, onWillUnmount, useComponent, useExternalListener } = owl;
const INPUT_KEYS = new Set(
['Delete', 'Backspace', '+1', '+2', '+5', '+10', '+20', '+50'].concat('0123456789+-.,'.split(''))
);
const CONTROL_KEYS = new Set(['Enter', 'Esc']);
const ALLOWED_KEYS = new Set([...INPUT_KEYS, ...CONTROL_KEYS]);
const getDefaultConfig = () => ({
decimalPoint: false,
triggerAtEnter: false,
triggerAtEsc: false,
triggerAtInput: false,
nonKeyboardInputEvent: false,
useWithBarcode: false,
});
const { EventBus, onMounted, onWillUnmount, useComponent, useExternalListener } = owl;
const INPUT_KEYS = new Set(
["Delete", "Backspace", "+1", "+2", "+5", "+10", "+20", "+50"].concat(
"0123456789+-.,".split("")
)
);
const CONTROL_KEYS = new Set(["Enter", "Esc"]);
const ALLOWED_KEYS = new Set([...INPUT_KEYS, ...CONTROL_KEYS]);
const getDefaultConfig = () => ({
decimalPoint: false,
triggerAtEnter: false,
triggerAtEsc: false,
triggerAtInput: false,
nonKeyboardInputEvent: false,
useWithBarcode: false,
});
/**
* This is a singleton.
*
* Only one component can `use` the buffer at a time.
* This is done by keeping track of each component (and its
* corresponding state and config) using a stack (bufferHolderStack).
* The component on top of the stack is the one that currently
* `holds` the buffer.
*
* When the current component is unmounted, the top of the stack
* is popped and NumberBuffer is set up again for the new component
* on top of the stack.
*
* Usage
* =====
* - Activate in the construction of root component. `NumberBuffer.activate()`
* - Use the buffer in a child component by calling `NumberBuffer.use(<config>)`
* in the constructor of the child component.
* - The component that `uses` the buffer has access to the following instance
* methods of the NumberBuffer:
* - get()
* - set(val)
* - reset()
* - getFloat()
* - capture()
*
* Note
* ====
* - No need to instantiate as it is a singleton created before exporting in this module.
*
* Possible Improvements
* =====================
* - Relieve the buffer from responsibility of handling `Enter` and other control keys.
* - Make the constants (ALLOWED_KEYS, etc.) more configurable.
* - Write more integration tests. NumberPopup can be used as test component.
*/
class NumberBuffer extends EventBus {
constructor() {
super();
this.isReset = false;
this.bufferHolderStack = [];
}
/**
* This is a singleton.
*
* Only one component can `use` the buffer at a time.
* This is done by keeping track of each component (and its
* corresponding state and config) using a stack (bufferHolderStack).
* The component on top of the stack is the one that currently
* `holds` the buffer.
*
* When the current component is unmounted, the top of the stack
* is popped and NumberBuffer is set up again for the new component
* on top of the stack.
*
* Usage
* =====
* - Activate in the construction of root component. `NumberBuffer.activate()`
* - Use the buffer in a child component by calling `NumberBuffer.use(<config>)`
* in the constructor of the child component.
* - The component that `uses` the buffer has access to the following instance
* methods of the NumberBuffer:
* - get()
* - set(val)
* - reset()
* - getFloat()
* - capture()
*
* Note
* ====
* - No need to instantiate as it is a singleton created before exporting in this module.
*
* Possible Improvements
* =====================
* - Relieve the buffer from responsibility of handling `Enter` and other control keys.
* - Make the constants (ALLOWED_KEYS, etc.) more configurable.
* - Write more integration tests. NumberPopup can be used as test component.
* @returns {String} value of the buffer, e.g. '-95.79'
*/
class NumberBuffer extends EventBus {
constructor() {
super();
this.isReset = false;
this.bufferHolderStack = [];
}
/**
* @returns {String} value of the buffer, e.g. '-95.79'
*/
get() {
return this.state ? this.state.buffer : null;
}
/**
* Takes a string that is convertible to float, and set it as
* value of the buffer. e.g. val = '2.99';
*
* @param {String} val
*/
set(val) {
this.state.buffer = !isNaN(parseFloat(val)) ? val : '';
this.trigger('buffer-update', this.state.buffer);
}
/**
* Resets the buffer to empty string.
*/
reset() {
this.isReset = true;
this.state.buffer = '';
this.trigger('buffer-update', this.state.buffer);
}
/**
* Calling this function, we immediately invoke the `handler` method
* that handles the contents of the input events buffer (`eventsBuffer`).
* This is helpful when we don't want to wait for the timeout that
* is supposed to invoke the handler.
*/
capture() {
if (this.handler) {
clearTimeout(this._timeout);
this.handler();
delete this.handler;
}
}
/**
* @returns {number} float equivalent of the value of buffer
*/
getFloat() {
return parse.float(this.get());
}
/**
* Add keyup listener to window via the useExternalListener hook.
* When the component calling this is unmounted, the listener is also
* removed from window.
*/
activate() {
this.defaultDecimalPoint = _t.database.parameters.decimal_point;
useExternalListener(window, 'keyup', this._onKeyboardInput.bind(this));
}
/**
* @param {Object} config Use to setup the buffer
* @param {String|null} config.decimalPoint The decimal character.
* @param {String|null} config.triggerAtEnter Event triggered when 'Enter' key is pressed.
* @param {String|null} config.triggerAtEsc Event triggered when 'Esc' key is pressed.
* @param {String|null} config.triggerAtInput Event triggered for every accepted input.
* @param {String|null} config.nonKeyboardInputEvent Also listen to a non-keyboard input event
* that carries a payload of { key }. The key is checked if it is a valid input. If valid,
* the number buffer is modified just as it is modified when a keyboard key is pressed.
* @param {Boolean} config.useWithBarcode Whether this buffer is used with barcode.
* @emits config.triggerAtEnter when 'Enter' key is pressed.
* @emits config.triggerAtEsc when 'Esc' key is pressed.
* @emits config.triggerAtInput when an input is accepted.
*/
use(config) {
this.eventsBuffer = [];
const currentComponent = useComponent();
config = Object.assign(getDefaultConfig(), config);
onMounted(() => {
this.bufferHolderStack.push({
component: currentComponent,
state: config.state ? config.state : { buffer: '', toStartOver: false },
config,
});
this._setUp();
});
onWillUnmount(() => {
this.bufferHolderStack.pop();
this._setUp();
});
// Add listener that accepts non keyboard inputs
if (typeof config.nonKeyboardInputEvent === 'string') {
useListener(config.nonKeyboardInputEvent, this._onNonKeyboardInput.bind(this));
}
}
get _currentBufferHolder() {
return this.bufferHolderStack[this.bufferHolderStack.length - 1];
}
_setUp() {
if (!this._currentBufferHolder) return;
const { component, state, config } = this._currentBufferHolder;
this.component = component;
this.state = state;
this.config = config;
this.decimalPoint = config.decimalPoint || this.defaultDecimalPoint;
this.maxTimeBetweenKeys = this.config.useWithBarcode
? barcodeService.maxTimeBetweenKeysInMs
: 0;
}
_onKeyboardInput(event) {
return this._bufferEvents(this._onInput(event => event.key))(event);
}
_onNonKeyboardInput(event) {
return this._bufferEvents(this._onInput(event => event.detail.key))(event);
}
_bufferEvents(handler) {
return event => {
if (['INPUT', 'TEXTAREA'].includes(event.target.tagName) || !this.eventsBuffer) return;
clearTimeout(this._timeout);
this.eventsBuffer.push(event);
this._timeout = setTimeout(handler, this.maxTimeBetweenKeys);
this.handler = handler
};
}
_onInput(keyAccessor) {
return () => {
if (this.eventsBuffer.length <= 2) {
// Check first the buffer if its contents are all valid
// number input.
for (let event of this.eventsBuffer) {
if (!ALLOWED_KEYS.has(keyAccessor(event))) {
this.eventsBuffer = [];
return;
}
}
// At this point, all the events in buffer
// contains number input. It's now okay to handle
// each input.
for (let event of this.eventsBuffer) {
this._handleInput(keyAccessor(event));
event.preventDefault();
event.stopPropagation();
}
}
this.eventsBuffer = [];
};
}
_handleInput(key) {
if (key === 'Enter' && this.config.triggerAtEnter) {
this.component.trigger(this.config.triggerAtEnter, this.state);
} else if (key === 'Esc' && this.config.triggerAtEsc) {
this.component.trigger(this.config.triggerAtEsc, this.state);
} else if (INPUT_KEYS.has(key)) {
this._updateBuffer(key);
if (this.config.triggerAtInput)
this.component.trigger(this.config.triggerAtInput, { buffer: this.state.buffer, key });
}
}
/**
* Updates the current buffer state using the given input.
* @param {String} input valid input
*/
_updateBuffer(input) {
const isEmpty = val => {
return val === '' || val === null;
};
if (input === undefined || input === null) return;
let isFirstInput = isEmpty(this.state.buffer);
if (input === ',' || input === '.') {
if (this.state.toStartOver) {
this.state.buffer = '';
}
if (isFirstInput) {
this.state.buffer = '0' + this.decimalPoint;
} else if (!this.state.buffer.length || this.state.buffer === '-') {
this.state.buffer += '0' + this.decimalPoint;
} else if (this.state.buffer.indexOf(this.decimalPoint) < 0) {
this.state.buffer = this.state.buffer + this.decimalPoint;
}
} else if (input === 'Delete') {
if (this.isReset) {
this.state.buffer = '';
this.isReset = false;
return;
}
this.state.buffer = isEmpty(this.state.buffer) ? null : '';
} else if (input === 'Backspace') {
if (this.isReset) {
this.state.buffer = '';
this.isReset = false;
return;
}
if (this.state.toStartOver) {
this.state.buffer = '';
}
const buffer = this.state.buffer;
if (isEmpty(buffer)) {
this.state.buffer = null;
} else {
const nCharToRemove = buffer[buffer.length - 1] === this.decimalPoint ? 2 : 1;
this.state.buffer = buffer.substring(0, buffer.length - nCharToRemove);
}
} else if (input === '+') {
if (this.state.buffer[0] === '-') {
this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length);
}
} else if (input === '-') {
if (isFirstInput) {
this.state.buffer = '-0';
} else if (this.state.buffer[0] === '-') {
this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length);
} else {
this.state.buffer = '-' + this.state.buffer;
}
} else if (input[0] === '+' && !isNaN(parseFloat(input))) {
// when input is like '+10', '+50', etc
const inputValue = parse.float(input.slice(1));
const currentBufferValue = this.state.buffer ? parse.float(this.state.buffer) : 0;
this.state.buffer = this.component.env.pos.formatFixed(
inputValue + currentBufferValue
);
} else if (!isNaN(parseInt(input, 10))) {
if (this.state.toStartOver) { // when we want to erase the current buffer for a new value
this.state.buffer = '';
}
if (isFirstInput) {
this.state.buffer = '' + input;
} else if (this.state.buffer.length > 12) {
Gui.playSound('bell');
} else {
this.state.buffer += input;
}
}
if (this.state.buffer === '-') {
this.state.buffer = '';
}
// once an input is accepted and updated the buffer,
// the buffer should not be in reset state anymore.
this.isReset = false;
// it should not be in a start the buffer over state anymore.
this.state.toStartOver = false;
if (this.config.maxValue && this.state.buffer > this.config.maxValue) {
this.state.buffer = this.config.maxValue.toString();
this.config.maxValueReached();
}
this.trigger('buffer-update', this.state.buffer);
get() {
return this.state ? this.state.buffer : null;
}
/**
* Takes a string that is convertible to float, and set it as
* value of the buffer. e.g. val = '2.99';
*
* @param {String} val
*/
set(val) {
this.state.buffer = !isNaN(parseFloat(val)) ? val : "";
this.trigger("buffer-update", this.state.buffer);
}
/**
* Resets the buffer to empty string.
*/
reset() {
this.isReset = true;
this.state.buffer = "";
this.trigger("buffer-update", this.state.buffer);
}
/**
* Calling this function, we immediately invoke the `handler` method
* that handles the contents of the input events buffer (`eventsBuffer`).
* This is helpful when we don't want to wait for the timeout that
* is supposed to invoke the handler.
*/
capture() {
if (this.handler) {
clearTimeout(this._timeout);
this.handler();
delete this.handler;
}
}
/**
* @returns {number} float equivalent of the value of buffer
*/
getFloat() {
return parse.float(this.get());
}
/**
* Add keyup listener to window via the useExternalListener hook.
* When the component calling this is unmounted, the listener is also
* removed from window.
*/
activate() {
this.defaultDecimalPoint = _t.database.parameters.decimal_point;
useExternalListener(window, "keyup", this._onKeyboardInput.bind(this));
}
/**
* @param {Object} config Use to setup the buffer
* @param {String|null} config.decimalPoint The decimal character.
* @param {String|null} config.triggerAtEnter Event triggered when 'Enter' key is pressed.
* @param {String|null} config.triggerAtEsc Event triggered when 'Esc' key is pressed.
* @param {String|null} config.triggerAtInput Event triggered for every accepted input.
* @param {String|null} config.nonKeyboardInputEvent Also listen to a non-keyboard input event
* that carries a payload of { key }. The key is checked if it is a valid input. If valid,
* the number buffer is modified just as it is modified when a keyboard key is pressed.
* @param {Boolean} config.useWithBarcode Whether this buffer is used with barcode.
* @emits config.triggerAtEnter when 'Enter' key is pressed.
* @emits config.triggerAtEsc when 'Esc' key is pressed.
* @emits config.triggerAtInput when an input is accepted.
*/
use(config) {
this.eventsBuffer = [];
const currentComponent = useComponent();
config = Object.assign(getDefaultConfig(), config);
onMounted(() => {
this.bufferHolderStack.push({
component: currentComponent,
state: config.state ? config.state : { buffer: "", toStartOver: false },
config,
});
this._setUp();
});
onWillUnmount(() => {
this.bufferHolderStack.pop();
this._setUp();
});
// Add listener that accepts non keyboard inputs
if (typeof config.nonKeyboardInputEvent === "string") {
useListener(config.nonKeyboardInputEvent, this._onNonKeyboardInput.bind(this));
}
}
get _currentBufferHolder() {
return this.bufferHolderStack[this.bufferHolderStack.length - 1];
}
_setUp() {
if (!this._currentBufferHolder) {
return;
}
const { component, state, config } = this._currentBufferHolder;
this.component = component;
this.state = state;
this.config = config;
this.decimalPoint = config.decimalPoint || this.defaultDecimalPoint;
this.maxTimeBetweenKeys = this.config.useWithBarcode
? barcodeService.maxTimeBetweenKeysInMs
: 0;
}
_onKeyboardInput(event) {
return this._bufferEvents(this._onInput((event) => event.key))(event);
}
_onNonKeyboardInput(event) {
return this._bufferEvents(this._onInput((event) => event.detail.key))(event);
}
_bufferEvents(handler) {
return (event) => {
if (["INPUT", "TEXTAREA"].includes(event.target.tagName) || !this.eventsBuffer) {
return;
}
clearTimeout(this._timeout);
this.eventsBuffer.push(event);
this._timeout = setTimeout(handler, this.maxTimeBetweenKeys);
this.handler = handler;
};
}
_onInput(keyAccessor) {
return () => {
if (this.eventsBuffer.length <= 2) {
// Check first the buffer if its contents are all valid
// number input.
for (const event of this.eventsBuffer) {
if (!ALLOWED_KEYS.has(keyAccessor(event))) {
this.eventsBuffer = [];
return;
}
}
// At this point, all the events in buffer
// contains number input. It's now okay to handle
// each input.
for (const event of this.eventsBuffer) {
this._handleInput(keyAccessor(event));
event.preventDefault();
event.stopPropagation();
}
}
this.eventsBuffer = [];
};
}
_handleInput(key) {
if (key === "Enter" && this.config.triggerAtEnter) {
this.component.trigger(this.config.triggerAtEnter, this.state);
} else if (key === "Esc" && this.config.triggerAtEsc) {
this.component.trigger(this.config.triggerAtEsc, this.state);
} else if (INPUT_KEYS.has(key)) {
this._updateBuffer(key);
if (this.config.triggerAtInput) {
this.component.trigger(this.config.triggerAtInput, {
buffer: this.state.buffer,
key,
});
}
}
}
/**
* Updates the current buffer state using the given input.
* @param {String} input valid input
*/
_updateBuffer(input) {
const isEmpty = (val) => {
return val === "" || val === null;
};
if (input === undefined || input === null) {
return;
}
const isFirstInput = isEmpty(this.state.buffer);
if (input === "," || input === ".") {
if (this.state.toStartOver) {
this.state.buffer = "";
}
if (isFirstInput) {
this.state.buffer = "0" + this.decimalPoint;
} else if (!this.state.buffer.length || this.state.buffer === "-") {
this.state.buffer += "0" + this.decimalPoint;
} else if (this.state.buffer.indexOf(this.decimalPoint) < 0) {
this.state.buffer = this.state.buffer + this.decimalPoint;
}
} else if (input === "Delete") {
if (this.isReset) {
this.state.buffer = "";
this.isReset = false;
return;
}
this.state.buffer = isEmpty(this.state.buffer) ? null : "";
} else if (input === "Backspace") {
if (this.isReset) {
this.state.buffer = "";
this.isReset = false;
return;
}
if (this.state.toStartOver) {
this.state.buffer = "";
}
const buffer = this.state.buffer;
if (isEmpty(buffer)) {
this.state.buffer = null;
} else {
const nCharToRemove = buffer[buffer.length - 1] === this.decimalPoint ? 2 : 1;
this.state.buffer = buffer.substring(0, buffer.length - nCharToRemove);
}
} else if (input === "+") {
if (this.state.buffer[0] === "-") {
this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length);
}
} else if (input === "-") {
if (isFirstInput) {
this.state.buffer = "-0";
} else if (this.state.buffer[0] === "-") {
this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length);
} else {
this.state.buffer = "-" + this.state.buffer;
}
} else if (input[0] === "+" && !isNaN(parseFloat(input))) {
// when input is like '+10', '+50', etc
const inputValue = parse.float(input.slice(1));
const currentBufferValue = this.state.buffer ? parse.float(this.state.buffer) : 0;
this.state.buffer = this.component.env.pos.formatFixed(inputValue + currentBufferValue);
} else if (!isNaN(parseInt(input, 10))) {
if (this.state.toStartOver) {
// when we want to erase the current buffer for a new value
this.state.buffer = "";
}
if (isFirstInput) {
this.state.buffer = "" + input;
} else if (this.state.buffer.length > 12) {
Gui.playSound("bell");
} else {
this.state.buffer += input;
}
}
if (this.state.buffer === "-") {
this.state.buffer = "";
}
// once an input is accepted and updated the buffer,
// the buffer should not be in reset state anymore.
this.isReset = false;
// it should not be in a start the buffer over state anymore.
this.state.toStartOver = false;
return new NumberBuffer();
});
if (this.config.maxValue && this.state.buffer > this.config.maxValue) {
this.state.buffer = this.config.maxValue.toString();
this.config.maxValueReached();
}
this.trigger("buffer-update", this.state.buffer);
}
}
export default new NumberBuffer();
@@ -1,110 +1,114 @@
odoo.define('point_of_sale.SearchBar', function (require) {
'use strict';
/** @odoo-module */
const { useAutofocus, useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useAutofocus, useListener } from "@web/core/utils/hooks";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { useExternalListener, useState } = owl;
const { useExternalListener, useState } = owl;
/**
* This is a simple configurable search bar component. It has search fields
* and selection filter. Search fields allow the users to specify the type
* of their searches. The filter is a dropdown menu for selection. Depending on
* user's action, this component emits corresponding event with the action
* information (payload).
*
* TODO: This component can be made more generic and be able to replace
* all the search bars across pos ui.
*
* @prop {{
* config: {
* searchFields: Map<string, string>,
* filter: { show: boolean, options: Map<string, { text: string, indented: boolean? }> }
* },
* placeholder: string,
* }}
* @emits search @payload { fieldName: string, searchTerm: '' }
* @emits filter-selected @payload { filter: string }
*
* NOTE: The payload of the emitted event is accessible via the `detail`
* field of the event.
*/
class SearchBar extends PosComponent {
setup() {
super.setup();
useAutofocus();
useExternalListener(window, "click", this._hideOptions);
useListener("click-search-field", this._onClickSearchField);
useListener("select-filter", this._onSelectFilter);
this.filterOptionsList = [...this.props.config.filter.options.keys()];
this.searchFieldsList = [...this.props.config.searchFields.keys()];
const defaultSearchFieldId = this.searchFieldsList.indexOf(
this.props.config.defaultSearchDetails.fieldName
);
this.state = useState({
searchInput: this.props.config.defaultSearchDetails.searchTerm || "",
selectedSearchFieldId: defaultSearchFieldId == -1 ? 0 : defaultSearchFieldId,
showSearchFields: false,
showFilterOptions: false,
selectedFilter: this.props.config.defaultFilter || this.filterOptionsList[0],
});
}
_onSelectFilter({ detail: key }) {
this.state.selectedFilter = key;
this.trigger("filter-selected", { filter: this.state.selectedFilter });
}
/**
* This is a simple configurable search bar component. It has search fields
* and selection filter. Search fields allow the users to specify the type
* of their searches. The filter is a dropdown menu for selection. Depending on
* user's action, this component emits corresponding event with the action
* information (payload).
*
* TODO: This component can be made more generic and be able to replace
* all the search bars across pos ui.
*
* @prop {{
* config: {
* searchFields: Map<string, string>,
* filter: { show: boolean, options: Map<string, { text: string, indented: boolean? }> }
* },
* placeholder: string,
* }}
* @emits search @payload { fieldName: string, searchTerm: '' }
* @emits filter-selected @payload { filter: string }
*
* NOTE: The payload of the emitted event is accessible via the `detail`
* field of the event.
* When pressing vertical arrow keys, do not move the input cursor.
*/
class SearchBar extends PosComponent {
setup() {
super.setup();
useAutofocus();
useExternalListener(window, 'click', this._hideOptions);
useListener('click-search-field', this._onClickSearchField);
useListener('select-filter', this._onSelectFilter);
this.filterOptionsList = [...this.props.config.filter.options.keys()];
this.searchFieldsList = [...this.props.config.searchFields.keys()];
const defaultSearchFieldId = this.searchFieldsList.indexOf(
this.props.config.defaultSearchDetails.fieldName
);
this.state = useState({
searchInput: this.props.config.defaultSearchDetails.searchTerm || '',
selectedSearchFieldId: defaultSearchFieldId == -1 ? 0 : defaultSearchFieldId,
showSearchFields: false,
showFilterOptions: false,
selectedFilter: this.props.config.defaultFilter || this.filterOptionsList[0],
});
}
_onSelectFilter({ detail: key }) {
this.state.selectedFilter = key;
this.trigger('filter-selected', { filter: this.state.selectedFilter });
}
/**
* When pressing vertical arrow keys, do not move the input cursor.
*/
onSearchInputKeydown(event) {
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
event.preventDefault();
}
}
/**
* When vertical arrow keys are pressed, select fields for searching.
* When enter key is pressed, trigger search event if there is searchInput.
*/
onSearchInputKeyup(event) {
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
this.state.selectedSearchFieldId = this._fieldIdToSelect(event.key);
} else if (event.key === 'Enter' || this.state.searchInput == '') {
this._onClickSearchField({ detail: this.searchFieldsList[this.state.selectedSearchFieldId] });
} else {
if (this.state.selectedSearchFieldId === -1 && this.searchFieldsList.length) {
this.state.selectedSearchFieldId = 0;
}
this.state.showSearchFields = true;
}
}
/**
* Called when a search field is clicked.
*/
_onClickSearchField({ detail: fieldName }) {
this.state.showSearchFields = false;
this.trigger('search', { fieldName, searchTerm: this.state.searchInput });
}
/**
* Given an arrow key, return the next selectedSearchFieldId.
* E.g. If the selectedSearchFieldId is 1 and ArrowDown is pressed, return 2.
*
* @param {string} key vertical arrow key
*/
_fieldIdToSelect(key) {
const length = this.searchFieldsList.length;
if (!length) return null;
if (this.state.selectedSearchFieldId === -1) return 0;
const current = this.state.selectedSearchFieldId || length;
return (current + (key === 'ArrowDown' ? 1 : -1)) % length;
}
_hideOptions() {
this.state.showFilterOptions = false;
this.state.showSearchFields = false;
onSearchInputKeydown(event) {
if (["ArrowUp", "ArrowDown"].includes(event.key)) {
event.preventDefault();
}
}
SearchBar.template = 'point_of_sale.SearchBar';
Registries.Component.add(SearchBar);
/**
* When vertical arrow keys are pressed, select fields for searching.
* When enter key is pressed, trigger search event if there is searchInput.
*/
onSearchInputKeyup(event) {
if (["ArrowUp", "ArrowDown"].includes(event.key)) {
this.state.selectedSearchFieldId = this._fieldIdToSelect(event.key);
} else if (event.key === "Enter" || this.state.searchInput == "") {
this._onClickSearchField({
detail: this.searchFieldsList[this.state.selectedSearchFieldId],
});
} else {
if (this.state.selectedSearchFieldId === -1 && this.searchFieldsList.length) {
this.state.selectedSearchFieldId = 0;
}
this.state.showSearchFields = true;
}
}
/**
* Called when a search field is clicked.
*/
_onClickSearchField({ detail: fieldName }) {
this.state.showSearchFields = false;
this.trigger("search", { fieldName, searchTerm: this.state.searchInput });
}
/**
* Given an arrow key, return the next selectedSearchFieldId.
* E.g. If the selectedSearchFieldId is 1 and ArrowDown is pressed, return 2.
*
* @param {string} key vertical arrow key
*/
_fieldIdToSelect(key) {
const length = this.searchFieldsList.length;
if (!length) {
return null;
}
if (this.state.selectedSearchFieldId === -1) {
return 0;
}
const current = this.state.selectedSearchFieldId || length;
return (current + (key === "ArrowDown" ? 1 : -1)) % length;
}
_hideOptions() {
this.state.showFilterOptions = false;
this.state.showSearchFields = false;
}
}
SearchBar.template = "point_of_sale.SearchBar";
Registries.Component.add(SearchBar);
return SearchBar;
});
export default SearchBar;
@@ -1,27 +1,25 @@
odoo.define('point_of_sale.Notification', function (require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useListener } from "@web/core/utils/hooks";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onMounted } = owl;
const { onMounted } = owl;
class Notification extends PosComponent {
setup() {
super.setup();
useListener('click', this.closeNotification);
class Notification extends PosComponent {
setup() {
super.setup();
useListener("click", this.closeNotification);
onMounted(() => {
setTimeout(() => {
this.closeNotification();
}, this.props.duration)
});
}
onMounted(() => {
setTimeout(() => {
this.closeNotification();
}, this.props.duration);
});
}
Notification.template = 'Notification';
}
Notification.template = "Notification";
Registries.Component.add(Notification);
Registries.Component.add(Notification);
return Notification;
});
export default Notification;
@@ -1,64 +1,62 @@
odoo.define('point_of_sale.AbstractAwaitablePopup', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const { useBus } = require('@web/core/utils/hooks');
import PosComponent from "@point_of_sale/js/PosComponent";
import { useBus } from "@web/core/utils/hooks";
/**
* Implement this abstract class by extending it like so:
* ```js
* class ConcretePopup extends AbstractAwaitablePopup {
* async getPayload() {
* return 'result';
* }
* }
* ConcretePopup.template = xml`
* <div>
* <button t-on-click="confirm">Okay</button>
* <button t-on-click="cancel">Cancel</button>
* </div>
* `
* ```
*
* The concrete popup can now be instantiated and be awaited for
* the user's response like so:
* ```js
* const { confirmed, payload } = await this.showPopup('ConcretePopup');
* // based on the implementation above,
* // if confirmed, payload = 'result'
* // otherwise, payload = null
* ```
*/
class AbstractAwaitablePopup extends PosComponent {
setup() {
super.setup();
if (this.props.confirmKey) {
useBus(this.env.posbus, `confirm-popup-${this.props.id}`, this.confirm);
}
if (this.props.cancelKey) {
useBus(this.env.posbus, `cancel-popup-${this.props.id}`, this.cancel);
}
/**
* Implement this abstract class by extending it like so:
* ```js
* class ConcretePopup extends AbstractAwaitablePopup {
* async getPayload() {
* return 'result';
* }
* }
* ConcretePopup.template = xml`
* <div>
* <button t-on-click="confirm">Okay</button>
* <button t-on-click="cancel">Cancel</button>
* </div>
* `
* ```
*
* The concrete popup can now be instantiated and be awaited for
* the user's response like so:
* ```js
* const { confirmed, payload } = await this.showPopup('ConcretePopup');
* // based on the implementation above,
* // if confirmed, payload = 'result'
* // otherwise, payload = null
* ```
*/
class AbstractAwaitablePopup extends PosComponent {
setup() {
super.setup();
if (this.props.confirmKey) {
useBus(this.env.posbus, `confirm-popup-${this.props.id}`, this.confirm);
}
async confirm() {
this.env.posbus.trigger('close-popup', {
popupId: this.props.id,
response: { confirmed: true, payload: await this.getPayload() },
});
}
cancel() {
this.env.posbus.trigger('close-popup', {
popupId: this.props.id,
response: { confirmed: false, payload: null },
});
}
/**
* Override this in the concrete popup implementation to set the
* payload when the popup is confirmed.
*/
async getPayload() {
return null;
if (this.props.cancelKey) {
useBus(this.env.posbus, `cancel-popup-${this.props.id}`, this.cancel);
}
}
async confirm() {
this.env.posbus.trigger("close-popup", {
popupId: this.props.id,
response: { confirmed: true, payload: await this.getPayload() },
});
}
cancel() {
this.env.posbus.trigger("close-popup", {
popupId: this.props.id,
response: { confirmed: false, payload: null },
});
}
/**
* Override this in the concrete popup implementation to set the
* payload when the popup is confirmed.
*/
async getPayload() {
return null;
}
}
return AbstractAwaitablePopup;
});
export default AbstractAwaitablePopup;
@@ -1,57 +1,55 @@
odoo.define('point_of_sale.CashMovePopup', function (require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { parse } = require('web.field_utils');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { parse } from "web.field_utils";
const { useRef, useState, onMounted } = owl;
const { useRef, useState, onMounted } = owl;
class CashMovePopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.state = useState({
inputType: 'out', // '' | 'in' | 'out'
inputAmount: '',
inputReason: '',
errorMessage: '',
});
this.inputAmountRef = useRef('input-amount-ref');
onMounted(() => this.inputAmountRef.el.focus());
class CashMovePopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.state = useState({
inputType: "out", // '' | 'in' | 'out'
inputAmount: "",
inputReason: "",
errorMessage: "",
});
this.inputAmountRef = useRef("input-amount-ref");
onMounted(() => this.inputAmountRef.el.focus());
}
confirm() {
try {
parse.float(this.state.inputAmount);
} catch {
this.state.errorMessage = this.env._t("Invalid amount");
return;
}
confirm() {
try {
parse.float(this.state.inputAmount);
} catch {
this.state.errorMessage = this.env._t('Invalid amount');
return;
}
if (this.state.inputAmount < 0) {
this.state.errorMessage = this.env._t('Insert a positive amount');
return;
}
return super.confirm();
if (this.state.inputAmount < 0) {
this.state.errorMessage = this.env._t("Insert a positive amount");
return;
}
_onAmountKeypress(event) {
if (['-', '+'].includes(event.key)) {
event.preventDefault();
}
}
onClickButton(type) {
this.state.inputType = type;
this.state.errorMessage = '';
this.inputAmountRef.el.focus();
}
getPayload() {
return {
amount: parse.float(this.state.inputAmount),
reason: this.state.inputReason.trim(),
type: this.state.inputType,
};
return super.confirm();
}
_onAmountKeypress(event) {
if (["-", "+"].includes(event.key)) {
event.preventDefault();
}
}
CashMovePopup.template = 'point_of_sale.CashMovePopup';
Registries.Component.add(CashMovePopup);
onClickButton(type) {
this.state.inputType = type;
this.state.errorMessage = "";
this.inputAmountRef.el.focus();
}
getPayload() {
return {
amount: parse.float(this.state.inputAmount),
reason: this.state.inputReason.trim(),
type: this.state.inputType,
};
}
}
CashMovePopup.template = "point_of_sale.CashMovePopup";
Registries.Component.add(CashMovePopup);
return CashMovePopup;
});
export default CashMovePopup;
@@ -1,58 +1,58 @@
odoo.define('point_of_sale.CashOpeningPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
const { useState } = owl;
const { useState } = owl;
class CashOpeningPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.manualInputCashCount = null;
this.moneyDetails = null;
this.state = useState({
notes: "",
openingCash: this.env.pos.pos_session.cash_register_balance_start || 0,
});
}
//@override
async confirm() {
this.env.pos.pos_session.cash_register_balance_start = this.state.openingCash;
this.env.pos.pos_session.state = 'opened';
this.rpc({
model: 'pos.session',
method: 'set_cashbox_pos',
args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes],
});
super.confirm();
}
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;
}
}
handleInputChange() {
this.manualInputCashCount = true;
this.moneyDetails = null;
this.state.notes = "";
if (typeof(this.state.openingCash) !== "number") {
this.state.openingCash = 0;
class CashOpeningPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.manualInputCashCount = null;
this.moneyDetails = null;
this.state = useState({
notes: "",
openingCash: this.env.pos.pos_session.cash_register_balance_start || 0,
});
}
//@override
async confirm() {
this.env.pos.pos_session.cash_register_balance_start = this.state.openingCash;
this.env.pos.pos_session.state = "opened";
this.rpc({
model: "pos.session",
method: "set_cashbox_pos",
args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes],
});
super.confirm();
}
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;
}
}
handleInputChange() {
this.manualInputCashCount = true;
this.moneyDetails = null;
this.state.notes = "";
if (typeof this.state.openingCash !== "number") {
this.state.openingCash = 0;
}
}
}
CashOpeningPopup.template = 'CashOpeningPopup';
CashOpeningPopup.defaultProps = { cancelKey: false };
Registries.Component.add(CashOpeningPopup);
CashOpeningPopup.template = "CashOpeningPopup";
CashOpeningPopup.defaultProps = { cancelKey: false };
Registries.Component.add(CashOpeningPopup);
return CashOpeningPopup;
});
export default CashOpeningPopup;
@@ -1,174 +1,192 @@
odoo.define('point_of_sale.ClosePosPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { identifyError } = require('point_of_sale.utils');
const { ConnectionLostError, ConnectionAbortedError} = require('@web/core/network/rpc_service')
const { useState } = owl;
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { identifyError } from "@point_of_sale/js/utils";
import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service";
const { useState } = owl;
class ClosePosPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.manualInputCashCount = false;
this.cashControl = this.env.pos.config.cash_control;
this.closeSessionClicked = false;
this.moneyDetails = null;
Object.assign(this, this.props.info);
this.state = useState({
displayMoneyDetailsPopup: false,
});
Object.assign(this.state, this.props.info.state);
}
//@override
async confirm() {
if (!this.cashControl || !this.hasDifference()) {
this.closeSession();
} else if (this.hasUserAuthority()) {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Payments Difference'),
body: this.env._t('Do you want to accept payments difference and post a profit/loss journal entry?'),
});
if (confirmed) {
this.closeSession();
}
} else {
await this.showPopup('ConfirmPopup', {
title: this.env._t('Payments Difference'),
body: _.str.sprintf(
this.env._t('The maximum difference allowed is %s.\n\
Please contact your manager to accept the closing difference.'),
this.env.pos.format_currency(this.amountAuthorizedDiff)
),
confirmText: this.env._t('OK'),
})
}
}
//@override
async cancel() {
if (this.canCancel()) {
super.cancel();
}
}
async openDetailsPopup() {
const { confirmed, payload } = await this.showPopup('MoneyDetailsPopup', {
moneyDetails: this.moneyDetails,
total: this.manualInputCashCount ? 0 : this.state.payments[this.defaultCashDetails.id].counted,
class ClosePosPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.manualInputCashCount = false;
this.cashControl = this.env.pos.config.cash_control;
this.closeSessionClicked = false;
this.moneyDetails = null;
Object.assign(this, this.props.info);
this.state = useState({
displayMoneyDetailsPopup: false,
});
Object.assign(this.state, this.props.info.state);
}
//@override
async confirm() {
if (!this.cashControl || !this.hasDifference()) {
this.closeSession();
} else if (this.hasUserAuthority()) {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Payments Difference"),
body: this.env._t(
"Do you want to accept payments difference and post a profit/loss journal entry?"
),
});
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;
this.closeSession();
}
}
async downloadSalesReport() {
await this.env.legacyActionManager.do_action('point_of_sale.sale_details_report', {
additional_context: {
active_ids: [this.env.pos.pos_session.id],
},
} else {
await this.showPopup("ConfirmPopup", {
title: this.env._t("Payments Difference"),
body: _.str.sprintf(
this.env._t(
"The maximum difference allowed is %s.\n\
Please contact your manager to accept the closing difference."
),
this.env.pos.format_currency(this.amountAuthorizedDiff)
),
confirmText: this.env._t("OK"),
});
}
handleInputChange(paymentId) {
let expectedAmount;
if (paymentId === this.defaultCashDetails.id) {
this.manualInputCashCount = true;
this.moneyDetails = null;
this.state.notes = '';
expectedAmount = this.defaultCashDetails.amount;
} else {
expectedAmount = this.otherPaymentMethods.find(pm => paymentId === pm.id).amount;
}
this.state.payments[paymentId].difference =
this.env.pos.round_decimals_currency(this.state.payments[paymentId].counted - expectedAmount);
}
hasDifference() {
return Object.entries(this.state.payments).find(pm => pm[1].difference != 0);
}
hasUserAuthority() {
const absDifferences = Object.entries(this.state.payments).map(pm => Math.abs(pm[1].difference));
return this.isManager || this.amountAuthorizedDiff == null || Math.max(...absDifferences) <= this.amountAuthorizedDiff;
}
canCancel() {
return true;
}
closePos() {
this.trigger('close-pos');
}
async closeSession() {
if (!this.closeSessionClicked) {
this.closeSessionClicked = true;
let response;
if (this.cashControl) {
response = await this.rpc({
model: 'pos.session',
method: 'post_closing_cash_details',
args: [this.env.pos.pos_session.id],
kwargs: {
counted_cash: this.state.payments[this.defaultCashDetails.id].counted,
}
})
if (!response.successful) {
return this.handleClosingError(response);
}
}
await this.rpc({
model: 'pos.session',
method: 'update_closing_control_state_session',
args: [this.env.pos.pos_session.id, this.state.notes]
})
try {
const bankPaymentMethodDiffPairs = this.otherPaymentMethods
.filter((pm) => pm.type == 'bank')
.map((pm) => [pm.id, this.state.payments[pm.id].difference]);
response = await this.rpc({
model: 'pos.session',
method: 'close_session_from_ui',
args: [this.env.pos.pos_session.id, bankPaymentMethodDiffPairs],
context: this.env.session.user_context,
});
if (!response.successful) {
return this.handleClosingError(response);
}
window.location = '/web#action=point_of_sale.action_client_pos_menu';
} catch (error) {
const iError = identifyError(error);
if (iError instanceof ConnectionLostError || iError instanceof ConnectionAbortedError) {
await this.showPopup('ErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Cannot close the session when offline.'),
});
} else {
await this.showPopup('ErrorPopup', {
title: this.env._t('Closing session error'),
body: this.env._t(
'An error has occurred when trying to close the session.\n' +
'You will be redirected to the back-end to manually close the session.')
})
window.location = '/web#action=point_of_sale.action_client_pos_menu';
}
}
this.closeSessionClicked = false;
}
}
async handleClosingError(response) {
await this.showPopup('ErrorPopup', {title: 'Error', body: response.message});
if (response.redirect) {
window.location = '/web#action=point_of_sale.action_client_pos_menu';
}
}
_getShowDiff(pm) {
return pm.type == 'bank' && pm.number !== 0;
}
//@override
async cancel() {
if (this.canCancel()) {
super.cancel();
}
}
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", {
additional_context: {
active_ids: [this.env.pos.pos_session.id],
},
});
}
handleInputChange(paymentId) {
let expectedAmount;
if (paymentId === this.defaultCashDetails.id) {
this.manualInputCashCount = true;
this.moneyDetails = null;
this.state.notes = "";
expectedAmount = this.defaultCashDetails.amount;
} else {
expectedAmount = this.otherPaymentMethods.find((pm) => paymentId === pm.id).amount;
}
this.state.payments[paymentId].difference = this.env.pos.round_decimals_currency(
this.state.payments[paymentId].counted - expectedAmount
);
}
hasDifference() {
return Object.entries(this.state.payments).find((pm) => pm[1].difference != 0);
}
hasUserAuthority() {
const absDifferences = Object.entries(this.state.payments).map((pm) =>
Math.abs(pm[1].difference)
);
return (
this.isManager ||
this.amountAuthorizedDiff == null ||
Math.max(...absDifferences) <= this.amountAuthorizedDiff
);
}
canCancel() {
return true;
}
closePos() {
this.trigger("close-pos");
}
async closeSession() {
if (!this.closeSessionClicked) {
this.closeSessionClicked = true;
let response;
if (this.cashControl) {
response = await this.rpc({
model: "pos.session",
method: "post_closing_cash_details",
args: [this.env.pos.pos_session.id],
kwargs: {
counted_cash: this.state.payments[this.defaultCashDetails.id].counted,
},
});
if (!response.successful) {
return this.handleClosingError(response);
}
}
await this.rpc({
model: "pos.session",
method: "update_closing_control_state_session",
args: [this.env.pos.pos_session.id, this.state.notes],
});
try {
const bankPaymentMethodDiffPairs = this.otherPaymentMethods
.filter((pm) => pm.type == "bank")
.map((pm) => [pm.id, this.state.payments[pm.id].difference]);
response = await this.rpc({
model: "pos.session",
method: "close_session_from_ui",
args: [this.env.pos.pos_session.id, bankPaymentMethodDiffPairs],
context: this.env.session.user_context,
});
if (!response.successful) {
return this.handleClosingError(response);
}
window.location = "/web#action=point_of_sale.action_client_pos_menu";
} catch (error) {
const iError = identifyError(error);
if (
iError instanceof ConnectionLostError ||
iError instanceof ConnectionAbortedError
) {
await this.showPopup("ErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t("Cannot close the session when offline."),
});
} else {
await this.showPopup("ErrorPopup", {
title: this.env._t("Closing session error"),
body: this.env._t(
"An error has occurred when trying to close the session.\n" +
"You will be redirected to the back-end to manually close the session."
),
});
window.location = "/web#action=point_of_sale.action_client_pos_menu";
}
}
this.closeSessionClicked = false;
}
}
async handleClosingError(response) {
await this.showPopup("ErrorPopup", { title: "Error", body: response.message });
if (response.redirect) {
window.location = "/web#action=point_of_sale.action_client_pos_menu";
}
}
_getShowDiff(pm) {
return pm.type == "bank" && pm.number !== 0;
}
}
ClosePosPopup.template = 'ClosePosPopup';
Registries.Component.add(ClosePosPopup);
ClosePosPopup.template = "ClosePosPopup";
Registries.Component.add(ClosePosPopup);
return ClosePosPopup;
});
export default ClosePosPopup;
@@ -1,21 +1,19 @@
odoo.define('point_of_sale.ConfirmPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
// formerly ConfirmPopupWidget
class ConfirmPopup extends AbstractAwaitablePopup {}
ConfirmPopup.template = 'ConfirmPopup';
ConfirmPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
title: _lt('Confirm ?'),
body: '',
};
// formerly ConfirmPopupWidget
class ConfirmPopup extends AbstractAwaitablePopup {}
ConfirmPopup.template = "ConfirmPopup";
ConfirmPopup.defaultProps = {
confirmText: _lt("Ok"),
cancelText: _lt("Cancel"),
title: _lt("Confirm ?"),
body: "",
};
Registries.Component.add(ConfirmPopup);
Registries.Component.add(ConfirmPopup);
return ConfirmPopup;
});
export default ConfirmPopup;
@@ -1,28 +1,26 @@
odoo.define('point_of_sale.ControlButtonPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
class ControlButtonPopup extends AbstractAwaitablePopup {
/**
* @param {Object} props
* @param {string} props.startingValue
*/
setup() {
super.setup();
this.controlButtons = this.props.controlButtons;
}
class ControlButtonPopup extends AbstractAwaitablePopup {
/**
* @param {Object} props
* @param {string} props.startingValue
*/
setup() {
super.setup();
this.controlButtons = this.props.controlButtons;
}
ControlButtonPopup.template = 'ControlButtonPopup';
ControlButtonPopup.defaultProps = {
cancelText: _lt('Back'),
controlButtons: [],
confirmKey: false,
};
}
ControlButtonPopup.template = "ControlButtonPopup";
ControlButtonPopup.defaultProps = {
cancelText: _lt("Back"),
controlButtons: [],
confirmKey: false,
};
Registries.Component.add(ControlButtonPopup);
Registries.Component.add(ControlButtonPopup);
return ControlButtonPopup;
});
export default ControlButtonPopup;
@@ -1,29 +1,27 @@
odoo.define('point_of_sale.EditListInput', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
/**
* props {
* createNewItem: callback,
* removeItem: callback,
* item: object,
* }
*/
class EditListInput extends PosComponent {
onKeyup(event) {
if (event.key === "Enter" && event.target.value.trim() !== '') {
this.props.createNewItem();
}
}
onInput(event) {
this.props.onInputChange(this.props.item._id, event.target.value);
/**
* props {
* createNewItem: callback,
* removeItem: callback,
* item: object,
* }
*/
class EditListInput extends PosComponent {
onKeyup(event) {
if (event.key === "Enter" && event.target.value.trim() !== "") {
this.props.createNewItem();
}
}
EditListInput.template = 'EditListInput';
onInput(event) {
this.props.onInputChange(this.props.item._id, event.target.value);
}
}
EditListInput.template = "EditListInput";
Registries.Component.add(EditListInput);
Registries.Component.add(EditListInput);
return EditListInput;
});
export default EditListInput;
@@ -1,109 +1,117 @@
odoo.define('point_of_sale.EditListPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { useAutoFocusToLast } = require('point_of_sale.custom_hooks');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { useAutoFocusToLast } from "@point_of_sale/js/custom_hooks";
import { _lt } from "@web/core/l10n/translation";
const { useState } = owl;
const { useState } = owl;
/**
* Given a array of { id, text }, we show the user this popup to be able to modify this given array.
* (used to replace PackLotLinePopupWidget)
*
* The expected return of showPopup when this popup is used is an array of { _id, [id], text }.
* - _id is the assigned unique identifier for each item.
* - id is the original id. if not provided, then it means that the item is new.
* - text is the modified/unmodified text.
*
* Example:
*
* ```
* -- perhaps inside a click handler --
* // gather the items to edit
* const names = [{ id: 1, text: 'Joseph'}, { id: 2, text: 'Kaykay' }];
*
* // supply the items to the popup and wait for user's response
* // when user pressed `confirm` in the popup, the changes he made will be returned by the showPopup function.
* const { confirmed, payload: newNames } = await this.showPopup('EditListPopup', {
* title: "Can you confirm this item?",
* array: names })
*
* // we then consume the new data. In this example, it is only logged.
* if (confirmed) {
* console.log(newNames);
* // the above might log the following:
* // [{ _id: 1, id: 1, text: 'Joseph Caburnay' }, { _id: 2, id: 2, 'Kaykay' }, { _id: 3, 'James' }]
* // The result showed that the original item with id=1 was changed to have text 'Joseph Caburnay' from 'Joseph'
* // The one with id=2 did not change. And a new item with text='James' is added.
* }
* ```
*/
class EditListPopup extends AbstractAwaitablePopup {
/**
* Given a array of { id, text }, we show the user this popup to be able to modify this given array.
* (used to replace PackLotLinePopupWidget)
*
* The expected return of showPopup when this popup is used is an array of { _id, [id], text }.
* - _id is the assigned unique identifier for each item.
* - id is the original id. if not provided, then it means that the item is new.
* - text is the modified/unmodified text.
*
* Example:
*
* ```
* -- perhaps inside a click handler --
* // gather the items to edit
* const names = [{ id: 1, text: 'Joseph'}, { id: 2, text: 'Kaykay' }];
*
* // supply the items to the popup and wait for user's response
* // when user pressed `confirm` in the popup, the changes he made will be returned by the showPopup function.
* const { confirmed, payload: newNames } = await this.showPopup('EditListPopup', {
* title: "Can you confirm this item?",
* array: names })
*
* // we then consume the new data. In this example, it is only logged.
* if (confirmed) {
* console.log(newNames);
* // the above might log the following:
* // [{ _id: 1, id: 1, text: 'Joseph Caburnay' }, { _id: 2, id: 2, 'Kaykay' }, { _id: 3, 'James' }]
* // The result showed that the original item with id=1 was changed to have text 'Joseph Caburnay' from 'Joseph'
* // The one with id=2 did not change. And a new item with text='James' is added.
* }
* ```
* @param {String} title required title of popup
* @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings
* @param {Boolean} [props.isSingleItem=false] true if only allowed to edit single item (the first item)
*/
class EditListPopup extends AbstractAwaitablePopup {
/**
* @param {String} title required title of popup
* @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings
* @param {Boolean} [props.isSingleItem=false] true if only allowed to edit single item (the first item)
*/
setup() {
super.setup();
this._id = 0;
this.state = useState({ array: this._initialize(this.props.array) });
useAutoFocusToLast();
}
_nextId() {
return this._id++;
}
_emptyItem() {
return {
text: '',
_id: this._nextId(),
};
}
_initialize(array) {
// If no array is provided, we initialize with one empty item.
if (array.length === 0) return [this._emptyItem()];
// 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}));
}
_hasMoreThanOneItem() {
return this.state.array.length > 1;
}
removeItem(itemId) {
this.state.array.splice(
this.state.array.findIndex(item => item._id == itemId),
1
);
}
onInputChange(itemId, text) {
const item = this.state.array.find(elem => elem._id === itemId);
item.text = text;
}
createNewItem() {
if (this.props.isSingleItem) return;
this.state.array.push(this._emptyItem());
}
/**
* @override
*/
getPayload() {
return {
newArray: this.state.array
.filter((item) => item.text.trim() !== '')
.map((item) => Object.assign({}, item)),
};
}
setup() {
super.setup();
this._id = 0;
this.state = useState({ array: this._initialize(this.props.array) });
useAutoFocusToLast();
}
EditListPopup.template = 'EditListPopup';
EditListPopup.defaultProps = {
confirmText: _lt('Add'),
cancelText: _lt('Discard'),
array: [],
isSingleItem: false,
};
_nextId() {
return this._id++;
}
_emptyItem() {
return {
text: "",
_id: this._nextId(),
};
}
_initialize(array) {
// If no array is provided, we initialize with one empty item.
if (array.length === 0) {
return [this._emptyItem()];
}
// 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 }
)
);
}
_hasMoreThanOneItem() {
return this.state.array.length > 1;
}
removeItem(itemId) {
this.state.array.splice(
this.state.array.findIndex((item) => item._id == itemId),
1
);
}
onInputChange(itemId, text) {
const item = this.state.array.find((elem) => elem._id === itemId);
item.text = text;
}
createNewItem() {
if (this.props.isSingleItem) {
return;
}
this.state.array.push(this._emptyItem());
}
/**
* @override
*/
getPayload() {
return {
newArray: this.state.array
.filter((item) => item.text.trim() !== "")
.map((item) => Object.assign({}, item)),
};
}
}
EditListPopup.template = "EditListPopup";
EditListPopup.defaultProps = {
confirmText: _lt("Add"),
cancelText: _lt("Discard"),
array: [],
isSingleItem: false,
};
Registries.Component.add(EditListPopup);
Registries.Component.add(EditListPopup);
return EditListPopup;
});
export default EditListPopup;
@@ -1,27 +1,26 @@
odoo.define('point_of_sale.ErrorBarcodePopup', function(require) {
'use strict';
/** @odoo-module */
const ErrorPopup = require('point_of_sale.ErrorPopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
// formerly ErrorBarcodePopupWidget
class ErrorBarcodePopup extends ErrorPopup {
get translatedMessage() {
return this.env._t(this.props.message);
}
// formerly ErrorBarcodePopupWidget
class ErrorBarcodePopup extends ErrorPopup {
get translatedMessage() {
return this.env._t(this.props.message);
}
ErrorBarcodePopup.template = 'ErrorBarcodePopup';
ErrorBarcodePopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
title: _lt('Error'),
body: '',
message:
_lt('The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode.'),
};
}
ErrorBarcodePopup.template = "ErrorBarcodePopup";
ErrorBarcodePopup.defaultProps = {
confirmText: _lt("Ok"),
cancelText: _lt("Cancel"),
title: _lt("Error"),
body: "",
message: _lt(
"The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode."
),
};
Registries.Component.add(ErrorBarcodePopup);
Registries.Component.add(ErrorBarcodePopup);
return ErrorBarcodePopup;
});
export default ErrorBarcodePopup;
@@ -1,29 +1,27 @@
odoo.define('point_of_sale.ErrorPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
// formerly ErrorPopupWidget
class ErrorPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
owl.onMounted(this.onMounted);
}
onMounted() {
this.playSound('error');
}
// formerly ErrorPopupWidget
class ErrorPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
owl.onMounted(this.onMounted);
}
ErrorPopup.template = 'ErrorPopup';
ErrorPopup.defaultProps = {
confirmText: _lt('Ok'),
title: _lt('Error'),
body: '',
cancelKey: false,
};
onMounted() {
this.playSound("error");
}
}
ErrorPopup.template = "ErrorPopup";
ErrorPopup.defaultProps = {
confirmText: _lt("Ok"),
title: _lt("Error"),
body: "",
cancelKey: false,
};
Registries.Component.add(ErrorPopup);
Registries.Component.add(ErrorPopup);
return ErrorPopup;
});
export default ErrorPopup;
@@ -1,46 +1,44 @@
odoo.define('point_of_sale.ErrorTracebackPopup', function(require) {
'use strict';
/** @odoo-module */
const ErrorPopup = require('point_of_sale.ErrorPopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
// formerly ErrorTracebackPopupWidget
class ErrorTracebackPopup extends ErrorPopup {
get tracebackUrl() {
const blob = new Blob([this.props.body]);
const URL = window.URL || window.webkitURL;
return URL.createObjectURL(blob);
}
get tracebackFilename() {
return `${this.env._t('error')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.txt`;
}
emailTraceback() {
const address = this.env.pos.company.email;
const subject = this.env._t('IMPORTANT: Bug Report From Odoo Point Of Sale');
window.open(
'mailto:' +
address +
'?subject=' +
(subject ? window.encodeURIComponent(subject) : '') +
'&body=' +
(this.props.body ? window.encodeURIComponent(this.props.body) : '')
);
}
// formerly ErrorTracebackPopupWidget
class ErrorTracebackPopup extends ErrorPopup {
get tracebackUrl() {
const blob = new Blob([this.props.body]);
const URL = window.URL || window.webkitURL;
return URL.createObjectURL(blob);
}
ErrorTracebackPopup.template = 'ErrorTracebackPopup';
ErrorTracebackPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
confirmKey: false,
title: _lt('Error with Traceback'),
body: '',
exitButtonIsShown: false,
exitButtonText: _lt('Exit Pos'),
exitButtonTrigger: 'close-pos'
};
get tracebackFilename() {
return `${this.env._t("error")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.txt`;
}
emailTraceback() {
const address = this.env.pos.company.email;
const subject = this.env._t("IMPORTANT: Bug Report From Odoo Point Of Sale");
window.open(
"mailto:" +
address +
"?subject=" +
(subject ? window.encodeURIComponent(subject) : "") +
"&body=" +
(this.props.body ? window.encodeURIComponent(this.props.body) : "")
);
}
}
ErrorTracebackPopup.template = "ErrorTracebackPopup";
ErrorTracebackPopup.defaultProps = {
confirmText: _lt("Ok"),
cancelText: _lt("Cancel"),
confirmKey: false,
title: _lt("Error with Traceback"),
body: "",
exitButtonIsShown: false,
exitButtonText: _lt("Exit Pos"),
exitButtonTrigger: "close-pos",
};
Registries.Component.add(ErrorTracebackPopup);
Registries.Component.add(ErrorTracebackPopup);
return ErrorTracebackPopup;
});
export default ErrorTracebackPopup;
@@ -1,48 +1,58 @@
odoo.define('point_of_sale.MoneyDetailsPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
const { useState } = owl;
const { useState } = owl;
class MoneyDetailsPopup extends AbstractAwaitablePopup{
setup() {
super.setup();
this.currency = this.env.pos.currency;
this.state = useState({
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,
});
}
get firstHalfMoneyDetails() {
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
return moneyDetailsKeys.slice(0, Math.ceil(moneyDetailsKeys.length/2));
}
get lastHalfMoneyDetails() {
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
return moneyDetailsKeys.slice(Math.ceil(moneyDetailsKeys.length/2), moneyDetailsKeys.length);
}
updateMoneyDetailsAmount() {
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);
}
//@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`;
}
})
return { total: this.state.total, moneyDetailsNotes, moneyDetails: { ...this.state.moneyDetails } };
}
class MoneyDetailsPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.currency = this.env.pos.currency;
this.state = useState({
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,
});
}
get firstHalfMoneyDetails() {
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
return moneyDetailsKeys.slice(0, Math.ceil(moneyDetailsKeys.length / 2));
}
get lastHalfMoneyDetails() {
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
return moneyDetailsKeys.slice(
Math.ceil(moneyDetailsKeys.length / 2),
moneyDetailsKeys.length
);
}
updateMoneyDetailsAmount() {
const 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);
}
//@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`;
}
});
return {
total: this.state.total,
moneyDetailsNotes,
moneyDetails: { ...this.state.moneyDetails },
};
}
}
MoneyDetailsPopup.template = 'MoneyDetailsPopup';
Registries.Component.add(MoneyDetailsPopup);
MoneyDetailsPopup.template = "MoneyDetailsPopup";
Registries.Component.add(MoneyDetailsPopup);
return MoneyDetailsPopup;
});
export default MoneyDetailsPopup;
@@ -1,80 +1,80 @@
odoo.define('point_of_sale.NumberPopup', function(require) {
'use strict';
var core = require('web.core');
var _t = core._t;
/** @odoo-module */
import core from "web.core";
var _t = core._t;
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
const { useState } = owl;
const { useState } = owl;
// formerly NumberPopupWidget
class NumberPopup extends AbstractAwaitablePopup {
/**
* @param {Object} props
* @param {Boolean} props.isPassword Show password popup.
* @param {number|null} props.startingValue Starting value of the popup.
* @param {Boolean} props.isInputSelected Input is highlighted and will reset upon a change.
*
* Resolve to { confirmed, payload } when used with showPopup method.
* @confirmed {Boolean}
* @payload {String}
*/
setup() {
super.setup();
useListener('accept-input', this.confirm);
useListener('close-this-popup', this.cancel);
let startingBuffer = '';
if (typeof this.props.startingValue === 'number' && this.props.startingValue > 0) {
startingBuffer = this.props.startingValue.toString().replace('.', this.decimalSeparator);
}
this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected });
NumberBuffer.use({
nonKeyboardInputEvent: 'numpad-click-input',
triggerAtEnter: 'accept-input',
triggerAtEscape: 'close-this-popup',
state: this.state,
});
// formerly NumberPopupWidget
class NumberPopup extends AbstractAwaitablePopup {
/**
* @param {Object} props
* @param {Boolean} props.isPassword Show password popup.
* @param {number|null} props.startingValue Starting value of the popup.
* @param {Boolean} props.isInputSelected Input is highlighted and will reset upon a change.
*
* Resolve to { confirmed, payload } when used with showPopup method.
* @confirmed {Boolean}
* @payload {String}
*/
setup() {
super.setup();
useListener("accept-input", this.confirm);
useListener("close-this-popup", this.cancel);
let startingBuffer = "";
if (typeof this.props.startingValue === "number" && this.props.startingValue > 0) {
startingBuffer = this.props.startingValue
.toString()
.replace(".", this.decimalSeparator);
}
get decimalSeparator() {
return this.env._t.database.parameters.decimal_point;
this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected });
NumberBuffer.use({
nonKeyboardInputEvent: "numpad-click-input",
triggerAtEnter: "accept-input",
triggerAtEscape: "close-this-popup",
state: this.state,
});
}
get decimalSeparator() {
return this.env._t.database.parameters.decimal_point;
}
get inputBuffer() {
if (this.state.buffer === null) {
return "";
}
get inputBuffer() {
if (this.state.buffer === null) {
return '';
}
if (this.props.isPassword) {
return this.state.buffer.replace(/./g, '•');
} else {
return this.state.buffer;
}
}
confirm(event) {
if (NumberBuffer.get()) {
super.confirm();
}
}
sendInput(key) {
this.trigger('numpad-click-input', { key });
}
getPayload() {
return NumberBuffer.get();
if (this.props.isPassword) {
return this.state.buffer.replace(/./g, "•");
} else {
return this.state.buffer;
}
}
NumberPopup.template = 'NumberPopup';
NumberPopup.defaultProps = {
confirmText: _t('Confirm'),
cancelText: _t('Discard'),
title: _t('Confirm ?'),
body: '',
cheap: false,
startingValue: null,
isPassword: false,
};
confirm(event) {
if (NumberBuffer.get()) {
super.confirm();
}
}
sendInput(key) {
this.trigger("numpad-click-input", { key });
}
getPayload() {
return NumberBuffer.get();
}
}
NumberPopup.template = "NumberPopup";
NumberPopup.defaultProps = {
confirmText: _t("Confirm"),
cancelText: _t("Discard"),
title: _t("Confirm ?"),
body: "",
cheap: false,
startingValue: null,
isPassword: false,
};
Registries.Component.add(NumberPopup);
Registries.Component.add(NumberPopup);
return NumberPopup;
});
export default NumberPopup;
@@ -1,30 +1,28 @@
odoo.define('point_of_sale.OfflineErrorPopup', function(require) {
'use strict';
/** @odoo-module */
const ErrorPopup = require('point_of_sale.ErrorPopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import ErrorPopup from "@point_of_sale/js/Popups/ErrorPopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
/**
* This is a special kind of error popup as it introduces
* an option to not show it again.
*/
class OfflineErrorPopup extends ErrorPopup {
dontShowAgain() {
this.constructor.dontShow = true;
this.cancel();
}
/**
* This is a special kind of error popup as it introduces
* an option to not show it again.
*/
class OfflineErrorPopup extends ErrorPopup {
dontShowAgain() {
this.constructor.dontShow = true;
this.cancel();
}
OfflineErrorPopup.template = 'OfflineErrorPopup';
OfflineErrorPopup.dontShow = false;
OfflineErrorPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelText: _lt('Cancel'),
title: _lt('Offline Error'),
body: _lt('Either the server is inaccessible or browser is not connected online.'),
};
}
OfflineErrorPopup.template = "OfflineErrorPopup";
OfflineErrorPopup.dontShow = false;
OfflineErrorPopup.defaultProps = {
confirmText: _lt("Ok"),
cancelText: _lt("Cancel"),
title: _lt("Offline Error"),
body: _lt("Either the server is inaccessible or browser is not connected online."),
};
Registries.Component.add(OfflineErrorPopup);
Registries.Component.add(OfflineErrorPopup);
return OfflineErrorPopup;
});
export default OfflineErrorPopup;
@@ -1,28 +1,26 @@
odoo.define('point_of_sale.OrderImportPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
// formerly OrderImportPopupWidget
class OrderImportPopup extends AbstractAwaitablePopup {
get unpaidSkipped() {
return (
(this.props.report.unpaid_skipped_existing || 0) +
(this.props.report.unpaid_skipped_session || 0)
);
}
getPayload() {}
// formerly OrderImportPopupWidget
class OrderImportPopup extends AbstractAwaitablePopup {
get unpaidSkipped() {
return (
(this.props.report.unpaid_skipped_existing || 0) +
(this.props.report.unpaid_skipped_session || 0)
);
}
OrderImportPopup.template = 'OrderImportPopup';
OrderImportPopup.defaultProps = {
confirmText: _lt('Ok'),
cancelKey: false,
body: '',
};
getPayload() {}
}
OrderImportPopup.template = "OrderImportPopup";
OrderImportPopup.defaultProps = {
confirmText: _lt("Ok"),
cancelKey: false,
body: "",
};
Registries.Component.add(OrderImportPopup);
Registries.Component.add(OrderImportPopup);
return OrderImportPopup;
});
export default OrderImportPopup;
@@ -1,123 +1,126 @@
odoo.define('point_of_sale.PosPopupController', function(require) {
'use strict';
/** @odoo-module */
const Registries = require('point_of_sale.Registries');
const PosComponent = require('point_of_sale.PosComponent');
const { useBus } = require('@web/core/utils/hooks');
import Registries from "@point_of_sale/js/Registries";
import PosComponent from "@point_of_sale/js/PosComponent";
import { useBus } from "@web/core/utils/hooks";
/**
* This component is responsible in controlling the popups. It does so
* by coordinating with them thru the `env.posbus`. The basic steps follow:
* 1. `showPopup` method triggers `show-popup` event resulting to the
* mounting of the requested popup.
* 2. When the popup is shown, the `confirm`/`cancel` method of the popup
* will be called after the popup is used. `confirming`/`cancelling`
* will trigger the `close-popup`, which this component also listens to,
* resulting to closing of the popup.
*
* Furthermore, Pressing `confirmKey`/`cancelKey` which defaults to
* 'Enter'/'Escape', will automatically `confirm`/`cancel` the `topPopup`.
* This behavior is accomplished by listening to `keyup` event of the window.
* When the `confirmKey`/`cancelKey` of the `topPopup` is pressed,
* 'cancel-popup-{top-popup-id}'/'confirm-popup-{top-popup-id}' will be triggered
* and since the popup is listening to that event (@see AbstractAwaitablePopup),
* it will result to the call of `confirm`/`cancel` method.
*
* @typedef {{ id: number, resolve: Function, keepBehind?: boolean, cancelKey?: string, confirmKey?: string }} BasePopupProps
* @typedef {{ name: string, component: AbstractAwaitablePopup, props: BasePopupProps, key: string }} Popup
*/
class PosPopupController extends PosComponent {
setup() {
super.setup();
useBus(this.env.posbus, 'show-popup', this._showPopup);
useBus(this.env.posbus, 'close-popup', this._closePopup);
owl.useExternalListener(window, 'keyup', this._onWindowKeyup);
this.popups = owl.useState([]);
}
_showPopup(event) {
let { id, name, props, resolve } = event.detail;
props = Object.assign(props || {}, { id, resolve });
const component = this.constructor.components[name];
if (!component) {
throw new Error(`'${name}' is not found. Make sure the file is loaded and the component is properly registered using 'Registries.Component.add'.`);
}
if (component.dontShow) {
resolve();
return;
}
this.popups.push({
name,
component,
props: this._constructPopupProps(component, props),
key: `${name}-${id}`,
});
}
_closePopup(event) {
const { popupId, response } = event.detail;
const index = this.popups.findIndex((popup) => popup.props.id == popupId);
if (index != -1) {
const popup = this.popups[index];
popup.props.resolve(response);
this.popups.splice(index, 1);
}
}
_onWindowKeyup(event) {
const eventIsFromInputField = event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA';
const shouldHandleKey = this.topPopup && !eventIsFromInputField;
if (!shouldHandleKey) return;
if (event.key === this.topPopup.props.cancelKey) {
this.env.posbus.trigger(`cancel-popup-${this.topPopup.props.id}`);
} else if (event.key === this.topPopup.props.confirmKey) {
this.env.posbus.trigger(`confirm-popup-${this.topPopup.props.id}`);
}
}
/**
* A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default.
* Also, if it's not the top popup, it is hidden from the view.
* This can be overridden by the default props of the popop component
* and the props used in requesting to show the popup.
*
* @param {AbstractAwaitablePopup} popupComponent
* @param {Object} props
* @returns {BasePopupProps}
*/
_constructPopupProps(popupComponent, props) {
const defaultProps = popupComponent.defaultProps || {};
return Object.assign(
{
keepBehind: false,
cancelKey: 'Escape',
confirmKey: 'Enter',
},
defaultProps,
props
/**
* This component is responsible in controlling the popups. It does so
* by coordinating with them thru the `env.posbus`. The basic steps follow:
* 1. `showPopup` method triggers `show-popup` event resulting to the
* mounting of the requested popup.
* 2. When the popup is shown, the `confirm`/`cancel` method of the popup
* will be called after the popup is used. `confirming`/`cancelling`
* will trigger the `close-popup`, which this component also listens to,
* resulting to closing of the popup.
*
* Furthermore, Pressing `confirmKey`/`cancelKey` which defaults to
* 'Enter'/'Escape', will automatically `confirm`/`cancel` the `topPopup`.
* This behavior is accomplished by listening to `keyup` event of the window.
* When the `confirmKey`/`cancelKey` of the `topPopup` is pressed,
* 'cancel-popup-{top-popup-id}'/'confirm-popup-{top-popup-id}' will be triggered
* and since the popup is listening to that event (@see AbstractAwaitablePopup),
* it will result to the call of `confirm`/`cancel` method.
*
* @typedef {{ id: number, resolve: Function, keepBehind?: boolean, cancelKey?: string, confirmKey?: string }} BasePopupProps
* @typedef {{ name: string, component: AbstractAwaitablePopup, props: BasePopupProps, key: string }} Popup
*/
class PosPopupController extends PosComponent {
setup() {
super.setup();
useBus(this.env.posbus, "show-popup", this._showPopup);
useBus(this.env.posbus, "close-popup", this._closePopup);
owl.useExternalListener(window, "keyup", this._onWindowKeyup);
this.popups = owl.useState([]);
}
_showPopup(event) {
let { id, name, props, resolve } = event.detail;
props = Object.assign(props || {}, { id, resolve });
const component = this.constructor.components[name];
if (!component) {
throw new Error(
`'${name}' is not found. Make sure the file is loaded and the component is properly registered using 'Registries.Component.add'.`
);
}
/**
* @returns {boolean} Hide the element of this component when this returns false.
*/
isShown() {
return this.popups.length > 0;
if (component.dontShow) {
resolve();
return;
}
get topPopup() {
return this.popups[this.popups.length - 1];
}
/**
* By default, only show the top popup. But always show a popup if
* `keepBehind` props is true. Meaning, if you have 2 popups, and
* the bottom popup has `keepBehind = true`, then the bottom popup
* will be visible if it's not blocked in the view by the top popup.
*
* @param {Popup} popup
* @returns {boolean}
*/
shouldShow(popup) {
return this.topPopup === popup || popup.props.keepBehind;
this.popups.push({
name,
component,
props: this._constructPopupProps(component, props),
key: `${name}-${id}`,
});
}
_closePopup(event) {
const { popupId, response } = event.detail;
const index = this.popups.findIndex((popup) => popup.props.id == popupId);
if (index != -1) {
const popup = this.popups[index];
popup.props.resolve(response);
this.popups.splice(index, 1);
}
}
PosPopupController.template = 'point_of_sale.PosPopupController';
Registries.Component.add(PosPopupController);
_onWindowKeyup(event) {
const eventIsFromInputField =
event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA";
const shouldHandleKey = this.topPopup && !eventIsFromInputField;
if (!shouldHandleKey) {
return;
}
return PosPopupController;
});
if (event.key === this.topPopup.props.cancelKey) {
this.env.posbus.trigger(`cancel-popup-${this.topPopup.props.id}`);
} else if (event.key === this.topPopup.props.confirmKey) {
this.env.posbus.trigger(`confirm-popup-${this.topPopup.props.id}`);
}
}
/**
* A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default.
* Also, if it's not the top popup, it is hidden from the view.
* This can be overridden by the default props of the popop component
* and the props used in requesting to show the popup.
*
* @param {AbstractAwaitablePopup} popupComponent
* @param {Object} props
* @returns {BasePopupProps}
*/
_constructPopupProps(popupComponent, props) {
const defaultProps = popupComponent.defaultProps || {};
return Object.assign(
{
keepBehind: false,
cancelKey: "Escape",
confirmKey: "Enter",
},
defaultProps,
props
);
}
/**
* @returns {boolean} Hide the element of this component when this returns false.
*/
isShown() {
return this.popups.length > 0;
}
get topPopup() {
return this.popups[this.popups.length - 1];
}
/**
* By default, only show the top popup. But always show a popup if
* `keepBehind` props is true. Meaning, if you have 2 popups, and
* the bottom popup has `keepBehind = true`, then the bottom popup
* will be visible if it's not blocked in the view by the top popup.
*
* @param {Popup} popup
* @returns {boolean}
*/
shouldShow(popup) {
return this.topPopup === popup || popup.props.keepBehind;
}
}
PosPopupController.template = "point_of_sale.PosPopupController";
Registries.Component.add(PosPopupController);
export default PosPopupController;
@@ -1,93 +1,84 @@
odoo.define('point_of_sale.ProductConfiguratorPopup', function(require) {
'use strict';
/** @odoo-module */
import PosComponent from "@point_of_sale/js/PosComponent";
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
const PosComponent = require('point_of_sale.PosComponent');
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { useState, useSubEnv } = owl;
const { useState, useSubEnv } = owl;
class ProductConfiguratorPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
useSubEnv({ attribute_components: [] });
}
getPayload() {
var selected_attributes = [];
var price_extra = 0.0;
this.env.attribute_components.forEach((attribute_component) => {
let { value, extra } = attribute_component.getValue();
selected_attributes.push(value);
price_extra += extra;
});
return {
selected_attributes,
price_extra,
};
}
}
ProductConfiguratorPopup.template = 'ProductConfiguratorPopup';
Registries.Component.add(ProductConfiguratorPopup);
class BaseProductAttribute extends PosComponent {
setup() {
super.setup();
this.env.attribute_components.push(this);
this.attribute = this.props.attribute;
this.values = this.attribute.values;
this.state = useState({
selected_value: parseFloat(this.values[0].id),
custom_value: '',
});
}
getValue() {
let selected_value = this.values.find((val) => val.id === parseFloat(this.state.selected_value));
let value = selected_value.name;
if (selected_value.is_custom && this.state.custom_value) {
value += `: ${this.state.custom_value}`;
}
return {
value,
extra: selected_value.price_extra
};
}
export class ProductConfiguratorPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
useSubEnv({ attribute_components: [] });
}
class RadioProductAttribute extends BaseProductAttribute {
setup() {
super.setup();
owl.onMounted(this.onMounted);
}
onMounted() {
// With radio buttons `t-model` selects the default input by searching for inputs with
// a matching `value` attribute. In our case, we use `t-att-value` so `value` is
// not found yet and no radio is selected by default.
// We then manually select the first input of each radio attribute.
$(this.el).find('input[type="radio"]:first').prop('checked', true);
}
getPayload() {
var selected_attributes = [];
var price_extra = 0.0;
this.env.attribute_components.forEach((attribute_component) => {
const { value, extra } = attribute_component.getValue();
selected_attributes.push(value);
price_extra += extra;
});
return {
selected_attributes,
price_extra,
};
}
RadioProductAttribute.template = 'RadioProductAttribute';
Registries.Component.add(RadioProductAttribute);
}
ProductConfiguratorPopup.template = "ProductConfiguratorPopup";
Registries.Component.add(ProductConfiguratorPopup);
class SelectProductAttribute extends BaseProductAttribute { }
SelectProductAttribute.template = 'SelectProductAttribute';
Registries.Component.add(SelectProductAttribute);
export class BaseProductAttribute extends PosComponent {
setup() {
super.setup();
this.env.attribute_components.push(this);
class ColorProductAttribute extends BaseProductAttribute {}
ColorProductAttribute.template = 'ColorProductAttribute';
Registries.Component.add(ColorProductAttribute);
this.attribute = this.props.attribute;
this.values = this.attribute.values;
this.state = useState({
selected_value: parseFloat(this.values[0].id),
custom_value: "",
});
}
return {
ProductConfiguratorPopup,
BaseProductAttribute,
RadioProductAttribute,
SelectProductAttribute,
ColorProductAttribute,
};
});
getValue() {
const selected_value = this.values.find(
(val) => val.id === parseFloat(this.state.selected_value)
);
let value = selected_value.name;
if (selected_value.is_custom && this.state.custom_value) {
value += `: ${this.state.custom_value}`;
}
return {
value,
extra: selected_value.price_extra,
};
}
}
export class RadioProductAttribute extends BaseProductAttribute {
setup() {
super.setup();
owl.onMounted(this.onMounted);
}
onMounted() {
// With radio buttons `t-model` selects the default input by searching for inputs with
// a matching `value` attribute. In our case, we use `t-att-value` so `value` is
// not found yet and no radio is selected by default.
// We then manually select the first input of each radio attribute.
$(this.el).find('input[type="radio"]:first').prop("checked", true);
}
}
RadioProductAttribute.template = "RadioProductAttribute";
Registries.Component.add(RadioProductAttribute);
export class SelectProductAttribute extends BaseProductAttribute {}
SelectProductAttribute.template = "SelectProductAttribute";
Registries.Component.add(SelectProductAttribute);
export class ColorProductAttribute extends BaseProductAttribute {}
ColorProductAttribute.template = "ColorProductAttribute";
Registries.Component.add(ColorProductAttribute);
@@ -1,32 +1,31 @@
odoo.define('point_of_sale.ProductInfoPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
/**
* Props:
* {
* info: {object of data}
* }
*/
class ProductInfoPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
Object.assign(this, this.props.info);
}
searchProduct(productName) {
this.env.posbus.trigger('search-product-from-info-popup', productName);
this.cancel()
}
_hasMarginsCostsAccessRights() {
const isAccessibleToEveryUser = this.env.pos.config.is_margins_costs_accessible_to_every_user;
const isCashierManager = this.env.pos.get_cashier().role === 'manager';
return isAccessibleToEveryUser || isCashierManager;
}
/**
* Props:
* {
* info: {object of data}
* }
*/
class ProductInfoPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
Object.assign(this, this.props.info);
}
searchProduct(productName) {
this.env.posbus.trigger("search-product-from-info-popup", productName);
this.cancel();
}
_hasMarginsCostsAccessRights() {
const isAccessibleToEveryUser =
this.env.pos.config.is_margins_costs_accessible_to_every_user;
const isCashierManager = this.env.pos.get_cashier().role === "manager";
return isAccessibleToEveryUser || isCashierManager;
}
}
ProductInfoPopup.template = 'ProductInfoPopup';
ProductInfoPopup.defaultProps= { confirmKey: false };
Registries.Component.add(ProductInfoPopup);
});
ProductInfoPopup.template = "ProductInfoPopup";
ProductInfoPopup.defaultProps = { confirmKey: false };
Registries.Component.add(ProductInfoPopup);
@@ -1,59 +1,57 @@
odoo.define('point_of_sale.SelectionPopup', function (require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
const { useState } = owl;
const { useState } = owl;
// formerly SelectionPopupWidget
class SelectionPopup extends AbstractAwaitablePopup {
/**
* Value of the `item` key of the selected element in the Selection
* Array is the payload of this popup.
*
* @param {Object} props
* @param {String} [props.confirmText='Confirm']
* @param {String} [props.cancelText='Cancel']
* @param {String} [props.title='Select']
* @param {String} [props.body='']
* @param {Array<Selection>} [props.list=[]]
* Selection {
* id: integer,
* label: string,
* isSelected: boolean,
* item: any,
* }
*/
setup() {
super.setup();
this.state = useState({ selectedId: this.props.list.find((item) => item.isSelected) });
}
selectItem(itemId) {
this.state.selectedId = itemId;
this.confirm();
}
/**
* We send as payload of the response the selected item.
*
* @override
*/
getPayload() {
const selected = this.props.list.find((item) => this.state.selectedId === item.id);
return selected && selected.item;
}
// formerly SelectionPopupWidget
class SelectionPopup extends AbstractAwaitablePopup {
/**
* Value of the `item` key of the selected element in the Selection
* Array is the payload of this popup.
*
* @param {Object} props
* @param {String} [props.confirmText='Confirm']
* @param {String} [props.cancelText='Cancel']
* @param {String} [props.title='Select']
* @param {String} [props.body='']
* @param {Array<Selection>} [props.list=[]]
* Selection {
* id: integer,
* label: string,
* isSelected: boolean,
* item: any,
* }
*/
setup() {
super.setup();
this.state = useState({ selectedId: this.props.list.find((item) => item.isSelected) });
}
SelectionPopup.template = 'SelectionPopup';
SelectionPopup.defaultProps = {
cancelText: _lt('Cancel'),
title: _lt('Select'),
body: '',
list: [],
confirmKey: false,
};
selectItem(itemId) {
this.state.selectedId = itemId;
this.confirm();
}
/**
* We send as payload of the response the selected item.
*
* @override
*/
getPayload() {
const selected = this.props.list.find((item) => this.state.selectedId === item.id);
return selected && selected.item;
}
}
SelectionPopup.template = "SelectionPopup";
SelectionPopup.defaultProps = {
cancelText: _lt("Cancel"),
title: _lt("Select"),
body: "",
list: [],
confirmKey: false,
};
Registries.Component.add(SelectionPopup);
Registries.Component.add(SelectionPopup);
return SelectionPopup;
});
export default SelectionPopup;
@@ -1,42 +1,40 @@
odoo.define('point_of_sale.TextAreaPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
const { onMounted, useRef, useState } = owl;
const { onMounted, useRef, useState } = owl;
// formerly TextAreaPopupWidget
// IMPROVEMENT: This code is very similar to TextInputPopup.
// Combining them would reduce the code.
class TextAreaPopup extends AbstractAwaitablePopup {
/**
* @param {Object} props
* @param {string} props.startingValue
*/
setup() {
super.setup();
this.state = useState({ inputValue: this.props.startingValue });
this.inputRef = useRef('input');
onMounted(this.onMounted);
}
onMounted() {
this.inputRef.el.focus();
}
getPayload() {
return this.state.inputValue;
}
// formerly TextAreaPopupWidget
// IMPROVEMENT: This code is very similar to TextInputPopup.
// Combining them would reduce the code.
class TextAreaPopup extends AbstractAwaitablePopup {
/**
* @param {Object} props
* @param {string} props.startingValue
*/
setup() {
super.setup();
this.state = useState({ inputValue: this.props.startingValue });
this.inputRef = useRef("input");
onMounted(this.onMounted);
}
TextAreaPopup.template = 'TextAreaPopup';
TextAreaPopup.defaultProps = {
confirmText: _lt('Add'),
cancelText: _lt('Discard'),
title: '',
body: '',
};
onMounted() {
this.inputRef.el.focus();
}
getPayload() {
return this.state.inputValue;
}
}
TextAreaPopup.template = "TextAreaPopup";
TextAreaPopup.defaultProps = {
confirmText: _lt("Add"),
cancelText: _lt("Discard"),
title: "",
body: "",
};
Registries.Component.add(TextAreaPopup);
Registries.Component.add(TextAreaPopup);
return TextAreaPopup;
});
export default TextAreaPopup;
@@ -1,38 +1,36 @@
odoo.define('point_of_sale.TextInputPopup', function(require) {
'use strict';
/** @odoo-module */
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
const { _lt } = require('@web/core/l10n/translation');
import AbstractAwaitablePopup from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import Registries from "@point_of_sale/js/Registries";
import { _lt } from "@web/core/l10n/translation";
const { onMounted, useRef, useState } = owl;
const { onMounted, useRef, useState } = owl;
// formerly TextInputPopupWidget
class TextInputPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.state = useState({ inputValue: this.props.startingValue });
this.inputRef = useRef('input');
onMounted(this.onMounted);
}
onMounted() {
this.inputRef.el.focus();
}
getPayload() {
return this.state.inputValue;
}
// formerly TextInputPopupWidget
class TextInputPopup extends AbstractAwaitablePopup {
setup() {
super.setup();
this.state = useState({ inputValue: this.props.startingValue });
this.inputRef = useRef("input");
onMounted(this.onMounted);
}
TextInputPopup.template = 'TextInputPopup';
TextInputPopup.defaultProps = {
confirmText: _lt('Confirm'),
cancelText: _lt('Discard'),
title: '',
body: '',
startingValue: '',
placeholder: '',
};
onMounted() {
this.inputRef.el.focus();
}
getPayload() {
return this.state.inputValue;
}
}
TextInputPopup.template = "TextInputPopup";
TextInputPopup.defaultProps = {
confirmText: _lt("Confirm"),
cancelText: _lt("Discard"),
title: "",
body: "",
startingValue: "",
placeholder: "",
};
Registries.Component.add(TextInputPopup);
Registries.Component.add(TextInputPopup);
return TextInputPopup;
});
export default TextInputPopup;
@@ -1,75 +1,73 @@
odoo.define('point_of_sale.PosComponent', function (require) {
'use strict';
/** @odoo-module */
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { onRendered } = owl;
import { LegacyComponent } from "@web/legacy/legacy_component";
const { onRendered } = owl;
let nextId = 0;
let nextId = 0;
class PosComponent extends LegacyComponent {
setup() {
onRendered(() => {
if (this.env.isDebug()) {
console.log('Rendered:', this.constructor.name);
}
});
}
/**
* This function is available to all Components that inherit this class.
* The goal of this function is to show an awaitable dialog (popup) that
* returns a response after user interaction. See the following for quick
* demonstration:
*
* ```
* async getUserName() {
* const userResponse = await this.showPopup(
* 'TextInputPopup',
* { title: 'What is your name?' }
* );
* // at this point, the TextInputPopup is displayed. Depending on how the popup is defined,
* // say the input contains the name, the result of the interaction with the user is
* // saved in `userResponse`.
* console.log(userResponse); // logs { confirmed: true, payload: <name> }
* }
* ```
*
* @param {String} name Name of the popup component
* @param {Object} props Object that will be used to render to popup
*/
showPopup(name, props) {
return new Promise((resolve) => {
this.env.posbus.trigger('show-popup', { name, props, resolve, id: nextId++ });
});
}
showTempScreen(name, props) {
return new Promise((resolve) => {
this.trigger('show-temp-screen', { name, props, resolve });
});
}
showScreen(name, props) {
this.trigger('show-main-screen', { name, props });
}
/**
* @param {String} name 'bell' | 'error'
*/
playSound(name) {
this.trigger('play-sound', name);
}
/**
* Control the SyncNotification component.
* @param {String} status 'connected' | 'connecting' | 'disconnected' | 'error'
* @param {String} pending number of pending orders to sync
*/
setSyncStatus(status, pending) {
this.trigger('set-sync-status', { status, pending });
}
showNotification(message, duration = 2000) {
this.trigger('show-notification', { message, duration });
}
closeNotification() {
this.trigger('close-notification');
}
class PosComponent extends LegacyComponent {
setup() {
onRendered(() => {
if (this.env.isDebug()) {
console.log("Rendered:", this.constructor.name);
}
});
}
/**
* This function is available to all Components that inherit this class.
* The goal of this function is to show an awaitable dialog (popup) that
* returns a response after user interaction. See the following for quick
* demonstration:
*
* ```
* async getUserName() {
* const userResponse = await this.showPopup(
* 'TextInputPopup',
* { title: 'What is your name?' }
* );
* // at this point, the TextInputPopup is displayed. Depending on how the popup is defined,
* // say the input contains the name, the result of the interaction with the user is
* // saved in `userResponse`.
* console.log(userResponse); // logs { confirmed: true, payload: <name> }
* }
* ```
*
* @param {String} name Name of the popup component
* @param {Object} props Object that will be used to render to popup
*/
showPopup(name, props) {
return new Promise((resolve) => {
this.env.posbus.trigger("show-popup", { name, props, resolve, id: nextId++ });
});
}
showTempScreen(name, props) {
return new Promise((resolve) => {
this.trigger("show-temp-screen", { name, props, resolve });
});
}
showScreen(name, props) {
this.trigger("show-main-screen", { name, props });
}
/**
* @param {String} name 'bell' | 'error'
*/
playSound(name) {
this.trigger("play-sound", name);
}
/**
* Control the SyncNotification component.
* @param {String} status 'connected' | 'connecting' | 'disconnected' | 'error'
* @param {String} pending number of pending orders to sync
*/
setSyncStatus(status, pending) {
this.trigger("set-sync-status", { status, pending });
}
showNotification(message, duration = 2000) {
this.trigger("show-notification", { message, duration });
}
closeNotification() {
this.trigger("close-notification");
}
}
return PosComponent;
});
export default PosComponent;
@@ -1,10 +1,6 @@
odoo.define('point_of_sale.PosContext', function (require) {
'use strict';
const { reactive } = owl;
/** @odoo-module */
const { reactive } = owl;
// Create global context objects
// e.g. component.env.device = new Context({ isMobile: false });
return {
orderManagement: reactive({ searchString: '', selectedOrder: null }),
};
});
// Create global context objects
// e.g. component.env.device = new Context({ isMobile: false });
export const orderManagement = reactive({ searchString: "", selectedOrder: null });
@@ -1,27 +1,27 @@
odoo.define('point_of_sale.Registries', function(require) {
'use strict';
/** @odoo-module */
/**
* This definition contains all the instances of ClassRegistry.
*/
/**
* This definition contains all the instances of ClassRegistry.
*/
import ComponentRegistry from "@point_of_sale/js/ComponentRegistry";
import ClassRegistry from "@point_of_sale/js/ClassRegistry";
const ComponentRegistry = require('point_of_sale.ComponentRegistry');
const ClassRegistry = require('point_of_sale.ClassRegistry');
class ModelRegistry extends ClassRegistry {
add(baseClass) {
super.add(baseClass);
/**
* Introduce a static method (`create`) to each base class that can be
* conveniently use to create an instance of the extended version
* of the class.
*/
baseClass.create = (...args) => {
const ExtendedClass = this.get(baseClass);
return new ExtendedClass(...args);
}
}
class ModelRegistry extends ClassRegistry {
add(baseClass) {
super.add(baseClass);
/**
* Introduce a static method (`create`) to each base class that can be
* conveniently use to create an instance of the extended version
* of the class.
*/
baseClass.create = (...args) => {
const ExtendedClass = this.get(baseClass);
return new ExtendedClass(...args);
};
}
}
return { Component: new ComponentRegistry(), Model: new ModelRegistry() };
});
export const Component = new ComponentRegistry();
export const Model = new ModelRegistry();
export default { Component, Model };
@@ -1,135 +1,131 @@
odoo.define('point_of_sale.PartnerDetailsEdit', function(require) {
'use strict';
/** @odoo-module */
const { _t } = require('web.core');
const { getDataURLFromFile } = require('web.utils');
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { _t } from "web.core";
import { getDataURLFromFile } from "web.utils";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onMounted, onWillUnmount } = owl;
const { onMounted, onWillUnmount } = owl;
class PartnerDetailsEdit extends PosComponent {
setup() {
super.setup();
this.intFields = ['country_id', 'state_id', 'property_product_pricelist'];
const partner = this.props.partner;
this.changes = {
'country_id': partner.country_id && partner.country_id[0],
'state_id': partner.state_id && partner.state_id[0],
};
if (!partner.property_product_pricelist)
this.changes['property_product_pricelist'] = this.env.pos.default_pricelist.id;
onMounted(() => {
this.env.bus.on('save-partner', this, this.saveChanges);
});
onWillUnmount(() => {
this.env.bus.off('save-partner', this);
});
class PartnerDetailsEdit extends PosComponent {
setup() {
super.setup();
this.intFields = ["country_id", "state_id", "property_product_pricelist"];
const partner = this.props.partner;
this.changes = {
country_id: partner.country_id && partner.country_id[0],
state_id: partner.state_id && partner.state_id[0],
};
if (!partner.property_product_pricelist) {
this.changes["property_product_pricelist"] = this.env.pos.default_pricelist.id;
}
get partnerImageUrl() {
// We prioritize image_1920 in the `changes` field because we want
// to show the uploaded image without fetching new data from the server.
const partner = this.props.partner;
if (this.changes.image_1920) {
return this.changes.image_1920;
} else if (partner.id) {
return `/web/image?model=res.partner&id=${partner.id}&field=avatar_128&unique=${partner.write_date}`;
} else {
return false;
}
}
/**
* Save to field `changes` all input changes from the form fields.
*/
captureChange(event) {
this.changes[event.target.name] = event.target.value;
}
saveChanges() {
let processedChanges = {};
for (let [key, value] of Object.entries(this.changes)) {
if (this.intFields.includes(key)) {
processedChanges[key] = parseInt(value) || false;
} else {
processedChanges[key] = value;
}
}
if ((!this.props.partner.name && !processedChanges.name) ||
processedChanges.name === '' ){
return this.showPopup('ErrorPopup', {
title: _t('A Customer Name Is Required'),
});
}
processedChanges.id = this.props.partner.id || false;
this.trigger('save-changes', { processedChanges });
}
async uploadImage(event) {
const file = event.target.files[0];
if (!file.type.match(/image.*/)) {
await this.showPopup('ErrorPopup', {
title: this.env._t('Unsupported File Format'),
body: this.env._t(
'Only web-compatible Image formats such as .png or .jpeg are supported.'
),
});
} else {
const imageUrl = await getDataURLFromFile(file);
const loadedImage = await this._loadImage(imageUrl);
if (loadedImage) {
const resizedImage = await this._resizeImage(loadedImage, 800, 600);
this.changes.image_1920 = resizedImage.toDataURL();
// Rerender to reflect the changes in the screen
this.render(true);
}
}
}
_resizeImage(img, maxwidth, maxheight) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var ratio = 1;
if (img.width > maxwidth) {
ratio = maxwidth / img.width;
}
if (img.height * ratio > maxheight) {
ratio = maxheight / img.height;
}
var width = Math.floor(img.width * ratio);
var height = Math.floor(img.height * ratio);
onMounted(() => {
this.env.bus.on("save-partner", this, this.saveChanges);
});
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
return canvas;
}
/**
* Loading image is converted to a Promise to allow await when
* loading an image. It resolves to the loaded image if succesful,
* else, resolves to false.
*
* [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await)
*/
_loadImage(url) {
return new Promise((resolve) => {
const img = new Image();
img.addEventListener('load', () => resolve(img));
img.addEventListener('error', () => {
this.showPopup('ErrorPopup', {
title: this.env._t('Loading Image Error'),
body: this.env._t(
'Encountered error when loading image. Please try again.'
),
});
resolve(false);
});
img.src = url;
});
onWillUnmount(() => {
this.env.bus.off("save-partner", this);
});
}
get partnerImageUrl() {
// We prioritize image_1920 in the `changes` field because we want
// to show the uploaded image without fetching new data from the server.
const partner = this.props.partner;
if (this.changes.image_1920) {
return this.changes.image_1920;
} else if (partner.id) {
return `/web/image?model=res.partner&id=${partner.id}&field=avatar_128&unique=${partner.write_date}`;
} else {
return false;
}
}
PartnerDetailsEdit.template = 'PartnerDetailsEdit';
/**
* Save to field `changes` all input changes from the form fields.
*/
captureChange(event) {
this.changes[event.target.name] = event.target.value;
}
saveChanges() {
const processedChanges = {};
for (const [key, value] of Object.entries(this.changes)) {
if (this.intFields.includes(key)) {
processedChanges[key] = parseInt(value) || false;
} else {
processedChanges[key] = value;
}
}
if ((!this.props.partner.name && !processedChanges.name) || processedChanges.name === "") {
return this.showPopup("ErrorPopup", {
title: _t("A Customer Name Is Required"),
});
}
processedChanges.id = this.props.partner.id || false;
this.trigger("save-changes", { processedChanges });
}
async uploadImage(event) {
const file = event.target.files[0];
if (!file.type.match(/image.*/)) {
await this.showPopup("ErrorPopup", {
title: this.env._t("Unsupported File Format"),
body: this.env._t(
"Only web-compatible Image formats such as .png or .jpeg are supported."
),
});
} else {
const imageUrl = await getDataURLFromFile(file);
const loadedImage = await this._loadImage(imageUrl);
if (loadedImage) {
const resizedImage = await this._resizeImage(loadedImage, 800, 600);
this.changes.image_1920 = resizedImage.toDataURL();
// Rerender to reflect the changes in the screen
this.render(true);
}
}
}
_resizeImage(img, maxwidth, maxheight) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var ratio = 1;
Registries.Component.add(PartnerDetailsEdit);
if (img.width > maxwidth) {
ratio = maxwidth / img.width;
}
if (img.height * ratio > maxheight) {
ratio = maxheight / img.height;
}
var width = Math.floor(img.width * ratio);
var height = Math.floor(img.height * ratio);
return PartnerDetailsEdit;
});
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
return canvas;
}
/**
* Loading image is converted to a Promise to allow await when
* loading an image. It resolves to the loaded image if succesful,
* else, resolves to false.
*
* [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await)
*/
_loadImage(url) {
return new Promise((resolve) => {
const img = new Image();
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => {
this.showPopup("ErrorPopup", {
title: this.env._t("Loading Image Error"),
body: this.env._t("Encountered error when loading image. Please try again."),
});
resolve(false);
});
img.src = url;
});
}
}
PartnerDetailsEdit.template = "PartnerDetailsEdit";
Registries.Component.add(PartnerDetailsEdit);
export default PartnerDetailsEdit;
@@ -1,24 +1,22 @@
odoo.define('point_of_sale.PartnerLine', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class PartnerLine extends PosComponent {
get highlight() {
return this._isPartnerSelected ? 'highlight' : '';
}
get shortAddress() {
const { partner } = this.props;
return [partner.zip, partner.city, partner.state_id[1]].filter(field => field).join(', ');
}
get _isPartnerSelected() {
return this.props.partner === this.props.selectedPartner;
}
class PartnerLine extends PosComponent {
get highlight() {
return this._isPartnerSelected ? "highlight" : "";
}
PartnerLine.template = 'PartnerLine';
get shortAddress() {
const { partner } = this.props;
return [partner.zip, partner.city, partner.state_id[1]].filter((field) => field).join(", ");
}
get _isPartnerSelected() {
return this.props.partner === this.props.selectedPartner;
}
}
PartnerLine.template = "PartnerLine";
Registries.Component.add(PartnerLine);
Registries.Component.add(PartnerLine);
return PartnerLine;
});
export default PartnerLine;
@@ -1,216 +1,228 @@
odoo.define('point_of_sale.PartnerListScreen', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { isConnectionError } = require('point_of_sale.utils');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { isConnectionError } from "@point_of_sale/js/utils";
const { debounce } = require("@web/core/utils/timing");
const { useListener } = require("@web/core/utils/hooks");
import { debounce } from "@web/core/utils/timing";
import { useListener } from "@web/core/utils/hooks";
const { onWillUnmount, useRef, onMounted } = owl;
const { onWillUnmount, useRef, onMounted } = owl;
/**
* Render this screen using `showTempScreen` to select partner.
* When the shown screen is confirmed ('Set Customer' or 'Deselect Customer'
* button is clicked), the call to `showTempScreen` resolves to the
* selected partner. E.g.
*
* ```js
* const { confirmed, payload: selectedPartner } = await showTempScreen('PartnerListScreen');
* if (confirmed) {
* // do something with the selectedPartner
* }
* ```
*
* @props partner - originally selected partner
*/
class PartnerListScreen extends PosComponent {
setup() {
super.setup();
useListener('click-save', () => this.env.bus.trigger('save-partner'));
useListener('save-changes', this.saveChanges);
this.searchWordInputRef = useRef('search-word-input-partner');
/**
* Render this screen using `showTempScreen` to select partner.
* When the shown screen is confirmed ('Set Customer' or 'Deselect Customer'
* button is clicked), the call to `showTempScreen` resolves to the
* selected partner. E.g.
*
* ```js
* const { confirmed, payload: selectedPartner } = await showTempScreen('PartnerListScreen');
* if (confirmed) {
* // do something with the selectedPartner
* }
* ```
*
* @props partner - originally selected partner
*/
class PartnerListScreen extends PosComponent {
setup() {
super.setup();
useListener("click-save", () => this.env.bus.trigger("save-partner"));
useListener("save-changes", this.saveChanges);
this.searchWordInputRef = useRef("search-word-input-partner");
// We are not using useState here because the object
// passed to useState converts the object and its contents
// to Observer proxy. Not sure of the side-effects of making
// a persistent object, such as pos, into Observer. But it
// is better to be safe.
this.state = {
query: null,
selectedPartner: this.props.partner,
detailIsShown: false,
editModeProps: {
partner: null,
},
};
this.updatePartnerList = debounce(this.updatePartnerList, 70);
onWillUnmount(this.updatePartnerList.cancel);
onMounted(() => {
if(!this.env.pos.config.limited_partners_loading)
this.env.pos.isEveryPartnerLoaded = true;
});
}
// Lifecycle hooks
back() {
if(this.state.detailIsShown) {
this.state.detailIsShown = false;
this.render(true);
} else {
this.props.resolve({ confirmed: false, payload: false });
this.trigger('close-temp-screen');
// We are not using useState here because the object
// passed to useState converts the object and its contents
// to Observer proxy. Not sure of the side-effects of making
// a persistent object, such as pos, into Observer. But it
// is better to be safe.
this.state = {
query: null,
selectedPartner: this.props.partner,
detailIsShown: false,
editModeProps: {
partner: null,
},
};
this.updatePartnerList = debounce(this.updatePartnerList, 70);
onWillUnmount(this.updatePartnerList.cancel);
onMounted(() => {
if (!this.env.pos.config.limited_partners_loading) {
this.env.pos.isEveryPartnerLoaded = true;
}
}
confirm() {
this.props.resolve({ confirmed: true, payload: this.state.selectedPartner });
this.trigger('close-temp-screen');
}
activateEditMode() {
this.state.detailIsShown = true;
});
}
// Lifecycle hooks
back() {
if (this.state.detailIsShown) {
this.state.detailIsShown = false;
this.render(true);
}
// Getters
get currentOrder() {
return this.env.pos.get_order();
}
get partners() {
let res;
if (this.state.query && this.state.query.trim() !== '') {
res = this.env.pos.db.search_partner(this.state.query.trim());
} else {
res = this.env.pos.db.get_partners_sorted(1000);
}
res.sort(function (a, b) { return (a.name || '').localeCompare(b.name || '') });
// the selected partner (if any) is displayed at the top of the list
if (this.state.selectedPartner) {
let indexOfSelectedPartner = res.findIndex( partner =>
partner.id === this.state.selectedPartner.id
);
if (indexOfSelectedPartner !== -1) {
res.splice(indexOfSelectedPartner, 1);
res.unshift(this.state.selectedPartner);
}
}
return res
}
get isBalanceDisplayed() {
return false;
}
get partnerLink() {
return `/web#model=res.partner&id=${this.state.editModeProps.partner.id}`;
}
// Methods
async _onPressEnterKey() {
if (!this.state.query) return;
if (!this.env.pos.isEveryPartnerLoaded) {
const result = await this.searchPartner();
this.showNotification(
_.str.sprintf(this.env._t('%s customer(s) found for "%s".'),
result.length,
this.state.query)
, 3000);
if(!result.length) this._clearSearch();
}
}
_clearSearch() {
this.searchWordInputRef.el.value = '';
this.state.query = '';
this.render(true);
}
// We declare this event handler as a debounce function in
// order to lower its trigger rate.
async updatePartnerList(event) {
this.state.query = event.target.value;
if (event.code === 'Enter') {
this._onPressEnterKey();
} else {
this.render(true);
}
}
clickPartner(partner) {
if (this.state.selectedPartner && this.state.selectedPartner.id === partner.id) {
this.state.selectedPartner = null;
} else {
this.state.selectedPartner = partner;
}
this.confirm();
}
editPartner(partner) {
this.state.editModeProps.partner = partner;
this.activateEditMode();
}
createPartner() {
// initialize the edit screen with default details about country & state
this.state.editModeProps.partner = {
country_id: this.env.pos.company.country_id,
state_id: this.env.pos.company.state_id,
}
this.activateEditMode();
}
async saveChanges(event) {
try {
let partnerId = await this.rpc({
model: 'res.partner',
method: 'create_from_ui',
args: [event.detail.processedChanges],
});
await this.env.pos.load_new_partners();
this.state.selectedPartner = this.env.pos.db.get_partner_by_id(partnerId);
this.confirm();
} catch (error) {
if (isConnectionError(error)) {
await this.showPopup('OfflineErrorPopup', {
title: this.env._t('Offline'),
body: this.env._t('Unable to save changes.'),
});
} else {
throw error;
}
}
}
async searchPartner() {
let result = await this.getNewPartners();
this.env.pos.addPartners(result);
if (!this.env.pos.isEveryPartnerLoaded) await this.env.pos.updateIsEveryPartnerLoaded();
this.render(true);
return result;
}
async getNewPartners() {
let domain = [];
if(this.state.query) {
domain = ['|', ["name", "ilike", this.state.query + "%"],
["parent_name", "ilike", this.state.query + "%"]];
}
const result = await this.env.services.rpc(
{
model: 'pos.session',
method: 'get_pos_ui_res_partner_by_params',
args: [
[odoo.pos_session_id],
{
domain,
limit: 10,
},
],
context: this.env.session.user_context,
},
{
timeout: 3000,
shadow: true,
}
);
return result;
} else {
this.props.resolve({ confirmed: false, payload: false });
this.trigger("close-temp-screen");
}
}
PartnerListScreen.template = 'PartnerListScreen';
confirm() {
this.props.resolve({ confirmed: true, payload: this.state.selectedPartner });
this.trigger("close-temp-screen");
}
activateEditMode() {
this.state.detailIsShown = true;
this.render(true);
}
// Getters
Registries.Component.add(PartnerListScreen);
get currentOrder() {
return this.env.pos.get_order();
}
return PartnerListScreen;
});
get partners() {
let res;
if (this.state.query && this.state.query.trim() !== "") {
res = this.env.pos.db.search_partner(this.state.query.trim());
} else {
res = this.env.pos.db.get_partners_sorted(1000);
}
res.sort(function (a, b) {
return (a.name || "").localeCompare(b.name || "");
});
// the selected partner (if any) is displayed at the top of the list
if (this.state.selectedPartner) {
const indexOfSelectedPartner = res.findIndex(
(partner) => partner.id === this.state.selectedPartner.id
);
if (indexOfSelectedPartner !== -1) {
res.splice(indexOfSelectedPartner, 1);
res.unshift(this.state.selectedPartner);
}
}
return res;
}
get isBalanceDisplayed() {
return false;
}
get partnerLink() {
return `/web#model=res.partner&id=${this.state.editModeProps.partner.id}`;
}
// Methods
async _onPressEnterKey() {
if (!this.state.query) {
return;
}
if (!this.env.pos.isEveryPartnerLoaded) {
const result = await this.searchPartner();
this.showNotification(
_.str.sprintf(
this.env._t('%s customer(s) found for "%s".'),
result.length,
this.state.query
),
3000
);
if (!result.length) {
this._clearSearch();
}
}
}
_clearSearch() {
this.searchWordInputRef.el.value = "";
this.state.query = "";
this.render(true);
}
// We declare this event handler as a debounce function in
// order to lower its trigger rate.
async updatePartnerList(event) {
this.state.query = event.target.value;
if (event.code === "Enter") {
this._onPressEnterKey();
} else {
this.render(true);
}
}
clickPartner(partner) {
if (this.state.selectedPartner && this.state.selectedPartner.id === partner.id) {
this.state.selectedPartner = null;
} else {
this.state.selectedPartner = partner;
}
this.confirm();
}
editPartner(partner) {
this.state.editModeProps.partner = partner;
this.activateEditMode();
}
createPartner() {
// initialize the edit screen with default details about country & state
this.state.editModeProps.partner = {
country_id: this.env.pos.company.country_id,
state_id: this.env.pos.company.state_id,
};
this.activateEditMode();
}
async saveChanges(event) {
try {
const partnerId = await this.rpc({
model: "res.partner",
method: "create_from_ui",
args: [event.detail.processedChanges],
});
await this.env.pos.load_new_partners();
this.state.selectedPartner = this.env.pos.db.get_partner_by_id(partnerId);
this.confirm();
} catch (error) {
if (isConnectionError(error)) {
await this.showPopup("OfflineErrorPopup", {
title: this.env._t("Offline"),
body: this.env._t("Unable to save changes."),
});
} else {
throw error;
}
}
}
async searchPartner() {
const result = await this.getNewPartners();
this.env.pos.addPartners(result);
if (!this.env.pos.isEveryPartnerLoaded) {
await this.env.pos.updateIsEveryPartnerLoaded();
}
this.render(true);
return result;
}
async getNewPartners() {
let domain = [];
if (this.state.query) {
domain = [
"|",
["name", "ilike", this.state.query + "%"],
["parent_name", "ilike", this.state.query + "%"],
];
}
const result = await this.env.services.rpc(
{
model: "pos.session",
method: "get_pos_ui_res_partner_by_params",
args: [
[odoo.pos_session_id],
{
domain,
limit: 10,
},
],
context: this.env.session.user_context,
},
{
timeout: 3000,
shadow: true,
}
);
return result;
}
}
PartnerListScreen.template = "PartnerListScreen";
Registries.Component.add(PartnerListScreen);
export default PartnerListScreen;
@@ -1,17 +1,15 @@
odoo.define('point_of_sale.PSNumpadInputButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class PSNumpadInputButton extends PosComponent {
get _class() {
return this.props.changeClassTo || 'input-button number-char';
}
class PSNumpadInputButton extends PosComponent {
get _class() {
return this.props.changeClassTo || "input-button number-char";
}
PSNumpadInputButton.template = 'PSNumpadInputButton';
}
PSNumpadInputButton.template = "PSNumpadInputButton";
Registries.Component.add(PSNumpadInputButton);
Registries.Component.add(PSNumpadInputButton);
return PSNumpadInputButton;
});
export default PSNumpadInputButton;
@@ -1,456 +1,499 @@
odoo.define('point_of_sale.PaymentScreen', function (require) {
'use strict';
/** @odoo-module */
const { parse } = require('web.field_utils');
const PosComponent = require('point_of_sale.PosComponent');
const { useErrorHandlers } = require('point_of_sale.custom_hooks');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
const { isConnectionError } = require('point_of_sale.utils');
const utils = require('web.utils');
import { parse } from "web.field_utils";
import PosComponent from "@point_of_sale/js/PosComponent";
import { useErrorHandlers } from "@point_of_sale/js/custom_hooks";
import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
import { isConnectionError } from "@point_of_sale/js/utils";
import utils from "web.utils";
class PaymentScreen extends PosComponent {
setup() {
super.setup();
useListener('delete-payment-line', this.deletePaymentLine);
useListener('select-payment-line', this.selectPaymentLine);
useListener('new-payment-line', this.addNewPaymentLine);
useListener('update-selected-paymentline', this._updateSelectedPaymentline);
useListener('send-payment-request', this._sendPaymentRequest);
useListener('send-payment-cancel', this._sendPaymentCancel);
useListener('send-payment-reverse', this._sendPaymentReverse);
useListener('send-force-done', this._sendForceDone);
useListener('validate-order', () => this.validateOrder(false));
this.payment_methods_from_config = this.env.pos.payment_methods.filter(method => this.env.pos.config.payment_method_ids.includes(method.id));
NumberBuffer.use(this._getNumberBufferConfig);
useErrorHandlers();
this.payment_interface = null;
this.error = false;
class PaymentScreen extends PosComponent {
setup() {
super.setup();
useListener("delete-payment-line", this.deletePaymentLine);
useListener("select-payment-line", this.selectPaymentLine);
useListener("new-payment-line", this.addNewPaymentLine);
useListener("update-selected-paymentline", this._updateSelectedPaymentline);
useListener("send-payment-request", this._sendPaymentRequest);
useListener("send-payment-cancel", this._sendPaymentCancel);
useListener("send-payment-reverse", this._sendPaymentReverse);
useListener("send-force-done", this._sendForceDone);
useListener("validate-order", () => this.validateOrder(false));
this.payment_methods_from_config = this.env.pos.payment_methods.filter((method) =>
this.env.pos.config.payment_method_ids.includes(method.id)
);
NumberBuffer.use(this._getNumberBufferConfig);
useErrorHandlers();
this.payment_interface = null;
this.error = false;
}
showMaxValueError() {
this.showPopup("ErrorPopup", {
title: this.env._t("Maximum value reached"),
body: this.env._t(
"The amount cannot be higher than the due amount if you don't have a cash payment method configured."
),
});
}
get _getNumberBufferConfig() {
const config = {
// The numberBuffer listens to this event to update its state.
// Basically means 'update the buffer when this event is triggered'
nonKeyboardInputEvent: "input-from-numpad",
// When the buffer is updated, trigger this event.
// Note that the component listens to it.
triggerAtInput: "update-selected-paymentline",
};
// Check if pos has a cash payment method
const hasCashPaymentMethod = this.payment_methods_from_config.some(
(method) => method.type === "cash"
);
if (!hasCashPaymentMethod) {
config["maxValue"] = this.currentOrder.get_due();
config["maxValueReached"] = this.showMaxValueError.bind(this);
}
showMaxValueError() {
this.showPopup('ErrorPopup', {
title: this.env._t('Maximum value reached'),
body: this.env._t('The amount cannot be higher than the due amount if you don\'t have a cash payment method configured.')
return config;
}
get currentOrder() {
return this.env.pos.get_order();
}
get paymentLines() {
return this.currentOrder.get_paymentlines();
}
get selectedPaymentLine() {
return this.currentOrder.selected_paymentline;
}
async selectPartner() {
// IMPROVEMENT: This code snippet is repeated multiple times.
// Maybe it's better to create a function for it.
const currentPartner = this.currentOrder.get_partner();
const { confirmed, payload: newPartner } = await this.showTempScreen("PartnerListScreen", {
partner: currentPartner,
});
if (confirmed) {
this.currentOrder.set_partner(newPartner);
this.currentOrder.updatePricelist(newPartner);
}
}
addNewPaymentLine({ detail: paymentMethod }) {
// original function: click_paymentmethods
const result = this.currentOrder.add_paymentline(paymentMethod);
if (result) {
NumberBuffer.reset();
return true;
} else {
this.showPopup("ErrorPopup", {
title: this.env._t("Error"),
body: this.env._t("There is already an electronic payment in progress."),
});
return false;
}
get _getNumberBufferConfig() {
let config = {
// The numberBuffer listens to this event to update its state.
// Basically means 'update the buffer when this event is triggered'
nonKeyboardInputEvent: 'input-from-numpad',
// When the buffer is updated, trigger this event.
// Note that the component listens to it.
triggerAtInput: 'update-selected-paymentline',
};
// Check if pos has a cash payment method
const hasCashPaymentMethod = this.payment_methods_from_config.some(
(method) => method.type === 'cash'
);
}
_updateSelectedPaymentline() {
if (this.paymentLines.every((line) => line.paid)) {
this.currentOrder.add_paymentline(this.payment_methods_from_config[0]);
}
if (!this.selectedPaymentLine) {
return;
} // do nothing if no selected payment line
// disable changing amount on paymentlines with running or done payments on a payment terminal
const payment_terminal = this.selectedPaymentLine.payment_method.payment_terminal;
if (
payment_terminal &&
!["pending", "retry"].includes(this.selectedPaymentLine.get_payment_status())
) {
return;
}
if (NumberBuffer.get() === null) {
this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } });
} else {
this.selectedPaymentLine.set_amount(NumberBuffer.getFloat());
}
}
toggleIsToInvoice() {
// click_invoice
this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice());
this.render(true);
}
openCashbox() {
this.env.proxy.printer.open_cashbox();
}
async addTip() {
// click_tip
const tip = this.currentOrder.get_tip();
const change = this.currentOrder.get_change();
const value = tip === 0 && change > 0 ? change : tip;
if (!hasCashPaymentMethod) {
config['maxValue'] = this.currentOrder.get_due();
config['maxValueReached'] = this.showMaxValueError.bind(this);
}
const { confirmed, payload } = await this.showPopup("NumberPopup", {
title: tip ? this.env._t("Change Tip") : this.env._t("Add Tip"),
startingValue: value,
isInputSelected: true,
});
return config;
if (confirmed) {
this.currentOrder.set_tip(parse.float(payload));
}
get currentOrder() {
return this.env.pos.get_order();
}
get paymentLines() {
return this.currentOrder.get_paymentlines();
}
get selectedPaymentLine() {
return this.currentOrder.selected_paymentline;
}
async selectPartner() {
// IMPROVEMENT: This code snippet is repeated multiple times.
// Maybe it's better to create a function for it.
const currentPartner = this.currentOrder.get_partner();
const { confirmed, payload: newPartner } = await this.showTempScreen(
'PartnerListScreen',
{ partner: currentPartner }
);
if (confirmed) {
this.currentOrder.set_partner(newPartner);
this.currentOrder.updatePricelist(newPartner);
}
}
addNewPaymentLine({ detail: paymentMethod }) {
// original function: click_paymentmethods
let result = this.currentOrder.add_paymentline(paymentMethod);
if (result){
NumberBuffer.reset();
return true;
}
else{
this.showPopup('ErrorPopup', {
title: this.env._t('Error'),
body: this.env._t('There is already an electronic payment in progress.'),
});
return false;
}
}
_updateSelectedPaymentline() {
if (this.paymentLines.every((line) => line.paid)) {
this.currentOrder.add_paymentline(this.payment_methods_from_config[0]);
}
if (!this.selectedPaymentLine) return; // do nothing if no selected payment line
// disable changing amount on paymentlines with running or done payments on a payment terminal
const payment_terminal = this.selectedPaymentLine.payment_method.payment_terminal;
if (
payment_terminal &&
!['pending', 'retry'].includes(this.selectedPaymentLine.get_payment_status())
) {
return;
}
if (NumberBuffer.get() === null) {
this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } });
} else {
this.selectedPaymentLine.set_amount(NumberBuffer.getFloat());
}
}
toggleIsToInvoice() {
// click_invoice
this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice());
this.render(true);
}
openCashbox() {
this.env.proxy.printer.open_cashbox();
}
async addTip() {
// click_tip
const tip = this.currentOrder.get_tip();
const change = this.currentOrder.get_change();
let value = tip === 0 && change > 0 ? change : tip;
}
toggleIsToShip() {
// click_ship
this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship());
this.render(true);
}
deletePaymentLine(event) {
var self = this;
const { cid } = event.detail;
const line = this.paymentLines.find((line) => line.cid === cid);
const { confirmed, payload } = await this.showPopup('NumberPopup', {
title: tip ? this.env._t('Change Tip') : this.env._t('Add Tip'),
startingValue: value,
isInputSelected: true,
});
if (confirmed) {
this.currentOrder.set_tip(parse.float(payload));
}
}
toggleIsToShip() {
// click_ship
this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship());
this.render(true);
}
deletePaymentLine(event) {
var self = this;
const { cid } = event.detail;
const line = this.paymentLines.find((line) => line.cid === cid);
// If a paymentline with a payment terminal linked to
// it is removed, the terminal should get a cancel
// request.
if (['waiting', 'waitingCard', 'timeout'].includes(line.get_payment_status())) {
line.set_payment_status('waitingCancel');
line.payment_method.payment_terminal.send_payment_cancel(this.currentOrder, cid).then(function() {
// If a paymentline with a payment terminal linked to
// it is removed, the terminal should get a cancel
// request.
if (["waiting", "waitingCard", "timeout"].includes(line.get_payment_status())) {
line.set_payment_status("waitingCancel");
line.payment_method.payment_terminal
.send_payment_cancel(this.currentOrder, cid)
.then(function () {
self.currentOrder.remove_paymentline(line);
NumberBuffer.reset();
self.render(true);
})
}
else if (line.get_payment_status() !== 'waitingCancel') {
this.currentOrder.remove_paymentline(line);
NumberBuffer.reset();
this.render(true);
}
}
selectPaymentLine(event) {
const { cid } = event.detail;
const line = this.paymentLines.find((line) => line.cid === cid);
this.currentOrder.select_paymentline(line);
});
} else if (line.get_payment_status() !== "waitingCancel") {
this.currentOrder.remove_paymentline(line);
NumberBuffer.reset();
this.render(true);
}
async validateOrder(isForceValidate) {
if(this.env.pos.config.cash_rounding) {
if(!this.env.pos.get_order().check_paymentlines_rounding()) {
this.showPopup('ErrorPopup', {
title: this.env._t('Rounding error in payment lines'),
body: this.env._t("The amount of your payment lines must be rounded to validate the transaction."),
});
return;
}
}
if (await this._isOrderValid(isForceValidate)) {
// remove pending payments before finalizing the validation
for (let line of this.paymentLines) {
if (!line.is_done()) this.currentOrder.remove_paymentline(line);
}
await this._finalizeValidation();
}
}
async _finalizeValidation() {
if ((this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer) {
this.env.proxy.printer.open_cashbox();
}
this.currentOrder.initialize_validation_date();
this.currentOrder.finalized = true;
let syncOrderResult, hasError;
try {
// 1. Save order to server.
syncOrderResult = await this.env.pos.push_single_order(this.currentOrder);
// 2. Invoice.
if (this.currentOrder.is_to_invoice()) {
if (syncOrderResult.length) {
await this.env.legacyActionManager.do_action('account.account_invoices', {
additional_context: {
active_ids: [syncOrderResult[0].account_move],
},
});
} else {
throw { code: 401, message: 'Backend Invoice', data: { order: this.currentOrder } };
}
}
// 3. Post process.
if (syncOrderResult.length && this.currentOrder.wait_for_push_order()) {
const postPushResult = await this._postPushOrderResolve(
this.currentOrder,
syncOrderResult.map((res) => res.id)
);
if (!postPushResult) {
this.showPopup('ErrorPopup', {
title: this.env._t('Error: no internet connection.'),
body: this.env._t('Some, if not all, post-processing after syncing order failed.'),
});
}
}
} catch (error) {
if (error.code == 700 || error.code == 701)
this.error = true;
if ('code' in error) {
// We started putting `code` in the rejected object for invoicing error.
// We can continue with that convention such that when the error has `code`,
// then it is an error when invoicing. Besides, _handlePushOrderError was
// introduce to handle invoicing error logic.
await this._handlePushOrderError(error);
} else {
// We don't block for connection error. But we rethrow for any other errors.
if (isConnectionError(error)) {
this.showPopup('OfflineErrorPopup', {
title: this.env._t('Connection Error'),
body: this.env._t('Order is not synced. Check your internet connection'),
});
} else {
throw error;
}
}
} finally {
// Always show the next screen regardless of error since pos has to
// continue working even offline.
this.showScreen(this.nextScreen);
// Remove the order from the local storage so that when we refresh the page, the order
// won't be there
this.env.pos.db.remove_unpaid_order(this.currentOrder);
// Ask the user to sync the remaining unsynced orders.
if (!hasError && syncOrderResult && this.env.pos.db.get_orders().length) {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Remaining unsynced orders'),
body: this.env._t(
'There are unsynced orders. Do you want to sync these orders?'
),
});
if (confirmed) {
// NOTE: Not yet sure if this should be awaited or not.
// If awaited, some operations like changing screen
// might not work.
this.env.pos.push_orders();
}
}
}
}
get nextScreen() {
return !this.error? 'ReceiptScreen' : 'ProductScreen';
}
async _isOrderValid(isForceValidate) {
if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) {
this.showPopup('ErrorPopup', {
title: this.env._t('Empty Order'),
}
selectPaymentLine(event) {
const { cid } = event.detail;
const line = this.paymentLines.find((line) => line.cid === cid);
this.currentOrder.select_paymentline(line);
NumberBuffer.reset();
this.render(true);
}
async validateOrder(isForceValidate) {
if (this.env.pos.config.cash_rounding) {
if (!this.env.pos.get_order().check_paymentlines_rounding()) {
this.showPopup("ErrorPopup", {
title: this.env._t("Rounding error in payment lines"),
body: this.env._t(
'There must be at least one product in your order before it can be validated and invoiced.'
"The amount of your payment lines must be rounded to validate the transaction."
),
});
return false;
}
const splitPayments = this.paymentLines.filter(payment => payment.payment_method.split_transactions)
if (splitPayments.length && !this.currentOrder.get_partner()) {
const paymentMethod = splitPayments[0].payment_method
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Customer Required'),
body: _.str.sprintf(this.env._t('Customer is required for %s payment method.'), paymentMethod.name),
});
if (confirmed) {
this.selectPartner();
}
return false;
}
if ((this.currentOrder.is_to_invoice() || this.currentOrder.is_to_ship()) && !this.currentOrder.get_partner()) {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Please select the Customer'),
body: this.env._t(
'You need to select the customer before you can invoice or ship an order.'
),
});
if (confirmed) {
this.selectPartner();
}
return false;
}
let partner = this.currentOrder.get_partner()
if (this.currentOrder.is_to_ship() && !(partner.name && partner.street && partner.city && partner.country_id)) {
this.showPopup('ErrorPopup', {
title: this.env._t('Incorrect address for shipping'),
body: this.env._t('The selected customer needs an address.'),
});
return false;
}
if (this.currentOrder.get_total_with_tax() != 0 && this.currentOrder.get_paymentlines().length === 0) {
this.showNotification(this.env._t('Select a payment method to validate the order.'));
return false;
}
if (!this.currentOrder.is_paid() || this.invoicing) {
return false;
}
if (this.currentOrder.has_not_valid_rounding()) {
var line = this.currentOrder.has_not_valid_rounding();
this.showPopup('ErrorPopup', {
title: this.env._t('Incorrect rounding'),
body: this.env._t(
'You have to round your payments lines.' + line.amount + ' is not rounded.'
),
});
return false;
}
// The exact amount must be paid if there is no cash payment method defined.
if (
Math.abs(
this.currentOrder.get_total_with_tax() - this.currentOrder.get_total_paid() + this.currentOrder.get_rounding_applied()
) > 0.00001
) {
var cash = false;
for (var i = 0; i < this.env.pos.payment_methods.length; i++) {
cash = cash || this.env.pos.payment_methods[i].is_cash_count;
}
if (!cash) {
this.showPopup('ErrorPopup', {
title: this.env._t('Cannot return change without a cash payment method'),
body: this.env._t(
'There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration'
),
});
return false;
}
}
// if the change is too large, it's probably an input error, make the user confirm.
if (
!isForceValidate &&
this.currentOrder.get_total_with_tax() > 0 &&
this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid()
) {
this.showPopup('ConfirmPopup', {
title: this.env._t('Please Confirm Large Amount'),
body:
this.env._t('Are you sure that the customer wants to pay') +
' ' +
this.env.pos.format_currency(this.currentOrder.get_total_paid()) +
' ' +
this.env._t('for an order of') +
' ' +
this.env.pos.format_currency(this.currentOrder.get_total_with_tax()) +
' ' +
this.env._t('? Clicking "Confirm" will validate the payment.'),
}).then(({ confirmed }) => {
if (confirmed) this.validateOrder(true);
});
return false;
}
if (!this.currentOrder._isValidEmptyOrder()) return false;
return true;
}
async _postPushOrderResolve(order, order_server_ids) {
return true;
}
async _sendPaymentRequest({ detail: line }) {
// Other payment lines can not be reversed anymore
this.paymentLines.forEach(function (line) {
line.can_be_reversed = false;
});
const payment_terminal = line.payment_method.payment_terminal;
line.set_payment_status('waiting');
const isPaymentSuccessful = await payment_terminal.send_payment_request(line.cid);
if (isPaymentSuccessful) {
line.set_payment_status('done');
line.can_be_reversed = payment_terminal.supports_reversals;
// Automatically validate the order when after an electronic payment,
// the current order is fully paid and due is zero.
if (
this.currentOrder.is_paid() &&
utils.float_is_zero(this.currentOrder.get_due(), this.env.pos.currency.decimal_places) &&
this.env.pos.config.auto_validate_terminal_payment
) {
this.trigger('validate-order');
}
} else {
line.set_payment_status('retry');
return;
}
}
async _sendPaymentCancel({ detail: line }) {
const payment_terminal = line.payment_method.payment_terminal;
line.set_payment_status('waitingCancel');
const isCancelSuccessful = await payment_terminal.send_payment_cancel(this.currentOrder, line.cid);
if (isCancelSuccessful) {
line.set_payment_status('retry');
} else {
line.set_payment_status('waitingCard');
if (await this._isOrderValid(isForceValidate)) {
// remove pending payments before finalizing the validation
for (const line of this.paymentLines) {
if (!line.is_done()) {
this.currentOrder.remove_paymentline(line);
}
}
}
async _sendPaymentReverse({ detail: line }) {
const payment_terminal = line.payment_method.payment_terminal;
line.set_payment_status('reversing');
const isReversalSuccessful = await payment_terminal.send_payment_reversal(line.cid);
if (isReversalSuccessful) {
line.set_amount(0);
line.set_payment_status('reversed');
} else {
line.can_be_reversed = false;
line.set_payment_status('done');
}
}
async _sendForceDone({ detail: line }) {
line.set_payment_status('done');
await this._finalizeValidation();
}
}
PaymentScreen.template = 'PaymentScreen';
async _finalizeValidation() {
if (
(this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) &&
this.env.pos.config.iface_cashdrawer
) {
this.env.proxy.printer.open_cashbox();
}
Registries.Component.add(PaymentScreen);
this.currentOrder.initialize_validation_date();
this.currentOrder.finalized = true;
return PaymentScreen;
});
let syncOrderResult, hasError;
try {
// 1. Save order to server.
syncOrderResult = await this.env.pos.push_single_order(this.currentOrder);
// 2. Invoice.
if (this.currentOrder.is_to_invoice()) {
if (syncOrderResult.length) {
await this.env.legacyActionManager.do_action("account.account_invoices", {
additional_context: {
active_ids: [syncOrderResult[0].account_move],
},
});
} else {
throw {
code: 401,
message: "Backend Invoice",
data: { order: this.currentOrder },
};
}
}
// 3. Post process.
if (syncOrderResult.length && this.currentOrder.wait_for_push_order()) {
const postPushResult = await this._postPushOrderResolve(
this.currentOrder,
syncOrderResult.map((res) => res.id)
);
if (!postPushResult) {
this.showPopup("ErrorPopup", {
title: this.env._t("Error: no internet connection."),
body: this.env._t(
"Some, if not all, post-processing after syncing order failed."
),
});
}
}
} catch (error) {
if (error.code == 700 || error.code == 701) {
this.error = true;
}
if ("code" in error) {
// We started putting `code` in the rejected object for invoicing error.
// We can continue with that convention such that when the error has `code`,
// then it is an error when invoicing. Besides, _handlePushOrderError was
// introduce to handle invoicing error logic.
await this._handlePushOrderError(error);
} else {
// We don't block for connection error. But we rethrow for any other errors.
if (isConnectionError(error)) {
this.showPopup("OfflineErrorPopup", {
title: this.env._t("Connection Error"),
body: this.env._t("Order is not synced. Check your internet connection"),
});
} else {
throw error;
}
}
} finally {
// Always show the next screen regardless of error since pos has to
// continue working even offline.
this.showScreen(this.nextScreen);
// Remove the order from the local storage so that when we refresh the page, the order
// won't be there
this.env.pos.db.remove_unpaid_order(this.currentOrder);
// Ask the user to sync the remaining unsynced orders.
if (!hasError && syncOrderResult && this.env.pos.db.get_orders().length) {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Remaining unsynced orders"),
body: this.env._t(
"There are unsynced orders. Do you want to sync these orders?"
),
});
if (confirmed) {
// NOTE: Not yet sure if this should be awaited or not.
// If awaited, some operations like changing screen
// might not work.
this.env.pos.push_orders();
}
}
}
}
get nextScreen() {
return !this.error ? "ReceiptScreen" : "ProductScreen";
}
async _isOrderValid(isForceValidate) {
if (this.currentOrder.get_orderlines().length === 0 && this.currentOrder.is_to_invoice()) {
this.showPopup("ErrorPopup", {
title: this.env._t("Empty Order"),
body: this.env._t(
"There must be at least one product in your order before it can be validated and invoiced."
),
});
return false;
}
const splitPayments = this.paymentLines.filter(
(payment) => payment.payment_method.split_transactions
);
if (splitPayments.length && !this.currentOrder.get_partner()) {
const paymentMethod = splitPayments[0].payment_method;
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Customer Required"),
body: _.str.sprintf(
this.env._t("Customer is required for %s payment method."),
paymentMethod.name
),
});
if (confirmed) {
this.selectPartner();
}
return false;
}
if (
(this.currentOrder.is_to_invoice() || this.currentOrder.is_to_ship()) &&
!this.currentOrder.get_partner()
) {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Please select the Customer"),
body: this.env._t(
"You need to select the customer before you can invoice or ship an order."
),
});
if (confirmed) {
this.selectPartner();
}
return false;
}
const partner = this.currentOrder.get_partner();
if (
this.currentOrder.is_to_ship() &&
!(partner.name && partner.street && partner.city && partner.country_id)
) {
this.showPopup("ErrorPopup", {
title: this.env._t("Incorrect address for shipping"),
body: this.env._t("The selected customer needs an address."),
});
return false;
}
if (
this.currentOrder.get_total_with_tax() != 0 &&
this.currentOrder.get_paymentlines().length === 0
) {
this.showNotification(this.env._t("Select a payment method to validate the order."));
return false;
}
if (!this.currentOrder.is_paid() || this.invoicing) {
return false;
}
if (this.currentOrder.has_not_valid_rounding()) {
var line = this.currentOrder.has_not_valid_rounding();
this.showPopup("ErrorPopup", {
title: this.env._t("Incorrect rounding"),
body: this.env._t(
"You have to round your payments lines." + line.amount + " is not rounded."
),
});
return false;
}
// The exact amount must be paid if there is no cash payment method defined.
if (
Math.abs(
this.currentOrder.get_total_with_tax() -
this.currentOrder.get_total_paid() +
this.currentOrder.get_rounding_applied()
) > 0.00001
) {
var cash = false;
for (var i = 0; i < this.env.pos.payment_methods.length; i++) {
cash = cash || this.env.pos.payment_methods[i].is_cash_count;
}
if (!cash) {
this.showPopup("ErrorPopup", {
title: this.env._t("Cannot return change without a cash payment method"),
body: this.env._t(
"There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration"
),
});
return false;
}
}
// if the change is too large, it's probably an input error, make the user confirm.
if (
!isForceValidate &&
this.currentOrder.get_total_with_tax() > 0 &&
this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid()
) {
this.showPopup("ConfirmPopup", {
title: this.env._t("Please Confirm Large Amount"),
body:
this.env._t("Are you sure that the customer wants to pay") +
" " +
this.env.pos.format_currency(this.currentOrder.get_total_paid()) +
" " +
this.env._t("for an order of") +
" " +
this.env.pos.format_currency(this.currentOrder.get_total_with_tax()) +
" " +
this.env._t('? Clicking "Confirm" will validate the payment.'),
}).then(({ confirmed }) => {
if (confirmed) {
this.validateOrder(true);
}
});
return false;
}
if (!this.currentOrder._isValidEmptyOrder()) {
return false;
}
return true;
}
async _postPushOrderResolve(order, order_server_ids) {
return true;
}
async _sendPaymentRequest({ detail: line }) {
// Other payment lines can not be reversed anymore
this.paymentLines.forEach(function (line) {
line.can_be_reversed = false;
});
const payment_terminal = line.payment_method.payment_terminal;
line.set_payment_status("waiting");
const isPaymentSuccessful = await payment_terminal.send_payment_request(line.cid);
if (isPaymentSuccessful) {
line.set_payment_status("done");
line.can_be_reversed = payment_terminal.supports_reversals;
// Automatically validate the order when after an electronic payment,
// the current order is fully paid and due is zero.
if (
this.currentOrder.is_paid() &&
utils.float_is_zero(
this.currentOrder.get_due(),
this.env.pos.currency.decimal_places
) &&
this.env.pos.config.auto_validate_terminal_payment
) {
this.trigger("validate-order");
}
} else {
line.set_payment_status("retry");
}
}
async _sendPaymentCancel({ detail: line }) {
const payment_terminal = line.payment_method.payment_terminal;
line.set_payment_status("waitingCancel");
const isCancelSuccessful = await payment_terminal.send_payment_cancel(
this.currentOrder,
line.cid
);
if (isCancelSuccessful) {
line.set_payment_status("retry");
} else {
line.set_payment_status("waitingCard");
}
}
async _sendPaymentReverse({ detail: line }) {
const payment_terminal = line.payment_method.payment_terminal;
line.set_payment_status("reversing");
const isReversalSuccessful = await payment_terminal.send_payment_reversal(line.cid);
if (isReversalSuccessful) {
line.set_amount(0);
line.set_payment_status("reversed");
} else {
line.can_be_reversed = false;
line.set_payment_status("done");
}
}
async _sendForceDone({ detail: line }) {
line.set_payment_status("done");
}
}
PaymentScreen.template = "PaymentScreen";
Registries.Component.add(PaymentScreen);
export default PaymentScreen;
@@ -1,18 +1,16 @@
odoo.define('point_of_sale.PaymentScreenNumpad', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class PaymentScreenNumpad extends PosComponent {
setup() {
super.setup();
this.decimalPoint = this.env._t.database.parameters.decimal_point;
}
class PaymentScreenNumpad extends PosComponent {
setup() {
super.setup();
this.decimalPoint = this.env._t.database.parameters.decimal_point;
}
PaymentScreenNumpad.template = 'PaymentScreenNumpad';
}
PaymentScreenNumpad.template = "PaymentScreenNumpad";
Registries.Component.add(PaymentScreenNumpad);
Registries.Component.add(PaymentScreenNumpad);
return PaymentScreenNumpad;
});
export default PaymentScreenNumpad;
@@ -1,23 +1,21 @@
odoo.define('point_of_sale.PaymentScreenPaymentLines', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class PaymentScreenPaymentLines extends PosComponent {
formatLineAmount(paymentline) {
return this.env.pos.format_currency_no_symbol(paymentline.get_amount());
}
selectedLineClass(line) {
return { 'payment-terminal': line.get_payment_status() };
}
unselectedLineClass(line) {
return {};
}
class PaymentScreenPaymentLines extends PosComponent {
formatLineAmount(paymentline) {
return this.env.pos.format_currency_no_symbol(paymentline.get_amount());
}
PaymentScreenPaymentLines.template = 'PaymentScreenPaymentLines';
selectedLineClass(line) {
return { "payment-terminal": line.get_payment_status() };
}
unselectedLineClass(line) {
return {};
}
}
PaymentScreenPaymentLines.template = "PaymentScreenPaymentLines";
Registries.Component.add(PaymentScreenPaymentLines);
Registries.Component.add(PaymentScreenPaymentLines);
return PaymentScreenPaymentLines;
});
export default PaymentScreenPaymentLines;
@@ -1,27 +1,25 @@
odoo.define('point_of_sale.PaymentScreenStatus', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class PaymentScreenStatus extends PosComponent {
get changeText() {
return this.env.pos.format_currency(this.props.order.get_change());
}
get totalDueText() {
return this.env.pos.format_currency(
this.props.order.get_total_with_tax() + this.props.order.get_rounding_applied()
);
}
get remainingText() {
return this.env.pos.format_currency(
this.props.order.get_due() > 0 ? this.props.order.get_due() : 0
);
}
class PaymentScreenStatus extends PosComponent {
get changeText() {
return this.env.pos.format_currency(this.props.order.get_change());
}
PaymentScreenStatus.template = 'PaymentScreenStatus';
get totalDueText() {
return this.env.pos.format_currency(
this.props.order.get_total_with_tax() + this.props.order.get_rounding_applied()
);
}
get remainingText() {
return this.env.pos.format_currency(
this.props.order.get_due() > 0 ? this.props.order.get_due() : 0
);
}
}
PaymentScreenStatus.template = "PaymentScreenStatus";
Registries.Component.add(PaymentScreenStatus);
Registries.Component.add(PaymentScreenStatus);
return PaymentScreenStatus;
});
export default PaymentScreenStatus;
@@ -1,25 +1,23 @@
odoo.define('point_of_sale.ActionpadWidget', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
/**
* @props partner
* @emits click-partner
* @emits click-pay
*/
class ActionpadWidget extends PosComponent {
get isLongName() {
return this.props.partner && this.props.partner.name.length > 10;
}
}
ActionpadWidget.template = 'ActionpadWidget';
ActionpadWidget.defaultProps = {
isActionButtonHighlighted: false,
/**
* @props partner
* @emits click-partner
* @emits click-pay
*/
class ActionpadWidget extends PosComponent {
get isLongName() {
return this.props.partner && this.props.partner.name.length > 10;
}
}
ActionpadWidget.template = "ActionpadWidget";
ActionpadWidget.defaultProps = {
isActionButtonHighlighted: false,
};
Registries.Component.add(ActionpadWidget);
Registries.Component.add(ActionpadWidget);
return ActionpadWidget;
});
export default ActionpadWidget;
@@ -1,18 +1,16 @@
odoo.define('point_of_sale.CategoryButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class CategoryButton extends PosComponent {
get imageUrl() {
const category = this.props.category
return `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`;
}
class CategoryButton extends PosComponent {
get imageUrl() {
const category = this.props.category;
return `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`;
}
CategoryButton.template = 'CategoryButton';
}
CategoryButton.template = "CategoryButton";
Registries.Component.add(CategoryButton);
Registries.Component.add(CategoryButton);
return CategoryButton;
});
export default CategoryButton;
@@ -1,37 +1,37 @@
odoo.define('point_of_sale.OrderlineCustomerNoteButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
class OrderlineCustomerNoteButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
class OrderlineCustomerNoteButton extends PosComponent {
setup() {
super.setup();
useListener("click", this.onClick);
}
async onClick() {
const selectedOrderline = this.env.pos.get_order().get_selected_orderline();
if (!selectedOrderline) {
return;
}
async onClick() {
const selectedOrderline = this.env.pos.get_order().get_selected_orderline();
if (!selectedOrderline) return;
const { confirmed, payload: inputNote } = await this.showPopup('TextAreaPopup', {
startingValue: selectedOrderline.get_customer_note(),
title: this.env._t('Add Customer Note'),
});
const { confirmed, payload: inputNote } = await this.showPopup("TextAreaPopup", {
startingValue: selectedOrderline.get_customer_note(),
title: this.env._t("Add Customer Note"),
});
if (confirmed) {
selectedOrderline.set_customer_note(inputNote);
}
if (confirmed) {
selectedOrderline.set_customer_note(inputNote);
}
}
OrderlineCustomerNoteButton.template = 'OrderlineCustomerNoteButton';
}
OrderlineCustomerNoteButton.template = "OrderlineCustomerNoteButton";
ProductScreen.addControlButton({
component: OrderlineCustomerNoteButton,
});
Registries.Component.add(OrderlineCustomerNoteButton);
return OrderlineCustomerNoteButton;
ProductScreen.addControlButton({
component: OrderlineCustomerNoteButton,
});
Registries.Component.add(OrderlineCustomerNoteButton);
export default OrderlineCustomerNoteButton;
@@ -1,50 +1,50 @@
odoo.define('point_of_sale.ProductInfoButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service')
const { identifyError } = require('point_of_sale.utils');
import PosComponent from "@point_of_sale/js/PosComponent";
import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service";
import { identifyError } from "@point_of_sale/js/utils";
class ProductInfoButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
}
async onClick() {
const orderline = this.env.pos.get_order().get_selected_orderline();
if (orderline) {
const product = orderline.get_product();
const quantity = orderline.get_quantity();
try {
const info = await this.env.pos.getProductInfo(product, quantity);
this.showPopup('ProductInfoPopup', { info: info , product: product });
} catch (e) {
if (identifyError(e) instanceof ConnectionLostError||ConnectionAbortedError) {
this.showPopup('OfflineErrorPopup', {
title: this.env._t('Network Error'),
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.'),
});
}
class ProductInfoButton extends PosComponent {
setup() {
super.setup();
useListener("click", this.onClick);
}
async onClick() {
const orderline = this.env.pos.get_order().get_selected_orderline();
if (orderline) {
const product = orderline.get_product();
const quantity = orderline.get_quantity();
try {
const info = await this.env.pos.getProductInfo(product, quantity);
this.showPopup("ProductInfoPopup", { info: info, product: product });
} catch (e) {
if (identifyError(e) instanceof ConnectionLostError || ConnectionAbortedError) {
this.showPopup("OfflineErrorPopup", {
title: this.env._t("Network Error"),
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."
),
});
}
}
}
}
ProductInfoButton.template = 'ProductInfoButton';
}
ProductInfoButton.template = "ProductInfoButton";
ProductScreen.addControlButton({
component: ProductInfoButton,
position: ['before', 'SetFiscalPositionButton'],
});
Registries.Component.add(ProductInfoButton);
return ProductInfoButton;
ProductScreen.addControlButton({
component: ProductInfoButton,
position: ["before", "SetFiscalPositionButton"],
});
Registries.Component.add(ProductInfoButton);
export default ProductInfoButton;
@@ -1,35 +1,33 @@
odoo.define('point_of_sale.RefundButton', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const Registries = require('point_of_sale.Registries');
const { useListener } = require("@web/core/utils/hooks");
import PosComponent from "@point_of_sale/js/PosComponent";
import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import Registries from "@point_of_sale/js/Registries";
import { useListener } from "@web/core/utils/hooks";
class RefundButton extends PosComponent {
setup() {
super.setup();
useListener('click', this._onClick);
}
_onClick() {
const partner = this.env.pos.get_order().get_partner();
const searchDetails = partner ? { fieldName: 'PARTNER', searchTerm: partner.name } : {};
this.showScreen('TicketScreen', {
ui: { filter: 'SYNCED', searchDetails },
destinationOrder: this.env.pos.get_order(),
});
}
class RefundButton extends PosComponent {
setup() {
super.setup();
useListener("click", this._onClick);
}
RefundButton.template = 'point_of_sale.RefundButton';
_onClick() {
const partner = this.env.pos.get_order().get_partner();
const searchDetails = partner ? { fieldName: "PARTNER", searchTerm: partner.name } : {};
this.showScreen("TicketScreen", {
ui: { filter: "SYNCED", searchDetails },
destinationOrder: this.env.pos.get_order(),
});
}
}
RefundButton.template = "point_of_sale.RefundButton";
ProductScreen.addControlButton({
component: RefundButton,
condition: function () {
return true;
},
});
Registries.Component.add(RefundButton);
return RefundButton;
ProductScreen.addControlButton({
component: RefundButton,
condition: function () {
return true;
},
});
Registries.Component.add(RefundButton);
export default RefundButton;
@@ -1,71 +1,69 @@
odoo.define('point_of_sale.SetFiscalPositionButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
class SetFiscalPositionButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
class SetFiscalPositionButton extends PosComponent {
setup() {
super.setup();
useListener("click", this.onClick);
}
get currentOrder() {
return this.env.pos.get_order();
}
get currentFiscalPositionName() {
return this.currentOrder && this.currentOrder.fiscal_position
? this.currentOrder.fiscal_position.display_name
: this.env._t("Tax");
}
async onClick() {
const currentFiscalPosition = this.currentOrder.fiscal_position;
const fiscalPosList = [
{
id: -1,
label: this.env._t("None"),
isSelected: !currentFiscalPosition,
},
];
for (const fiscalPos of this.env.pos.fiscal_positions) {
fiscalPosList.push({
id: fiscalPos.id,
label: fiscalPos.name,
isSelected: currentFiscalPosition
? fiscalPos.id === currentFiscalPosition.id
: false,
item: fiscalPos,
});
}
get currentOrder() {
return this.env.pos.get_order();
}
get currentFiscalPositionName() {
return this.currentOrder && this.currentOrder.fiscal_position
? this.currentOrder.fiscal_position.display_name
: this.env._t('Tax');
}
async onClick() {
const currentFiscalPosition = this.currentOrder.fiscal_position;
const fiscalPosList = [
{
id: -1,
label: this.env._t('None'),
isSelected: !currentFiscalPosition,
},
];
for (let fiscalPos of this.env.pos.fiscal_positions) {
fiscalPosList.push({
id: fiscalPos.id,
label: fiscalPos.name,
isSelected: currentFiscalPosition
? fiscalPos.id === currentFiscalPosition.id
: false,
item: fiscalPos,
});
const { confirmed, payload: selectedFiscalPosition } = await this.showPopup(
"SelectionPopup",
{
title: this.env._t("Select Fiscal Position"),
list: fiscalPosList,
}
const { confirmed, payload: selectedFiscalPosition } = await this.showPopup(
'SelectionPopup',
{
title: this.env._t('Select Fiscal Position'),
list: fiscalPosList,
}
);
if (confirmed) {
this.currentOrder.set_fiscal_position(selectedFiscalPosition);
// IMPROVEMENT: The following is the old implementation and I believe
// there could be a better way of doing it.
for (let line of this.currentOrder.orderlines) {
line.set_quantity(line.quantity);
}
);
if (confirmed) {
this.currentOrder.set_fiscal_position(selectedFiscalPosition);
// IMPROVEMENT: The following is the old implementation and I believe
// there could be a better way of doing it.
for (const line of this.currentOrder.orderlines) {
line.set_quantity(line.quantity);
}
}
}
SetFiscalPositionButton.template = 'SetFiscalPositionButton';
}
SetFiscalPositionButton.template = "SetFiscalPositionButton";
ProductScreen.addControlButton({
component: SetFiscalPositionButton,
condition: function() {
return this.env.pos.fiscal_positions.length > 0;
},
position: ['before', 'SetPricelistButton'],
});
Registries.Component.add(SetFiscalPositionButton);
return SetFiscalPositionButton;
ProductScreen.addControlButton({
component: SetFiscalPositionButton,
condition: function () {
return this.env.pos.fiscal_positions.length > 0;
},
position: ["before", "SetPricelistButton"],
});
Registries.Component.add(SetFiscalPositionButton);
export default SetFiscalPositionButton;
@@ -1,59 +1,52 @@
odoo.define('point_of_sale.SetPricelistButton', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import ProductScreen from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
class SetPricelistButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
}
get currentOrder() {
return this.env.pos.get_order();
}
get currentPricelistName() {
const order = this.currentOrder;
return order && order.pricelist
? order.pricelist.display_name
: this.env._t('Pricelist');
}
async onClick() {
// Create the list to be passed to the SelectionPopup.
// Pricelist object is passed as item in the list because it
// is the object that will be returned when the popup is confirmed.
const selectionList = this.env.pos.pricelists.map(pricelist => ({
id: pricelist.id,
label: pricelist.name,
isSelected: pricelist.id === this.currentOrder.pricelist.id,
item: pricelist,
}));
class SetPricelistButton extends PosComponent {
setup() {
super.setup();
useListener("click", this.onClick);
}
get currentOrder() {
return this.env.pos.get_order();
}
get currentPricelistName() {
const order = this.currentOrder;
return order && order.pricelist ? order.pricelist.display_name : this.env._t("Pricelist");
}
async onClick() {
// Create the list to be passed to the SelectionPopup.
// Pricelist object is passed as item in the list because it
// is the object that will be returned when the popup is confirmed.
const selectionList = this.env.pos.pricelists.map((pricelist) => ({
id: pricelist.id,
label: pricelist.name,
isSelected: pricelist.id === this.currentOrder.pricelist.id,
item: pricelist,
}));
const { confirmed, payload: selectedPricelist } = await this.showPopup(
'SelectionPopup',
{
title: this.env._t('Select the pricelist'),
list: selectionList,
}
);
const { confirmed, payload: selectedPricelist } = await this.showPopup("SelectionPopup", {
title: this.env._t("Select the pricelist"),
list: selectionList,
});
if (confirmed) {
this.currentOrder.set_pricelist(selectedPricelist);
}
if (confirmed) {
this.currentOrder.set_pricelist(selectedPricelist);
}
}
SetPricelistButton.template = 'SetPricelistButton';
}
SetPricelistButton.template = "SetPricelistButton";
ProductScreen.addControlButton({
component: SetPricelistButton,
condition: function() {
return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 1;
},
});
Registries.Component.add(SetPricelistButton);
return SetPricelistButton;
ProductScreen.addControlButton({
component: SetPricelistButton,
condition: function () {
return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 1;
},
});
Registries.Component.add(SetPricelistButton);
export default SetPricelistButton;
@@ -1,49 +1,49 @@
odoo.define('point_of_sale.NumpadWidget', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
/**
* @prop {'quantity' | 'price' | 'discount'} activeMode
* @prop {Array<'quantity' | 'price' | 'discount'>} disabledModes
* @prop {boolean} disableSign
* @event set-numpad-mode - triggered when mode button is clicked
* @event numpad-click-input - triggered when numpad button is clicked
*/
class NumpadWidget extends PosComponent {
get hasPriceControlRights() {
return (
this.env.pos.cashierHasPriceControlRights() &&
!this.props.disabledModes.includes('price')
);
}
get hasManualDiscount() {
return this.env.pos.config.manual_discount && !this.props.disabledModes.includes('discount');
}
changeMode(mode) {
if (!this.hasPriceControlRights && mode === 'price') {
return;
}
if (!this.hasManualDiscount && mode === 'discount') {
return;
}
this.trigger('set-numpad-mode', { mode });
}
sendInput(key) {
this.trigger('numpad-click-input', { key });
}
get decimalSeparator() {
return this.env._t.database.parameters.decimal_point;
}
/**
* @prop {'quantity' | 'price' | 'discount'} activeMode
* @prop {Array<'quantity' | 'price' | 'discount'>} disabledModes
* @prop {boolean} disableSign
* @event set-numpad-mode - triggered when mode button is clicked
* @event numpad-click-input - triggered when numpad button is clicked
*/
class NumpadWidget extends PosComponent {
get hasPriceControlRights() {
return (
this.env.pos.cashierHasPriceControlRights() &&
!this.props.disabledModes.includes("price")
);
}
NumpadWidget.template = 'NumpadWidget';
NumpadWidget.defaultProps = {
disabledModes: [],
disableSign: false,
get hasManualDiscount() {
return (
this.env.pos.config.manual_discount && !this.props.disabledModes.includes("discount")
);
}
changeMode(mode) {
if (!this.hasPriceControlRights && mode === "price") {
return;
}
if (!this.hasManualDiscount && mode === "discount") {
return;
}
this.trigger("set-numpad-mode", { mode });
}
sendInput(key) {
this.trigger("numpad-click-input", { key });
}
get decimalSeparator() {
return this.env._t.database.parameters.decimal_point;
}
}
NumpadWidget.template = "NumpadWidget";
NumpadWidget.defaultProps = {
disabledModes: [],
disableSign: false,
};
Registries.Component.add(NumpadWidget);
Registries.Component.add(NumpadWidget);
return NumpadWidget;
});
export default NumpadWidget;
@@ -1,27 +1,25 @@
odoo.define('point_of_sale.OrderSummary', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { float_is_zero } = require('web.utils');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { float_is_zero } from "web.utils";
class OrderSummary extends PosComponent {
getTotal() {
return this.env.pos.format_currency(this.props.order.get_total_with_tax());
}
getTax() {
const total = this.props.order.get_total_with_tax();
const totalWithoutTax = this.props.order.get_total_without_tax();
const taxAmount = total - totalWithoutTax;
return {
hasTax: !float_is_zero(taxAmount, this.env.pos.currency.decimal_places),
displayAmount: this.env.pos.format_currency(taxAmount),
};
}
class OrderSummary extends PosComponent {
getTotal() {
return this.env.pos.format_currency(this.props.order.get_total_with_tax());
}
OrderSummary.template = 'OrderSummary';
getTax() {
const total = this.props.order.get_total_with_tax();
const totalWithoutTax = this.props.order.get_total_without_tax();
const taxAmount = total - totalWithoutTax;
return {
hasTax: !float_is_zero(taxAmount, this.env.pos.currency.decimal_places),
displayAmount: this.env.pos.format_currency(taxAmount),
};
}
}
OrderSummary.template = "OrderSummary";
Registries.Component.add(OrderSummary);
Registries.Component.add(OrderSummary);
return OrderSummary;
});
export default OrderSummary;
@@ -1,72 +1,70 @@
odoo.define('point_of_sale.OrderWidget', function(require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useListener } from "@web/core/utils/hooks";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onPatched, useRef } = owl;
const { onPatched, useRef } = owl;
class OrderWidget extends PosComponent {
setup() {
super.setup();
useListener('select-line', this._selectLine);
useListener('edit-pack-lot-lines', this._editPackLotLines);
this.scrollableRef = useRef('scrollable');
this.scrollToBottom = false;
onPatched(() => {
// IMPROVEMENT
// This one just stays at the bottom of the orderlines list.
// Perhaps it is better to scroll to the added or modified orderline.
if (this.scrollToBottom) {
this.scrollableRef.el.scrollTop = this.scrollableRef.el.scrollHeight;
this.scrollToBottom = false;
}
});
}
get order() {
return this.env.pos.get_order();
}
get orderlinesArray() {
return this.order ? this.order.get_orderlines() : [];
}
_selectLine(event) {
this.order.select_orderline(event.detail.orderline);
}
// IMPROVEMENT: Might be better to lift this to ProductScreen
// because there is similar operation when clicking a product.
//
// Furthermore, what if a number different from 1 (or -1) is specified
// to an orderline that has product tracked by lot. Lot tracking (based
// on the current implementation) requires that 1 item per orderline is
// allowed.
async _editPackLotLines(event) {
const orderline = event.detail.orderline;
const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot();
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,
});
if (confirmed) {
// Segregate the old and new packlot lines
const modifiedPackLotLines = Object.fromEntries(
payload.newArray.filter(item => item.id).map(item => [item.id, item.text])
);
const newPackLotLines = payload.newArray
.filter(item => !item.id)
.map(item => ({ lot_name: item.text }));
orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines });
class OrderWidget extends PosComponent {
setup() {
super.setup();
useListener("select-line", this._selectLine);
useListener("edit-pack-lot-lines", this._editPackLotLines);
this.scrollableRef = useRef("scrollable");
this.scrollToBottom = false;
onPatched(() => {
// IMPROVEMENT
// This one just stays at the bottom of the orderlines list.
// Perhaps it is better to scroll to the added or modified orderline.
if (this.scrollToBottom) {
this.scrollableRef.el.scrollTop = this.scrollableRef.el.scrollHeight;
this.scrollToBottom = false;
}
this.order.select_orderline(event.detail.orderline);
}
});
}
OrderWidget.template = 'OrderWidget';
get order() {
return this.env.pos.get_order();
}
get orderlinesArray() {
return this.order ? this.order.get_orderlines() : [];
}
_selectLine(event) {
this.order.select_orderline(event.detail.orderline);
}
// IMPROVEMENT: Might be better to lift this to ProductScreen
// because there is similar operation when clicking a product.
//
// Furthermore, what if a number different from 1 (or -1) is specified
// to an orderline that has product tracked by lot. Lot tracking (based
// on the current implementation) requires that 1 item per orderline is
// allowed.
async _editPackLotLines(event) {
const orderline = event.detail.orderline;
const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot();
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,
});
if (confirmed) {
// Segregate the old and new packlot lines
const modifiedPackLotLines = Object.fromEntries(
payload.newArray.filter((item) => item.id).map((item) => [item.id, item.text])
);
const newPackLotLines = payload.newArray
.filter((item) => !item.id)
.map((item) => ({ lot_name: item.text }));
Registries.Component.add(OrderWidget);
orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines });
}
this.order.select_orderline(event.detail.orderline);
}
}
OrderWidget.template = "OrderWidget";
return OrderWidget;
});
Registries.Component.add(OrderWidget);
export default OrderWidget;
@@ -1,28 +1,26 @@
odoo.define('point_of_sale.Orderline', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class Orderline extends PosComponent {
selectLine() {
this.trigger('select-line', { orderline: this.props.line });
}
lotIconClicked() {
this.trigger('edit-pack-lot-lines', { orderline: this.props.line });
}
get addedClasses() {
return {
selected: this.props.line.selected,
};
}
get customerNote() {
return this.props.line.get_customer_note();
}
class Orderline extends PosComponent {
selectLine() {
this.trigger("select-line", { orderline: this.props.line });
}
Orderline.template = 'Orderline';
lotIconClicked() {
this.trigger("edit-pack-lot-lines", { orderline: this.props.line });
}
get addedClasses() {
return {
selected: this.props.line.selected,
};
}
get customerNote() {
return this.props.line.get_customer_note();
}
}
Orderline.template = "Orderline";
Registries.Component.add(Orderline);
Registries.Component.add(Orderline);
return Orderline;
});
export default Orderline;
@@ -1,49 +1,47 @@
odoo.define('point_of_sale.ProductItem', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class ProductItem extends PosComponent {
/**
* For accessibility, pressing <space> should be like clicking the product.
* <enter> is not considered because it conflicts with the barcode.
*
* @param {KeyPressEvent} event
*/
spaceClickProduct(event) {
if (event.which === 32) {
this.trigger('click-product', this.props.product);
}
}
get imageUrl() {
const product = this.props.product;
return `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`;
}
get pricelist() {
const current_order = this.env.pos.get_order();
if (current_order) {
return current_order.pricelist;
}
return this.env.pos.default_pricelist;
}
get price() {
const formattedUnitPrice = this.env.pos.format_currency(
this.props.product.get_display_price(this.pricelist, 1),
'Product Price'
);
if (this.props.product.to_weight) {
return `${formattedUnitPrice}/${
this.env.pos.units_by_id[this.props.product.uom_id[0]].name
}`;
} else {
return formattedUnitPrice;
}
class ProductItem extends PosComponent {
/**
* For accessibility, pressing <space> should be like clicking the product.
* <enter> is not considered because it conflicts with the barcode.
*
* @param {KeyPressEvent} event
*/
spaceClickProduct(event) {
if (event.which === 32) {
this.trigger("click-product", this.props.product);
}
}
ProductItem.template = 'ProductItem';
get imageUrl() {
const product = this.props.product;
return `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`;
}
get pricelist() {
const current_order = this.env.pos.get_order();
if (current_order) {
return current_order.pricelist;
}
return this.env.pos.default_pricelist;
}
get price() {
const formattedUnitPrice = this.env.pos.format_currency(
this.props.product.get_display_price(this.pricelist, 1),
"Product Price"
);
if (this.props.product.to_weight) {
return `${formattedUnitPrice}/${
this.env.pos.units_by_id[this.props.product.uom_id[0]].name
}`;
} else {
return formattedUnitPrice;
}
}
}
ProductItem.template = "ProductItem";
Registries.Component.add(ProductItem);
Registries.Component.add(ProductItem);
return ProductItem;
});
export default ProductItem;
@@ -1,390 +1,421 @@
odoo.define('point_of_sale.ProductScreen', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
const { useBarcodeReader } = require('point_of_sale.custom_hooks');
const { isConnectionError } = require('point_of_sale.utils');
const { parse } = require('web.field_utils');
import PosComponent from "@point_of_sale/js/PosComponent";
import ControlButtonsMixin from "@point_of_sale/js/ControlButtonsMixin";
import NumberBuffer from "@point_of_sale/js/Misc/NumberBuffer";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
import { useBarcodeReader } from "@point_of_sale/js/custom_hooks";
import { isConnectionError } from "@point_of_sale/js/utils";
import { parse } from "web.field_utils";
const { onMounted, useState } = owl;
const { onMounted, useState } = owl;
class ProductScreen extends ControlButtonsMixin(PosComponent) {
setup() {
super.setup();
useListener('update-selected-orderline', this._updateSelectedOrderline);
useListener('select-line', this._selectLine);
useListener('set-numpad-mode', this._setNumpadMode);
useListener('click-product', this._clickProduct);
useListener('click-partner', this.onClickPartner);
useListener('click-pay', this._onClickPay);
useBarcodeReader({
product: this._barcodeProductAction,
weight: this._barcodeProductAction,
price: this._barcodeProductAction,
client: this._barcodePartnerAction,
discount: this._barcodeDiscountAction,
error: this._barcodeErrorAction,
class ProductScreen extends ControlButtonsMixin(PosComponent) {
setup() {
super.setup();
useListener("update-selected-orderline", this._updateSelectedOrderline);
useListener("select-line", this._selectLine);
useListener("set-numpad-mode", this._setNumpadMode);
useListener("click-product", this._clickProduct);
useListener("click-partner", this.onClickPartner);
useListener("click-pay", this._onClickPay);
useBarcodeReader({
product: this._barcodeProductAction,
weight: this._barcodeProductAction,
price: this._barcodeProductAction,
client: this._barcodePartnerAction,
discount: this._barcodeDiscountAction,
error: this._barcodeErrorAction,
});
NumberBuffer.use({
nonKeyboardInputEvent: "numpad-click-input",
triggerAtInput: "update-selected-orderline",
useWithBarcode: true,
});
onMounted(this.onMounted);
// Call `reset` when the `onMounted` callback in `NumberBuffer.use` is done.
// We don't do this in the `mounted` lifecycle method because it is called before
// the callbacks in `onMounted` hook.
onMounted(() => NumberBuffer.reset());
this.state = useState({
mobile_pane: this.props.mobile_pane || "right",
});
}
onMounted() {
this.env.posbus.trigger("start-cash-control");
}
/**
* To be overridden by modules that checks availability of
* connected scale.
* @see _onScaleNotAvailable
*/
get isScaleAvailable() {
return true;
}
get partner() {
return this.currentOrder ? this.currentOrder.get_partner() : null;
}
get currentOrder() {
return this.env.pos.get_order();
}
async _getAddProductOptions(product, base_code) {
let price_extra = 0.0;
let draftPackLotLines, weight, description, packLotLinesToEdit;
if (_.some(product.attribute_line_ids, (id) => id in this.env.pos.attributes_by_ptal_id)) {
const attributes = _.map(
product.attribute_line_ids,
(id) => this.env.pos.attributes_by_ptal_id[id]
).filter((attr) => attr !== undefined);
const { confirmed, payload } = await this.showPopup("ProductConfiguratorPopup", {
product: product,
attributes: attributes,
});
NumberBuffer.use({
nonKeyboardInputEvent: 'numpad-click-input',
triggerAtInput: 'update-selected-orderline',
useWithBarcode: true,
});
onMounted(this.onMounted);
// Call `reset` when the `onMounted` callback in `NumberBuffer.use` is done.
// We don't do this in the `mounted` lifecycle method because it is called before
// the callbacks in `onMounted` hook.
onMounted(() => NumberBuffer.reset());
this.state = useState({
mobile_pane: this.props.mobile_pane || 'right',
});
}
onMounted() {
this.env.posbus.trigger('start-cash-control');
}
/**
* To be overridden by modules that checks availability of
* connected scale.
* @see _onScaleNotAvailable
*/
get isScaleAvailable() {
return true;
}
get partner() {
return this.currentOrder ? this.currentOrder.get_partner() : null;
}
get currentOrder() {
return this.env.pos.get_order();
}
async _getAddProductOptions(product, base_code) {
let price_extra = 0.0;
let draftPackLotLines, weight, description, packLotLinesToEdit;
if (_.some(product.attribute_line_ids, (id) => id in this.env.pos.attributes_by_ptal_id)) {
let attributes = _.map(product.attribute_line_ids, (id) => this.env.pos.attributes_by_ptal_id[id])
.filter((attr) => attr !== undefined);
let { confirmed, payload } = await this.showPopup('ProductConfiguratorPopup', {
product: product,
attributes: attributes,
});
if (confirmed) {
description = payload.selected_attributes.join(', ');
price_extra += payload.price_extra;
} else {
return;
}
if (confirmed) {
description = payload.selected_attributes.join(", ");
price_extra += payload.price_extra;
} else {
return;
}
}
// Gather lot information if required.
if (['serial', 'lot'].includes(product.tracking) && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots)) {
const isAllowOnlyOneLot = product.isAllowOnlyOneLot();
if (isAllowOnlyOneLot) {
// Gather lot information if required.
if (
["serial", "lot"].includes(product.tracking) &&
(this.env.pos.picking_type.use_create_lots ||
this.env.pos.picking_type.use_existing_lots)
) {
const isAllowOnlyOneLot = product.isAllowOnlyOneLot();
if (isAllowOnlyOneLot) {
packLotLinesToEdit = [];
} else {
const orderline = this.currentOrder
.get_orderlines()
.filter((line) => !line.get_discount())
.find((line) => line.product.id === product.id);
if (orderline) {
packLotLinesToEdit = orderline.getPackLotLinesToEdit();
} else {
packLotLinesToEdit = [];
} else {
const orderline = this.currentOrder
.get_orderlines()
.filter(line => !line.get_discount())
.find(line => line.product.id === product.id);
if (orderline) {
packLotLinesToEdit = orderline.getPackLotLinesToEdit();
} else {
packLotLinesToEdit = [];
}
}
const { confirmed, payload } = await this.showPopup('EditListPopup', {
title: this.env._t('Lot/Serial Number(s) Required'),
name: product.display_name,
isSingleItem: isAllowOnlyOneLot,
array: packLotLinesToEdit,
}
const { confirmed, payload } = await this.showPopup("EditListPopup", {
title: this.env._t("Lot/Serial Number(s) Required"),
name: product.display_name,
isSingleItem: isAllowOnlyOneLot,
array: packLotLinesToEdit,
});
if (confirmed) {
// Segregate the old and new packlot lines
const modifiedPackLotLines = Object.fromEntries(
payload.newArray.filter((item) => item.id).map((item) => [item.id, item.text])
);
const newPackLotLines = payload.newArray
.filter((item) => !item.id)
.map((item) => ({ lot_name: item.text }));
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
} else {
// We don't proceed on adding product.
return;
}
}
// Take the weight if necessary.
if (product.to_weight && this.env.pos.config.iface_electronic_scale) {
// Show the ScaleScreen to weigh the product.
if (this.isScaleAvailable) {
const { confirmed, payload } = await this.showTempScreen("ScaleScreen", {
product,
});
if (confirmed) {
// Segregate the old and new packlot lines
const modifiedPackLotLines = Object.fromEntries(
payload.newArray.filter(item => item.id).map(item => [item.id, item.text])
);
const newPackLotLines = payload.newArray
.filter(item => !item.id)
.map(item => ({ lot_name: item.text }));
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
weight = payload.weight;
} else {
// We don't proceed on adding product.
// do not add the product;
return;
}
}
// Take the weight if necessary.
if (product.to_weight && this.env.pos.config.iface_electronic_scale) {
// Show the ScaleScreen to weigh the product.
if (this.isScaleAvailable) {
const { confirmed, payload } = await this.showTempScreen('ScaleScreen', {
product,
});
if (confirmed) {
weight = payload.weight;
} else {
// do not add the product;
return;
}
} else {
await this._onScaleNotAvailable();
}
}
if (base_code && this.env.pos.db.product_packaging_by_barcode[base_code.code]) {
weight = this.env.pos.db.product_packaging_by_barcode[base_code.code].qty;
}
return { draftPackLotLines, quantity: weight, description, price_extra };
}
async _addProduct(product, options) {
this.currentOrder.add_product(product, options);
}
async _clickProduct(event) {
if (!this.currentOrder) {
this.env.pos.add_new_order();
}
const product = event.detail;
const options = await this._getAddProductOptions(product);
// Do not add product if options is undefined.
if (!options) return;
// Add the product after having the extra information.
await this._addProduct(product, options);
NumberBuffer.reset();
}
_setNumpadMode(event) {
const { mode } = event.detail;
NumberBuffer.capture();
NumberBuffer.reset();
this.env.pos.numpadMode = mode;
}
_selectLine() {
NumberBuffer.reset();
}
async _updateSelectedOrderline(event) {
if (this.env.pos.numpadMode === 'quantity' && this.env.pos.disallowLineQuantityChange()) {
let order = this.env.pos.get_order();
if(!order.orderlines.length)
return;
let selectedLine = order.get_selected_orderline();
let orderlines = order.orderlines;
let lastId = orderlines.length !== 0 && orderlines.at(orderlines.length - 1).cid;
let currentQuantity = this.env.pos.get_order().get_selected_orderline().get_quantity();
if(selectedLine.noDecrease) {
this.showPopup('ErrorPopup', {
title: this.env._t('Invalid action'),
body: this.env._t('You are not allowed to change this quantity'),
});
return;
}
const parsedInput = event.detail.buffer && parse.float(event.detail.buffer) || 0;
if(lastId != selectedLine.cid)
this._showDecreaseQuantityPopup();
else if(currentQuantity < parsedInput)
this._setValue(event.detail.buffer);
else if(parsedInput < currentQuantity)
this._showDecreaseQuantityPopup();
} else {
let { buffer } = event.detail;
let val = buffer === null ? 'remove' : buffer;
this._setValue(val);
if (val == 'remove') {
NumberBuffer.reset();
this.env.pos.numpadMode = 'quantity';
}
await this._onScaleNotAvailable();
}
}
_setValue(val) {
if (this.currentOrder.get_selected_orderline()) {
if (this.env.pos.numpadMode === 'quantity') {
const result = this.currentOrder.get_selected_orderline().set_quantity(val);
if (!result) NumberBuffer.reset();
} else if (this.env.pos.numpadMode === 'discount') {
this.currentOrder.get_selected_orderline().set_discount(val);
} else if (this.env.pos.numpadMode === 'price') {
var selected_orderline = this.currentOrder.get_selected_orderline();
selected_orderline.price_manually_set = true;
selected_orderline.set_unit_price(val);
}
}
}
async _barcodeProductAction(code) {
let product = this.env.pos.db.get_product_by_barcode(code.base_code);
if (!product) {
// find the barcode in the backend
let foundProductIds = [];
try {
foundProductIds = await this.rpc({
model: 'product.product',
method: 'search',
args: [[['barcode', '=', code.base_code]]],
context: this.env.session.user_context,
});
} catch (error) {
if (isConnectionError(error)) {
return this.showPopup('OfflineErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t("Product is not loaded. Tried loading the product from the server but there is a network error."),
});
} else {
throw error;
}
}
if (foundProductIds.length) {
await this.env.pos._addProducts(foundProductIds);
// assume that the result is unique.
product = this.env.pos.db.get_product_by_id(foundProductIds[0]);
} else {
return this._barcodeErrorAction(code);
}
}
const options = await this._getAddProductOptions(product, code);
// Do not proceed on adding the product when no options is returned.
// This is consistent with _clickProduct.
if (!options) return;
// update the options depending on the type of the scanned code
if (code.type === 'price') {
Object.assign(options, {
price: code.value,
extras: {
price_manually_set: true,
},
});
} else if (code.type === 'weight') {
Object.assign(options, {
quantity: code.value,
merge: false,
});
} else if (code.type === 'discount') {
Object.assign(options, {
discount: code.value,
merge: false,
});
}
this.currentOrder.add_product(product, options)
if (base_code && this.env.pos.db.product_packaging_by_barcode[base_code.code]) {
weight = this.env.pos.db.product_packaging_by_barcode[base_code.code].qty;
}
_barcodePartnerAction(code) {
const partner = this.env.pos.db.get_partner_by_barcode(code.code);
if (partner) {
if (this.currentOrder.get_partner() !== partner) {
this.currentOrder.set_partner(partner);
this.currentOrder.set_pricelist(
_.findWhere(this.env.pos.pricelists, {
id: partner.property_product_pricelist[0],
}) || this.env.pos.default_pricelist
);
}
return true;
}
this._barcodeErrorAction(code);
return false;
}
_barcodeDiscountAction(code) {
var last_orderline = this.currentOrder.get_last_orderline();
if (last_orderline) {
last_orderline.set_discount(code.value);
}
}
// IMPROVEMENT: The following two methods should be in PosScreenComponent?
// Why? Because once we start declaring barcode actions in different
// screens, these methods will also be declared over and over.
_barcodeErrorAction(code) {
this.showPopup('ErrorBarcodePopup', { code: this._codeRepr(code) });
}
_codeRepr(code) {
if (code.code.length > 32) {
return code.code.substring(0, 29) + '...';
} else {
return code.code;
}
}
async _displayAllControlPopup() {
await this.showPopup('ControlButtonPopup', {
controlButtons: this.controlButtons
});
}
/**
* override this method to perform procedure if the scale is not available.
* @see isScaleAvailable
*/
async _onScaleNotAvailable() {}
async _showDecreaseQuantityPopup() {
const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', {
startingValue: 0,
title: this.env._t('Set the new quantity'),
});
let newQuantity = inputNumber && inputNumber !== "" ? parse.float(inputNumber) : null;
if (confirmed && newQuantity !== null) {
let order = this.env.pos.get_order();
let selectedLine = this.env.pos.get_order().get_selected_orderline();
let currentQuantity = selectedLine.get_quantity()
if(selectedLine.is_last_line() && currentQuantity === 1 && newQuantity < currentQuantity)
selectedLine.set_quantity(newQuantity);
else if(newQuantity >= currentQuantity)
selectedLine.set_quantity(newQuantity);
else {
let newLine = selectedLine.clone();
let decreasedQuantity = currentQuantity - newQuantity
newLine.order = order;
newLine.set_quantity( - decreasedQuantity, true);
order.add_orderline(newLine);
}
}
return { draftPackLotLines, quantity: weight, description, price_extra };
}
async _addProduct(product, options) {
this.currentOrder.add_product(product, options);
}
async _clickProduct(event) {
if (!this.currentOrder) {
this.env.pos.add_new_order();
}
async onClickPartner() {
// IMPROVEMENT: This code snippet is very similar to selectPartner of PaymentScreen.
const currentPartner = this.currentOrder.get_partner();
if (currentPartner && this.currentOrder.getHasRefundLines()) {
this.showPopup('ErrorPopup', {
title: this.env._t("Can't change customer"),
body: _.str.sprintf(
this.env._t(
"This order already has refund lines for %s. We can't change the customer associated to it. Create a new order for the new customer."
),
currentPartner.name
),
const product = event.detail;
const options = await this._getAddProductOptions(product);
// Do not add product if options is undefined.
if (!options) {
return;
}
// Add the product after having the extra information.
await this._addProduct(product, options);
NumberBuffer.reset();
}
_setNumpadMode(event) {
const { mode } = event.detail;
NumberBuffer.capture();
NumberBuffer.reset();
this.env.pos.numpadMode = mode;
}
_selectLine() {
NumberBuffer.reset();
}
async _updateSelectedOrderline(event) {
if (this.env.pos.numpadMode === "quantity" && this.env.pos.disallowLineQuantityChange()) {
const order = this.env.pos.get_order();
if (!order.orderlines.length) {
return;
}
const selectedLine = order.get_selected_orderline();
const orderlines = order.orderlines;
const lastId = orderlines.length !== 0 && orderlines.at(orderlines.length - 1).cid;
const currentQuantity = this.env.pos
.get_order()
.get_selected_orderline()
.get_quantity();
if (selectedLine.noDecrease) {
this.showPopup("ErrorPopup", {
title: this.env._t("Invalid action"),
body: this.env._t("You are not allowed to change this quantity"),
});
return;
}
const { confirmed, payload: newPartner } = await this.showTempScreen(
'PartnerListScreen',
{ partner: currentPartner }
);
if (confirmed) {
this.currentOrder.set_partner(newPartner);
this.currentOrder.updatePricelist(newPartner);
const parsedInput = (event.detail.buffer && parse.float(event.detail.buffer)) || 0;
if (lastId != selectedLine.cid) {
this._showDecreaseQuantityPopup();
} else if (currentQuantity < parsedInput) {
this._setValue(event.detail.buffer);
} else if (parsedInput < currentQuantity) {
this._showDecreaseQuantityPopup();
}
}
async _onClickPay() {
if (this.env.pos.get_order().orderlines.some(line => line.get_product().tracking !== 'none' && !line.has_valid_product_lot()) && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots)) {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Some Serial/Lot Numbers are missing'),
body: this.env._t('You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?'),
confirmText: this.env._t('Yes'),
cancelText: this.env._t('No')
});
if (confirmed) {
this.showScreen('PaymentScreen');
}
} else {
this.showScreen('PaymentScreen');
} else {
const { buffer } = event.detail;
const val = buffer === null ? "remove" : buffer;
this._setValue(val);
if (val == "remove") {
NumberBuffer.reset();
this.env.pos.numpadMode = "quantity";
}
}
switchPane() {
this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left";
}
}
ProductScreen.template = 'ProductScreen';
_setValue(val) {
if (this.currentOrder.get_selected_orderline()) {
if (this.env.pos.numpadMode === "quantity") {
const result = this.currentOrder.get_selected_orderline().set_quantity(val);
if (!result) {
NumberBuffer.reset();
}
} else if (this.env.pos.numpadMode === "discount") {
this.currentOrder.get_selected_orderline().set_discount(val);
} else if (this.env.pos.numpadMode === "price") {
var selected_orderline = this.currentOrder.get_selected_orderline();
selected_orderline.price_manually_set = true;
selected_orderline.set_unit_price(val);
}
}
}
async _barcodeProductAction(code) {
let product = this.env.pos.db.get_product_by_barcode(code.base_code);
if (!product) {
// find the barcode in the backend
let foundProductIds = [];
try {
foundProductIds = await this.rpc({
model: "product.product",
method: "search",
args: [[["barcode", "=", code.base_code]]],
context: this.env.session.user_context,
});
} catch (error) {
if (isConnectionError(error)) {
return this.showPopup("OfflineErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t(
"Product is not loaded. Tried loading the product from the server but there is a network error."
),
});
} else {
throw error;
}
}
if (foundProductIds.length) {
await this.env.pos._addProducts(foundProductIds);
// assume that the result is unique.
product = this.env.pos.db.get_product_by_id(foundProductIds[0]);
} else {
return this._barcodeErrorAction(code);
}
}
const options = await this._getAddProductOptions(product, code);
// Do not proceed on adding the product when no options is returned.
// This is consistent with _clickProduct.
if (!options) {
return;
}
Registries.Component.add(ProductScreen);
// update the options depending on the type of the scanned code
if (code.type === "price") {
Object.assign(options, {
price: code.value,
extras: {
price_manually_set: true,
},
});
} else if (code.type === "weight") {
Object.assign(options, {
quantity: code.value,
merge: false,
});
} else if (code.type === "discount") {
Object.assign(options, {
discount: code.value,
merge: false,
});
}
this.currentOrder.add_product(product, options);
}
_barcodePartnerAction(code) {
const partner = this.env.pos.db.get_partner_by_barcode(code.code);
if (partner) {
if (this.currentOrder.get_partner() !== partner) {
this.currentOrder.set_partner(partner);
this.currentOrder.set_pricelist(
_.findWhere(this.env.pos.pricelists, {
id: partner.property_product_pricelist[0],
}) || this.env.pos.default_pricelist
);
}
return true;
}
this._barcodeErrorAction(code);
return false;
}
_barcodeDiscountAction(code) {
var last_orderline = this.currentOrder.get_last_orderline();
if (last_orderline) {
last_orderline.set_discount(code.value);
}
}
// IMPROVEMENT: The following two methods should be in PosScreenComponent?
// Why? Because once we start declaring barcode actions in different
// screens, these methods will also be declared over and over.
_barcodeErrorAction(code) {
this.showPopup("ErrorBarcodePopup", { code: this._codeRepr(code) });
}
_codeRepr(code) {
if (code.code.length > 32) {
return code.code.substring(0, 29) + "...";
} else {
return code.code;
}
}
async _displayAllControlPopup() {
await this.showPopup("ControlButtonPopup", {
controlButtons: this.controlButtons,
});
}
/**
* override this method to perform procedure if the scale is not available.
* @see isScaleAvailable
*/
async _onScaleNotAvailable() {}
async _showDecreaseQuantityPopup() {
const { confirmed, payload: inputNumber } = await this.showPopup("NumberPopup", {
startingValue: 0,
title: this.env._t("Set the new quantity"),
});
const newQuantity = inputNumber && inputNumber !== "" ? parse.float(inputNumber) : null;
if (confirmed && newQuantity !== null) {
const order = this.env.pos.get_order();
const selectedLine = this.env.pos.get_order().get_selected_orderline();
const currentQuantity = selectedLine.get_quantity();
if (
selectedLine.is_last_line() &&
currentQuantity === 1 &&
newQuantity < currentQuantity
) {
selectedLine.set_quantity(newQuantity);
} else if (newQuantity >= currentQuantity) {
selectedLine.set_quantity(newQuantity);
} else {
const newLine = selectedLine.clone();
const decreasedQuantity = currentQuantity - newQuantity;
newLine.order = order;
return ProductScreen;
});
newLine.set_quantity(-decreasedQuantity, true);
order.add_orderline(newLine);
}
}
}
async onClickPartner() {
// IMPROVEMENT: This code snippet is very similar to selectPartner of PaymentScreen.
const currentPartner = this.currentOrder.get_partner();
if (currentPartner && this.currentOrder.getHasRefundLines()) {
this.showPopup("ErrorPopup", {
title: this.env._t("Can't change customer"),
body: _.str.sprintf(
this.env._t(
"This order already has refund lines for %s. We can't change the customer associated to it. Create a new order for the new customer."
),
currentPartner.name
),
});
return;
}
const { confirmed, payload: newPartner } = await this.showTempScreen("PartnerListScreen", {
partner: currentPartner,
});
if (confirmed) {
this.currentOrder.set_partner(newPartner);
this.currentOrder.updatePricelist(newPartner);
}
}
async _onClickPay() {
if (
this.env.pos
.get_order()
.orderlines.some(
(line) =>
line.get_product().tracking !== "none" && !line.has_valid_product_lot()
) &&
(this.env.pos.picking_type.use_create_lots ||
this.env.pos.picking_type.use_existing_lots)
) {
const { confirmed } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Some Serial/Lot Numbers are missing"),
body: this.env._t(
"You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?"
),
confirmText: this.env._t("Yes"),
cancelText: this.env._t("No"),
});
if (confirmed) {
this.showScreen("PaymentScreen");
}
} else {
this.showScreen("PaymentScreen");
}
}
switchPane() {
this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left";
}
}
ProductScreen.template = "ProductScreen";
Registries.Component.add(ProductScreen);
export default ProductScreen;
@@ -1,84 +1,88 @@
odoo.define('point_of_sale.ProductsWidget', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const { useListener } = require("@web/core/utils/hooks");
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import { useListener } from "@web/core/utils/hooks";
import Registries from "@point_of_sale/js/Registries";
const { onWillUnmount, useState } = owl;
const { onWillUnmount, useState } = owl;
class ProductsWidget extends PosComponent {
/**
* @param {Object} props
* @param {number?} props.startCategoryId
*/
setup() {
super.setup();
useListener('switch-category', this._switchCategory);
useListener('update-search', this._updateSearch);
useListener('clear-search', this._clearSearch);
useListener('update-product-list', this._updateProductList);
this.state = useState({ searchWord: '' });
onWillUnmount(this.onWillUnmount);
}
onWillUnmount() {
this.trigger('toggle-mobile-searchbar', false);
}
get selectedCategoryId() {
return this.env.pos.selectedCategoryId;
}
get searchWord() {
return this.state.searchWord.trim();
}
get productsToDisplay() {
let list = [];
if (this.searchWord !== '') {
list = this.env.pos.db.search_product_in_category(
this.selectedCategoryId,
this.searchWord
);
} else {
list = this.env.pos.db.get_product_by_category(this.selectedCategoryId);
}
return list.sort(function (a, b) { return a.display_name.localeCompare(b.display_name) });
}
get subcategories() {
return this.env.pos.db
.get_category_childs_ids(this.selectedCategoryId)
.map(id => this.env.pos.db.get_category_by_id(id));
}
get breadcrumbs() {
if (this.selectedCategoryId === this.env.pos.db.root_category_id) return [];
return [
...this.env.pos.db
.get_category_ancestors_ids(this.selectedCategoryId)
.slice(1),
this.selectedCategoryId,
].map(id => this.env.pos.db.get_category_by_id(id));
}
get hasNoCategories() {
return this.env.pos.db.get_category_childs_ids(0).length === 0;
}
get shouldShowButton() {
return this.productsToDisplay.length === 0 && this.searchWord && !this.env.pos.isEveryProductLoaded;
}
_switchCategory(event) {
this.env.pos.setSelectedCategoryId(event.detail);
}
_updateSearch(event) {
this.state.searchWord = event.detail;
}
_clearSearch() {
this.state.searchWord = '';
}
_updateProductList(event) {
this.render(true);
this.trigger('switch-category', 0);
}
class ProductsWidget extends PosComponent {
/**
* @param {Object} props
* @param {number?} props.startCategoryId
*/
setup() {
super.setup();
useListener("switch-category", this._switchCategory);
useListener("update-search", this._updateSearch);
useListener("clear-search", this._clearSearch);
useListener("update-product-list", this._updateProductList);
this.state = useState({ searchWord: "" });
onWillUnmount(this.onWillUnmount);
}
ProductsWidget.template = 'ProductsWidget';
onWillUnmount() {
this.trigger("toggle-mobile-searchbar", false);
}
get selectedCategoryId() {
return this.env.pos.selectedCategoryId;
}
get searchWord() {
return this.state.searchWord.trim();
}
get productsToDisplay() {
let list = [];
if (this.searchWord !== "") {
list = this.env.pos.db.search_product_in_category(
this.selectedCategoryId,
this.searchWord
);
} else {
list = this.env.pos.db.get_product_by_category(this.selectedCategoryId);
}
return list.sort(function (a, b) {
return a.display_name.localeCompare(b.display_name);
});
}
get subcategories() {
return this.env.pos.db
.get_category_childs_ids(this.selectedCategoryId)
.map((id) => this.env.pos.db.get_category_by_id(id));
}
get breadcrumbs() {
if (this.selectedCategoryId === this.env.pos.db.root_category_id) {
return [];
}
return [
...this.env.pos.db.get_category_ancestors_ids(this.selectedCategoryId).slice(1),
this.selectedCategoryId,
].map((id) => this.env.pos.db.get_category_by_id(id));
}
get hasNoCategories() {
return this.env.pos.db.get_category_childs_ids(0).length === 0;
}
get shouldShowButton() {
return (
this.productsToDisplay.length === 0 &&
this.searchWord &&
!this.env.pos.isEveryProductLoaded
);
}
_switchCategory(event) {
this.env.pos.setSelectedCategoryId(event.detail);
}
_updateSearch(event) {
this.state.searchWord = event.detail;
}
_clearSearch() {
this.state.searchWord = "";
}
_updateProductList(event) {
this.render(true);
this.trigger("switch-category", 0);
}
}
ProductsWidget.template = "ProductsWidget";
Registries.Component.add(ProductsWidget);
Registries.Component.add(ProductsWidget);
return ProductsWidget;
});
export default ProductsWidget;
@@ -1,99 +1,124 @@
odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
'use strict';
/** @odoo-module */
const { identifyError } = require('point_of_sale.utils');
const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service');
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { debounce } = require("@web/core/utils/timing");
import { identifyError } from "@point_of_sale/js/utils";
import { ConnectionLostError, ConnectionAbortedError } from "@web/core/network/rpc_service";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { debounce } from "@web/core/utils/timing";
const { onMounted, onWillUnmount } = owl;
const { onMounted, onWillUnmount } = owl;
class ProductsWidgetControlPanel extends PosComponent {
setup() {
super.setup();
this.updateSearch = debounce(this.updateSearch, 100);
this.state = { searchInput: '' };
class ProductsWidgetControlPanel extends PosComponent {
setup() {
super.setup();
this.updateSearch = debounce(this.updateSearch, 100);
this.state = { searchInput: "" };
onMounted(() => {
this.env.posbus.on('search-product-from-info-popup', this, this.searchProductFromInfo)
if(!this.env.pos.config.limited_products_loading)
this.env.pos.isEveryProductLoaded = true;
});
onWillUnmount(() => {
this.env.posbus.off('search-product-from-info-popup', this);
});
}
_clearSearch() {
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', this.state.searchInput);
if (event.key === 'Enter') {
this._onPressEnterKey()
onMounted(() => {
this.env.posbus.on("search-product-from-info-popup", this, this.searchProductFromInfo);
if (!this.env.pos.config.limited_products_loading) {
this.env.pos.isEveryProductLoaded = true;
}
}
async _onPressEnterKey() {
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.state.searchInput)
, 3000);
if (!result.length) this._clearSearch();
}
}
searchProductFromInfo(productName) {
this.state.searchInput = productName;
this.trigger('switch-category', 0);
this.trigger('update-search', productName);
}
_toggleMobileSearchbar() {
this.trigger('toggle-mobile-searchbar');
}
async loadProductFromDB() {
if(!this.state.searchInput)
return;
});
try {
let ProductIds = await this.rpc({
model: 'product.product',
method: 'search',
args: [['&',['available_in_pos', '=', true], '|','|','|',
['name', 'ilike', this.state.searchInput],
['default_code', 'ilike', this.state.searchInput],
['barcode', 'ilike', this.state.searchInput]]],
context: this.env.session.user_context,
});
if(ProductIds.length) {
if (!this.env.pos.isEveryProductLoaded) await this.env.pos.updateIsEveryProductLoaded();
await this.env.pos._addProducts(ProductIds, false);
}
this.trigger('update-product-list');
return ProductIds;
} catch (error) {
const identifiedError = identifyError(error)
if (identifiedError instanceof ConnectionLostError || identifiedError instanceof ConnectionAbortedError) {
return this.showPopup('OfflineErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t("Product is not loaded. Tried loading the product from the server but there is a network error."),
});
} else {
throw error;
}
onWillUnmount(() => {
this.env.posbus.off("search-product-from-info-popup", this);
});
}
_clearSearch() {
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", this.state.searchInput);
if (event.key === "Enter") {
this._onPressEnterKey();
}
}
async _onPressEnterKey() {
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.state.searchInput
),
3000
);
if (!result.length) {
this._clearSearch();
}
}
}
ProductsWidgetControlPanel.template = 'ProductsWidgetControlPanel';
searchProductFromInfo(productName) {
this.state.searchInput = productName;
this.trigger("switch-category", 0);
this.trigger("update-search", productName);
}
_toggleMobileSearchbar() {
this.trigger("toggle-mobile-searchbar");
}
async loadProductFromDB() {
if (!this.state.searchInput) {
return;
}
Registries.Component.add(ProductsWidgetControlPanel);
try {
const ProductIds = await this.rpc({
model: "product.product",
method: "search",
args: [
[
"&",
["available_in_pos", "=", true],
"|",
"|",
"|",
["name", "ilike", this.state.searchInput],
["default_code", "ilike", this.state.searchInput],
["barcode", "ilike", this.state.searchInput],
],
],
context: this.env.session.user_context,
});
if (ProductIds.length) {
if (!this.env.pos.isEveryProductLoaded) {
await this.env.pos.updateIsEveryProductLoaded();
}
await this.env.pos._addProducts(ProductIds, false);
}
this.trigger("update-product-list");
return ProductIds;
} catch (error) {
const identifiedError = identifyError(error);
if (
identifiedError instanceof ConnectionLostError ||
identifiedError instanceof ConnectionAbortedError
) {
return this.showPopup("OfflineErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t(
"Product is not loaded. Tried loading the product from the server but there is a network error."
),
});
} else {
throw error;
}
}
}
}
ProductsWidgetControlPanel.template = "ProductsWidgetControlPanel";
return ProductsWidgetControlPanel;
});
Registries.Component.add(ProductsWidgetControlPanel);
export default ProductsWidgetControlPanel;
@@ -1,50 +1,45 @@
odoo.define('point_of_sale.OrderReceipt', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
const { onWillUpdateProps } = owl;
const { onWillUpdateProps } = owl;
class OrderReceipt extends PosComponent {
setup() {
super.setup();
this._receiptEnv = this.props.order.getOrderReceiptEnv();
class OrderReceipt extends PosComponent {
setup() {
super.setup();
this._receiptEnv = this.props.order.getOrderReceiptEnv();
onWillUpdateProps((nextProps) => {
this._receiptEnv = nextProps.order.getOrderReceiptEnv();
});
}
get receipt() {
return this.receiptEnv.receipt;
}
get orderlines() {
return this.receiptEnv.orderlines;
}
get paymentlines() {
return this.receiptEnv.paymentlines;
}
get isTaxIncluded() {
return Math.abs(this.receipt.subtotal - this.receipt.total_with_tax) <= 0.000001;
}
get receiptEnv () {
return this._receiptEnv;
}
isSimple(line) {
return (
line.discount === 0 &&
line.is_in_unit &&
line.quantity === 1 &&
!(
line.display_discount_policy == 'without_discount' &&
line.price < line.price_lst
)
);
}
onWillUpdateProps((nextProps) => {
this._receiptEnv = nextProps.order.getOrderReceiptEnv();
});
}
OrderReceipt.template = 'OrderReceipt';
get receipt() {
return this.receiptEnv.receipt;
}
get orderlines() {
return this.receiptEnv.orderlines;
}
get paymentlines() {
return this.receiptEnv.paymentlines;
}
get isTaxIncluded() {
return Math.abs(this.receipt.subtotal - this.receipt.total_with_tax) <= 0.000001;
}
get receiptEnv() {
return this._receiptEnv;
}
isSimple(line) {
return (
line.discount === 0 &&
line.is_in_unit &&
line.quantity === 1 &&
!(line.display_discount_policy == "without_discount" && line.price < line.price_lst)
);
}
}
OrderReceipt.template = "OrderReceipt";
Registries.Component.add(OrderReceipt);
Registries.Component.add(OrderReceipt);
return OrderReceipt;
});
export default OrderReceipt;
@@ -1,139 +1,152 @@
odoo.define('point_of_sale.ReceiptScreen', function (require) {
'use strict';
/** @odoo-module */
const { Printer } = require('point_of_sale.Printer');
const { is_email } = require('web.utils');
const { useErrorHandlers } = require('point_of_sale.custom_hooks');
const Registries = require('point_of_sale.Registries');
const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen');
import { Printer } from "@point_of_sale/js/printers";
import { is_email } from "web.utils";
import { useErrorHandlers } from "@point_of_sale/js/custom_hooks";
import Registries from "@point_of_sale/js/Registries";
import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen";
const { onMounted, useRef, status } = owl;
const { onMounted, useRef, status } = owl;
const ReceiptScreen = (AbstractReceiptScreen) => {
class ReceiptScreen extends AbstractReceiptScreen {
setup() {
super.setup();
useErrorHandlers();
this.orderReceipt = useRef('order-receipt');
const order = this.currentOrder;
const partner = order.get_partner();
this.orderUiState = order.uiState.ReceiptScreen;
this.orderUiState.inputEmail = this.orderUiState.inputEmail || (partner && partner.email) || '';
this.is_email = is_email;
const ReceiptScreen = (AbstractReceiptScreen) => {
class ReceiptScreen extends AbstractReceiptScreen {
setup() {
super.setup();
useErrorHandlers();
this.orderReceipt = useRef("order-receipt");
const order = this.currentOrder;
const partner = order.get_partner();
this.orderUiState = order.uiState.ReceiptScreen;
this.orderUiState.inputEmail =
this.orderUiState.inputEmail || (partner && partner.email) || "";
this.is_email = is_email;
onMounted(() => {
// Here, we send a task to the event loop that handles
// the printing of the receipt when the component is mounted.
// We are doing this because we want the receipt screen to be
// displayed regardless of what happen to the handleAutoPrint
// call.
setTimeout(async () => {
if (status(this) === "mounted") {
let images = this.orderReceipt.el.getElementsByTagName('img');
for (let image of images) {
await image.decode();
}
await this.handleAutoPrint();
onMounted(() => {
// Here, we send a task to the event loop that handles
// the printing of the receipt when the component is mounted.
// We are doing this because we want the receipt screen to be
// displayed regardless of what happen to the handleAutoPrint
// call.
setTimeout(async () => {
if (status(this) === "mounted") {
const images = this.orderReceipt.el.getElementsByTagName("img");
for (const image of images) {
await image.decode();
}
}, 0);
});
}
_addNewOrder() {
this.env.pos.add_new_order();
}
async onSendEmail() {
if (!is_email(this.orderUiState.inputEmail)) {
this.orderUiState.emailSuccessful = false;
this.orderUiState.emailNotice = this.env._t('Invalid email.');
return;
}
try {
await this._sendReceiptToCustomer();
this.orderUiState.emailSuccessful = true;
this.orderUiState.emailNotice = this.env._t('Email sent.');
} catch {
this.orderUiState.emailSuccessful = false;
this.orderUiState.emailNotice = this.env._t('Sending email failed. Please try again.');
}
}
get orderAmountPlusTip() {
const order = this.currentOrder;
const orderTotalAmount = order.get_total_with_tax();
const tip_product_id = this.env.pos.config.tip_product_id && this.env.pos.config.tip_product_id[0];
const tipLine = order
.get_orderlines()
.find((line) => tip_product_id && line.product.id === tip_product_id);
const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0;
const orderAmountStr = this.env.pos.format_currency(orderTotalAmount - tipAmount);
if (!tipAmount) return orderAmountStr;
const tipAmountStr = this.env.pos.format_currency(tipAmount);
return `${orderAmountStr} + ${tipAmountStr} tip`;
}
get currentOrder() {
return this.env.pos.get_order();
}
get nextScreen() {
return { name: 'ProductScreen' };
}
whenClosing() {
this.orderDone();
}
/**
* This function is called outside the rendering call stack. This way,
* we don't block the displaying of ReceiptScreen when it is mounted; additionally,
* any error that can happen during the printing does not affect the rendering.
*/
async handleAutoPrint() {
if (this._shouldAutoPrint()) {
await this.printReceipt();
if (this.currentOrder._printed && this._shouldCloseImmediately()) {
this.whenClosing();
await this.handleAutoPrint();
}
}
}, 0);
});
}
_addNewOrder() {
this.env.pos.add_new_order();
}
async onSendEmail() {
if (!is_email(this.orderUiState.inputEmail)) {
this.orderUiState.emailSuccessful = false;
this.orderUiState.emailNotice = this.env._t("Invalid email.");
return;
}
orderDone() {
this.env.pos.removeOrder(this.currentOrder);
this._addNewOrder();
const { name, props } = this.nextScreen;
this.showScreen(name, props);
if (this.env.pos.config.iface_customer_facing_display) {
this.env.pos.send_current_order_to_customer_facing_display();
}
}
async printReceipt() {
const isPrinted = await this._printReceipt();
if (isPrinted) {
this.currentOrder._printed = true;
}
}
_shouldAutoPrint() {
return this.env.pos.config.iface_print_auto && !this.currentOrder._printed;
}
_shouldCloseImmediately() {
var invoiced_finalized = this.currentOrder.is_to_invoice() ? this.currentOrder.finalized : true;
return this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen && invoiced_finalized;
}
async _sendReceiptToCustomer() {
const printer = new Printer(null, this.env.pos);
const receiptString = this.orderReceipt.el.innerHTML;
const ticketImage = await printer.htmlToImg(receiptString);
const order = this.currentOrder;
const partner = order.get_partner();
const orderName = order.get_name();
const orderPartner = { email: this.orderUiState.inputEmail, name: partner ? partner.name : this.orderUiState.inputEmail };
const order_server_id = this.env.pos.validated_orders_name_server_id_map[orderName];
await this.rpc({
model: 'pos.order',
method: 'action_receipt_to_customer',
args: [[order_server_id], orderName, orderPartner, ticketImage],
});
try {
await this._sendReceiptToCustomer();
this.orderUiState.emailSuccessful = true;
this.orderUiState.emailNotice = this.env._t("Email sent.");
} catch {
this.orderUiState.emailSuccessful = false;
this.orderUiState.emailNotice = this.env._t(
"Sending email failed. Please try again."
);
}
}
ReceiptScreen.template = 'ReceiptScreen';
return ReceiptScreen;
};
Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen);
get orderAmountPlusTip() {
const order = this.currentOrder;
const orderTotalAmount = order.get_total_with_tax();
const tip_product_id =
this.env.pos.config.tip_product_id && this.env.pos.config.tip_product_id[0];
const tipLine = order
.get_orderlines()
.find((line) => tip_product_id && line.product.id === tip_product_id);
const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0;
const orderAmountStr = this.env.pos.format_currency(orderTotalAmount - tipAmount);
if (!tipAmount) {
return orderAmountStr;
}
const tipAmountStr = this.env.pos.format_currency(tipAmount);
return `${orderAmountStr} + ${tipAmountStr} tip`;
}
get currentOrder() {
return this.env.pos.get_order();
}
get nextScreen() {
return { name: "ProductScreen" };
}
whenClosing() {
this.orderDone();
}
/**
* This function is called outside the rendering call stack. This way,
* we don't block the displaying of ReceiptScreen when it is mounted; additionally,
* any error that can happen during the printing does not affect the rendering.
*/
async handleAutoPrint() {
if (this._shouldAutoPrint()) {
await this.printReceipt();
if (this.currentOrder._printed && this._shouldCloseImmediately()) {
this.whenClosing();
}
}
}
orderDone() {
this.env.pos.removeOrder(this.currentOrder);
this._addNewOrder();
const { name, props } = this.nextScreen;
this.showScreen(name, props);
if (this.env.pos.config.iface_customer_facing_display) {
this.env.pos.send_current_order_to_customer_facing_display();
}
}
async printReceipt() {
const isPrinted = await this._printReceipt();
if (isPrinted) {
this.currentOrder._printed = true;
}
}
_shouldAutoPrint() {
return this.env.pos.config.iface_print_auto && !this.currentOrder._printed;
}
_shouldCloseImmediately() {
var invoiced_finalized = this.currentOrder.is_to_invoice()
? this.currentOrder.finalized
: true;
return (
this.env.proxy.printer &&
this.env.pos.config.iface_print_skip_screen &&
invoiced_finalized
);
}
async _sendReceiptToCustomer() {
const printer = new Printer(null, this.env.pos);
const receiptString = this.orderReceipt.el.innerHTML;
const ticketImage = await printer.htmlToImg(receiptString);
const order = this.currentOrder;
const partner = order.get_partner();
const orderName = order.get_name();
const orderPartner = {
email: this.orderUiState.inputEmail,
name: partner ? partner.name : this.orderUiState.inputEmail,
};
const order_server_id = this.env.pos.validated_orders_name_server_id_map[orderName];
await this.rpc({
model: "pos.order",
method: "action_receipt_to_customer",
args: [[order_server_id], orderName, orderPartner, ticketImage],
});
}
}
ReceiptScreen.template = "ReceiptScreen";
return ReceiptScreen;
});
};
Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen);
export default ReceiptScreen;
@@ -1,13 +1,11 @@
odoo.define('point_of_sale.WrappedProductNameLines', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class WrappedProductNameLines extends PosComponent {}
WrappedProductNameLines.template = 'WrappedProductNameLines';
class WrappedProductNameLines extends PosComponent {}
WrappedProductNameLines.template = "WrappedProductNameLines";
Registries.Component.add(WrappedProductNameLines);
Registries.Component.add(WrappedProductNameLines);
return WrappedProductNameLines;
});
export default WrappedProductNameLines;
@@ -1,105 +1,102 @@
odoo.define('point_of_sale.ScaleScreen', function(require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const { round_precision: round_pr } = require('web.utils');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import { round_precision as round_pr } from "web.utils";
import Registries from "@point_of_sale/js/Registries";
const { onMounted, onWillUnmount, useExternalListener, useState } = owl;
const { onMounted, onWillUnmount, useExternalListener, useState } = owl;
class ScaleScreen extends PosComponent {
/**
* @param {Object} props
* @param {Object} props.product The product to weight.
*/
setup() {
super.setup();
useExternalListener(document, 'keyup', this._onHotkeys);
this.state = useState({ weight: 0 });
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
onMounted() {
// start the scale reading
this._readScale();
}
onWillUnmount() {
// stop the scale reading
this.env.proxy_queue.clear();
}
back() {
this.props.resolve({ confirmed: false, payload: null });
this.trigger('close-temp-screen');
}
confirm() {
this.props.resolve({
confirmed: true,
payload: { weight: this.state.weight },
});
this.trigger('close-temp-screen');
}
_onHotkeys(event) {
if (event.key === 'Escape') {
this.back();
} else if (event.key === 'Enter') {
this.confirm();
}
}
_readScale() {
this.env.proxy_queue.schedule(this._setWeight.bind(this), {
duration: 500,
repeat: true,
});
}
async _setWeight() {
const reading = await this.env.proxy.scale_read();
this.state.weight = reading.weight;
}
get _activePricelist() {
const current_order = this.env.pos.get_order();
let current_pricelist = this.env.pos.default_pricelist;
if (current_order) {
current_pricelist = current_order.pricelist;
}
return current_pricelist;
}
get productWeightString() {
const defaultstr = (this.state.weight || 0).toFixed(3) + ' Kg';
if (!this.props.product || !this.env.pos) {
return defaultstr;
}
const unit_id = this.props.product.uom_id;
if (!unit_id) {
return defaultstr;
}
const unit = this.env.pos.units_by_id[unit_id[0]];
const weight = round_pr(this.state.weight || 0, unit.rounding);
let weightstr = weight.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
weightstr += ' ' + unit.name;
return weightstr;
}
get computedPriceString() {
return this.env.pos.format_currency(this.productPrice * this.state.weight);
}
get productPrice() {
const product = this.props.product;
return (product ? product.get_price(this._activePricelist, this.state.weight) : 0) || 0;
}
get productName() {
return (
(this.props.product ? this.props.product.display_name : undefined) ||
'Unnamed Product'
);
}
get productUom() {
return this.props.product
? this.env.pos.units_by_id[this.props.product.uom_id[0]].name
: '';
class ScaleScreen extends PosComponent {
/**
* @param {Object} props
* @param {Object} props.product The product to weight.
*/
setup() {
super.setup();
useExternalListener(document, "keyup", this._onHotkeys);
this.state = useState({ weight: 0 });
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
onMounted() {
// start the scale reading
this._readScale();
}
onWillUnmount() {
// stop the scale reading
this.env.proxy_queue.clear();
}
back() {
this.props.resolve({ confirmed: false, payload: null });
this.trigger("close-temp-screen");
}
confirm() {
this.props.resolve({
confirmed: true,
payload: { weight: this.state.weight },
});
this.trigger("close-temp-screen");
}
_onHotkeys(event) {
if (event.key === "Escape") {
this.back();
} else if (event.key === "Enter") {
this.confirm();
}
}
ScaleScreen.template = 'ScaleScreen';
_readScale() {
this.env.proxy_queue.schedule(this._setWeight.bind(this), {
duration: 500,
repeat: true,
});
}
async _setWeight() {
const reading = await this.env.proxy.scale_read();
this.state.weight = reading.weight;
}
get _activePricelist() {
const current_order = this.env.pos.get_order();
let current_pricelist = this.env.pos.default_pricelist;
if (current_order) {
current_pricelist = current_order.pricelist;
}
return current_pricelist;
}
get productWeightString() {
const defaultstr = (this.state.weight || 0).toFixed(3) + " Kg";
if (!this.props.product || !this.env.pos) {
return defaultstr;
}
const unit_id = this.props.product.uom_id;
if (!unit_id) {
return defaultstr;
}
const unit = this.env.pos.units_by_id[unit_id[0]];
const weight = round_pr(this.state.weight || 0, unit.rounding);
let weightstr = weight.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
weightstr += " " + unit.name;
return weightstr;
}
get computedPriceString() {
return this.env.pos.format_currency(this.productPrice * this.state.weight);
}
get productPrice() {
const product = this.props.product;
return (product ? product.get_price(this._activePricelist, this.state.weight) : 0) || 0;
}
get productName() {
return (
(this.props.product ? this.props.product.display_name : undefined) || "Unnamed Product"
);
}
get productUom() {
return this.props.product
? this.env.pos.units_by_id[this.props.product.uom_id[0]].name
: "";
}
}
ScaleScreen.template = "ScaleScreen";
Registries.Component.add(ScaleScreen);
Registries.Component.add(ScaleScreen);
return ScaleScreen;
});
export default ScaleScreen;
@@ -1,128 +1,131 @@
odoo.define('point_of_sale.InvoiceButton', function (require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const { isConnectionError } = require('point_of_sale.utils');
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useListener } from "@web/core/utils/hooks";
import { isConnectionError } from "@point_of_sale/js/utils";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class InvoiceButton extends PosComponent {
setup() {
super.setup();
useListener('click', this._onClick);
class InvoiceButton extends PosComponent {
setup() {
super.setup();
useListener("click", this._onClick);
}
get isAlreadyInvoiced() {
if (!this.props.order) {
return false;
}
get isAlreadyInvoiced() {
if (!this.props.order) return false;
return Boolean(this.props.order.account_move);
return Boolean(this.props.order.account_move);
}
get commandName() {
if (!this.props.order) {
return this.env._t("Invoice");
} else {
return this.isAlreadyInvoiced ? this.env._t("Reprint Invoice") : this.env._t("Invoice");
}
get commandName() {
if (!this.props.order) {
return this.env._t('Invoice');
}
async _downloadInvoice(orderId) {
try {
const [orderWithInvoice] = await this.rpc({
method: "read",
model: "pos.order",
args: [orderId, ["account_move"]],
kwargs: { load: false },
});
if (orderWithInvoice && orderWithInvoice.account_move) {
await this.env.legacyActionManager.do_action("account.account_invoices", {
additional_context: {
active_ids: [orderWithInvoice.account_move],
},
});
}
} catch (error) {
if (error instanceof Error) {
throw error;
} else {
return this.isAlreadyInvoiced
? this.env._t('Reprint Invoice')
: this.env._t('Invoice');
}
}
async _downloadInvoice(orderId) {
try {
const [orderWithInvoice] = await this.rpc({
method: 'read',
model: 'pos.order',
args: [orderId, ['account_move']],
kwargs: { load: false },
// NOTE: error here is most probably undefined
this.showPopup("ErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t("Unable to download invoice."),
});
if (orderWithInvoice && orderWithInvoice.account_move) {
await this.env.legacyActionManager.do_action('account.account_invoices', {
additional_context: {
active_ids: [orderWithInvoice.account_move],
},
});
}
} catch (error) {
if (error instanceof Error) {
throw error;
} else {
// NOTE: error here is most probably undefined
this.showPopup('ErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Unable to download invoice.'),
});
}
}
}
async _invoiceOrder() {
const order = this.props.order;
if (!order) return;
const orderId = order.backendId;
// Part 0. If already invoiced, print the invoice.
if (this.isAlreadyInvoiced) {
await this._downloadInvoice(orderId);
return;
}
// Part 1: Handle missing partner.
// Write to pos.order the selected partner.
if (!order.get_partner()) {
const { confirmed: confirmedPopup } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Need customer to invoice'),
body: this.env._t('Do you want to open the customer list to select customer?'),
});
if (!confirmedPopup) return;
const { confirmed: confirmedTempScreen, payload: newPartner } = await this.showTempScreen(
'PartnerListScreen'
);
if (!confirmedTempScreen) return;
await this.rpc({
model: 'pos.order',
method: 'write',
args: [[orderId], { partner_id: newPartner.id }],
kwargs: { context: this.env.session.user_context },
});
}
// Part 2: Invoice the order.
await this.rpc(
{
model: 'pos.order',
method: 'action_pos_order_invoice',
args: [orderId],
kwargs: { context: this.env.session.user_context },
},
{
timeout: 30000,
shadow: true,
}
);
// Part 3: Download invoice.
await this._downloadInvoice(orderId);
this.trigger('order-invoiced', orderId);
}
async _onClick() {
try {
this.el.style.pointerEvents = 'none';
await this._invoiceOrder();
} catch (error) {
if (isConnectionError(error)) {
this.showPopup('ErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Unable to invoice order.'),
});
} else {
throw error;
}
} finally {
this.el.style.pointerEvents = 'auto';
}
}
}
InvoiceButton.template = 'InvoiceButton';
Registries.Component.add(InvoiceButton);
async _invoiceOrder() {
const order = this.props.order;
if (!order) {
return;
}
return InvoiceButton;
});
const orderId = order.backendId;
// Part 0. If already invoiced, print the invoice.
if (this.isAlreadyInvoiced) {
await this._downloadInvoice(orderId);
return;
}
// Part 1: Handle missing partner.
// Write to pos.order the selected partner.
if (!order.get_partner()) {
const { confirmed: confirmedPopup } = await this.showPopup("ConfirmPopup", {
title: this.env._t("Need customer to invoice"),
body: this.env._t("Do you want to open the customer list to select customer?"),
});
if (!confirmedPopup) {
return;
}
const { confirmed: confirmedTempScreen, payload: newPartner } =
await this.showTempScreen("PartnerListScreen");
if (!confirmedTempScreen) {
return;
}
await this.rpc({
model: "pos.order",
method: "write",
args: [[orderId], { partner_id: newPartner.id }],
kwargs: { context: this.env.session.user_context },
});
}
// Part 2: Invoice the order.
await this.rpc(
{
model: "pos.order",
method: "action_pos_order_invoice",
args: [orderId],
kwargs: { context: this.env.session.user_context },
},
{
timeout: 30000,
shadow: true,
}
);
// Part 3: Download invoice.
await this._downloadInvoice(orderId);
this.trigger("order-invoiced", orderId);
}
async _onClick() {
try {
this.el.style.pointerEvents = "none";
await this._invoiceOrder();
} catch (error) {
if (isConnectionError(error)) {
this.showPopup("ErrorPopup", {
title: this.env._t("Network Error"),
body: this.env._t("Unable to invoice order."),
});
} else {
throw error;
}
} finally {
this.el.style.pointerEvents = "auto";
}
}
}
InvoiceButton.template = "InvoiceButton";
Registries.Component.add(InvoiceButton);
export default InvoiceButton;
@@ -1,22 +1,22 @@
odoo.define('point_of_sale.ReprintReceiptButton', function (require) {
'use strict';
/** @odoo-module */
const { useListener } = require("@web/core/utils/hooks");
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import { useListener } from "@web/core/utils/hooks";
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
class ReprintReceiptButton extends PosComponent {
setup() {
super.setup();
useListener('click', this._onClick);
}
async _onClick() {
if (!this.props.order) return;
this.showScreen('ReprintReceiptScreen', { order: this.props.order });
}
class ReprintReceiptButton extends PosComponent {
setup() {
super.setup();
useListener("click", this._onClick);
}
ReprintReceiptButton.template = 'ReprintReceiptButton';
Registries.Component.add(ReprintReceiptButton);
async _onClick() {
if (!this.props.order) {
return;
}
this.showScreen("ReprintReceiptScreen", { order: this.props.order });
}
}
ReprintReceiptButton.template = "ReprintReceiptButton";
Registries.Component.add(ReprintReceiptButton);
return ReprintReceiptButton;
});
export default ReprintReceiptButton;
@@ -1,29 +1,27 @@
odoo.define('point_of_sale.OrderDetails', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
/**
* @props {models.Order} order
*/
class OrderDetails extends PosComponent {
get order() {
return this.props.order;
}
get orderlines() {
return this.order ? this.order.orderlines : [];
}
get total() {
return this.env.pos.format_currency(this.order ? this.order.get_total_with_tax() : 0);
}
get tax() {
return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0)
}
/**
* @props {models.Order} order
*/
class OrderDetails extends PosComponent {
get order() {
return this.props.order;
}
OrderDetails.template = 'OrderDetails';
get orderlines() {
return this.order ? this.order.orderlines : [];
}
get total() {
return this.env.pos.format_currency(this.order ? this.order.get_total_with_tax() : 0);
}
get tax() {
return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0);
}
}
OrderDetails.template = "OrderDetails";
Registries.Component.add(OrderDetails);
Registries.Component.add(OrderDetails);
return OrderDetails;
});
export default OrderDetails;
@@ -1,81 +1,79 @@
odoo.define('point_of_sale.OrderlineDetails', function (require) {
'use strict';
/** @odoo-module */
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { format } = require('web.field_utils');
const { round_precision: round_pr } = require('web.utils');
import PosComponent from "@point_of_sale/js/PosComponent";
import Registries from "@point_of_sale/js/Registries";
import { format } from "web.field_utils";
import { round_precision as round_pr } from "web.utils";
/**
* @props {pos.order.line} line
*/
class OrderlineDetails extends PosComponent {
get line() {
const line = this.props.line;
const formatQty = (line) => {
const quantity = line.get_quantity();
const unit = line.get_unit();
const decimals = this.env.pos.dp['Product Unit of Measure'];
const rounding = Math.max(unit.rounding, Math.pow(10, -decimals));
const roundedQuantity = round_pr(quantity, rounding);
return format.float(roundedQuantity, { digits: [69, decimals] });
};
return {
productName: line.get_full_product_name(),
totalPrice: line.get_price_with_tax(),
quantity: formatQty(line),
unit: line.get_unit().name,
unitPrice: line.get_unit_price(),
};
}
get productName() {
return this.line.productName;
}
get totalPrice() {
return this.env.pos.format_currency(this.line.totalPrice);
}
get quantity() {
return this.line.quantity;
}
get unitPrice() {
return this.env.pos.format_currency(this.line.unitPrice);
}
get unit() {
return this.line.unit;
}
get pricePerUnit() {
return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`;
}
get customerNote() {
return this.props.line.get_customer_note();
}
getToRefundDetail() {
return this.env.pos.toRefundLines[this.props.line.id];
}
hasRefundedQty() {
return !this.env.pos.isProductQtyZero(this.props.line.refunded_qty);
}
getFormattedRefundedQty() {
return this.env.pos.formatProductQty(this.props.line.refunded_qty);
}
hasToRefundQty() {
const toRefundDetail = this.getToRefundDetail();
return !this.env.pos.isProductQtyZero(toRefundDetail && toRefundDetail.qty);
}
getFormattedToRefundQty() {
const toRefundDetail = this.getToRefundDetail();
return this.env.pos.formatProductQty(toRefundDetail && toRefundDetail.qty);
}
getRefundingMessage() {
return _.str.sprintf(this.env._t('Refunding %s in '), this.getFormattedToRefundQty());
}
getToRefundMessage() {
return _.str.sprintf(this.env._t('To Refund: %s'), this.getFormattedToRefundQty());
}
/**
* @props {pos.order.line} line
*/
class OrderlineDetails extends PosComponent {
get line() {
const line = this.props.line;
const formatQty = (line) => {
const quantity = line.get_quantity();
const unit = line.get_unit();
const decimals = this.env.pos.dp["Product Unit of Measure"];
const rounding = Math.max(unit.rounding, Math.pow(10, -decimals));
const roundedQuantity = round_pr(quantity, rounding);
return format.float(roundedQuantity, { digits: [69, decimals] });
};
return {
productName: line.get_full_product_name(),
totalPrice: line.get_price_with_tax(),
quantity: formatQty(line),
unit: line.get_unit().name,
unitPrice: line.get_unit_price(),
};
}
OrderlineDetails.template = 'OrderlineDetails';
get productName() {
return this.line.productName;
}
get totalPrice() {
return this.env.pos.format_currency(this.line.totalPrice);
}
get quantity() {
return this.line.quantity;
}
get unitPrice() {
return this.env.pos.format_currency(this.line.unitPrice);
}
get unit() {
return this.line.unit;
}
get pricePerUnit() {
return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`;
}
get customerNote() {
return this.props.line.get_customer_note();
}
getToRefundDetail() {
return this.env.pos.toRefundLines[this.props.line.id];
}
hasRefundedQty() {
return !this.env.pos.isProductQtyZero(this.props.line.refunded_qty);
}
getFormattedRefundedQty() {
return this.env.pos.formatProductQty(this.props.line.refunded_qty);
}
hasToRefundQty() {
const toRefundDetail = this.getToRefundDetail();
return !this.env.pos.isProductQtyZero(toRefundDetail && toRefundDetail.qty);
}
getFormattedToRefundQty() {
const toRefundDetail = this.getToRefundDetail();
return this.env.pos.formatProductQty(toRefundDetail && toRefundDetail.qty);
}
getRefundingMessage() {
return _.str.sprintf(this.env._t("Refunding %s in "), this.getFormattedToRefundQty());
}
getToRefundMessage() {
return _.str.sprintf(this.env._t("To Refund: %s"), this.getFormattedToRefundQty());
}
}
OrderlineDetails.template = "OrderlineDetails";
Registries.Component.add(OrderlineDetails);
Registries.Component.add(OrderlineDetails);
return OrderlineDetails;
});
export default OrderlineDetails;
@@ -1,36 +1,35 @@
odoo.define('point_of_sale.ReprintReceiptScreen', function (require) {
'use strict';
/** @odoo-module */
const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen');
const Registries = require('point_of_sale.Registries');
import AbstractReceiptScreen from "@point_of_sale/js/Misc/AbstractReceiptScreen";
import Registries from "@point_of_sale/js/Registries";
const ReprintReceiptScreen = (AbstractReceiptScreen) => {
class ReprintReceiptScreen extends AbstractReceiptScreen {
setup() {
super.setup();
owl.onMounted(this.onMounted);
}
onMounted() {
this.printReceipt();
}
confirm() {
this.showScreen('TicketScreen', { reuseSavedUIState: true });
}
async printReceipt() {
if(this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) {
let result = await this._printReceipt();
if(result)
this.showScreen('TicketScreen', { reuseSavedUIState: true });
const ReprintReceiptScreen = (AbstractReceiptScreen) => {
class ReprintReceiptScreen extends AbstractReceiptScreen {
setup() {
super.setup();
owl.onMounted(this.onMounted);
}
onMounted() {
this.printReceipt();
}
confirm() {
this.showScreen("TicketScreen", { reuseSavedUIState: true });
}
async printReceipt() {
if (this.env.proxy.printer && this.env.pos.config.iface_print_skip_screen) {
const result = await this._printReceipt();
if (result) {
this.showScreen("TicketScreen", { reuseSavedUIState: true });
}
}
async tryReprint() {
await this._printReceipt();
}
}
ReprintReceiptScreen.template = 'ReprintReceiptScreen';
return ReprintReceiptScreen;
};
Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen);
async tryReprint() {
await this._printReceipt();
}
}
ReprintReceiptScreen.template = "ReprintReceiptScreen";
return ReprintReceiptScreen;
});
};
Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen);
export default ReprintReceiptScreen;
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,7 @@
odoo.define('point_of_sale.BarcodeReader', function (require) {
"use strict";
/** @odoo-module */
var concurrency = require('web.concurrency');
var core = require('web.core');
import concurrency from "web.concurrency";
import core from "web.core";
var Mutex = concurrency.Mutex;
// this module interfaces with the barcode reader. It assumes the barcode reader
@@ -10,11 +9,7 @@ var Mutex = concurrency.Mutex;
// and deactivate the barcode reader. Use set_action_callbacks to tell it
// what to do when it reads a barcode.
var BarcodeReader = core.Class.extend({
actions:[
'product',
'cashier',
'client',
],
actions: ["product", "cashier", "client"],
init: function (attributes) {
this.mutex = new Mutex();
@@ -29,7 +24,7 @@ var BarcodeReader = core.Class.extend({
this.action_callback_stack = [];
core.bus.on('barcode_scanned', this, function (barcode) {
core.bus.on("barcode_scanned", this, function (barcode) {
// use mutex to make sure scans are done one after the other
this.mutex.exec(async () => {
await this.scan(barcode);
@@ -59,7 +54,7 @@ var BarcodeReader = core.Class.extend({
}
},
remove_action_callback: function(name, callback) {
remove_action_callback: function (name, callback) {
if (!callback) {
delete this.action_callbacks[name];
return;
@@ -105,13 +100,15 @@ var BarcodeReader = core.Class.extend({
},
scan: async function (code) {
if (!code) return;
if (!code) {
return;
}
const callbacks = Object.keys(this.exclusive_callbacks).length
? this.exclusive_callbacks
: this.action_callbacks;
let parsed_results = this.barcode_parser.parse_barcode(code);
if (! Array.isArray(parsed_results)) {
if (!Array.isArray(parsed_results)) {
parsed_results = [parsed_results];
}
for (const parsed_result of parsed_results) {
@@ -120,9 +117,9 @@ var BarcodeReader = core.Class.extend({
await cb(parsed_result);
}
} else if (callbacks.error) {
[...callbacks.error].map(cb => cb(parsed_result));
[...callbacks.error].map((cb) => cb(parsed_result));
} else {
console.warn('Ignored Barcode Scan:', parsed_result);
console.warn("Ignored Barcode Scan:", parsed_result);
}
}
},
@@ -137,23 +134,26 @@ var BarcodeReader = core.Class.extend({
}
this.remote_active = 1;
function waitforbarcode(){
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500})
.then(function (barcode) {
if (!self.remote_scanning) {
self.remote_active = 0;
return;
function waitforbarcode() {
return self.proxy.connection
.rpc("/hw_proxy/scanner", {}, { shadow: true, timeout: 7500 })
.then(
function (barcode) {
if (!self.remote_scanning) {
self.remote_active = 0;
return;
}
self.scan(barcode);
waitforbarcode();
},
function () {
if (!self.remote_scanning) {
self.remote_active = 0;
return;
}
waitforbarcode();
}
self.scan(barcode);
waitforbarcode();
},
function () {
if (!self.remote_scanning) {
self.remote_active = 0;
return;
}
waitforbarcode();
});
);
}
waitforbarcode();
},
@@ -164,6 +164,4 @@ var BarcodeReader = core.Class.extend({
},
});
return BarcodeReader;
});
export default BarcodeReader;
+105 -107
View File
@@ -1,118 +1,116 @@
odoo.define('point_of_sale.custom_hooks', function (require) {
'use strict';
/** @odoo-module */
const { onMounted, onPatched, onWillUnmount, useComponent } = owl;
const { onMounted, onPatched, onWillUnmount, useComponent } = owl;
/**
* Introduce error handlers in the component.
*
* IMPROVEMENT: This is a terrible hook. There could be a better way to handle
* the error when the order failed to sync.
*/
export function useErrorHandlers() {
const component = useComponent();
/**
* Introduce error handlers in the component.
*
* IMPROVEMENT: This is a terrible hook. There could be a better way to handle
* the error when the order failed to sync.
*/
function useErrorHandlers() {
const component = useComponent();
component._handlePushOrderError = async function (error) {
// This error handler receives `error` equivalent to `error.message` of the rpc error.
if (error.message === 'Backend Invoice') {
await this.showPopup('ConfirmPopup', {
title: this.env._t('Please print the invoice from the backend'),
body:
this.env._t(
'The order has been synchronized earlier. Please make the invoice from the backend for the order: '
) + error.data.order.name,
});
} else if (error.code < 0) {
// XmlHttpRequest Errors
const title = this.env._t('Unable to sync order');
const body = this.env._t(
'Check the internet connection then try to sync again by clicking on the red wifi button (upper right of the screen).'
);
await this.showPopup('OfflineErrorPopup', { title, body });
} else if (error.code === 200) {
// OpenERP Server Errors
await this.showPopup('ErrorTracebackPopup', {
title: error.data.message || this.env._t('Server Error'),
body:
error.data.debug ||
this.env._t('The server encountered an error while receiving your order.'),
});
} else if (error.code === 700) {
// Sweden Fiscal module errors
await this.showPopup('ErrorPopup', {
title: this.env._t('Fiscal data module error'),
body:
error.data.error.status ||
this.env._t('The fiscal data module encountered an error while receiving your order.'),
});
} else if (error.code === 701) {
// Belgian Fiscal module errors
let bodyMessage = "";
if(error.error.errorCode)
bodyMessage = "'" + error.error.errorCode + "': " + error.error.errorMessage;
else
bodyMessage = "Fiscal data module is not on.";
await this.showPopup('ErrorPopup', {
title: this.env._t('Fiscal data module error'),
body: bodyMessage
});
} else {
// ???
await this.showPopup('ErrorPopup', {
title: this.env._t('Unknown Error'),
body: this.env._t(
'The order could not be sent to the server due to an unknown error'
component._handlePushOrderError = async function (error) {
// This error handler receives `error` equivalent to `error.message` of the rpc error.
if (error.message === "Backend Invoice") {
await this.showPopup("ConfirmPopup", {
title: this.env._t("Please print the invoice from the backend"),
body:
this.env._t(
"The order has been synchronized earlier. Please make the invoice from the backend for the order: "
) + error.data.order.name,
});
} else if (error.code < 0) {
// XmlHttpRequest Errors
const title = this.env._t("Unable to sync order");
const body = this.env._t(
"Check the internet connection then try to sync again by clicking on the red wifi button (upper right of the screen)."
);
await this.showPopup("OfflineErrorPopup", { title, body });
} else if (error.code === 200) {
// OpenERP Server Errors
await this.showPopup("ErrorTracebackPopup", {
title: error.data.message || this.env._t("Server Error"),
body:
error.data.debug ||
this.env._t("The server encountered an error while receiving your order."),
});
} else if (error.code === 700) {
// Sweden Fiscal module errors
await this.showPopup("ErrorPopup", {
title: this.env._t("Fiscal data module error"),
body:
error.data.error.status ||
this.env._t(
"The fiscal data module encountered an error while receiving your order."
),
});
}
};
}
function useAutoFocusToLast() {
const current = useComponent();
let target = null;
function autofocus() {
const prevTarget = target;
const allInputs = current.el.querySelectorAll('input');
target = allInputs[allInputs.length - 1];
if (target && target !== prevTarget) {
target.focus();
target.selectionStart = target.selectionEnd = target.value.length;
});
} else if (error.code === 701) {
// Belgian Fiscal module errors
let bodyMessage = "";
if (error.error.errorCode) {
bodyMessage = "'" + error.error.errorCode + "': " + error.error.errorMessage;
} else {
bodyMessage = "Fiscal data module is not on.";
}
await this.showPopup("ErrorPopup", {
title: this.env._t("Fiscal data module error"),
body: bodyMessage,
});
} else {
// ???
await this.showPopup("ErrorPopup", {
title: this.env._t("Unknown Error"),
body: this.env._t(
"The order could not be sent to the server due to an unknown error"
),
});
}
onMounted(autofocus);
onPatched(autofocus);
}
};
}
function useBarcodeReader(callbackMap, exclusive = false) {
const current = useComponent();
const barcodeReader = current.env.barcode_reader;
for (let [key, callback] of Object.entries(callbackMap)) {
callbackMap[key] = callback.bind(current);
export function useAutoFocusToLast() {
const current = useComponent();
let target = null;
function autofocus() {
const prevTarget = target;
const allInputs = current.el.querySelectorAll("input");
target = allInputs[allInputs.length - 1];
if (target && target !== prevTarget) {
target.focus();
target.selectionStart = target.selectionEnd = target.value.length;
}
onMounted(() => {
if (barcodeReader) {
for (let key in callbackMap) {
if (exclusive) {
barcodeReader.set_exclusive_callback(key, callbackMap[key]);
} else {
barcodeReader.set_action_callback(key, callbackMap[key]);
}
}
onMounted(autofocus);
onPatched(autofocus);
}
export function useBarcodeReader(callbackMap, exclusive = false) {
const current = useComponent();
const barcodeReader = current.env.barcode_reader;
for (const [key, callback] of Object.entries(callbackMap)) {
callbackMap[key] = callback.bind(current);
}
onMounted(() => {
if (barcodeReader) {
for (const key in callbackMap) {
if (exclusive) {
barcodeReader.set_exclusive_callback(key, callbackMap[key]);
} else {
barcodeReader.set_action_callback(key, callbackMap[key]);
}
}
});
onWillUnmount(() => {
if (barcodeReader) {
for (let key in callbackMap) {
if (exclusive) {
barcodeReader.remove_exclusive_callback(key, callbackMap[key]);
} else {
barcodeReader.remove_action_callback(key, callbackMap[key]);
}
}
});
onWillUnmount(() => {
if (barcodeReader) {
for (const key in callbackMap) {
if (exclusive) {
barcodeReader.remove_exclusive_callback(key, callbackMap[key]);
} else {
barcodeReader.remove_action_callback(key, callbackMap[key]);
}
}
});
}
return { useErrorHandlers, useAutoFocusToLast, useBarcodeReader };
});
}
});
}
+204 -182
View File
@@ -1,14 +1,12 @@
odoo.define('point_of_sale.DB', function (require) {
"use strict";
/** @odoo-module */
var core = require('web.core');
var utils = require('web.utils');
import core from "web.core";
import utils from "web.utils";
/* The PosDB holds reference to data that is either
* - static: does not change between pos reloads
* - persistent : must stay between reloads ( orders )
*/
/**
* cache the data in memory to avoid roundtrips to the localstorage
*
@@ -21,15 +19,15 @@ var utils = require('web.utils');
const CACHE = {};
var PosDB = core.Class.extend({
name: 'openerp_pos_db', //the prefix of the localstorage data
limit: 100, // the maximum number of results returned by a search
init: function(options){
name: "openerp_pos_db", //the prefix of the localstorage data
limit: 100, // the maximum number of results returned by a search
init: function (options) {
options = options || {};
this.name = options.name || this.name;
this.limit = options.limit || this.limit;
if (options.uuid) {
this.name = this.name + '_' + options.uuid;
this.name = this.name + "_" + options.uuid;
}
this.product_by_id = {};
@@ -44,11 +42,11 @@ var PosDB = core.Class.extend({
this.partner_write_date = null;
this.category_by_id = {};
this.root_category_id = 0;
this.root_category_id = 0;
this.category_products = {};
this.category_ancestors = {};
this.category_childs = {};
this.category_parent = {};
this.category_parent = {};
this.category_search_string = {};
},
@@ -59,87 +57,87 @@ var PosDB = core.Class.extend({
*
* @param {string} uuid Unique identifier of the PoS Config linked to the current session.
*/
set_uuid: function(uuid){
this.name = this.name + '_' + uuid;
set_uuid: function (uuid) {
this.name = this.name + "_" + uuid;
},
/* returns the category object from its id. If you pass a list of id as parameters, you get
* a list of category objects.
*/
get_category_by_id: function(categ_id){
if(categ_id instanceof Array){
get_category_by_id: function (categ_id) {
if (categ_id instanceof Array) {
var list = [];
for(var i = 0, len = categ_id.length; i < len; i++){
for (var i = 0, len = categ_id.length; i < len; i++) {
var cat = this.category_by_id[categ_id[i]];
if(cat){
if (cat) {
list.push(cat);
}else{
console.error("get_category_by_id: no category has id:",categ_id[i]);
} else {
console.error("get_category_by_id: no category has id:", categ_id[i]);
}
}
return list;
}else{
} else {
return this.category_by_id[categ_id];
}
},
/* returns a list of the category's child categories ids, or an empty list
* if a category has no childs */
get_category_childs_ids: function(categ_id){
get_category_childs_ids: function (categ_id) {
return this.category_childs[categ_id] || [];
},
/* returns a list of all ancestors (parent, grand-parent, etc) categories ids
* starting from the root category to the direct parent */
get_category_ancestors_ids: function(categ_id){
get_category_ancestors_ids: function (categ_id) {
return this.category_ancestors[categ_id] || [];
},
/* returns the parent category's id of a category, or the root_category_id if no parent.
* the root category is parent of itself. */
get_category_parent_id: function(categ_id){
get_category_parent_id: function (categ_id) {
return this.category_parent[categ_id] || this.root_category_id;
},
/* adds categories definitions to the database. categories is a list of categories objects as
* returned by the openerp server. Categories must be inserted before the products or the
* product/ categories association may (will) not work properly */
add_categories: function(categories){
add_categories: function (categories) {
var self = this;
if(!this.category_by_id[this.root_category_id]){
if (!this.category_by_id[this.root_category_id]) {
this.category_by_id[this.root_category_id] = {
id : this.root_category_id,
name : 'Root',
id: this.root_category_id,
name: "Root",
};
}
categories.forEach(function(cat){
categories.forEach(function (cat) {
self.category_by_id[cat.id] = cat;
});
categories.forEach(function(cat){
categories.forEach(function (cat) {
var parent_id = cat.parent_id[0];
if(!(parent_id && self.category_by_id[parent_id])){
if (!(parent_id && self.category_by_id[parent_id])) {
parent_id = self.root_category_id;
}
self.category_parent[cat.id] = parent_id;
if(!self.category_childs[parent_id]){
if (!self.category_childs[parent_id]) {
self.category_childs[parent_id] = [];
}
self.category_childs[parent_id].push(cat.id);
});
function make_ancestors(cat_id, ancestors){
function make_ancestors(cat_id, ancestors) {
self.category_ancestors[cat_id] = ancestors;
ancestors = ancestors.slice(0);
ancestors.push(cat_id);
var childs = self.category_childs[cat_id] || [];
for(var i=0, len = childs.length; i < len; i++){
for (var i = 0, len = childs.length; i < len; i++) {
make_ancestors(childs[i], ancestors);
}
}
make_ancestors(this.root_category_id, []);
},
category_contains: function(categ_id, product_id) {
category_contains: function (categ_id, product_id) {
var product = this.product_by_id[product_id];
if (product) {
var cid = product.pos_categ_id[0];
while (cid && cid !== categ_id){
while (cid && cid !== categ_id) {
cid = this.category_parent[cid];
}
return !!cid;
@@ -147,139 +145,151 @@ var PosDB = core.Class.extend({
return false;
},
/* loads a record store from the database. returns default if nothing is found */
load: function(store,deft){
if(CACHE[store] !== undefined){
load: function (store, deft) {
if (CACHE[store] !== undefined) {
return CACHE[store];
}
var data = localStorage[this.name + '_' + store];
if(data !== undefined && data !== ""){
var data = localStorage[this.name + "_" + store];
if (data !== undefined && data !== "") {
data = JSON.parse(data);
CACHE[store] = data;
return data;
}else{
} else {
return deft;
}
},
/* saves a record store to the database */
save: function(store,data){
localStorage[this.name + '_' + store] = JSON.stringify(data);
save: function (store, data) {
localStorage[this.name + "_" + store] = JSON.stringify(data);
CACHE[store] = data;
},
_product_search_string: function(product){
_product_search_string: function (product) {
var str = product.display_name;
if (product.barcode) {
str += '|' + product.barcode;
str += "|" + product.barcode;
}
if (product.default_code) {
str += '|' + product.default_code;
str += "|" + product.default_code;
}
if (product.description) {
str += '|' + product.description;
str += "|" + product.description;
}
if (product.description_sale) {
str += '|' + product.description_sale;
str += "|" + product.description_sale;
}
str = product.id + ':' + str.replace(/:/g,'') + '\n';
str = product.id + ":" + str.replace(/:/g, "") + "\n";
return str;
},
add_products: function(products){
add_products: function (products) {
var stored_categories = this.product_by_category_id;
if(!(products instanceof Array)){
if (!(products instanceof Array)) {
products = [products];
}
for(var i = 0, len = products.length; i < len; i++){
for (var i = 0, len = products.length; i < len; i++) {
var product = products[i];
if (product.id in this.product_by_id) continue;
if (product.available_in_pos){
if (product.id in this.product_by_id) {
continue;
}
if (product.available_in_pos) {
var search_string = utils.unaccent(this._product_search_string(product));
var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id;
var categ_id = product.pos_categ_id
? product.pos_categ_id[0]
: this.root_category_id;
product.product_tmpl_id = product.product_tmpl_id[0];
if(!stored_categories[categ_id]){
if (!stored_categories[categ_id]) {
stored_categories[categ_id] = [];
}
stored_categories[categ_id].push(product.id);
if(this.category_search_string[categ_id] === undefined){
this.category_search_string[categ_id] = '';
if (this.category_search_string[categ_id] === undefined) {
this.category_search_string[categ_id] = "";
}
this.category_search_string[categ_id] += search_string;
var ancestors = this.get_category_ancestors_ids(categ_id) || [];
for(var j = 0, jlen = ancestors.length; j < jlen; j++){
for (var j = 0, jlen = ancestors.length; j < jlen; j++) {
var ancestor = ancestors[j];
if(! stored_categories[ancestor]){
if (!stored_categories[ancestor]) {
stored_categories[ancestor] = [];
}
stored_categories[ancestor].push(product.id);
if( this.category_search_string[ancestor] === undefined){
this.category_search_string[ancestor] = '';
if (this.category_search_string[ancestor] === undefined) {
this.category_search_string[ancestor] = "";
}
this.category_search_string[ancestor] += search_string;
}
}
this.product_by_id[product.id] = product;
if(product.barcode){
if (product.barcode) {
this.product_by_barcode[product.barcode] = product;
}
}
},
add_packagings: function(product_packagings){
add_packagings: function (product_packagings) {
var self = this;
_.map(product_packagings, function (product_packaging) {
if (_.find(self.product_by_id, {'id': product_packaging.product_id[0]})) {
if (_.find(self.product_by_id, { id: product_packaging.product_id[0] })) {
self.product_packaging_by_barcode[product_packaging.barcode] = product_packaging;
}
});
},
_partner_search_string: function(partner){
var str = partner.name || '';
if(partner.barcode){
str += '|' + partner.barcode;
_partner_search_string: function (partner) {
var str = partner.name || "";
if (partner.barcode) {
str += "|" + partner.barcode;
}
if(partner.address){
str += '|' + partner.address;
if (partner.address) {
str += "|" + partner.address;
}
if(partner.phone){
str += '|' + partner.phone.split(' ').join('');
if (partner.phone) {
str += "|" + partner.phone.split(" ").join("");
}
if(partner.mobile){
str += '|' + partner.mobile.split(' ').join('');
if (partner.mobile) {
str += "|" + partner.mobile.split(" ").join("");
}
if(partner.email){
str += '|' + partner.email;
if (partner.email) {
str += "|" + partner.email;
}
if(partner.vat){
str += '|' + partner.vat;
if (partner.vat) {
str += "|" + partner.vat;
}
if(partner.parent_name){
str += '|' + partner.parent_name;
if (partner.parent_name) {
str += "|" + partner.parent_name;
}
str = '' + partner.id + ':' + str.replace(':', '').replace(/\n/g, ' ') + '\n';
str = "" + partner.id + ":" + str.replace(":", "").replace(/\n/g, " ") + "\n";
return str;
},
add_partners: function(partners){
add_partners: function (partners) {
var updated_count = 0;
var new_write_date = '';
var new_write_date = "";
var partner;
for(var i = 0, len = partners.length; i < len; i++){
for (var i = 0, len = partners.length; i < len; i++) {
partner = partners[i];
var local_partner_date = (this.partner_write_date || '').replace(/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, '$1T$2Z');
var dist_partner_date = (partner.write_date || '').replace(/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/, '$1T$2Z');
if ( this.partner_write_date &&
this.partner_by_id[partner.id] &&
new Date(local_partner_date).getTime() + 1000 >=
new Date(dist_partner_date).getTime() ) {
var local_partner_date = (this.partner_write_date || "").replace(
/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/,
"$1T$2Z"
);
var dist_partner_date = (partner.write_date || "").replace(
/^(\d{4}-\d{2}-\d{2}) ((\d{2}:?){3})$/,
"$1T$2Z"
);
if (
this.partner_write_date &&
this.partner_by_id[partner.id] &&
new Date(local_partner_date).getTime() + 1000 >=
new Date(dist_partner_date).getTime()
) {
// FIXME: The write_date is stored with milisec precision in the database
// but the dates we get back are only precise to the second. This means when
// you read partners modified strictly after time X, you get back partners that were
// modified X - 1 sec ago.
continue;
} else if ( new_write_date < partner.write_date ) {
new_write_date = partner.write_date;
} else if (new_write_date < partner.write_date) {
new_write_date = partner.write_date;
}
if (!this.partner_by_id[partner.id]) {
this.partner_sorted.push(partner.id);
@@ -301,14 +311,15 @@ var PosDB = core.Class.extend({
for (var id in this.partner_by_id) {
partner = this.partner_by_id[id];
if(partner.barcode){
if (partner.barcode) {
this.partner_by_barcode[partner.barcode] = partner;
}
partner.address = (partner.street ? partner.street + ', ': '') +
(partner.zip ? partner.zip + ', ': '') +
(partner.city ? partner.city + ', ': '') +
(partner.state_id ? partner.state_id[1] + ', ': '') +
(partner.country_id ? partner.country_id[1]: '');
partner.address =
(partner.street ? partner.street + ", " : "") +
(partner.zip ? partner.zip + ", " : "") +
(partner.city ? partner.city + ", " : "") +
(partner.state_id ? partner.state_id[1] + ", " : "") +
(partner.country_id ? partner.country_id[1] : "");
this.partner_search_string += this._partner_search_string(partner);
}
@@ -316,77 +327,82 @@ var PosDB = core.Class.extend({
}
return updated_count;
},
get_partner_write_date: function(){
get_partner_write_date: function () {
return this.partner_write_date || "1970-01-01 00:00:00";
},
get_partner_by_id: function(id){
get_partner_by_id: function (id) {
return this.partner_by_id[id];
},
get_partner_by_barcode: function(barcode){
get_partner_by_barcode: function (barcode) {
return this.partner_by_barcode[barcode];
},
get_partners_sorted: function(max_count){
max_count = max_count ? Math.min(this.partner_sorted.length, max_count) : this.partner_sorted.length;
get_partners_sorted: function (max_count) {
max_count = max_count
? Math.min(this.partner_sorted.length, max_count)
: this.partner_sorted.length;
var partners = [];
for (var i = 0; i < max_count; i++) {
partners.push(this.partner_by_id[this.partner_sorted[i]]);
}
return partners;
},
search_partner: function(query){
search_partner: function (query) {
try {
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
query = query.replace(/ /g,'.+');
var re = RegExp("([0-9]+):.*?"+utils.unaccent(query),"gi");
}catch{
// eslint-disable-next-line no-useless-escape
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, ".");
query = query.replace(/ /g, ".+");
var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi");
} catch {
return [];
}
var results = [];
for(var i = 0; i < this.limit; i++){
for (var i = 0; i < this.limit; i++) {
var r = re.exec(this.partner_search_string);
if(r){
if (r) {
var id = Number(r[1]);
results.push(this.get_partner_by_id(id));
}else{
} else {
break;
}
}
return results;
},
/* removes all the data from the database. TODO : being able to selectively remove data */
clear: function(){
for(var i = 0, len = arguments.length; i < len; i++){
localStorage.removeItem(this.name + '_' + arguments[i]);
clear: function () {
for (var i = 0, len = arguments.length; i < len; i++) {
localStorage.removeItem(this.name + "_" + arguments[i]);
}
},
/* this internal methods returns the count of properties in an object. */
_count_props : function(obj){
_count_props: function (obj) {
var count = 0;
for(var prop in obj){
if(obj.hasOwnProperty(prop)){
for (var prop in obj) {
if (Object.hasOwnProperty.call(obj, prop)) {
count++;
}
}
return count;
},
get_product_by_id: function(id){
get_product_by_id: function (id) {
return this.product_by_id[id];
},
get_product_by_barcode: function(barcode){
if(this.product_by_barcode[barcode]){
get_product_by_barcode: function (barcode) {
if (this.product_by_barcode[barcode]) {
return this.product_by_barcode[barcode];
} else if (this.product_packaging_by_barcode[barcode]) {
return this.product_by_id[this.product_packaging_by_barcode[barcode].product_id[0]];
}
return undefined;
},
get_product_by_category: function(category_id){
var product_ids = this.product_by_category_id[category_id];
get_product_by_category: function (category_id) {
var product_ids = this.product_by_category_id[category_id];
var list = [];
if (product_ids) {
for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) {
const product = this.product_by_id[product_ids[i]];
if (!(product.active && product.available_in_pos)) continue;
if (!(product.active && product.available_in_pos)) {
continue;
}
list.push(product);
}
}
@@ -396,23 +412,26 @@ var PosDB = core.Class.extend({
* - a category that is or is a child of category_id,
* - a name, package or barcode containing the query (case insensitive)
*/
search_product_in_category: function(category_id, query){
search_product_in_category: function (category_id, query) {
try {
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
query = query.replace(/ /g,'.+');
var re = RegExp("([0-9]+):.*?"+utils.unaccent(query),"gi");
}catch{
// eslint-disable-next-line no-useless-escape
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, ".");
query = query.replace(/ /g, ".+");
var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi");
} catch {
return [];
}
var results = [];
for(var i = 0; i < this.limit; i++){
for (var i = 0; i < this.limit; i++) {
var r = re.exec(this.category_search_string[category_id]);
if(r){
if (r) {
var id = Number(r[1]);
const product = this.get_product_by_id(id);
if (!(product.active && product.available_in_pos)) continue;
if (!(product.active && product.available_in_pos)) {
continue;
}
results.push(product);
}else{
} else {
break;
}
}
@@ -422,11 +441,12 @@ var PosDB = core.Class.extend({
* true if the product belongs to one of the provided category
* or one of its child categories.
*/
is_product_in_category: function(category_ids, product_id) {
is_product_in_category: function (category_ids, product_id) {
let cat = this.get_product_by_id(product_id).pos_categ_id[0];
while (cat) {
for (let cat_id of category_ids) {
if (cat == cat_id) { // The == is important, ids may be strings
for (const cat_id of category_ids) {
if (cat == cat_id) {
// The == is important, ids may be strings
return true;
}
}
@@ -436,15 +456,15 @@ var PosDB = core.Class.extend({
},
/* paid orders */
add_order: function(order){
add_order: function (order) {
var order_id = order.uid;
var orders = this.load('orders',[]);
var orders = this.load("orders", []);
// if the order was already stored, we overwrite its data
for(var i = 0, len = orders.length; i < len; i++){
if(orders[i].id === order_id){
for (var i = 0, len = orders.length; i < len; i++) {
if (orders[i].id === order_id) {
orders[i].data = order;
this.save('orders',orders);
this.save("orders", orders);
return order_id;
}
}
@@ -453,27 +473,27 @@ var PosDB = core.Class.extend({
// that where already stored should already have been removed.
this.remove_unpaid_order(order);
orders.push({id: order_id, data: order});
this.save('orders',orders);
orders.push({ id: order_id, data: order });
this.save("orders", orders);
return order_id;
},
remove_order: function(order_id){
var orders = this.load('orders',[]);
orders = _.filter(orders, function(order){
remove_order: function (order_id) {
var orders = this.load("orders", []);
orders = _.filter(orders, function (order) {
return order.id !== order_id;
});
this.save('orders',orders);
this.save("orders", orders);
},
remove_all_orders: function(){
this.save('orders',[]);
remove_all_orders: function () {
this.save("orders", []);
},
get_orders: function(){
return this.load('orders',[]);
get_orders: function () {
return this.load("orders", []);
},
get_order: function(order_id){
get_order: function (order_id) {
var orders = this.get_orders();
for(var i = 0, len = orders.length; i < len; i++){
if(orders[i].id === order_id){
for (var i = 0, len = orders.length; i < len; i++) {
if (orders[i].id === order_id) {
return orders[i];
}
}
@@ -481,35 +501,35 @@ var PosDB = core.Class.extend({
},
/* working orders */
save_unpaid_order: function(order){
save_unpaid_order: function (order) {
var order_id = order.uid;
var orders = this.load('unpaid_orders',[]);
var orders = this.load("unpaid_orders", []);
var serialized = order.export_as_JSON();
for (var i = 0; i < orders.length; i++) {
if (orders[i].id === order_id){
if (orders[i].id === order_id) {
orders[i].data = serialized;
this.save('unpaid_orders',orders);
this.save("unpaid_orders", orders);
return order_id;
}
}
orders.push({id: order_id, data: serialized});
this.save('unpaid_orders',orders);
orders.push({ id: order_id, data: serialized });
this.save("unpaid_orders", orders);
return order_id;
},
remove_unpaid_order: function(order){
var orders = this.load('unpaid_orders',[]);
orders = _.filter(orders, function(o){
remove_unpaid_order: function (order) {
var orders = this.load("unpaid_orders", []);
orders = _.filter(orders, function (o) {
return o.id !== order.uid;
});
this.save('unpaid_orders',orders);
this.save("unpaid_orders", orders);
},
remove_all_unpaid_orders: function(){
this.save('unpaid_orders',[]);
remove_all_unpaid_orders: function () {
this.save("unpaid_orders", []);
},
get_unpaid_orders: function(){
var saved = this.load('unpaid_orders',[]);
get_unpaid_orders: function () {
var saved = this.load("unpaid_orders", []);
var orders = [];
for (var i = 0; i < saved.length; i++) {
orders.push(saved[i].data);
@@ -521,9 +541,13 @@ var PosDB = core.Class.extend({
* @param {array<number>} ids order_ids.
* @return {array<object>} list of orders.
*/
get_unpaid_orders_to_sync: function(ids){
const savedOrders = this.load('unpaid_orders',[]);
return savedOrders.filter(order => ids.includes(order.id) && (order.data.server_id || order.data.lines.length || order.data.statement_ids.length));
get_unpaid_orders_to_sync: function (ids) {
const savedOrders = this.load("unpaid_orders", []);
return savedOrders.filter(
(order) =>
ids.includes(order.id) &&
(order.data.server_id || order.data.lines.length || order.data.statement_ids.length)
);
},
/**
* Add a given order to the orders to be removed from the server.
@@ -532,34 +556,32 @@ var PosDB = core.Class.extend({
* after syncing. This function will add the server_id of the order to a list of orders still to be removed.
* @param {object} order object.
*/
set_order_to_remove_from_server: function(order){
set_order_to_remove_from_server: function (order) {
if (order.server_id !== undefined) {
var to_remove = this.load('unpaid_orders_to_remove',[]);
var to_remove = this.load("unpaid_orders_to_remove", []);
to_remove.push(order.server_id);
this.save('unpaid_orders_to_remove', to_remove);
this.save("unpaid_orders_to_remove", to_remove);
}
},
/**
* Get a list of server_ids of orders to be removed.
* @return {array<number>} list of server_ids.
*/
get_ids_to_remove_from_server: function(){
return this.load('unpaid_orders_to_remove',[]);
get_ids_to_remove_from_server: function () {
return this.load("unpaid_orders_to_remove", []);
},
/**
* Remove server_ids from the list of orders to be removed.
* @param {array<number>} ids
*/
set_ids_removed_from_server: function(ids){
var to_remove = this.load('unpaid_orders_to_remove',[]);
set_ids_removed_from_server: function (ids) {
var to_remove = this.load("unpaid_orders_to_remove", []);
to_remove = _.filter(to_remove, function(id){
to_remove = _.filter(to_remove, function (id) {
return !ids.includes(id);
});
this.save('unpaid_orders_to_remove', to_remove);
this.save("unpaid_orders_to_remove", to_remove);
},
});
return PosDB;
});
export default PosDB;
+147 -146
View File
@@ -1,17 +1,15 @@
odoo.define('point_of_sale.devices', function (require) {
"use strict";
var core = require('web.core');
var mixins = require('web.mixins');
var Session = require('web.Session');
var Printer = require('point_of_sale.Printer').Printer;
/** @odoo-module */
import core from "web.core";
import mixins from "web.mixins";
import Session from "web.Session";
import { Printer } from "@point_of_sale/js/printers";
// the JobQueue schedules a sequence of 'jobs'. each job is
// a function returning a promise. The queue waits for each job to finish
// before launching the next. Each job can also be scheduled with a delay.
// the is used to prevent parallel requests to the proxy.
var JobQueue = function(){
export const JobQueue = function () {
var queue = [];
var running = false;
var scheduled_end_time = 0;
@@ -33,7 +31,7 @@ var JobQueue = function(){
}
// the time scheduled for this job
scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
scheduled_end_time = new Date().getTime() + (job.opts.duration || 0);
// we run the job and put in prom when it finishes
var prom = job.fun() || Promise.resolve();
@@ -41,10 +39,7 @@ var JobQueue = function(){
var always = function () {
// we run the next job after the scheduled_end_time, even if it finishes before
return new Promise(function (resolve, reject) {
setTimeout(
resolve,
Math.max(0, scheduled_end_time - (new Date()).getTime())
);
setTimeout(resolve, Math.max(0, scheduled_end_time - new Date().getTime()));
});
};
// we don't care if a job fails ...
@@ -65,20 +60,22 @@ var JobQueue = function(){
* @param {boolean} [opts.repeat] if true, the job will be endlessly repeated
* @param {boolean} [opts.important] if true, the scheduled job cannot be canceled by a queue.clear()
*/
this.schedule = function (fun, opts) {
queue.push({fun:fun, opts:opts || {}});
if(!running){
this.schedule = function (fun, opts) {
queue.push({ fun: fun, opts: opts || {} });
if (!running) {
run();
}
};
// remove all jobs from the schedule (except the ones marked as important)
this.clear = function(){
queue = _.filter(queue,function(job){return job.opts.important === true;});
this.clear = function () {
queue = _.filter(queue, function (job) {
return job.opts.important === true;
});
};
// end the repetition of the current job
this.stoprepeat = function(){
this.stoprepeat = function () {
stoprepeat = true;
};
@@ -91,16 +88,14 @@ var JobQueue = function(){
this.finished = function () {
return end_of_queue;
};
};
// this object interfaces with the local proxy to communicate to the various hardware devices
// connected to the Point of Sale. As the communication only goes from the POS to the proxy,
// methods are used both to signal an event, and to fetch information.
var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
init: function(options){
export const ProxyDevice = core.Class.extend(mixins.PropertiesMixin, {
init: function (options) {
mixins.PropertiesMixin.init.call(this);
var self = this;
options = options || {};
@@ -117,16 +112,16 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
this.bypass_proxy = false;
this.connection = null;
this.host = '';
this.keptalive = false;
this.host = "";
this.keptalive = false;
this.set('status',{});
this.set("status", {});
this.set_connection_status('disconnected');
this.set_connection_status("disconnected");
this.on('change:status',this,function(eh,status){
this.on("change:status", this, function (eh, status) {
status = status.newValue;
if(status.status === 'connected' && self.printer) {
if (status.status === "connected" && self.printer) {
self.printer.print_receipt();
}
});
@@ -135,23 +130,23 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
window.hw_proxy = this;
},
set_pos: function(pos) {
set_pos: function (pos) {
this.setParent(pos);
this.pos = pos;
},
set_connection_status: function(status, drivers, msg=''){
var oldstatus = this.get('status');
set_connection_status: function (status, drivers, msg = "") {
var oldstatus = this.get("status");
var newstatus = {};
newstatus.status = status;
newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
newstatus.drivers = status === "disconnected" ? {} : oldstatus.drivers;
newstatus.drivers = drivers ? drivers : newstatus.drivers;
newstatus.msg = msg;
this.set('status',newstatus);
this.set("status", newstatus);
},
disconnect: function(){
if(this.get('status').status !== 'disconnected'){
disconnect: function () {
if (this.get("status").status !== "disconnected") {
this.connection.destroy();
this.set_connection_status('disconnected');
this.set_connection_status("disconnected");
}
},
@@ -161,28 +156,31 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
* @param {string} url
* @returns {Promise}
*/
connect: function(url){
connect: function (url) {
var self = this;
this.connection = new Session(undefined,url, { use_cors: true});
this.connection = new Session(undefined, url, { use_cors: true });
this.host = url;
if (this.pos.config.iface_print_via_proxy) {
this.connect_to_printer();
}
this.set_connection_status('connecting',{});
this.set_connection_status("connecting", {});
return this.message('handshake').then(function(response){
if(response){
self.set_connection_status('connected');
return this.message("handshake").then(
function (response) {
if (response) {
self.set_connection_status("connected");
localStorage.hw_proxy_url = url;
self.keepalive();
}else{
self.set_connection_status('disconnected');
console.error('Connection refused by the Proxy');
} else {
self.set_connection_status("disconnected");
console.error("Connection refused by the Proxy");
}
},function(){
self.set_connection_status('disconnected');
console.error('Could not connect to the Proxy');
});
},
function () {
self.set_connection_status("disconnected");
console.error("Could not connect to the Proxy");
}
);
},
connect_to_printer: function () {
@@ -200,7 +198,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
*/
autoconnect: function (options) {
var self = this;
this.set_connection_status('connecting',{});
this.set_connection_status("connecting", {});
if (this.pos.config.iface_print_via_proxy) {
this.connect_to_printer();
}
@@ -211,15 +209,16 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
found_url = this.try_hard_to_connect(options.force_ip, options);
} else if (localStorage.hw_proxy_url) {
// try harder when we remember a good proxy url
found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options)
.catch(function () {
if (window.location.protocol != 'https:') {
found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options).catch(
function () {
if (window.location.protocol != "https:") {
return self.find_proxy(options);
}
});
}
);
} else {
// just find something quick
if (window.location.protocol != 'https:'){
if (window.location.protocol != "https:") {
found_url = this.find_proxy(options);
}
}
@@ -229,7 +228,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
});
successProm.catch(function () {
self.set_connection_status('disconnected');
self.set_connection_status("disconnected");
});
return successProm;
@@ -239,18 +238,23 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
keepalive: function () {
var self = this;
function status(){
function status() {
var always = function () {
setTimeout(status, 5000);
};
self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500})
.then(function (driver_status) {
self.set_connection_status('connected',driver_status);
}, function () {
if(self.get('status').status !== 'connecting'){
self.set_connection_status('disconnected');
self.connection
.rpc("/hw_proxy/status_json", {}, { shadow: true, timeout: 2500 })
.then(
function (driver_status) {
self.set_connection_status("connected", driver_status);
},
function () {
if (self.get("status").status !== "connecting") {
self.set_connection_status("disconnected");
}
}
}).then(always, always);
)
.then(always, always);
}
if (!this.keptalive) {
@@ -264,13 +268,13 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
* @param {Object} [params]
* @returns {Promise}
*/
message : function (name, params) {
message: function (name, params) {
var callbacks = this.notifications[name] || [];
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](params);
}
if (this.get('status').status !== 'disconnected') {
return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true});
if (this.get("status").status !== "disconnected") {
return this.connection.rpc("/hw_proxy/" + name, params || {}, { shadow: true });
} else {
return Promise.reject();
}
@@ -285,17 +289,17 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
* @returns {Promise<string|Array>}
*/
try_hard_to_connect: function (url, options) {
options = options || {};
options = options || {};
var protocol = window.location.protocol;
var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069');
var port = !options.port && protocol == "https:" ? ":443" : ":" + (options.port || "8069");
this.set_connection_status('connecting');
this.set_connection_status("connecting");
if(url.indexOf('//') < 0){
url = protocol + '//' + url;
if (url.indexOf("//") < 0) {
url = protocol + "//" + url;
}
if(url.indexOf(':',5) < 0){
if (url.indexOf(":", 5) < 0) {
url = url + port;
}
@@ -303,19 +307,21 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
function try_real_hard_to_connect(url, retries) {
return Promise.resolve(
$.ajax({
url: url + '/hw_proxy/hello',
method: 'GET',
url: url + "/hw_proxy/hello",
method: "GET",
timeout: 1000,
})
.then(function () {
return Promise.resolve(url);
}, function (resp) {
if (retries > 0) {
return try_real_hard_to_connect(url, retries-1);
} else {
return Promise.reject([resp.statusText, url]);
}).then(
function () {
return Promise.resolve(url);
},
function (resp) {
if (retries > 0) {
return try_real_hard_to_connect(url, retries - 1);
} else {
return Promise.reject([resp.statusText, url]);
}
}
})
)
);
}
@@ -330,34 +336,33 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
* @param {function} [options.progress] callback for search progress ( fac in [0,1] )
* @returns {Promise<string>} will be resolved with the proxy valid url
*/
find_proxy: function(options){
find_proxy: function (options) {
options = options || {};
var self = this;
var port = ':' + (options.port || '8069');
var urls = [];
var self = this;
var port = ":" + (options.port || "8069");
var urls = [];
var found = false;
var parallel = 8;
var threads = [];
var threads = [];
var progress = 0;
urls.push('http://localhost'+port);
for(var i = 0; i < 256; i++){
urls.push('http://192.168.0.'+i+port);
urls.push('http://192.168.1.'+i+port);
urls.push('http://10.0.0.'+i+port);
urls.push("http://localhost" + port);
for (var i = 0; i < 256; i++) {
urls.push("http://192.168.0." + i + port);
urls.push("http://192.168.1." + i + port);
urls.push("http://10.0.0." + i + port);
}
var prog_inc = 1/urls.length;
var prog_inc = 1 / urls.length;
function update_progress(){
function update_progress() {
progress = found ? 1 : progress + prog_inc;
if(options.progress){
if (options.progress) {
options.progress(progress);
}
}
function thread () {
function thread() {
var url = urls.shift();
if (!url || found || !self.searching_for_proxy) {
@@ -366,32 +371,35 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
return Promise.resolve(
$.ajax({
url: url + '/hw_proxy/hello',
method: 'GET',
url: url + "/hw_proxy/hello",
method: "GET",
timeout: 400,
}).then(function () {
found = true;
update_progress();
return Promise.resolve(url);
}, function () {
update_progress();
return thread();
})
}).then(
function () {
found = true;
update_progress();
return Promise.resolve(url);
},
function () {
update_progress();
return thread();
}
)
);
}
this.searching_for_proxy = true;
var len = Math.min(parallel, urls.length);
for(i = 0; i < len; i++){
var len = Math.min(parallel, urls.length);
for (i = 0; i < len; i++) {
threads.push(thread());
}
return new Promise(function (resolve, reject) {
Promise.all(threads).then(function(results){
Promise.all(threads).then(function (results) {
var urls = [];
for(var i = 0; i < results.length; i++){
if(results[i]){
for (var i = 0; i < results.length; i++) {
if (results[i]) {
urls.push(results[i]);
}
}
@@ -400,15 +408,15 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
});
},
stop_searching: function(){
stop_searching: function () {
this.searching_for_proxy = false;
this.set_connection_status('disconnected');
this.set_connection_status("disconnected");
},
// this allows the client to be notified when a proxy call is made. The notification
// callback will be executed with the same arguments as the proxy call
add_notification: function(name, callback){
if(!this.notifications[name]){
add_notification: function (name, callback) {
if (!this.notifications[name]) {
this.notifications[name] = [];
}
this.notifications[name].push(callback);
@@ -422,35 +430,36 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
scale_read: function () {
var self = this;
if (self.use_debug_weight) {
return Promise.resolve({weight:this.debug_weight, unit:'Kg', info:'ok'});
return Promise.resolve({ weight: this.debug_weight, unit: "Kg", info: "ok" });
}
return new Promise(function (resolve, reject) {
self.message('scale_read',{})
.then(function (weight) {
resolve(weight);
}, function () { //failed to read weight
resolve({weight:0.0, unit:'Kg', info:'ok'});
});
self.message("scale_read", {}).then(
function (weight) {
resolve(weight);
},
function () {
//failed to read weight
resolve({ weight: 0.0, unit: "Kg", info: "ok" });
}
);
});
},
// sets a custom weight, ignoring the proxy returned value.
debug_set_weight: function(kg){
debug_set_weight: function (kg) {
this.use_debug_weight = true;
this.debug_weight = kg;
},
// resets the custom weight and re-enable listening to the proxy for weight values
debug_reset_weight: function(){
debug_reset_weight: function () {
this.use_debug_weight = false;
this.debug_weight = 0;
},
update_customer_facing_display: function(html) {
if (this.posbox_supports_display && this.get('status').status == 'connected') {
return this.message('customer_facing_display',
{ html: html },
{ timeout: 5000 });
update_customer_facing_display: function (html) {
if (this.posbox_supports_display && this.get("status").status == "connected") {
return this.message("customer_facing_display", { html: html }, { timeout: 5000 });
}
},
@@ -458,30 +467,22 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
* @param {string} html
* @returns {Promise}
*/
take_ownership_over_customer_screen: function(html) {
take_ownership_over_customer_screen: function (html) {
return this.message("take_control", { html: html });
},
/**
* @returns {Promise}
*/
test_ownership_of_customer_screen: function() {
test_ownership_of_customer_screen: function () {
if (this.connection) {
return this.message("test_ownership", {});
}
return Promise.reject({abort: true});
return Promise.reject({ abort: true });
},
// asks the proxy to log some information, as with the debug.log you can provide several arguments.
log: function(){
return this.message('log',{'arguments': _.toArray(arguments)});
log: function () {
return this.message("log", { arguments: _.toArray(arguments) });
},
});
return {
JobQueue: JobQueue,
ProxyDevice: ProxyDevice,
};
});
File diff suppressed because it is too large Load Diff
@@ -1,20 +1,19 @@
odoo.define('point_of_sale.PaymentInterface', function (require) {
"use strict";
/** @odoo-module */
var core = require('web.core');
import core from "web.core";
/**
* Implement this interface to support a new payment method in the POS:
*
* var PaymentInterface = require('point_of_sale.PaymentInterface');
import var PaymentInterface from "@point_of_sale/js/payment";
* var MyPayment = PaymentInterface.extend({
* ...
* })
*
* To connect the interface to the right payment methods register it:
*
* var models = require('point_of_sale.models');
* models.register_payment_method('my_payment', MyPayment);
* import { register_payment_method } models from "@point_of_sale/js/models";
* register_payment_method('my_payment', MyPayment);
*
* my_payment is the technical name of the added selection in
* use_payment_terminal.
@@ -91,5 +90,4 @@ var PaymentInterface = core.Class.extend({
close: function () {},
});
return PaymentInterface;
});
export default PaymentInterface;
@@ -1,19 +1,19 @@
/* @odoo-module alias=point_of_sale.env */
/* @odoo-module */
// This module is basically web.env but with added fields
// that are specific to point_of_sale and extensions.
import env from 'web.env';
import concurrency from 'web.concurrency';
import devices from 'point_of_sale.devices';
import BarcodeReader from 'point_of_sale.BarcodeReader';
import env from "web.env";
import concurrency from "web.concurrency";
import { JobQueue, ProxyDevice } from "@point_of_sale/js/devices";
import BarcodeReader from "@point_of_sale/js/barcode_reader";
// Create new env object base on web.env.
// Mutating this new object won't affect the original object.
let pos_env = Object.create(env);
const pos_env = Object.create(env);
pos_env.proxy_queue = new devices.JobQueue(); // used to prevent parallels communications to the proxy
pos_env.proxy = new devices.ProxyDevice({ env: pos_env }); // used to communicate to the hardware devices via a local proxy
pos_env.proxy_queue = new JobQueue(); // used to prevent parallels communications to the proxy
pos_env.proxy = new ProxyDevice({ env: pos_env }); // used to communicate to the hardware devices via a local proxy
pos_env.barcode_reader = new BarcodeReader({ env: pos_env, proxy: pos_env.proxy });
pos_env.posbus = new owl.EventBus();
pos_env.posMutex = new concurrency.Mutex();
+44 -48
View File
@@ -1,27 +1,26 @@
/** @odoo-module */
/* global html2canvas */
odoo.define('point_of_sale.Printer', function (require) {
"use strict";
var Session = require('web.Session');
var core = require('web.core');
const { Gui } = require('point_of_sale.Gui');
import Session from "web.Session";
import core from "web.core";
import { Gui } from "@point_of_sale/js/Gui";
var _t = core._t;
// IMPROVEMENT: This is too much. We can get away from this class.
class PrintResult {
export class PrintResult {
constructor({ successful, message }) {
this.successful = successful;
this.message = message;
}
}
class PrintResultGenerator {
export class PrintResultGenerator {
IoTActionError() {
return new PrintResult({
successful: false,
message: {
title: _t('Connection to IoT Box failed'),
body: _t('Please check if the IoT Box is still connected.'),
title: _t("Connection to IoT Box failed"),
body: _t("Please check if the IoT Box is still connected."),
},
});
}
@@ -29,11 +28,12 @@ class PrintResultGenerator {
return new PrintResult({
successful: false,
message: {
title: _t('Connection to the printer failed'),
body: _t('Please check if the printer is still connected. \n' +
'Some browsers don\'t allow HTTP calls from websites to devices in the network (for security reasons). ' +
'If it is the case, you will need to follow Odoo\'s documentation for ' +
'\'Self-signed certificate for ePOS printers\' and \'Secure connection (HTTPS)\' to solve the issue'
title: _t("Connection to the printer failed"),
body: _t(
"Please check if the printer is still connected. \n" +
"Some browsers don't allow HTTP calls from websites to devices in the network (for security reasons). " +
"If it is the case, you will need to follow Odoo's documentation for " +
"'Self-signed certificate for ePOS printers' and 'Secure connection (HTTPS)' to solve the issue"
),
},
});
@@ -45,7 +45,7 @@ class PrintResultGenerator {
}
}
var PrinterMixin = {
export const PrinterMixin = {
init: function (pos) {
this.receipt_queue = [];
this.printResultGenerator = new PrintResultGenerator();
@@ -58,7 +58,7 @@ var PrinterMixin = {
* @param {String} receipt: The receipt to be printed, in HTML
* @returns {PrintResult}
*/
print_receipt: async function(receipt) {
print_receipt: async function (receipt) {
if (receipt) {
this.receipt_queue.push(receipt);
}
@@ -88,7 +88,7 @@ var PrinterMixin = {
* @param {DOMElement} canvas
*/
process_canvas: function (canvas) {
return canvas.toDataURL('image/jpeg').replace('data:image/jpeg;base64,','');
return canvas.toDataURL("image/jpeg").replace("data:image/jpeg;base64,", "");
},
/**
@@ -96,45 +96,47 @@ var PrinterMixin = {
* @param {String} receipt: The receipt to be printed, in HTML
*/
htmlToImg: function (receipt) {
$('.pos-receipt-print').html(receipt);
this.receipt = $('.pos-receipt-print>.pos-receipt');
$(".pos-receipt-print").html(receipt);
this.receipt = $(".pos-receipt-print>.pos-receipt");
// Odoo RTL support automatically flip left into right but html2canvas
// won't work as expected if the receipt is aligned to the right of the
// screen so we need to flip it back.
this.receipt.parent().css({ left: 0, right: 'auto' });
this.receipt.parent().css({ left: 0, right: "auto" });
return html2canvas(this.receipt[0], {
height: Math.ceil(this.receipt.outerHeight() + this.receipt.offset().top),
width: Math.ceil(this.receipt.outerWidth() + 2 * this.receipt.offset().left),
scale: 1,
}).then(canvas => {
$('.pos-receipt-print').empty();
}).then((canvas) => {
$(".pos-receipt-print").empty();
return this.process_canvas(canvas);
});
},
_onIoTActionResult: function (data){
_onIoTActionResult: function (data) {
if (this.pos && (data === false || data.result === false)) {
Gui.showPopup('ErrorPopup',{
'title': _t('Connection to the printer failed'),
'body': _t('Please check if the printer is still connected.'),
Gui.showPopup("ErrorPopup", {
title: _t("Connection to the printer failed"),
body: _t("Please check if the printer is still connected."),
});
}
},
_onIoTActionFail: function () {
if (this.pos) {
Gui.showPopup('ErrorPopup',{
'title': _t('Connection to IoT Box failed'),
'body': _t('Please check if the IoT Box is still connected.'),
Gui.showPopup("ErrorPopup", {
title: _t("Connection to IoT Box failed"),
body: _t("Please check if the IoT Box is still connected."),
});
}
},
}
};
var Printer = core.Class.extend(PrinterMixin, {
export const Printer = core.Class.extend(PrinterMixin, {
init: function (url, pos) {
PrinterMixin.init.call(this, pos);
this.connection = new Session(undefined, url || 'http://localhost:8069', { use_cors: true});
this.connection = new Session(undefined, url || "http://localhost:8069", {
use_cors: true,
});
},
/**
@@ -144,11 +146,13 @@ var Printer = core.Class.extend(PrinterMixin, {
*/
open_cashbox: function () {
var self = this;
return this.connection.rpc('/hw_proxy/default_printer_action', {
data: {
action: 'cashbox'
}
}).then(self._onIoTActionResult.bind(self))
return this.connection
.rpc("/hw_proxy/default_printer_action", {
data: {
action: "cashbox",
},
})
.then(self._onIoTActionResult.bind(self))
.guardedCatch(self._onIoTActionFail.bind(self));
},
@@ -157,19 +161,11 @@ var Printer = core.Class.extend(PrinterMixin, {
* @param {String} img : The receipt to be printed, as an image
*/
send_printing_job: function (img) {
return this.connection.rpc('/hw_proxy/default_printer_action', {
return this.connection.rpc("/hw_proxy/default_printer_action", {
data: {
action: 'print_receipt',
action: "print_receipt",
receipt: img,
}
},
});
},
});
return {
PrinterMixin: PrinterMixin,
Printer: Printer,
PrintResult,
PrintResultGenerator,
}
});

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