[REF] point_of_sale,*: minimize the use of legacy web modules

*: l10n_fr_pos_cert,portal_rating,pos_adyen,pos_epson_printer,pos_loyalty,
   pos_mercury,pos_restaurant,pos_restaurant_adyen,pos_restaurant_stripe,
   pos_sale,pos_six,pos_stripe,web

This is part of the continuous effort of refactoring pos addons towards using
more modern modules and programming patterns (such as services). After this
commit, point_of_sale addons are now left with the use of the legacy
`web.concurrency` module because of the `MutexedDropPrevious`. It's okay to keep
it because it's relatively an independent module compared to other legacy web
module.

The following summarizes the changes in this commit.

- import `_t` from `@web/core/l10n/translation`.

- convert `PosDB` to js native class

- remove use of `format` in `TicketScreen`
  - To determine the cached orders are up-to-date, we now deserialize the dates
    coming from the server using web's `deserializeDateTime` function. Then,
    instead of initiating `cacheDate` as native js Date, we use the luxon's
    `DateTime` which is supported by the web date utility methods.
  - There is no need for the `format` function from `web.utils` legacy module.

- convert `PaymentInterface` to native class

- remove use of `web.config` module

- remove use of `web.time` module
  - use `serializeDateTime` from web.

- remove use of `web.rpc`

- remove use of `web.utils` module
  - introduce simple check for email address input
  - Replace use of `web.utils.Markup` with `@odoo/owl.markup`.
  - 'web.utils'.{round_decimals,round_precision,float_is_zero} copied to
    '@web/core/utils/numbers'.{roundDecimals,roundPrecision,floatIsZero}.
    - These helper functions are not removed from the web addon because they
      are also used from other addons that are not linked to pos.
    - `floatIsZero` is now computed by directly comparing the result of
      `roundDecimals` to zero. This works because rounding a decimal number
      which will result to zero will exactly give zero.

- remove use of `web.field_utils`
  - Replace `web.field_utils.parse.float` with
    `@web/views/fields/parsers.parseFloat`.
  - Replace `web.field_utils.format.float` with
    `@web/views/fields/formatters.formatFloat`.
  - Replace `web.field_utils.format.date` with
    `@web/core/l10n/dates.formatDate`.
  - Replace `web.field_utils.format.datetime` with
    `@web/core/l10n/dates.formatDateTime`.

- convert `PrinterMixin` and dependents to native class
  - `PrinterMixin` is converted to `BasePrinter`.
  - `Printer` is converted to `HWPrinter` (extending `BasePrinter`).
  - `EpsonPrinter` retained its name and is converted to extend `BasePrinter`.
  - Moreover, we also removed the convoluted `PrintResultGenerator`, replaced by
    simply creating object with the following signature:
    ```js
    { successful: boolean; message?: { title: string, body?: string } }
    ```

- remove use of `web.Session`

- replace use of `qweb.render`
  - We use `renderToElement` as replacement to templates that produces valid
    html.
  - Note that `renderToElement` is introduced in `@web/core/utils/render` module
    which is extracted from the original `renderToString` method.
  - For the epson printer template, we kept the xml layout and manually add
    required xml element to contruct the xml that will be sent to the epson in
    making print requests.

- use `Mutex` from `@web/core/utils/concurrency`

- remove use of `Markup` when rendering receipt info (`ticket`) in order to
  render new lines.

- Remove use of `jquery` in `htmlToImg`.

closes odoo/odoo#117231

