[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,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>