odoo18/addons/point_of_sale/static/src/utils.js

258 lines
8.8 KiB
JavaScript

import { parseDateTime, deserializeDateTime } from "@web/core/l10n/dates";
import { roundDecimals, floatIsZero } from "@web/core/utils/numbers";
/*
* comes from o_spreadsheet.js
* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
* */
export function uuidv4() {
// mainly for jest and other browsers that do not have the crypto functionality
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
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;
}
export function constructFullProductName(line) {
let attributeString = "";
if (line.attribute_value_ids && line.attribute_value_ids.length > 0) {
for (const value of line.attribute_value_ids) {
if (value.is_custom) {
const customValue = line.custom_attribute_value_ids.find(
(cus) =>
cus.custom_product_template_attribute_value_id?.id == parseInt(value.id)
);
if (customValue) {
attributeString += `${value.attribute_id.name}: ${value.name}: ${customValue.custom_value}, `;
}
} else {
attributeString += `${value.name}, `;
}
}
attributeString = attributeString.slice(0, -2);
attributeString = ` (${attributeString})`;
}
return `${line?.product_id?.display_name}${attributeString}`;
}
/**
* Returns a random 5 digits alphanumeric code
* @returns {string}
*/
export function random5Chars() {
let code = "";
while (code.length != 5) {
code = Math.random().toString(36).slice(2, 7);
}
return code;
}
export function qrCodeSrc(url, { size = 200 } = {}) {
return `/report/barcode/QR/${encodeURIComponent(url)}?width=${size}&height=${size}`;
}
/**
* @template T
* @param {T[]} entries - The array of objects to search through.
* @param {Function} [criterion=(x) => x] - A function that returns a number for each entry. The entry with the highest value of this function will be returned. If not provided, defaults to an identity function that returns the entry itself.
* @param {boolean} [inverted=false] - If true, the entry with the lowest value of the criterion function will be returned instead.
* @returns {T} The entry with the highest or lowest value of the criterion function, depending on the value of `inverted`.
*/
export function getMax(entries, { criterion = (x) => x, inverted = false } = {}) {
return entries.reduce((prev, current) => {
const res = criterion(prev) > criterion(current);
return (inverted ? !res : res) ? prev : current;
});
}
export function getMin(entries, options) {
return getMax(entries, { ...options, inverted: true });
}
export function getOnNotified(bus, channel) {
if (!channel || typeof channel !== "string") {
return () => false;
}
bus.addChannel(channel);
return (notif, callback) => bus.subscribe(`${channel}-${notif}`, callback);
}
/**
* Loading image is converted to a Promise to allow await when
* loading an image. It resolves to the loaded image if successful,
* else, resolves to false.
*
* [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await)
*/
export function loadImage(url, options = {}) {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => {
if (options.onError) {
options.onError();
}
reject(new Error(`Failed to load image at ${url}`));
});
img.src = url;
});
}
/**
* Load all images in the given element.
* @param {HTMLElement} el
*/
export function loadAllImages(el) {
if (!el) {
return Promise.resolve();
}
const images = el.querySelectorAll("img");
return Promise.all(Array.from(images).map((img) => loadImage(img.src)));
}
export function parseUTCString(utcStr) {
return parseDateTime(utcStr, { format: "yyyy-MM-dd HH:mm:ss", tz: "utc" });
}
export function floatCompare(a, b, { decimals } = {}) {
if (decimals === undefined) {
throw new Error("decimals must be provided");
}
a = roundDecimals(a, decimals);
b = roundDecimals(b, decimals);
const delta = a - b;
if (floatIsZero(delta, decimals)) {
return 0;
}
return delta < 0 ? -1 : 1;
}
export function gte(a, b, { decimals } = {}) {
return floatCompare(a, b, { decimals }) >= 0;
}
export function gt(a, b, { decimals } = {}) {
return floatCompare(a, b, { decimals }) > 0;
}
export function lte(a, b, { decimals } = {}) {
return floatCompare(a, b, { decimals }) <= 0;
}
export function lt(a, b, { decimals } = {}) {
return floatCompare(a, b, { decimals }) < 0;
}
export function computeProductPricelistCache(service, data = []) {
// This function is called via the addEventListener callback initiated in the
// processServerData function when new products or pricelists are loaded into the PoS.
// It caches the heavy pricelist calculation when there are many products and pricelists.
const date = luxon.DateTime.now();
let pricelistItems = service.models["product.pricelist.item"].getAll();
let products = service.models["product.product"].getAll();
if (data.length > 0) {
if (data[0].model.modelName === "product.product") {
products = data;
}
if (data[0].model.modelName === "product.pricelist.item") {
pricelistItems = data;
// it needs only to compute for the products that are affected by the pricelist items
const productTmplIds = new Set(data.map((item) => item.raw.product_tmpl_id));
const productIds = new Set(data.map((item) => item.raw.product_id));
products = products.filter(
(product) =>
productTmplIds.has(product.raw.product_tmpl_id) || productIds.has(product.id)
);
}
}
const pushItem = (targetArray, key, item) => {
if (!targetArray[key]) {
targetArray[key] = [];
}
targetArray[key].push(item);
};
const pricelistRules = {};
for (const item of pricelistItems) {
if (
(item.date_start && deserializeDateTime(item.date_start) > date) ||
(item.date_end && deserializeDateTime(item.date_end) < date)
) {
continue;
}
const pricelistId = item.pricelist_id.id;
if (!pricelistRules[pricelistId]) {
pricelistRules[pricelistId] = {
productItems: {},
productTmlpItems: {},
categoryItems: {},
globalItems: [],
};
}
const productId = item.raw.product_id;
if (productId) {
pushItem(pricelistRules[pricelistId].productItems, productId, item);
continue;
}
const productTmplId = item.raw.product_tmpl_id;
if (productTmplId) {
pushItem(pricelistRules[pricelistId].productTmlpItems, productTmplId, item);
continue;
}
const categId = item.raw.categ_id;
if (categId) {
pushItem(pricelistRules[pricelistId].categoryItems, categId, item);
} else {
pricelistRules[pricelistId].globalItems.push(item);
}
}
for (const product of products) {
const applicableRules = product.getApplicablePricelistRules(pricelistRules);
for (const pricelistId in applicableRules) {
if (product.cachedPricelistRules[pricelistId]) {
const existingRuleIds = product.cachedPricelistRules[pricelistId].map(
(rule) => rule.id
);
const newRules = applicableRules[pricelistId].filter(
(rule) => !existingRuleIds.includes(rule.id)
);
product.cachedPricelistRules[pricelistId] = [
...newRules,
...product.cachedPricelistRules[pricelistId],
];
} else {
product.cachedPricelistRules[pricelistId] = applicableRules[pricelistId];
}
}
}
if (data.length > 0 && data[0].model.modelName === "product.product") {
service._loadMissingPricelistItems(products);
}
}