Related: odoo/enterprise#39078
Signed-off-by: Samuel Degueldre <sad@odoo.com>
This commit is contained in:
Joseph Caburnay
2023-04-25 14:07:09 +02:00
parent edab62fe38
commit 8d50daeefa
52 changed files with 942 additions and 800 deletions
+1 -2
View File
@@ -1,11 +1,10 @@
/** @odoo-module */
import { PosGlobalState, Order, Orderline } from "@point_of_sale/js/models";
import core from "web.core";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { sprintf } from "@web/core/utils/strings";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
var _t = core._t;
patch(PosGlobalState.prototype, "l10n_fr_pos_cert.PosGlobalState", {
is_french_country() {
@@ -1,6 +1,6 @@
/** @odoo-module */
import { parse } from "web.field_utils";
import { parseFloat } from "@web/views/fields/parsers";
import { Transition } from "@web/core/transition";
import { constrain, getLimits, useMovable } from "@point_of_sale/app/movable_hook";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
@@ -65,7 +65,7 @@ export class DebugWidget extends Component {
this.state.isShown = !this.state.isShown;
}
setWeight() {
var weightInKg = parse.float(this.state.weightInput);
var weightInKg = parseFloat(this.state.weightInput);
if (!isNaN(weightInKg)) {
this.hardwareProxy.setDebugWeight(weightInKg);
}
@@ -1,9 +1,11 @@
/** @odoo-module */
import { Printer } from "@point_of_sale/js/printers";
import { HWPrinter } from "../printer/hw_printer";
import { EventBus, reactive } from "@odoo/owl";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { effect } from "@point_of_sale/utils";
import { deduceUrl } from "@point_of_sale/js/utils";
/**
* This object interfaces with the local proxy to communicate to the various hardware devices
@@ -27,7 +29,7 @@ export class HardwareProxy extends EventBus {
effect(
(info) => {
if (info.status === "connected" && this.printer) {
this.printer.print_receipt();
this.printer.printReceipt();
}
},
[this.connectionInfo]
@@ -68,7 +70,7 @@ export class HardwareProxy extends EventBus {
}
connectToPrinter() {
this.printer = new Printer(this.host, this.pos);
this.printer = new HWPrinter({ rpc: this.rpc, url: this.host });
}
/**
@@ -88,13 +90,7 @@ export class HardwareProxy extends EventBus {
return new Promise(() => {});
}
const { protocol } = window.location;
if (!url.includes("//")) {
url = `${protocol}//${url}`;
}
if (url.indexOf(":", 5) < 0) {
url += ":" + (options.port || (protocol === "https:" ? 443 : 8069));
}
url = deduceUrl(url);
if (await this.checkProxyAvailability(url)) {
this.host = url;
@@ -2,7 +2,7 @@
import { _t } from "@web/core/l10n/translation";
import { sprintf } from "@web/core/utils/strings";
import { renderToString } from "@web/core/utils/render";
import { renderToElement } from "@web/core/utils/render";
import { useAutofocus, useService } from "@web/core/utils/hooks";
import { parseFloat, InvalidNumberError } from "@web/views/fields/parsers";
import { useState } from "@odoo/owl";
@@ -66,7 +66,7 @@ export class CashMovePopup extends AbstractAwaitablePopup {
extras,
]);
if (this.hardwareProxy.printer) {
const renderedReceipt = renderToString("point_of_sale.CashMoveReceipt", {
const renderedReceipt = renderToElement("point_of_sale.CashMoveReceipt", {
_receipt: {
type,
reason,
@@ -77,7 +77,7 @@ export class CashMovePopup extends AbstractAwaitablePopup {
company: this.pos.globalState.company,
},
});
const printResult = await this.hardwareProxy.printer.print_receipt(renderedReceipt);
const printResult = await this.hardwareProxy.printer.printReceipt(renderedReceipt);
if (!printResult.successful) {
this.popup.add(ErrorPopup, {
title: printResult.message.title,
@@ -1,6 +1,6 @@
/** @odoo-module */
import { parse } from "web.field_utils";
import { parseFloat as oParseFloat } from "@web/views/fields/parsers";
import { barcodeService } from "@barcodes/barcode_service";
import { registry } from "@web/core/registry";
import { EventBus, onWillDestroy, useComponent } from "@odoo/owl";
@@ -109,7 +109,7 @@ class NumberBuffer extends EventBus {
* @returns {number} float equivalent of the value of buffer
*/
getFloat() {
return parse.float(this.get());
return oParseFloat(this.get());
}
/**
* @param {Object} config Use to setup the buffer
@@ -284,8 +284,8 @@ class NumberBuffer extends EventBus {
}
} 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;
const inputValue = oParseFloat(input.slice(1));
const currentBufferValue = this.state.buffer ? oParseFloat(this.state.buffer) : 0;
this.state.buffer = this.component.env.pos.formatFixed(inputValue + currentBufferValue);
} else if (!isNaN(parseInt(input, 10))) {
if (this.state.toStartOver) {
@@ -0,0 +1,124 @@
/* @odoo-module */
/* global html2canvas */
import { _t } from "@web/core/l10n/translation";
/**
* Implements basic printer functions.
*/
export class BasePrinter {
constructor() {
this.setup(...arguments);
}
setup() {
this.receiptQueue = [];
}
/**
* Add the receipt to the queue of receipts to be printed and process it.
* We clear the print queue if printing is not successful.
* @param {String} receipt: The receipt to be printed, in HTML
* @returns {{ successful: boolean; message?: { title: string; body?: string }}}
*/
async printReceipt(receipt) {
if (receipt) {
this.receiptQueue.push(receipt);
}
let image, printResult;
while (this.receiptQueue.length > 0) {
receipt = this.receiptQueue.shift();
image = await this.htmlToImg(receipt);
try {
printResult = await this.sendPrintingJob(image);
} catch {
// Error in communicating to the IoT box.
this.receiptQueue.length = 0;
return this.getActionError();
}
// rpc call is okay but printing failed because
// IoT box can't find a printer.
if (!printResult || printResult.result === false) {
this.receiptQueue.length = 0;
return this.getResultsError(printResult);
}
}
return { successful: true };
}
async sendPrintingJob() {
throw new Error("Not implemented");
}
openCashbox() {
throw new Error("Not implemented");
}
/**
* Generate a jpeg image from a canvas
* @param {DOMElement} canvas
*/
processCanvas(canvas) {
return canvas.toDataURL("image/jpeg").replace("data:image/jpeg;base64,", "");
}
/**
* This function renders `el` as an image for printing assuming the following
* - a `div.pos-receipt-print` is present in the dom and will serve as the container
* - a `.pos-receipt` is the root element of `el`.
* @param {HTMLElement} el The element to be converted as image
* @returns {HTMLCanvasElement}
*/
async htmlToImg(el) {
const elClone = el.cloneNode(true);
const receiptContainer = document.querySelector(".pos-receipt-print");
receiptContainer.appendChild(elClone);
const receipt = receiptContainer.querySelector(".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.
const parent = receipt.parentElement;
parent.style.left = 0;
parent.style.right = "auto";
const canvas = await html2canvas(receipt, {
height: Math.ceil(receipt.clientHeight),
width: Math.ceil(receipt.clientWidth),
scale: 1,
});
receipt.remove();
return this.processCanvas(canvas);
}
/**
* Return value of this method will be the result of calling `printReceipt`
* if it failed to connect to the IoT box.
*/
getActionError() {
return {
successful: false,
message: {
title: _t("Connection to IoT Box failed"),
body: _t("Please check if the IoT Box is still connected."),
},
};
}
/**
* Return value of this method will be the result of calling `printReceipt`
* if the result coming from the IoT box is empty.
*/
getResultsError(_printResult) {
return {
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"
),
},
};
}
}
@@ -0,0 +1,39 @@
/* @odoo-module */
import { BasePrinter } from "@point_of_sale/app/printer/base_printer";
/**
* Printer that sends print requests thru /hw_proxy endpoints.
* Doesn't require pos_iot to be installed.
*/
export class HWPrinter extends BasePrinter {
/**
* @param {Object} params
* @param {Function} params.rpc the web's rpc service.
* @param {string} params.url full address of the iot box. E.g. `http://10.23.45.67:8069`.
*/
setup(params) {
super.setup(...arguments);
const { rpc, url } = params;
this.rpc = rpc;
this.url = url;
}
sendAction(data) {
return this.rpc(`${this.url}/hw_proxy/default_printer_action`, { data });
}
/**
* @override
*/
openCashbox() {
return this.sendAction({ action: "cashbox" });
}
/**
* @override
*/
sendPrintingJob(img) {
return this.sendAction({ action: "print_receipt", receipt: img });
}
}
@@ -1,40 +1,38 @@
/** @odoo-module */
import { _t } from "web.core";
import { Markup } from "web.utils";
import { _t } from "@web/core/l10n/translation";
import { markup } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
registry.category("web_tour.tours").add(
"point_of_sale_tour",
{
url: "/web",
rainbowMan: false,
sequence: 45,
steps: [
registry.category("web_tour.tours").add("point_of_sale_tour", {
url: "/web",
rainbowMan: false,
sequence: 45,
steps: [
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>?")),
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>?")),
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(
content: markup(
_t(
"<p>Ready to have a look at the <b>POS Interface</b>? Let's start our first session.</p>"
)
),
position: "bottom",
},
]
],
});
@@ -68,6 +68,10 @@
font-size: 75%;
}
.pos-payment-terminal-receipt pre {
font-family: inherit;
}
.responsive-price {
display: flex;
flex-wrap: wrap;
@@ -1,7 +1,7 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { renderToString } from "@web/core/utils/render";
import { renderToElement } from "@web/core/utils/render";
import { ErrorPopup } from "../Popups/ErrorPopup";
import { Component } from "@odoo/owl";
@@ -24,14 +24,14 @@ export class SaleDetailsButton extends Component {
"get_sale_details",
[false, false, false, [this.env.pos.pos_session.id]]
);
const report = renderToString(
const report = renderToElement(
"SaleDetailsReport",
Object.assign({}, saleDetails, {
date: new Date().toLocaleString(),
pos: this.env.pos,
})
);
const { successful, message } = await this.hardwareProxy.printer.print_receipt(report);
const { successful, message } = await this.hardwareProxy.printer.printReceipt(report);
if (!successful) {
await this.popup.add(ErrorPopup, {
title: message.title,
@@ -20,8 +20,8 @@ export class AbstractReceiptScreen extends Component {
}
async _printReceipt() {
if (this.hardwareProxy.printer) {
const printResult = await this.hardwareProxy.printer.print_receipt(
this.orderReceipt.el.innerHTML
const printResult = await this.hardwareProxy.printer.printReceipt(
this.orderReceipt.el.firstChild
);
if (printResult.successful) {
return true;
@@ -1,7 +1,6 @@
/** @odoo-module */
import core from "web.core";
var _t = core._t;
import { _t } from "@web/core/l10n/translation";
import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup";
import { useService } from "@web/core/utils/hooks";
import { useState } from "@odoo/owl";
@@ -1,10 +1,10 @@
/** @odoo-module */
import { _t } from "web.core";
import { getDataURLFromFile } from "web.utils";
import { _t } from "@web/core/l10n/translation";
import { getDataURLFromFile } from "@web/core/utils/urls";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { useService } from "@web/core/utils/hooks";
import { Component, onMounted, onWillUnmount } from "@odoo/owl";
import { Component, onMounted, onWillUnmount } from "@odoo/owl";
export class PartnerDetailsEdit extends Component {
static template = "PartnerDetailsEdit";
@@ -1,10 +1,10 @@
/** @odoo-module */
import { parse } from "web.field_utils";
import { parseFloat } from "@web/views/fields/parsers";
import { useErrorHandlers } from "@point_of_sale/js/custom_hooks";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import utils from "web.utils";
import { floatIsZero } from "@web/core/utils/numbers";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup";
@@ -135,7 +135,7 @@ export class PaymentScreen extends Component {
this.render(true);
}
openCashbox() {
this.hardwareProxy.printer.open_cashbox();
this.hardwareProxy.printer.openCashbox();
}
async addTip() {
// click_tip
@@ -150,7 +150,7 @@ export class PaymentScreen extends Component {
});
if (confirmed) {
this.currentOrder.set_tip(parse.float(payload));
this.currentOrder.set_tip(parseFloat(payload));
}
}
async toggleShippingDatePicker() {
@@ -220,7 +220,7 @@ export class PaymentScreen extends Component {
(this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) &&
this.env.pos.config.iface_cashdrawer
) {
this.hardwareProxy.printer.open_cashbox();
this.hardwareProxy.printer.openCashbox();
}
this.currentOrder.initialize_validation_date();
@@ -475,10 +475,7 @@ export class PaymentScreen extends Component {
// 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
) &&
floatIsZero(this.currentOrder.get_due(), this.env.pos.currency.decimal_places) &&
this.env.pos.config.auto_validate_terminal_payment
) {
this.validateOrder(false);
@@ -1,7 +1,7 @@
/** @odoo-module */
import { Component } from "@odoo/owl";
import { float_is_zero } from "web.utils";
import { floatIsZero } from "@web/core/utils/numbers";
export class OrderSummary extends Component {
static template = "OrderSummary";
@@ -14,7 +14,7 @@ export class OrderSummary extends Component {
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),
hasTax: !floatIsZero(taxAmount, this.env.pos.currency.decimal_places),
displayAmount: this.env.pos.format_currency(taxAmount),
};
}
@@ -4,7 +4,7 @@ import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
import { parse } from "web.field_utils";
import { parseFloat } from "@web/views/fields/parsers";
import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
@@ -97,7 +97,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
});
return;
}
const parsedInput = (buffer && parse.float(buffer)) || 0;
const parsedInput = (buffer && parseFloat(buffer)) || 0;
if (lastId != selectedLine.cid) {
this._showDecreaseQuantityPopup();
} else if (currentQuantity < parsedInput) {
@@ -203,7 +203,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
startingValue: 0,
title: this.env._t("Set the new quantity"),
});
const newQuantity = inputNumber && inputNumber !== "" ? parse.float(inputNumber) : null;
const newQuantity = inputNumber && inputNumber !== "" ? parseFloat(inputNumber) : null;
if (confirmed && newQuantity !== null) {
const order = this.env.pos.get_order();
const selectedLine = this.env.pos.get_order().get_selected_orderline();
@@ -1,7 +1,5 @@
/** @odoo-module */
import { Printer } from "@point_of_sale/js/printers";
import { is_email } from "web.utils";
import { useErrorHandlers } from "@point_of_sale/js/custom_hooks";
import { AbstractReceiptScreen } from "@point_of_sale/js/Misc/AbstractReceiptScreen";
import { OfflineErrorPopup } from "@point_of_sale/js/Popups/OfflineErrorPopup";
@@ -10,6 +8,7 @@ import { OrderReceipt } from "./OrderReceipt";
import { onMounted, useRef, status } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/pos_hook";
import { useService } from "@web/core/utils/hooks";
import { BasePrinter } from "@point_of_sale/app/printer/base_printer";
export class ReceiptScreen extends AbstractReceiptScreen {
static template = "ReceiptScreen";
@@ -28,7 +27,6 @@ export class ReceiptScreen extends AbstractReceiptScreen {
this.orderUiState = this.currentOrder.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
@@ -59,7 +57,7 @@ export class ReceiptScreen extends AbstractReceiptScreen {
this.buttonMailReceipt.el.className = "fa fa-fw fa-spin fa-circle-o-notch";
this.orderUiState.emailNotice = "";
if (!is_email(this.orderUiState.inputEmail)) {
if (!this.isValidEmail()) {
this.orderUiState.emailSuccessful = false;
this.buttonMailReceipt.el.className = "fa fa-paper-plane";
this.orderUiState.emailNotice = this.env._t("Invalid email.");
@@ -81,6 +79,10 @@ export class ReceiptScreen extends AbstractReceiptScreen {
this.buttonMailReceipt.el.className = "fa fa-paper-plane";
}, 1000);
}
isValidEmail() {
// A basic check of whether the `inputEmail` is an email or not.
return /^.+@.+$/.test(this.orderUiState.inputEmail);
}
get orderAmountPlusTip() {
const order = this.currentOrder;
const orderTotalAmount = order.get_total_with_tax();
@@ -165,8 +167,8 @@ export class ReceiptScreen extends AbstractReceiptScreen {
);
}
async _sendReceiptToCustomer() {
const printer = new Printer(null, this.env.pos);
const receiptString = this.orderReceipt.el.innerHTML;
const printer = new BasePrinter();
const receiptString = this.orderReceipt.el.firstChild;
const ticketImage = await printer.htmlToImg(receiptString);
const order = this.currentOrder;
const partner = order.get_partner();
@@ -1,6 +1,6 @@
/** @odoo-module */
import { round_precision as round_pr } from "web.utils";
import { roundPrecision as round_pr } from "@web/core/utils/numbers";
import { registry } from "@web/core/registry";
import { usePos } from "@point_of_sale/app/pos_hook";
import { Component, onMounted, onWillUnmount, useExternalListener, useState } from "@odoo/owl";
@@ -2,8 +2,8 @@
import { Component } from "@odoo/owl";
import { sprintf } from "@web/core/utils/strings";
import { format } from "web.field_utils";
import { round_precision as round_pr } from "web.utils";
import { formatFloat } from "@web/views/fields/formatters";
import { roundPrecision as round_pr } from "@web/core/utils/numbers";
/**
* @props {pos.order.line} line
@@ -19,7 +19,7 @@ export class OrderlineDetails extends Component {
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 formatFloat(roundedQuantity, { digits: [69, decimals] });
};
return {
productName: line.get_full_product_name(),
@@ -4,7 +4,8 @@ import { Order } from "@point_of_sale/js/models";
import { IndependentToOrderScreen } from "@point_of_sale/js/Misc/IndependentToOrderScreen";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { parse, format } from "web.field_utils";
import { deserializeDateTime } from "@web/core/l10n/dates";
import { parseFloat } from "@web/views/fields/parsers";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
@@ -19,6 +20,8 @@ import { usePos } from "@point_of_sale/app/pos_hook";
import { onMounted, useState } from "@odoo/owl";
import { sprintf } from "@web/core/utils/strings";
const { DateTime } = luxon;
export class TicketScreen extends IndependentToOrderScreen {
static template = "TicketScreen";
static components = {
@@ -203,7 +206,7 @@ export class TicketScreen extends IndependentToOrderScreen {
if (buffer == null || buffer == "") {
toRefundDetail.qty = 0;
} else {
const quantity = Math.abs(parse.float(buffer));
const quantity = Math.abs(parseFloat(buffer));
if (quantity > refundableQty) {
this.numberBuffer.reset();
this.popup.add(ErrorPopup, {
@@ -671,11 +674,11 @@ export class TicketScreen extends IndependentToOrderScreen {
const idsNotInCache = ordersInfo.filter(
(orderInfo) => !(orderInfo[0] in this._state.syncedOrders.cache)
);
const idsNotUpToDate = ordersInfo.filter(
(orderInfo) =>
format.datetime(moment(orderInfo[1]), {}, {}) >
format.datetime(moment(this._state.syncedOrders.cacheDate), {}, { timezone: false })
);
// If no cacheDate, then assume reasonable earlier date.
const cacheDate = this._state.syncedOrders.cacheDate || DateTime.fromMillis(0);
const idsNotUpToDate = ordersInfo.filter((orderInfo) => {
return deserializeDateTime(orderInfo[1]) > cacheDate;
});
const idsToLoad = idsNotInCache.concat(idsNotUpToDate).map((info) => info[0]);
if (idsToLoad.length > 0) {
const fetchedOrders = await this.orm.call("pos.order", "export_for_ui", [idsToLoad]);
@@ -691,7 +694,7 @@ export class TicketScreen extends IndependentToOrderScreen {
);
});
//Update the datetime indicator of the cache refresh
this._state.syncedOrders.cacheDate = new Date();
this._state.syncedOrders.cacheDate = DateTime.local();
}
const ids = ordersInfo.map((info) => info[0]);
+90 -90
View File
@@ -1,7 +1,7 @@
/** @odoo-module */
import core from "web.core";
import utils from "web.utils";
import { unaccent } from "@web/core/utils/strings";
/* The PosDB holds reference to data that is either
* - static: does not change between pos reloads
* - persistent : must stay between reloads ( orders )
@@ -18,10 +18,10 @@ import utils from "web.utils";
*/
const CACHE = {};
export const 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) {
export class PosDB {
name = "openerp_pos_db"; //the prefix of the localstorage data
limit = 100; // the maximum number of results returned by a search
constructor(options) {
options = options || {};
this.name = options.name || this.name;
this.limit = options.limit || this.limit;
@@ -48,7 +48,7 @@ export const PosDB = core.Class.extend({
this.category_childs = {};
this.category_parent = {};
this.category_search_string = {};
},
}
/**
* sets an uuid to prevent conflict in locally stored data between multiple PoS Configs. By
@@ -57,14 +57,14 @@ export const PosDB = core.Class.extend({
*
* @param {string} uuid Unique identifier of the PoS Config linked to the current session.
*/
set_uuid: function (uuid) {
set_uuid(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) {
get_category_by_id(categ_id) {
if (categ_id instanceof Array) {
var list = [];
for (var i = 0, len = categ_id.length; i < len; i++) {
@@ -79,26 +79,26 @@ export const PosDB = core.Class.extend({
} 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(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(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(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(categories) {
var self = this;
if (!this.category_by_id[this.root_category_id]) {
this.category_by_id[this.root_category_id] = {
@@ -132,8 +132,8 @@ export const PosDB = core.Class.extend({
}
}
make_ancestors(this.root_category_id, []);
},
category_contains: function (categ_id, product_id) {
}
category_contains(categ_id, product_id) {
var product = this.product_by_id[product_id];
if (product) {
var cid = product.pos_categ_id[0];
@@ -143,9 +143,9 @@ export const PosDB = core.Class.extend({
return !!cid;
}
return false;
},
}
/* loads a record store from the database. returns default if nothing is found */
load: function (store, deft) {
load(store, deft) {
if (CACHE[store] !== undefined) {
return CACHE[store];
}
@@ -157,13 +157,13 @@ export const PosDB = core.Class.extend({
} else {
return deft;
}
},
}
/* saves a record store to the database */
save: function (store, data) {
save(store, data) {
localStorage[this.name + "_" + store] = JSON.stringify(data);
CACHE[store] = data;
},
_product_search_string: function (product) {
}
_product_search_string(product) {
var str = product.display_name;
if (product.barcode) {
str += "|" + product.barcode;
@@ -179,8 +179,8 @@ export const PosDB = core.Class.extend({
}
str = product.id + ":" + str.replace(/[\n:]/g, "") + "\n";
return str;
},
add_products: function (products) {
}
add_products(products) {
var stored_categories = this.product_by_category_id;
if (!(products instanceof Array)) {
@@ -192,7 +192,7 @@ export const PosDB = core.Class.extend({
continue;
}
if (product.available_in_pos) {
var search_string = utils.unaccent(this._product_search_string(product));
var search_string = unaccent(this._product_search_string(product));
var categ_id = product.pos_categ_id
? product.pos_categ_id[0]
: this.root_category_id;
@@ -227,8 +227,8 @@ export const PosDB = core.Class.extend({
this.product_by_barcode[product.barcode] = product;
}
}
},
add_packagings: function (product_packagings) {
}
add_packagings(product_packagings) {
var self = this;
Object.values(product_packagings || {}).map((product_packaging) => {
const products = Object.values(self.product_by_id);
@@ -236,8 +236,8 @@ export const PosDB = core.Class.extend({
self.product_packaging_by_barcode[product_packaging.barcode] = product_packaging;
}
});
},
_partner_search_string: function (partner) {
}
_partner_search_string(partner) {
var str = partner.name || "";
if (partner.barcode) {
str += "|" + partner.barcode;
@@ -262,8 +262,8 @@ export const PosDB = core.Class.extend({
}
str = "" + partner.id + ":" + str.replace(":", "").replace(/\n/g, " ") + "\n";
return str;
},
add_partners: function (partners) {
}
add_partners(partners) {
var updated = {};
var new_write_date = "";
var partner;
@@ -335,20 +335,20 @@ export const PosDB = core.Class.extend({
searchString += this._partner_search_string(partner);
}
this.partner_search_strings[chunkId] = utils.unaccent(searchString);
this.partner_search_strings[chunkId] = unaccent(searchString);
}
return Object.keys(updated).length;
},
get_partner_write_date: function () {
}
get_partner_write_date() {
return this.partner_write_date || "1970-01-01 00:00:00";
},
get_partner_by_id: function (id) {
}
get_partner_by_id(id) {
return this.partner_by_id[id];
},
get_partner_by_barcode: function (barcode) {
}
get_partner_by_barcode(barcode) {
return this.partner_by_barcode[barcode];
},
get_partners_sorted: function (max_count) {
}
get_partners_sorted(max_count) {
max_count = max_count
? Math.min(this.partner_sorted.length, max_count)
: this.partner_sorted.length;
@@ -357,13 +357,13 @@ export const PosDB = core.Class.extend({
partners.push(this.partner_by_id[this.partner_sorted[i]]);
}
return partners;
},
search_partner: function (query) {
}
search_partner(query) {
try {
// eslint-disable-next-line no-useless-escape
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, ".");
query = query.replace(/ /g, ".+");
var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi");
var re = RegExp("([0-9]+):.*?" + unaccent(query), "gi");
} catch {
return [];
}
@@ -380,15 +380,15 @@ export const PosDB = core.Class.extend({
}
}
return results;
},
}
/* removes all the data from the database. TODO : being able to selectively remove data */
clear: function () {
clear() {
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(obj) {
var count = 0;
for (var prop in obj) {
if (Object.hasOwnProperty.call(obj, prop)) {
@@ -396,19 +396,19 @@ export const PosDB = core.Class.extend({
}
}
return count;
},
get_product_by_id: function (id) {
}
get_product_by_id(id) {
return this.product_by_id[id];
},
get_product_by_barcode: function (barcode) {
}
get_product_by_barcode(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) {
}
get_product_by_category(category_id) {
var product_ids = this.product_by_category_id[category_id];
var list = [];
if (product_ids) {
@@ -421,17 +421,17 @@ export const PosDB = core.Class.extend({
}
}
return list;
},
}
/* returns a list of products with :
* - 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(category_id, query) {
try {
// eslint-disable-next-line no-useless-escape
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g, ".");
query = query.replace(/ /g, ".+");
var re = RegExp("([0-9]+):.*?" + utils.unaccent(query), "gi");
var re = RegExp("([0-9]+):.*?" + unaccent(query), "gi");
} catch {
return [];
}
@@ -450,12 +450,12 @@ export const PosDB = core.Class.extend({
}
}
return results;
},
}
/* from a product id, and a list of category ids, returns
* 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(category_ids, product_id) {
let cat = this.get_product_by_id(product_id).pos_categ_id[0];
while (cat) {
for (const cat_id of category_ids) {
@@ -467,10 +467,10 @@ export const PosDB = core.Class.extend({
cat = this.get_category_parent_id(cat);
}
return false;
},
}
/* paid orders */
add_order: function (order) {
add_order(order) {
var order_id = order.uid;
var orders = this.load("orders", []);
@@ -490,19 +490,19 @@ export const PosDB = core.Class.extend({
orders.push({ id: order_id, data: order });
this.save("orders", orders);
return order_id;
},
remove_order: function (order_id) {
}
remove_order(order_id) {
var orders = this.load("orders", []);
orders = orders.filter((order) => order.id !== order_id);
this.save("orders", orders);
},
remove_all_orders: function () {
}
remove_all_orders() {
this.save("orders", []);
},
get_orders: function () {
}
get_orders() {
return this.load("orders", []);
},
get_order: function (order_id) {
}
get_order(order_id) {
var orders = this.get_orders();
for (var i = 0, len = orders.length; i < len; i++) {
if (orders[i].id === order_id) {
@@ -510,10 +510,10 @@ export const PosDB = core.Class.extend({
}
}
return undefined;
},
}
/* working orders */
save_unpaid_order: function (order) {
save_unpaid_order(order) {
var order_id = order.uid;
var orders = this.load("unpaid_orders", []);
var serialized = order.export_as_JSON();
@@ -529,36 +529,36 @@ export const PosDB = core.Class.extend({
orders.push({ id: order_id, data: serialized });
this.save("unpaid_orders", orders);
return order_id;
},
remove_unpaid_order: function (order) {
}
remove_unpaid_order(order) {
var orders = this.load("unpaid_orders", []);
orders = orders.filter((o) => o.id !== order.uid);
this.save("unpaid_orders", orders);
},
remove_all_unpaid_orders: function () {
}
remove_all_unpaid_orders() {
this.save("unpaid_orders", []);
},
get_unpaid_orders: function () {
}
get_unpaid_orders() {
var saved = this.load("unpaid_orders", []);
var orders = [];
for (var i = 0; i < saved.length; i++) {
orders.push(saved[i].data);
}
return orders;
},
}
/**
* Return the orders with requested ids if they are unpaid.
* @param {array<number>} ids order_ids.
* @return {array<object>} list of orders.
*/
get_unpaid_orders_to_sync: function (ids) {
get_unpaid_orders_to_sync(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.
*
@@ -566,28 +566,28 @@ export const 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(order) {
if (order.server_id !== undefined) {
var to_remove = this.load("unpaid_orders_to_remove", []);
to_remove.push(order.server_id);
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 () {
get_ids_to_remove_from_server() {
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) {
set_ids_removed_from_server(ids) {
var to_remove = this.load("unpaid_orders_to_remove", []);
to_remove = to_remove.filter((id) => !ids.includes(id));
this.save("unpaid_orders_to_remove", to_remove);
},
});
}
}
+29 -45
View File
@@ -2,22 +2,29 @@
/* global waitForWebfonts */
import { PosDB } from "@point_of_sale/js/db";
import config from "web.config";
import field_utils from "web.field_utils";
import time from "web.time";
import utils from "web.utils";
import { formatFloat } from "@web/views/fields/formatters";
// FIXME POSREF - unify use of native parseFloat and web's parseFloat. We probably don't need the native version.
import { parseFloat as oParseFloat } from "@web/views/fields/parsers";
import { batched, uuidv4 } from "@point_of_sale/js/utils";
import { formatDate, formatDateTime, serializeDateTime } from "@web/core/l10n/dates";
import {
roundDecimals as round_di,
roundPrecision as round_pr,
floatIsZero,
} from "@web/core/utils/numbers";
import { ErrorPopup } from "./Popups/ErrorPopup";
import { ProductConfiguratorPopup } from "@point_of_sale/js/Popups/ProductConfiguratorPopup";
import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup";
import { markRaw, reactive } from "@odoo/owl";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
import { escape, sprintf } from "@web/core/utils/strings";
import { sprintf } from "@web/core/utils/strings";
import { Mutex } from "@web/core/utils/concurrency";
import { memoize } from "@web/core/utils/functions";
import { _t } from "@web/core/l10n/translation";
import { renderToString } from "@web/core/utils/render";
const { DateTime } = luxon;
/* Returns an array containing all elements of the given
* array corresponding to the rule function {agg} and without duplicates
*
@@ -38,10 +45,6 @@ export function uniqueBy(array, agg) {
return [...map.values()];
}
var round_di = utils.round_decimals;
var round_pr = utils.round_precision;
const Markup = utils.Markup;
/**
* Gets a product image as a base64 string so that it can be sent to the
* customer display, as the display won't be able to fetch it, since the image
@@ -138,7 +141,6 @@ export class PosGlobalState extends PosModel {
super.setup(...arguments);
this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders
this.debug = config.isDebug(); //debug mode
this.unwatched = markRaw({});
this.pushOrderMutex = new Mutex();
@@ -1443,11 +1445,11 @@ export class PosGlobalState extends PosModel {
}
isProductQtyZero(qty) {
return utils.float_is_zero(qty, this.dp["Product Unit of Measure"]);
return floatIsZero(qty, this.dp["Product Unit of Measure"]);
}
formatProductQty(qty) {
return field_utils.format.float(qty, {
return formatFloat(qty, {
digits: [true, this.dp["Product Unit of Measure"]],
});
}
@@ -1474,7 +1476,7 @@ export class PosGlobalState extends PosModel {
if (typeof amount === "number") {
amount = round_di(amount, decimals).toFixed(decimals);
amount = field_utils.format.float(round_di(amount, decimals), {
amount = formatFloat(round_di(amount, decimals), {
digits: [69, decimals],
});
}
@@ -1934,7 +1936,7 @@ export class Orderline extends PosModel {
? discount
: isNaN(parseFloat(discount))
? 0
: field_utils.parse.float("" + discount);
: oParseFloat("" + discount);
var disc = Math.min(Math.max(parsed_discount || 0, 0), 100);
this.discount = disc;
this.discountStr = "" + disc;
@@ -1977,7 +1979,7 @@ export class Orderline extends PosModel {
var quant =
typeof quantity === "number"
? quantity
: field_utils.parse.float("" + (quantity ? quantity : 0));
: oParseFloat("" + (quantity ? quantity : 0));
if (this.refunded_orderline_id in this.pos.toRefundLines) {
const toRefundDetail = this.pos.toRefundLines[this.refunded_orderline_id];
const maxQtyToRefund =
@@ -2013,7 +2015,7 @@ export class Orderline extends PosModel {
var decimals = this.pos.dp["Product Unit of Measure"];
var rounding = Math.max(unit.rounding, Math.pow(10, -decimals));
this.quantity = round_pr(quant, rounding);
this.quantityStr = field_utils.format.float(this.quantity, {
this.quantityStr = formatFloat(this.quantity, {
digits: [69, decimals],
});
} else {
@@ -2140,7 +2142,7 @@ export class Orderline extends PosModel {
this.get_unit().is_pos_groupable &&
// don't merge discounted orderlines
this.get_discount() === 0 &&
utils.float_is_zero(
floatIsZero(
price - order_line_price - orderline.get_price_extra(),
this.pos.currency.decimal_places
) &&
@@ -2247,7 +2249,7 @@ export class Orderline extends PosModel {
? price
: isNaN(parseFloat(price))
? 0
: field_utils.parse.float("" + price);
: oParseFloat("" + price);
this.price = round_di(parsed_price || 0, this.pos.dp["Product Price"]);
}
get_unit_price() {
@@ -2573,7 +2575,7 @@ export class Payment extends PosModel {
return this.amount;
}
get_amount_str() {
return field_utils.format.float(this.amount, {
return formatFloat(this.amount, {
digits: [69, this.pos.currency.decimal_places],
});
}
@@ -2632,7 +2634,7 @@ export class Payment extends PosModel {
//exports as JSON for server communication
export_as_JSON() {
return {
name: time.datetime_to_str(new Date()),
name: serializeDateTime(DateTime.local()),
payment_method_id: this.payment_method.id,
amount: this.get_amount(),
payment_status: this.payment_status,
@@ -2645,12 +2647,11 @@ export class Payment extends PosModel {
}
//exports as JSON for receipt printing
export_for_printing() {
const ticket = escape(this.ticket).replace(/\n/g, "<br />"); // formatting
return {
cid: this.cid,
amount: this.get_amount(),
name: this.name,
ticket: Markup(ticket),
ticket: this.ticket,
};
}
// If payment status is a non-empty string, then it is an electronic payment.
@@ -2860,17 +2861,8 @@ export class Order extends PosModel {
return json;
}
_exportShippingDateForPrinting() {
const shippingDate = new Date(this.shippingDate);
const localeShippingDate = field_utils.format.date(
moment(shippingDate),
{},
{ timezone: false }
);
const exportedDate = {
localestring: localeShippingDate,
validationDate: shippingDate,
};
return exportedDate;
const shippingDate = DateTime.fromJSDate(new Date(this.shippingDate));
return formatDate(shippingDate);
}
export_for_printing() {
var orderlines = [];
@@ -2951,11 +2943,7 @@ export class Order extends PosModel {
receipt.footer = (isHeaderOrFooter && this.pos.config.receipt_footer) || "";
if (!receipt.date.localestring && (!this.state || this.state == "draft")) {
receipt.date.localestring = field_utils.format.datetime(
moment(new Date()),
{},
{ timezone: false }
);
receipt.date.localestring = formatDateTime(DateTime.local());
}
return receipt;
@@ -3112,11 +3100,7 @@ export class Order extends PosModel {
initialize_validation_date() {
this.validation_date = new Date();
this.formatted_validation_date = field_utils.format.datetime(
moment(this.validation_date),
{},
{ timezone: false }
);
this.formatted_validation_date = formatDateTime(DateTime.fromJSDate(this.validation_date));
}
set_tip(tip) {
@@ -3591,7 +3575,7 @@ export class Order extends PosModel {
var rounding_applied = total - remaining;
// because floor and ceil doesn't include decimals in calculation, we reuse the value of the half-up and adapt it.
if (utils.float_is_zero(rounding_applied, this.pos.currency.decimal_places)) {
if (floatIsZero(rounding_applied, this.pos.currency.decimal_places)) {
// https://xkcd.com/217/
return 0;
} else if (
@@ -3632,7 +3616,7 @@ export class Order extends PosModel {
}
if (
!utils.float_is_zero(
!floatIsZero(
line.amount - round_pr(line.amount, this.pos.cash_rounding[0].rounding),
6
)
+16 -17
View File
@@ -1,14 +1,10 @@
/** @odoo-module */
import core from "web.core";
/**
* Implement this interface to support a new payment method in the POS:
*
import const PaymentInterface from "@point_of_sale/js/payment";
* var MyPayment = PaymentInterface.extend({
* ...
* })
* import { PaymentInterface } from "@point_of_sale/js/payment";
* class MyPayment extends PaymentInterface {}
*
* To connect the interface to the right payment methods register it:
*
@@ -22,22 +18,25 @@ import core from "web.core";
* by overriding the loader_params of the models in the back end
* in the `pos.session` model
*/
// FIXME POSREF convert this to a class.
export const PaymentInterface = core.Class.extend({
init: function (pos, payment_method) {
export class PaymentInterface {
constructor(pos, payment_method) {
this.setup(pos, payment_method);
}
setup(pos, payment_method) {
this.pos = pos;
this.payment_method = payment_method;
this.supports_reversals = false;
},
}
/**
* Call this function to enable UI elements that allow a user to
* reverse a payment. This requires that you implement
* send_payment_reversal.
*/
enable_reversals: function () {
enable_reversals() {
this.supports_reversals = true;
},
}
/**
* Called when a user clicks the "Send" button in the
@@ -55,7 +54,7 @@ export const PaymentInterface = core.Class.extend({
* the payment should be retried. Rejected when the status of the
* paymentline will be manually updated.
*/
send_payment_request: function (cid) {},
send_payment_request(cid) {}
/**
* Called when a user removes a payment line that's still waiting
@@ -69,7 +68,7 @@ export const PaymentInterface = core.Class.extend({
* @param {string} cid - The id of the paymentline
* @returns {Promise}
*/
send_payment_cancel: function (order, cid) {},
send_payment_cancel(order, cid) {}
/**
* This is an optional method. When implementing this make sure to
@@ -81,12 +80,12 @@ export const PaymentInterface = core.Class.extend({
* @param {string} cid - The id of the paymentline
* @returns {Promise} returns true if the reversal was successful.
*/
send_payment_reversal: function (cid) {},
send_payment_reversal(cid) {}
/**
* Called when the payment screen in the POS is closed (by
* e.g. clicking the "Back" button). Could be used to cancel in
* progress payments.
*/
close: function () {},
});
close() {}
}
@@ -1,171 +0,0 @@
/** @odoo-module */
/* global html2canvas */
import Session from "web.Session";
import core from "web.core";
import { ErrorPopup } from "./Popups/ErrorPopup";
var _t = core._t;
// IMPROVEMENT: This is too much. We can get away from this class.
export class PrintResult {
constructor({ successful, message }) {
this.successful = successful;
this.message = message;
}
}
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."),
},
});
}
IoTResultError() {
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"
),
},
});
}
Successful() {
return new PrintResult({
successful: true,
});
}
}
export const PrinterMixin = {
init: function (pos) {
this.receipt_queue = [];
this.printResultGenerator = new PrintResultGenerator();
this.pos = pos;
},
/**
* Add the receipt to the queue of receipts to be printed and process it.
* We clear the print queue if printing is not successful.
* @param {String} receipt: The receipt to be printed, in HTML
* @returns {PrintResult}
*/
print_receipt: async function (receipt) {
if (receipt) {
this.receipt_queue.push(receipt);
}
let image, sendPrintResult;
while (this.receipt_queue.length > 0) {
receipt = this.receipt_queue.shift();
image = await this.htmlToImg(receipt);
try {
sendPrintResult = await this.send_printing_job(image);
} catch {
// Error in communicating to the IoT box.
this.receipt_queue.length = 0;
return this.printResultGenerator.IoTActionError();
}
// rpc call is okay but printing failed because
// IoT box can't find a printer.
if (!sendPrintResult || sendPrintResult.result === false) {
this.receipt_queue.length = 0;
return this.printResultGenerator.IoTResultError(sendPrintResult.printerErrorCode);
}
}
return this.printResultGenerator.Successful();
},
/**
* Generate a jpeg image from a canvas
* @param {DOMElement} canvas
*/
process_canvas: function (canvas) {
return canvas.toDataURL("image/jpeg").replace("data:image/jpeg;base64,", "");
},
/**
* Renders the html as an image to print it
* @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");
// 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" });
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();
return this.process_canvas(canvas);
});
},
_onIoTActionResult: function (data) {
if (this.pos && (data === false || data.result === false)) {
this.pos.env.services.popup.add(ErrorPopup, {
title: _t("Connection to the printer failed"),
body: _t("Please check if the printer is still connected."),
});
}
},
_onIoTActionFail: function () {
if (this.pos) {
this.pos.env.services.popup.add(ErrorPopup, {
title: _t("Connection to IoT Box failed"),
body: _t("Please check if the IoT Box is still connected."),
});
}
},
};
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,
});
},
/**
* Sends a command to the connected proxy to open the cashbox
* (the physical box where you store the cash). Updates the status of
* the printer with the answer from the proxy.
*/
open_cashbox: function () {
var self = this;
return this.connection
.rpc("/hw_proxy/default_printer_action", {
data: {
action: "cashbox",
},
})
.then(self._onIoTActionResult.bind(self))
.guardedCatch(self._onIoTActionFail.bind(self));
},
/**
* Sends the printing command the connected proxy
* @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", {
data: {
action: "print_receipt",
receipt: img,
},
});
},
});
@@ -37,3 +37,20 @@ export function uuidv4() {
return v.toString(16);
});
}
/**
* Formats the given `url` with correct protocol and port.
* Useful for communicating to local iot box instance.
* @param {string} url
* @returns {string}
*/
export function deduceUrl(url) {
const { protocol } = window.location;
if (!url.includes("//")) {
url = `${protocol}//${url}`;
}
if (url.indexOf(":", 6) < 0) {
url += ":" + (protocol === "https:" ? 443 : 8069);
}
return url;
}
@@ -150,7 +150,7 @@
<t t-if="line.ticket">
<br />
<div class="pos-payment-terminal-receipt">
<t t-out="line.ticket" />
<pre t-esc="line.ticket" />
</div>
</t>
</t>
@@ -160,12 +160,7 @@
<t t-if="shippingDate">
<div class="pos-receipt-order-data">
Expected delivery:
<t t-if="shippingDate.localestring">
<div><t t-esc="shippingDate.localestring" /></div>
</t>
<t t-else="">
<div><t t-esc="shippingDate.validationDate" /></div>
</t>
<div><t t-esc="shippingDate" /></div>
</div>
</t>
@@ -22,7 +22,7 @@
<form t-on-submit.prevent="onSendEmail" class="send-email">
<div class="input-email">
<input type="email" placeholder="Send receipt by email" t-model="orderUiState.inputEmail" />
<button class="send" t-att-class="{ highlight: is_email(orderUiState.inputEmail) }" type="submit">
<button class="send" t-att-class="{ highlight: isValidEmail() }" type="submit">
<i class="fa fa-paper-plane" aria-hidden="true" t-ref="order-mail-receipt-button" />
</button>
</div>
@@ -1,5 +1,4 @@
/** @odoo-module */
import config from "web.config";
/**
* USAGE
@@ -72,7 +71,7 @@ export function getSteps() {
const methodProxyHandler = {
apply(target, thisArg, args) {
const res = target.call(thisArg, ...args);
if (config.isDebug()) {
if (odoo.debug) {
// This step is added before the real steps.
// Very useful when debugging because we know which
// method call failed and what were the parameters.
@@ -2,8 +2,7 @@
/* global posmodel */
import { registry } from "@web/core/registry";
import utils from "web.utils";
var round_di = utils.round_decimals;
import { roundDecimals as round_di } from "@web/core/utils/numbers";
function assert(condition, message) {
if (!condition) {
@@ -2,7 +2,7 @@
import core from "web.core";
import portalChatter from "portal.chatter";
import utils from "web.utils";
import { roundPrecision } from "@web/core/utils/numbers";
import time from "web.time";
var _t = core._t;
@@ -141,7 +141,7 @@ PortalChatter.include({
.forEach((rating) => {
ratingData["percent"].push({
num: self.roundToHalf(rating),
percent: utils.round_precision(result["rating_stats"]["percent"][rating], 0.01),
percent: roundPrecision(result["rating_stats"]["percent"][rating], 0.01),
});
});
this.set('rating_card_values', ratingData);
+49 -69
View File
@@ -1,30 +1,24 @@
/** @odoo-module */
import core from "web.core";
import rpc from "web.rpc";
import { _t } from "@web/core/l10n/translation";
import { PaymentInterface } from "@point_of_sale/js/payment";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { sprintf } from "@web/core/utils/strings";
var _t = core._t;
export const PaymentAdyen = PaymentInterface.extend({
send_payment_request: function (cid) {
this._super.apply(this, arguments);
export class PaymentAdyen extends PaymentInterface {
send_payment_request(cid) {
super.send_payment_request(cid);
this._reset_state();
return this._adyen_pay(cid);
},
send_payment_cancel: function (order, cid) {
this._super.apply(this, arguments);
}
send_payment_cancel(order, cid) {
super.send_payment_cancel(order, cid);
return this._adyen_cancel();
},
close: function () {
this._super.apply(this, arguments);
},
}
set_most_recent_service_id(id) {
this.most_recent_service_id = id;
},
}
pending_adyen_line() {
return this.pos
@@ -34,16 +28,16 @@ export const PaymentAdyen = PaymentInterface.extend({
paymentLine.payment_method.use_payment_terminal === "adyen" &&
!paymentLine.is_done()
);
},
}
// private methods
_reset_state: function () {
_reset_state() {
this.was_cancelled = false;
this.remaining_polls = 4;
clearTimeout(this.polling);
},
}
_handle_odoo_connection_failure: function (data) {
_handle_odoo_connection_failure(data) {
// handle timeout
var line = this.pending_adyen_line();
if (line) {
@@ -56,33 +50,25 @@ export const PaymentAdyen = PaymentInterface.extend({
);
return Promise.reject(data); // prevent subsequent onFullFilled's from being called
},
}
_call_adyen: function (data, operation) {
return rpc
.query(
{
model: "pos.payment.method",
method: "proxy_adyen_request",
args: [[this.payment_method.id], data, operation],
},
{
// When a payment terminal is disconnected it takes Adyen
// a while to return an error (~6s). So wait 10 seconds
// before concluding Odoo is unreachable.
timeout: 10000,
shadow: true,
}
)
_call_adyen(data, operation) {
// FIXME POSREF TIMEOUT 10000
return this.pos.env.services.orm.silent
.call("pos.payment.method", "proxy_adyen_request", [
[this.payment_method.id],
data,
operation,
])
.catch(this._handle_odoo_connection_failure.bind(this));
},
}
_adyen_get_sale_id: function () {
_adyen_get_sale_id() {
var config = this.pos.config;
return sprintf("%s (ID: %s)", config.display_name, config.id);
},
}
_adyen_common_message_header: function () {
_adyen_common_message_header() {
var config = this.pos.config;
this.most_recent_service_id = Math.floor(Math.random() * Math.pow(2, 64)).toString(); // random ID to identify request/response pairs
this.most_recent_service_id = this.most_recent_service_id.substring(0, 10); // max length is 10
@@ -95,9 +81,9 @@ export const PaymentAdyen = PaymentInterface.extend({
ServiceID: this.most_recent_service_id,
POIID: this.payment_method.adyen_terminal_identifier,
};
},
}
_adyen_pay_data: function () {
_adyen_pay_data() {
var order = this.pos.get_order();
var config = this.pos.config;
var line = order.selected_paymentline;
@@ -129,9 +115,9 @@ export const PaymentAdyen = PaymentInterface.extend({
}
return data;
},
}
_adyen_pay: function (cid) {
_adyen_pay(cid) {
var self = this;
var order = this.pos.get_order();
@@ -151,9 +137,9 @@ export const PaymentAdyen = PaymentInterface.extend({
return this._call_adyen(data).then(function (data) {
return self._adyen_handle_response(data);
});
},
}
_adyen_cancel: function (ignore_error) {
_adyen_cancel(ignore_error) {
var self = this;
var config = this.pos.config;
var previous_service_id = this.most_recent_service_id;
@@ -187,9 +173,9 @@ export const PaymentAdyen = PaymentInterface.extend({
self.was_cancelled = !!self.polling;
}
});
},
}
_convert_receipt_info: function (output_text) {
_convert_receipt_info(output_text) {
return output_text.reduce(function (acc, entry) {
var params = new URLSearchParams(entry.Text);
if (params.get("name") && !params.get("value")) {
@@ -200,27 +186,21 @@ export const PaymentAdyen = PaymentInterface.extend({
return acc;
}, "");
},
}
_poll_for_response: function (resolve, reject) {
_poll_for_response(resolve, reject) {
var self = this;
if (this.was_cancelled) {
resolve(false);
return Promise.resolve();
}
return rpc
.query(
{
model: "pos.payment.method",
method: "get_latest_adyen_status",
args: [[this.payment_method.id], this._adyen_get_sale_id()],
},
{
timeout: 5000,
shadow: true,
}
)
// FIXME POSREF TIMEOUT 5000
return this.pos.env.services.orm.silent
.call("pos.payment.method", "get_latest_adyen_status", [
[this.payment_method.id],
this._adyen_get_sale_id(),
])
.catch(function (data) {
if (self.remaining_polls != 0) {
self.remaining_polls--;
@@ -302,9 +282,9 @@ export const PaymentAdyen = PaymentInterface.extend({
line.set_payment_status("waitingCard");
}
});
},
}
_adyen_handle_response: function (response) {
_adyen_handle_response(response) {
var line = this.pending_adyen_line();
if (response.error && response.error.status_code == 401) {
@@ -339,7 +319,7 @@ export const PaymentAdyen = PaymentInterface.extend({
line.set_payment_status("waitingCard");
return this.start_get_status_polling();
}
},
}
start_get_status_polling() {
var self = this;
@@ -359,9 +339,9 @@ export const PaymentAdyen = PaymentInterface.extend({
});
return res;
},
}
_show_error: function (msg, title) {
_show_error(msg, title) {
if (!title) {
title = _t("Adyen Error");
}
@@ -369,5 +349,5 @@ export const PaymentAdyen = PaymentInterface.extend({
title: title,
body: msg,
});
},
});
}
}
@@ -1,112 +1,99 @@
/** @odoo-module */
/* @odoo-module */
import core from "web.core";
import { PrinterMixin, PrintResult, PrintResultGenerator } from "@point_of_sale/js/printers";
import { BasePrinter } from "@point_of_sale/app/printer/base_printer";
import { _t } from "@web/core/l10n/translation";
import { sprintf } from "@web/core/utils/strings";
import { templates } from "@web/core/assets";
import { createElement, append, createTextNode } from "@web/core/utils/xml";
var QWeb = core.qweb;
var _t = core._t;
class EpsonPrintResultGenerator extends PrintResultGenerator {
constructor(address) {
super();
this.address = address;
}
IoTActionError() {
var printRes = 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"
),
},
});
if (window.location.protocol === "https:") {
printRes.message.body += sprintf(
_t(
"If you are on a secure server (HTTPS) please make sure you manually accepted the certificate by accessing %s"
),
this.address
);
}
return printRes;
}
IoTResultError(printerErrorCode) {
let message =
_t("The printer was successfully reached, but it wasn't able to print.") + "\n";
if (printerErrorCode) {
message +=
"\n" +
_t("The following error code was given by the printer:") +
"\n" +
printerErrorCode;
const extra_messages = {
DeviceNotFound:
_t(
"Check on the printer configuration for the 'Device ID' setting. " +
"It should be set to: "
) + "\nlocal_printer",
EPTR_REC_EMPTY: _t("No paper was detected by the printer"),
};
if (printerErrorCode in extra_messages) {
message += "\n" + extra_messages[printerErrorCode];
}
message +=
"\n" +
_t("To find more details on the error reason, please search online for:") +
"\n" +
" Epson Server Direct Print " +
printerErrorCode;
} else {
message += _t("Please check if the printer has enough paper and is ready to print.");
}
return new PrintResult({
successful: false,
message: {
title: _t("Printing failed"),
body: message,
},
});
function ePOSPrint(children) {
let ePOSLayout = templates.querySelector("[t-name='ePOSLayout'");
if (!ePOSLayout) {
throw new Error("'ePOSLayout' not loaded");
}
ePOSLayout = ePOSLayout.cloneNode(true);
const [eposPrintEl] = ePOSLayout.getElementsByTagName("epos-print");
append(eposPrintEl, children);
// IMPORTANT: Need to remove `xmlns=""` in the image and cut elements.
// > Otherwise, the print request will succeed but it the printer device won't actually do the printing.
return ePOSLayout.innerHTML.replaceAll(`xmlns=""`, "");
}
export const EpsonPrinter = core.Class.extend(PrinterMixin, {
init(ip, pos) {
PrinterMixin.init.call(this, pos);
var url = window.location.protocol + "//" + ip;
this.address = url + "/cgi-bin/epos/service.cgi?devid=local_printer";
this.printResultGenerator = new EpsonPrintResultGenerator(url);
},
/**
* Sends print request to ePos printer that is directly connected to the local network.
*/
export class EpsonPrinter extends BasePrinter {
setup({ ip }) {
super.setup(...arguments);
this.url = window.location.protocol + "//" + ip;
this.address = this.url + "/cgi-bin/epos/service.cgi?devid=local_printer";
}
/**
* @override
* Create the raster data from a canvas
*/
processCanvas(canvas) {
const rasterData = this.canvasToRaster(canvas);
const encodedData = this.encodeRaster(rasterData);
return ePOSPrint([
createElement(
"image",
{
width: canvas.width,
height: canvas.height,
align: "center",
},
[createTextNode(encodedData)]
),
createElement("cut", { type: "feed" }),
]);
}
/**
* @override
*/
openCashbox() {
const pulse = ePOSPrint([createElement("pulse")]);
this.sendPrintingJob(pulse);
}
/**
* @override
*/
async sendPrintingJob(img) {
const res = await $.ajax({
url: this.address,
method: "POST",
data: img,
});
const response = $(res).find("response");
return {
result: response.attr("success") === "true",
printerErrorCode: response.attr("code"),
};
}
/**
* Transform a (potentially colored) canvas into a monochrome raster image.
* We will use Floyd-Steinberg dithering.
*/
_canvasToRaster(canvas) {
var imageData = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height);
var pixels = imageData.data;
var width = imageData.width;
var height = imageData.height;
var errors = Array.from(Array(width), (_) => Array(height).fill(0));
var rasterData = new Array(width * height).fill(0);
canvasToRaster(canvas) {
const imageData = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
const width = imageData.width;
const height = imageData.height;
const errors = Array.from(Array(width), (_) => Array(height).fill(0));
const rasterData = new Array(width * height).fill(0);
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
var idx, oldColor, newColor;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let oldColor, newColor;
// Compute grayscale level. Those coefficients were found online
// as R, G and B have different impacts on the darkness
// perception (e.g. pure blue is darker than red or green).
idx = (y * width + x) * 4;
const idx = (y * width + x) * 4;
oldColor = pixels[idx] * 0.299 + pixels[idx + 1] * 0.587 + pixels[idx + 2] * 0.114;
// Propagate the error from neighbor pixels
@@ -125,7 +112,7 @@ export const EpsonPrinter = core.Class.extend(PrinterMixin, {
// Propagate the error to the following pixels, based on
// Floyd-Steinberg dithering.
var error = oldColor - newColor;
const error = oldColor - newColor;
if (error) {
if (x < width - 1) {
// Pixel on the right
@@ -148,56 +135,84 @@ export const EpsonPrinter = core.Class.extend(PrinterMixin, {
}
return rasterData.join("");
},
}
/**
* Base 64 encode a raster image
*/
_encodeRaster(rasterData) {
var encodedData = "";
for (var i = 0; i < rasterData.length; i += 8) {
var sub = rasterData.substr(i, 8);
encodeRaster(rasterData) {
let encodedData = "";
for (let i = 0; i < rasterData.length; i += 8) {
const sub = rasterData.substr(i, 8);
encodedData += String.fromCharCode(parseInt(sub, 2));
}
return btoa(encodedData);
},
/**
* Create the raster data from a canvas
*
* @override
*/
process_canvas(canvas) {
var rasterData = this._canvasToRaster(canvas);
var encodedData = this._encodeRaster(rasterData);
return QWeb.render("ePOSPrintImage", {
image: encodedData,
width: canvas.width,
height: canvas.height,
});
},
}
/**
* @override
*/
open_cashbox() {
var pulse = QWeb.render("ePOSDrawer");
this.send_printing_job(pulse);
},
/**
* @override
*/
async send_printing_job(img) {
const res = await $.ajax({
url: this.address,
method: "POST",
data: img,
});
const response = $(res).find("response");
return {
result: response.attr("success") === "true",
printerErrorCode: response.attr("code"),
getActionError() {
const printRes = {
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"
),
},
};
},
});
if (window.location.protocol === "https:") {
printRes.message.body += sprintf(
_t(
"If you are on a secure server (HTTPS) please make sure you manually accepted the certificate by accessing %s"
),
this.url
);
}
return printRes;
}
/**
* @override
*/
getResultsError(printResult) {
const errorCode = printResult.printerErrorCode;
let message =
_t("The printer was successfully reached, but it wasn't able to print.") + "\n";
if (errorCode) {
message +=
"\n" + _t("The following error code was given by the printer:") + "\n" + errorCode;
const extra_messages = {
DeviceNotFound:
_t(
"Check on the printer configuration for the 'Device ID' setting. " +
"It should be set to: "
) + "\nlocal_printer",
EPTR_REC_EMPTY: _t("No paper was detected by the printer"),
};
if (errorCode in extra_messages) {
message += "\n" + extra_messages[errorCode];
}
message +=
"\n" +
_t("To find more details on the error reason, please search online for:") +
"\n" +
" Epson Server Direct Print " +
errorCode;
} else {
message += _t("Please check if the printer has enough paper and is ready to print.");
}
return {
successful: false,
message: {
title: _t("Printing failed"),
body: message,
},
};
}
}
@@ -1,7 +1,7 @@
/** @odoo-module */
import { PosGlobalState } from "@point_of_sale/js/models";
import { EpsonPrinter } from "@pos_epson_printer/js/printers";
import { EpsonPrinter } from "@pos_epson_printer/js/epson_printer";
import { patch } from "@web/core/utils/patch";
patch(PosGlobalState.prototype, "pos_epson_printer.PosGlobalState", {
@@ -9,7 +9,7 @@ patch(PosGlobalState.prototype, "pos_epson_printer.PosGlobalState", {
var self = this;
return this._super(...arguments).then(function () {
if (self.config.other_devices && self.config.epson_printer_ip) {
self.hardwareProxy.printer = new EpsonPrinter(self.config.epson_printer_ip, self);
self.hardwareProxy.printer = new EpsonPrinter({ ip: self.config.epson_printer_ip });
}
});
},
@@ -1,25 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ePOSTemplate">
<t t-name="ePOSLayout" owl="1">
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print">
<t t-out="0"/>
</epos-print>
<epos-print xmlns="http://www.epson-pos.com/schemas/2011/03/epos-print" />
</s:Body>
</s:Envelope>
</t>
<t t-name="ePOSPrintImage">
<t t-call="ePOSTemplate">
<image t-att-width="width" t-att-height="height" align="center" t-esc="image"/>
<cut type="feed"/>
</t>
</t>
<t t-name="ePOSDrawer">
<t t-call="ePOSTemplate">
<pulse/>
</t>
</t>
</templates>
@@ -1,7 +1,7 @@
/** @odoo-module */
import { PosGlobalState } from "@point_of_sale/js/models";
import { EpsonPrinter } from "@pos_epson_printer/js/printers";
import { EpsonPrinter } from "@pos_epson_printer/js/epson_printer";
import { patch } from "@web/core/utils/patch";
// The override of create_printer needs to happen after its declaration in
@@ -12,7 +12,7 @@ import "@pos_restaurant/js/models";
patch(PosGlobalState.prototype, "pos_epson_printer_restaurant.PosGlobalState", {
create_printer(config) {
if (config.printer_type === "epson_epos") {
return new EpsonPrinter(config.epson_printer_ip, this);
return new EpsonPrinter({ ip: config.epson_printer_ip });
} else {
return this._super(...arguments);
}
+12 -9
View File
@@ -1,15 +1,18 @@
/** @odoo-module **/
import { Order, Orderline, PosGlobalState } from "@point_of_sale/js/models";
import { Mutex } from "@web/core/utils/concurrency";
import concurrency from "web.concurrency";
import { round_decimals, round_precision } from "web.utils";
import core from "web.core";
import { roundDecimals, roundPrecision } from "@web/core/utils/numbers";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
const _t = core._t;
// FIXME: Perhaps MutexedDropPrevious can be replaced by the new KeepLast.
// > This might require thorough investigation on how _updateRewards work.
// > If successful, we can fully get rid of the legacy concurrency module.
const dropPrevious = new concurrency.MutexedDropPrevious(); // Used for queuing reward updates
const mutex = new concurrency.Mutex(); // Used for sequential cache updates
const mutex = new Mutex(); // Used for sequential cache updates
const COUPON_CACHE_MAX_SIZE = 4096; // Maximum coupon cache size, prevents long run memory issues and (to some extent) invalid data
@@ -743,7 +746,7 @@ patch(Order.prototype, "pos_loyalty.Order", {
if (rule.reward_point_mode === "order") {
res += rule.reward_point_amount;
} else if (rule.reward_point_mode === "money") {
res -= round_precision(
res -= roundPrecision(
rule.reward_point_amount * line.get_price_with_tax(),
0.01
);
@@ -934,7 +937,7 @@ patch(Order.prototype, "pos_loyalty.Order", {
if (program.program_type === "gift_card") {
price_to_use = line.price;
}
const pointsPerUnit = round_precision(
const pointsPerUnit = roundPrecision(
(rule.reward_point_amount * price_to_use) / line.get_quantity(),
0.01
);
@@ -960,7 +963,7 @@ patch(Order.prototype, "pos_loyalty.Order", {
points += rule.reward_point_amount;
} else if (rule.reward_point_mode === "money") {
// NOTE: unlike in sale_loyalty this performs a round half-up instead of round down
points += round_precision(
points += roundPrecision(
rule.reward_point_amount * orderedProductPaid,
0.01
);
@@ -1460,7 +1463,7 @@ patch(Order.prototype, "pos_loyalty.Order", {
if (rule.reward_point_mode === "order") {
result += rule.reward_point_amount;
} else if (rule.reward_point_mode === "money") {
result += round_precision(
result += roundPrecision(
rule.reward_point_amount * product.lst_price,
0.01
);
@@ -1545,7 +1548,7 @@ patch(Order.prototype, "pos_loyalty.Order", {
return [
{
product: reward.discount_line_product_id,
price: -round_decimals(
price: -roundDecimals(
product.get_price(this.pricelist, freeQuantity),
this.pos.currency.decimal_places
),
@@ -1,6 +1,6 @@
/** @odoo-module */
import { _t } from "web.core";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen";
import { useBarcodeReader } from "@point_of_sale/app/barcode_reader_hook";
@@ -2,7 +2,7 @@
import { TicketScreen } from "@point_of_sale/js/Screens/TicketScreen/TicketScreen";
import { useAutofocus } from "@web/core/utils/hooks";
import { patch } from "@web/core/utils/patch";
import { parse } from "web.field_utils";
import { parseFloat } from "@web/views/fields/parsers";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
import { Component, useState } from "@odoo/owl";
@@ -57,7 +57,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
async settleTips() {
// set tip in each order
for (const order of this.getFilteredOrderList()) {
const tipAmount = parse.float(order.uiState.TipScreen.inputTipAmount || "0");
const tipAmount = parseFloat(order.uiState.TipScreen.inputTipAmount || "0");
const serverId = this.env.pos.validated_orders_name_server_id_map[order.name];
if (!serverId) {
console.warn(
@@ -164,7 +164,7 @@ export class TipCell extends Component {
useAutofocus();
}
get tipAmountStr() {
return this.env.pos.format_currency(parse.float(this.orderUiState.inputTipAmount || "0"));
return this.env.pos.format_currency(parseFloat(this.orderUiState.inputTipAmount || "0"));
}
onBlur() {
this.state.isEditing = false;
@@ -1,7 +1,7 @@
/** @odoo-module */
import { parse } from "web.field_utils";
import { renderToString } from "@web/core/utils/render";
import { parseFloat } from "@web/views/fields/parsers";
import { renderToElement } from "@web/core/utils/render";
import { registry } from "@web/core/registry";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
@@ -27,7 +27,7 @@ export class TipScreen extends Component {
});
}
get overallAmountStr() {
const tipAmount = parse.float(this.state.inputTipAmount || "0");
const tipAmount = parseFloat(this.state.inputTipAmount || "0");
const original = this.env.pos.format_currency(this.totalAmount);
const tip = this.env.pos.format_currency(tipAmount);
const overall = this.env.pos.format_currency(this.totalAmount + tipAmount);
@@ -47,7 +47,7 @@ export class TipScreen extends Component {
];
}
async validateTip() {
const amount = parse.float(this.state.inputTipAmount) || 0;
const amount = parseFloat(this.state.inputTipAmount) || 0;
const order = this.env.pos.get_order();
const serverId = this.env.pos.validated_orders_name_server_id_map[order.name];
@@ -119,7 +119,7 @@ export class TipScreen extends Component {
for (let i = 0; i < receipts.length; i++) {
const data = receipts[i];
var receipt = renderToString("TipReceipt", {
var receipt = renderToElement("TipReceipt", {
receipt: this.currentOrder.getOrderReceiptEnv().receipt,
data: data,
total: this.env.pos.format_currency(this.totalAmount),
@@ -134,7 +134,7 @@ export class TipScreen extends Component {
}
async _printIoT(receipt) {
const printResult = await this.hardwareProxy.printer.print_receipt(receipt);
const printResult = await this.hardwareProxy.printer.printReceipt(receipt);
if (!printResult.successful) {
await this.popup.add(ErrorPopup, {
title: printResult.message.title,
+9 -17
View File
@@ -1,14 +1,12 @@
/** @odoo-module */
import { PosGlobalState, Order, Orderline, Payment } from "@point_of_sale/js/models";
import { uuidv4, batched } from "@point_of_sale/js/utils";
import core from "web.core";
import { Printer } from "@point_of_sale/js/printers";
import { uuidv4, batched, deduceUrl } from "@point_of_sale/js/utils";
import { HWPrinter } from "@point_of_sale/app/printer/hw_printer";
import { patch } from "@web/core/utils/patch";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
const QWeb = core.qweb;
const _t = core._t;
import { _t } from "@web/core/l10n/translation";
import { renderToElement } from "@web/core/utils/render";
patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
setup() {
@@ -233,21 +231,15 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
return tableOrders.reduce((count, order) => count + order.getCustomerCount(), 0);
},
create_printer(config) {
var url = config.proxy_ip || "";
if (url.indexOf("//") < 0) {
url = window.location.protocol + "//" + url;
}
if (url.indexOf(":", url.indexOf("//") + 2) < 0 && window.location.protocol !== "https:") {
url = url + ":8069";
}
return new Printer(url, this);
const url = deduceUrl(config.proxy_ip || "");
return new HWPrinter({ rpc: this.env.services.rpc, url });
},
isOpenOrderShareable() {
return this._super(...arguments) || this.config.is_table_management;
},
toggleEditMode() {
this.isEditMode = !this.isEditMode;
}
},
});
// New orders are now associated with the current table, if any.
@@ -508,8 +500,8 @@ patch(Order.prototype, "pos_restaurant.Order", {
minutes,
},
};
const receipt = QWeb.render("OrderChangeReceipt", { changes: printingChanges });
const result = await printer.print_receipt(receipt);
const receipt = renderToElement("OrderChangeReceipt", { changes: printingChanges });
const result = await printer.printReceipt(receipt);
if (!result.successful) {
isPrintSuccessful = false;
}
@@ -1,8 +1,9 @@
/** @odoo-module */
import { PaymentInterface } from "@point_of_sale/js/payment";
import { patch } from "@web/core/utils/patch";
PaymentInterface.include({
patch(PaymentInterface.prototype, "pos_restaurant.PaymentInterface", {
/**
* Return true if the amount that was authorized can be modified,
* false otherwise
@@ -18,5 +19,5 @@ PaymentInterface.include({
* canBeAdjusted returns True
* @param {string} cid - The id of the paymentline
*/
send_payment_adjust: function (cid) {},
send_payment_adjust(cid) {},
});
@@ -42,7 +42,7 @@
<br/>
<div class="pos-payment-terminal-receipt">
<t t-out="data"/>
<pre t-esc="data" />
</div>
<br/>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="OrderChangeReceipt">
<t t-name="OrderChangeReceipt" owl="">
<div class="pos-receipt">
<div class="pos-receipt-order-data"><t t-esc="changes.name" /></div>
<t t-if="changes.floor_name || changes.table_name">
@@ -20,7 +20,7 @@
</div>
<br />
<br />
<t t-foreach="changes.cancelled" t-as="change">
<t t-foreach="changes.cancelled" t-as="change" t-key="change_index">
<div class="multiprint-flex">
<span class="product-quantity" t-esc="change.quantity"/>
<span class="product-name" t-esc="change.name"/>
@@ -45,7 +45,7 @@
</div>
<br />
<br />
<t t-foreach="changes.new" t-as="change">
<t t-foreach="changes.new" t-as="change" t-key="change_index">
<div class="multiprint-flex">
<span class="product-quantity" t-esc="change.quantity"/>
<span class="product-name" t-esc="change.name"/>
@@ -1,9 +1,10 @@
/** @odoo-module */
import { PaymentAdyen } from "@pos_adyen/js/payment_adyen";
import { patch } from "@web/core/utils/patch";
PaymentAdyen.include({
_adyen_pay_data: function () {
patch(PaymentAdyen.prototype, "pos_restaurant_adyen.PaymentAdyen", {
_adyen_pay_data() {
var data = this._super(...arguments);
if (data.SaleToPOIRequest.PaymentRequest.SaleData.SaleToAcquirerData) {
@@ -17,7 +18,7 @@ PaymentAdyen.include({
return data;
},
send_payment_adjust: function (cid) {
send_payment_adjust(cid) {
var order = this.pos.get_order();
var line = order.get_paymentline(cid);
var data = {
@@ -35,7 +36,7 @@ PaymentAdyen.include({
return this._call_adyen(data, "adjust");
},
canBeAdjusted: function (cid) {
canBeAdjusted(cid) {
var order = this.pos.get_order();
var line = order.get_paymentline(cid);
return ["mc", "visa", "amex", "discover"].includes(line.card_type);
@@ -1,8 +1,9 @@
/** @odoo-module */
import { PaymentStripe } from "@pos_stripe/js/payment_stripe";
import { patch } from "@web/core/utils/patch";
PaymentStripe.include({
patch(PaymentStripe.prototype, "pos_restaurant_stripe.PaymentStripe", {
captureAfterPayment: async function (processPayment, line) {
// Don't capture if the customer can tip, in that case we
// will capture later.
@@ -1,7 +1,7 @@
/** @odoo-module */
import { sprintf } from "web.utils";
import { parse } from "web.field_utils";
import { sprintf } from "@web/core/utils/strings";
import { parseFloat } from "@web/views/fields/parsers";
import { useBus, useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { ControlButtonsMixin } from "@point_of_sale/js/ControlButtonsMixin";
@@ -256,7 +256,7 @@ export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentTo
startingValue: 0,
});
if (confirmed) {
down_payment = (down_payment * parse.float(payload)) / 100;
down_payment = (down_payment * parseFloat(payload)) / 100;
}
if (down_payment > sale_order.amount_unpaid) {
+1 -1
View File
@@ -1,6 +1,6 @@
/** @odoo-module */
import { register_payment_method } from "@point_of_sale/js/models";
import PaymentSix from "@pos_six/js/payment_six";
import { PaymentSix } from "@pos_six/js/payment_six";
register_payment_method("six", PaymentSix);
+26 -28
View File
@@ -1,13 +1,11 @@
/** @odoo-module */
/* global timapi */
import core from "web.core";
import { _t } from "@web/core/l10n/translation";
import { PaymentInterface } from "@point_of_sale/js/payment";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { escape } from "@web/core/utils/strings";
var _t = core._t;
window.onTimApiReady = function () {};
window.onTimApiPublishLogRecord = function (record) {
// Log only warning or errors
@@ -16,7 +14,7 @@ window.onTimApiPublishLogRecord = function (record) {
}
};
export const PaymentSix = PaymentInterface.extend({
export class PaymentSix extends PaymentInterface {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -24,8 +22,8 @@ export const PaymentSix = PaymentInterface.extend({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
setup() {
super.setup(...arguments);
this.enable_reversals();
var terminal_ip = this.payment_method.six_terminal_ip;
@@ -77,44 +75,44 @@ export const PaymentSix = PaymentInterface.extend({
options.push(option);
});
this.terminal.setPrintOptions(options);
},
}
/**
* @override
*/
send_payment_cancel: function () {
this._super.apply(this, arguments);
send_payment_cancel() {
super.send_payment_cancel(...arguments);
this.terminal.cancel();
return Promise.resolve();
},
}
/**
* @override
*/
send_payment_request: function () {
this._super.apply(this, arguments);
send_payment_request() {
super.send_payment_request(...arguments);
this.pos.get_order().selected_paymentline.set_payment_status("waitingCard");
return this._sendTransaction(timapi.constants.TransactionType.purchase);
},
}
/**
* @override
*/
send_payment_reversal: function () {
this._super.apply(this, arguments);
send_payment_reversal() {
super.send_payment_reversal(...arguments);
this.pos.get_order().selected_paymentline.set_payment_status("reversing");
return this._sendTransaction(timapi.constants.TransactionType.reversal);
},
}
send_balance: function () {
send_balance() {
this.terminal.balanceAsync();
},
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_onTransactionComplete: function (event, data) {
_onTransactionComplete(event, data) {
timapi.DefaultTerminalListener.prototype.transactionCompleted(event, data);
if (event.exception) {
@@ -138,9 +136,9 @@ export const PaymentSix = PaymentInterface.extend({
this.transactionResolve(true);
}
},
}
_onBalanceComplete: function (event, data) {
_onBalanceComplete(event, data) {
if (event.exception) {
this.pos.env.services.popup.add(ErrorPopup, {
title: _t("Balance Failed"),
@@ -149,15 +147,15 @@ export const PaymentSix = PaymentInterface.extend({
} else {
this._printReceipts(data.printData.receipts);
}
},
}
_printReceipts: function (receipts) {
_printReceipts(receipts) {
_.forEach(receipts, (receipt) => {
if (
receipt.recipient === timapi.constants.Recipient.merchant &&
this.pos.hardwareProxy.printer
) {
this.pos.hardwareProxy.printer.print_receipt(
this.pos.hardwareProxy.printer.printReceipt(
"<div class='pos-receipt'><div class='pos-payment-terminal-receipt'>" +
escape(receipt.value).replace(/\n/g, "<br />") +
"</div></div>"
@@ -166,9 +164,9 @@ export const PaymentSix = PaymentInterface.extend({
this.pos.get_order().selected_paymentline.set_receipt_info(receipt.value);
}
});
},
}
_sendTransaction: function (transactionType) {
_sendTransaction(transactionType) {
var amount = new timapi.Amount(
Math.round(
this.pos.get_order().selected_paymentline.amount / this.pos.currency.rounding
@@ -181,5 +179,5 @@ export const PaymentSix = PaymentInterface.extend({
this.transactionResolve = resolve;
this.terminal.transactionAsync(transactionType, amount);
});
},
});
}
}
@@ -1,48 +1,41 @@
/** @odoo-module */
/* global StripeTerminal */
import core from "web.core";
import rpc from "web.rpc";
import { _t } from "@web/core/l10n/translation";
import { PaymentInterface } from "@point_of_sale/js/payment";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { sprintf } from "@web/core/utils/strings";
const _t = core._t;
export const PaymentStripe = PaymentInterface.extend({
init: function (pos, payment_method) {
this._super(...arguments);
export class PaymentStripe extends PaymentInterface {
setup() {
super.setup(...arguments);
this.createStripeTerminal();
},
}
stripeUnexpectedDisconnect: function () {
stripeUnexpectedDisconnect() {
// Include a way to attempt to reconnect to a reader ?
this._showError(_t("Reader disconnected"));
},
}
stripeFetchConnectionToken: async function () {
async stripeFetchConnectionToken() {
// Do not cache or hardcode the ConnectionToken.
try {
const data = await rpc.query(
{
model: "pos.payment.method",
method: "stripe_connection_token",
},
{
silent: true,
}
const data = await this.pos.env.services.orm.silent.call(
"pos.payment.method",
"stripe_connection_token",
[]
);
if (data.error) {
throw data.error;
}
return data.secret;
} catch (error) {
this._showError(error.message.message, 'Fetch Token');
this._showError(error.message.message, "Fetch Token");
this.terminal = false;
};
},
}
}
discoverReaders: async function () {
async discoverReaders() {
const discoverResult = await this.terminal.discoverReaders({});
if (discoverResult.error) {
this._showError(sprintf(_t("Failed to discover: %s"), discoverResult.error));
@@ -53,21 +46,21 @@ export const PaymentStripe = PaymentInterface.extend({
// for the Stripe SDK
this.pos.discoveredReaders = JSON.stringify(discoverResult.discoveredReaders);
}
},
}
checkReader: async function () {
async checkReader() {
try {
if ( !this.terminal ) {
let createStripeTerminal = this.createStripeTerminal();
if ( !createStripeTerminal ) {
throw _t('Failed to load resource: net::ERR_INTERNET_DISCONNECTED.');
if (!this.terminal) {
const createStripeTerminal = this.createStripeTerminal();
if (!createStripeTerminal) {
throw _t("Failed to load resource: net::ERR_INTERNET_DISCONNECTED.");
}
}
} catch (error) {
this._showError(error);
return false;
}
let line = this.pos.get_order().selected_paymentline;
const line = this.pos.get_order().selected_paymentline;
// Because the reader can only connect to one instance of the SDK at a time.
// We need the disconnect this reader if we want to use another one
if (
@@ -87,15 +80,17 @@ export const PaymentStripe = PaymentInterface.extend({
} else {
return true;
}
},
}
connectReader: async function () {
async connectReader() {
const line = this.pos.get_order().selected_paymentline;
const discoveredReaders = JSON.parse(this.pos.discoveredReaders);
for (const selectedReader of discoveredReaders) {
if (selectedReader.serial_number == this.payment_method.stripe_serial_number) {
try {
let connectResult = await this.terminal.connectReader(selectedReader, {fail_if_in_use: true});
const connectResult = await this.terminal.connectReader(selectedReader, {
fail_if_in_use: true,
});
if (connectResult.error) {
throw connectResult;
}
@@ -107,20 +102,20 @@ export const PaymentStripe = PaymentInterface.extend({
} else {
this._showError(error);
}
line.set_payment_status('retry');
line.set_payment_status("retry");
return false;
}
}
}
this._showError(
_.str.sprintf(
_t('Stripe readers %s not listed in your account'),
_t("Stripe readers %s not listed in your account"),
this.payment_method.stripe_serial_number
)
);
},
}
collectPayment: async function (amount) {
async collectPayment(amount) {
const line = this.pos.get_order().selected_paymentline;
const clientSecret = await this.fetchPaymentIntentClientSecret(line.payment_method, amount);
if (!clientSecret) {
@@ -150,9 +145,9 @@ export const PaymentStripe = PaymentInterface.extend({
return true;
}
}
},
}
createStripeTerminal: function () {
createStripeTerminal() {
try {
this.terminal = StripeTerminal.create({
onFetchConnectionToken: this.stripeFetchConnectionToken.bind(this),
@@ -161,72 +156,62 @@ export const PaymentStripe = PaymentInterface.extend({
this.discoverReaders();
return true;
} catch (error) {
this._showError(_t('Failed to load resource: net::ERR_INTERNET_DISCONNECTED.'), error);
this._showError(_t("Failed to load resource: net::ERR_INTERNET_DISCONNECTED."), error);
this.terminal = false;
return false;
}
},
}
captureAfterPayment: async function (processPayment, line) {
async captureAfterPayment(processPayment, line) {
const capturePayment = await this.capturePayment(processPayment.paymentIntent.id);
if (capturePayment.charges) {
line.card_type =
capturePayment.charges.data[0].payment_method_details.card_present.brand;
}
line.transaction_id = capturePayment.id;
},
}
capturePayment: async function (paymentIntentId) {
async capturePayment(paymentIntentId) {
try {
const data = await rpc.query(
{
model: "pos.payment.method",
method: "stripe_capture_payment",
args: [paymentIntentId],
},
{
silent: true,
}
const data = await this.pos.env.services.orm.silent.call(
"pos.payment.method",
"stripe_capture_payment",
[paymentIntentId]
);
if (data.error) {
throw data.error;
}
return data;
} catch (error) {
this._showError(error.message.message, 'Capture Payment');
this._showError(error.message.message, "Capture Payment");
return false;
}
},
}
fetchPaymentIntentClientSecret: async function (payment_method, amount) {
async fetchPaymentIntentClientSecret(payment_method, amount) {
try {
const data = await rpc.query(
{
model: "pos.payment.method",
method: "stripe_payment_intent",
args: [[payment_method.id], amount],
},
{
silent: true,
}
const data = await this.pos.env.services.orm.silent.call(
"pos.payment.method",
"stripe_payment_intent",
[[payment_method.id], amount]
);
if (data.error) {
throw data.error;
}
return data.client_secret;
} catch (error) {
this._showError(error.message.message, 'Fetch Secret');
this._showError(error.message.message, "Fetch Secret");
return false;
}
},
}
send_payment_request: async function (cid) {
async send_payment_request(cid) {
/**
* Override
*/
await this._super.apply(this, arguments);
let line = this.pos.get_order().selected_paymentline;
line.set_payment_status('waiting');
await super.send_payment_request(...arguments);
const line = this.pos.get_order().selected_paymentline;
line.set_payment_status("waiting");
try {
if (await this.checkReader()) {
return await this.collectPayment(line.amount);
@@ -235,26 +220,26 @@ export const PaymentStripe = PaymentInterface.extend({
this._showError(error);
return false;
}
},
}
send_payment_cancel: async function (order, cid) {
async send_payment_cancel(order, cid) {
/**
* Override
*/
this._super.apply(this, arguments);
super.send_payment_cancel(...arguments);
const line = this.pos.get_order().selected_paymentline;
const stripeCancel = await this.stripeCancel();
if (stripeCancel) {
line.set_payment_status("retry");
return true;
}
},
}
stripeCancel: async function () {
async stripeCancel() {
if (!this.terminal) {
return true;
} else if (this.terminal.getConnectionStatus() != 'connected') {
this._showError(_t('Payment canceled because not reader connected'));
} else if (this.terminal.getConnectionStatus() != "connected") {
this._showError(_t("Payment canceled because not reader connected"));
return true;
} else {
const cancelCollectPaymentMethod = await this.terminal.cancelCollectPaymentMethod();
@@ -266,11 +251,11 @@ export const PaymentStripe = PaymentInterface.extend({
}
return true;
}
},
}
// private methods
_showError: function (msg, title) {
_showError(msg, title) {
if (!title) {
title = _t("Stripe Error");
}
@@ -278,5 +263,5 @@ export const PaymentStripe = PaymentInterface.extend({
title: title,
body: msg,
});
},
});
}
}
@@ -29,3 +29,54 @@ export function range(start, stop, step = 1) {
}
return array;
}
/**
* performs a half up rounding with arbitrary precision, correcting for float loss of precision
* See the corresponding float_round() in server/tools/float_utils.py for more info
*
* @param {number} value the value to be rounded
* @param {number} precision a precision parameter. eg: 0.01 rounds to two digits.
*/
export function roundPrecision(value, precision) {
if (!value) {
return 0;
} else if (!precision || precision < 0) {
precision = 1;
}
let normalizedValue = value / precision;
const epsilonMagnitude = Math.log2(Math.abs(normalizedValue));
const epsilon = Math.pow(2, epsilonMagnitude - 52);
normalizedValue += normalizedValue >= 0 ? epsilon : -epsilon;
/**
* Javascript performs strictly the round half up method, which is asymmetric. However, in
* Python, the method is symmetric. For example:
* - In JS, Math.round(-0.5) is equal to -0.
* - In Python, round(-0.5) is equal to -1.
* We want to keep the Python behavior for consistency.
*/
const sign = normalizedValue < 0 ? -1.0 : 1.0;
const roundedValue = sign * Math.round(Math.abs(normalizedValue));
return roundedValue * precision;
}
export function roundDecimals(value, decimals) {
/**
* The following decimals introduce numerical errors:
* Math.pow(10, -4) = 0.00009999999999999999
* Math.pow(10, -5) = 0.000009999999999999999
*
* Such errors will propagate in roundPrecision and lead to inconsistencies between Python
* and JavaScript. To avoid this, we parse the scientific notation.
*/
return roundPrecision(value, parseFloat("1e" + -decimals));
}
/**
* @param {number} value
* @param {integer} decimals
* @returns {boolean}
*/
export function floatIsZero(value, decimals) {
return roundDecimals(value, decimals) === 0;
}
+9 -1
View File
@@ -2,6 +2,10 @@
import { blockDom, markup } from "@odoo/owl";
export function renderToElement(template, context = {}) {
return render(template, context).firstChild;
}
/**
* renders a template with an (optional) context and outputs it as a string
*
@@ -10,6 +14,10 @@ import { blockDom, markup } from "@odoo/owl";
* @returns string: the html of the template
*/
export function renderToString(template, context = {}) {
return render(template, context).innerHTML;
}
function render(template, context = {}) {
const app = renderToString.app;
if (!app) {
throw new Error("an app must be configured before using renderToString");
@@ -18,7 +26,7 @@ export function renderToString(template, context = {}) {
const bdom = templateFn(context, {});
const div = document.createElement("div");
blockDom.mount(bdom, div);
return div.innerHTML;
return div;
}
/**
@@ -0,0 +1,139 @@
/** @odoo-module **/
import { roundPrecision, roundDecimals, floatIsZero } from "@web/core/utils/numbers";
QUnit.module("utils", () => {
QUnit.module("numbers");
QUnit.test("roundPrecision", function (assert) {
assert.expect(26);
assert.strictEqual(String(roundPrecision(1.0, 1)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.1)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.01)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.001)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.0001)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.00001)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.000001)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.0000001)), "1");
assert.strictEqual(String(roundPrecision(1.0, 0.00000001)), "1");
assert.strictEqual(String(roundPrecision(0.5, 1)), "1");
assert.strictEqual(String(roundPrecision(-0.5, 1)), "-1");
assert.strictEqual(String(roundPrecision(2.6745, 0.001)), "2.6750000000000003");
assert.strictEqual(String(roundPrecision(-2.6745, 0.001)), "-2.6750000000000003");
assert.strictEqual(String(roundPrecision(2.6744, 0.001)), "2.674");
assert.strictEqual(String(roundPrecision(-2.6744, 0.001)), "-2.674");
assert.strictEqual(String(roundPrecision(0.0004, 0.001)), "0");
assert.strictEqual(String(roundPrecision(-0.0004, 0.001)), "0");
assert.strictEqual(String(roundPrecision(357.4555, 0.001)), "357.456");
assert.strictEqual(String(roundPrecision(-357.4555, 0.001)), "-357.456");
assert.strictEqual(String(roundPrecision(457.4554, 0.001)), "457.455");
assert.strictEqual(String(roundPrecision(-457.4554, 0.001)), "-457.455");
assert.strictEqual(String(roundPrecision(-457.4554, 0.05)), "-457.45000000000005");
assert.strictEqual(String(roundPrecision(457.444, 0.5)), "457.5");
assert.strictEqual(String(roundPrecision(457.3, 5)), "455");
assert.strictEqual(String(roundPrecision(457.5, 5)), "460");
assert.strictEqual(String(roundPrecision(457.1, 3)), "456");
});
QUnit.test("roundDecimals", function (assert) {
assert.expect(21);
assert.strictEqual(String(roundDecimals(1.0, 0)), "1");
assert.strictEqual(String(roundDecimals(1.0, 1)), "1");
assert.strictEqual(String(roundDecimals(1.0, 2)), "1");
assert.strictEqual(String(roundDecimals(1.0, 3)), "1");
assert.strictEqual(String(roundDecimals(1.0, 4)), "1");
assert.strictEqual(String(roundDecimals(1.0, 5)), "1");
assert.strictEqual(String(roundDecimals(1.0, 6)), "1");
assert.strictEqual(String(roundDecimals(1.0, 7)), "1");
assert.strictEqual(String(roundDecimals(1.0, 8)), "1");
assert.strictEqual(String(roundDecimals(0.5, 0)), "1");
assert.strictEqual(String(roundDecimals(-0.5, 0)), "-1");
assert.strictEqual(String(roundDecimals(2.6745, 3)), "2.6750000000000003");
assert.strictEqual(String(roundDecimals(-2.6745, 3)), "-2.6750000000000003");
assert.strictEqual(String(roundDecimals(2.6744, 3)), "2.674");
assert.strictEqual(String(roundDecimals(-2.6744, 3)), "-2.674");
assert.strictEqual(String(roundDecimals(0.0004, 3)), "0");
assert.strictEqual(String(roundDecimals(-0.0004, 3)), "0");
assert.strictEqual(String(roundDecimals(357.4555, 3)), "357.456");
assert.strictEqual(String(roundDecimals(-357.4555, 3)), "-357.456");
assert.strictEqual(String(roundDecimals(457.4554, 3)), "457.455");
assert.strictEqual(String(roundDecimals(-457.4554, 3)), "-457.455");
});
QUnit.test("floatIsZero", function (assert) {
assert.strictEqual(floatIsZero(1, 0), false);
assert.strictEqual(floatIsZero(0.9999, 0), false);
assert.strictEqual(floatIsZero(0.50001, 0), false);
assert.strictEqual(floatIsZero(0.5, 0), false);
assert.strictEqual(floatIsZero(0.49999, 0), true);
assert.strictEqual(floatIsZero(0, 0), true);
assert.strictEqual(floatIsZero(0.49999, 0), true);
assert.strictEqual(floatIsZero(-0.50001, 0), false);
assert.strictEqual(floatIsZero(-0.5, 0), false);
assert.strictEqual(floatIsZero(-0.9999, 0), false);
assert.strictEqual(floatIsZero(-1, 0), false);
assert.strictEqual(floatIsZero(0.1, 1), false);
assert.strictEqual(floatIsZero(0.099999, 1), false);
assert.strictEqual(floatIsZero(0.050001, 1), false);
assert.strictEqual(floatIsZero(0.05, 1), false);
assert.strictEqual(floatIsZero(0.049999, 1), true);
assert.strictEqual(floatIsZero(0, 1), true);
assert.strictEqual(floatIsZero(-0.049999, 1), true);
assert.strictEqual(floatIsZero(-0.05, 1), false);
assert.strictEqual(floatIsZero(-0.050001, 1), false);
assert.strictEqual(floatIsZero(-0.099999, 1), false);
assert.strictEqual(floatIsZero(-0.1, 1), false);
assert.strictEqual(floatIsZero(0.01, 2), false);
assert.strictEqual(floatIsZero(0.0099999, 2), false);
assert.strictEqual(floatIsZero(0.005, 2), false);
assert.strictEqual(floatIsZero(0.0050001, 2), false);
assert.strictEqual(floatIsZero(0.0049999, 2), true);
assert.strictEqual(floatIsZero(0, 2), true);
assert.strictEqual(floatIsZero(-0.0049999, 2), true);
assert.strictEqual(floatIsZero(-0.0050001, 2), false);
assert.strictEqual(floatIsZero(-0.005, 2), false);
assert.strictEqual(floatIsZero(-0.0099999, 2), false);
assert.strictEqual(floatIsZero(-0.01, 2), false);
// 4 and 5 decimal places are mentioned as special cases in `roundDecimals` method.
assert.strictEqual(floatIsZero(0.0001, 4), false);
assert.strictEqual(floatIsZero(0.000099999, 4), false);
assert.strictEqual(floatIsZero(0.00005, 4), false);
assert.strictEqual(floatIsZero(0.000050001, 4), false);
assert.strictEqual(floatIsZero(0.000049999, 4), true);
assert.strictEqual(floatIsZero(0, 4), true);
assert.strictEqual(floatIsZero(-0.000049999, 4), true);
assert.strictEqual(floatIsZero(-0.000050001, 4), false);
assert.strictEqual(floatIsZero(-0.00005, 4), false);
assert.strictEqual(floatIsZero(-0.000099999, 4), false);
assert.strictEqual(floatIsZero(-0.0001, 4), false);
assert.strictEqual(floatIsZero(0.00001, 5), false);
assert.strictEqual(floatIsZero(0.0000099999, 5), false);
assert.strictEqual(floatIsZero(0.000005, 5), false);
assert.strictEqual(floatIsZero(0.0000050001, 5), false);
assert.strictEqual(floatIsZero(0.0000049999, 5), true);
assert.strictEqual(floatIsZero(0, 5), true);
assert.strictEqual(floatIsZero(-0.0000049999, 5), true);
assert.strictEqual(floatIsZero(-0.0000050001, 5), false);
assert.strictEqual(floatIsZero(-0.000005, 5), false);
assert.strictEqual(floatIsZero(-0.0000099999, 5), false);
assert.strictEqual(floatIsZero(-0.00001, 5), false);
assert.strictEqual(floatIsZero(0.0000001, 7), false);
assert.strictEqual(floatIsZero(0.000000099999, 7), false);
assert.strictEqual(floatIsZero(0.00000005, 7), false);
assert.strictEqual(floatIsZero(0.000000050001, 7), false);
assert.strictEqual(floatIsZero(0.000000049999, 7), true);
assert.strictEqual(floatIsZero(0, 7), true);
assert.strictEqual(floatIsZero(-0.000000049999, 7), true);
assert.strictEqual(floatIsZero(-0.000000050001, 7), false);
assert.strictEqual(floatIsZero(-0.00000005, 7), false);
assert.strictEqual(floatIsZero(-0.000000099999, 7), false);
assert.strictEqual(floatIsZero(-0.0000001, 7), false);
});
});