/** @odoo-module **/ export default class LazyBarcodeCache { constructor(cacheData, params) { this.rpc = params.rpc; this.dbIdCache = {}; // Cache by model + id this.dbBarcodeCache = {}; // Cache by model + barcode this.missingBarcode = new Set(); // Used as a cache by `_getMissingRecord` this.barcodeFieldByModel = { 'stock.location': 'barcode', 'product.product': 'barcode', 'product.packaging': 'barcode', 'stock.package.type': 'barcode', 'stock.picking': 'name', 'stock.quant.package': 'name', 'stock.lot': 'name', // Also ref, should take in account multiple fields ? }; this.gs1LengthsByModel = { 'product.product': 14, 'product.packaging': 14, 'stock.location': 13, 'stock.quant.package': 18, }; // If there is only one active barcode nomenclature, set the cache to be compliant with it. if (cacheData['barcode.nomenclature'].length === 1) { this.nomenclature = cacheData['barcode.nomenclature'][0]; } this.setCache(cacheData); } /** * Adds records to the barcode application's cache. * * @param {Object} cacheData each key is a model's name and contains an array of records. */ setCache(cacheData) { for (const model in cacheData) { const records = cacheData[model]; // Adds the model's key in the cache's DB. if (!this.dbIdCache.hasOwnProperty(model)) { this.dbIdCache[model] = {}; } if (!this.dbBarcodeCache.hasOwnProperty(model)) { this.dbBarcodeCache[model] = {}; } // Adds the record in the cache. const barcodeField = this._getBarcodeField(model); for (const record of records) { this.dbIdCache[model][record.id] = record; if (barcodeField) { const barcode = record[barcodeField]; if (!this.dbBarcodeCache[model][barcode]) { this.dbBarcodeCache[model][barcode] = []; } if (!this.dbBarcodeCache[model][barcode].includes(record.id)) { this.dbBarcodeCache[model][barcode].push(record.id); if (this.nomenclature && this.nomenclature.is_gs1_nomenclature && this.gs1LengthsByModel[model]) { this._setBarcodeInCacheForGS1(barcode, model, record); } } } } } } /** * Get record from the cache, throw a error if we don't find in the cache * (the server should have return this information). * * @param {int} id id of the record * @param {string} model model_name of the record * @param {boolean} [copy=true] if true, returns a deep copy (to avoid to write the cache) * @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode) */ getRecord(model, id) { if (!this.dbIdCache.hasOwnProperty(model)) { throw new Error(`Model ${model} doesn't exist in the cache`); } if (!this.dbIdCache[model].hasOwnProperty(id)) { throw new Error(`Record ${model} with id=${id} doesn't exist in the cache, it should return by the server`); } const record = this.dbIdCache[model][id]; return JSON.parse(JSON.stringify(record)); } /** * @param {string} barcode barcode to match with a record * @param {string} [model] model name of the record to match (if empty search on all models) * @param {boolean} [onlyInCache] search only in the cache * @param {Object} [filters] * @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode) */ async getRecordByBarcode(barcode, model = false, onlyInCache = false, filters = {}) { if (model) { if (!this.dbBarcodeCache.hasOwnProperty(model)) { throw new Error(`Model ${model} doesn't exist in the cache`); } if (!this.dbBarcodeCache[model].hasOwnProperty(barcode)) { if (onlyInCache) { return null; } await this._getMissingRecord(barcode, model); return await this.getRecordByBarcode(barcode, model, true); } const id = this.dbBarcodeCache[model][barcode][0]; return this.getRecord(model, id); } else { const result = new Map(); // Returns object {model: record} of possible record. const models = Object.keys(this.dbBarcodeCache); for (const model of models) { if (this.dbBarcodeCache[model].hasOwnProperty(barcode)) { const ids = this.dbBarcodeCache[model][barcode]; for (const id of ids) { const record = this.dbIdCache[model][id]; let pass = true; if (filters[model]) { const fields = Object.keys(filters[model]); for (const field of fields) { if (record[field] != filters[model][field]) { pass = false; break; } } } if (pass) { result.set(model, JSON.parse(JSON.stringify(record))); break; } } } } if (result.size < 1) { if (onlyInCache) { return result; } await this._getMissingRecord(barcode, model, filters); return await this.getRecordByBarcode(barcode, model, true, filters); } return result; } } _getBarcodeField(model) { if (!this.barcodeFieldByModel.hasOwnProperty(model)) { return null; } return this.barcodeFieldByModel[model]; } async _getMissingRecord(barcode, model, filters) { const missCache = this.missingBarcode; const params = { barcode, model_name: model }; // Check if we already try to fetch this missing record. if (missCache.has(barcode) || missCache.has(`${barcode}_${model}`)) { return false; } // Creates and passes a domain if some filters are provided. if (filters) { const domainsByModel = {}; for (const filter of Object.entries(filters)) { const modelName = filter[0]; const filtersByField = filter[1]; domainsByModel[modelName] = []; for (const filterByField of Object.entries(filtersByField)) { domainsByModel[modelName].push([filterByField[0], '=', filterByField[1]]); } } params.domains_by_model = domainsByModel; } const result = await this.rpc('/stock_barcode/get_specific_barcode_data', params); this.setCache(result); // Set the missing cache if no filters (the barcode's result can vary if there is filter) if (!filters) { const keyCache = (model && `${barcode}_${model}`) || barcode; missCache.add(keyCache); } } /** * Sets in the cache an entry for the given record with its formatted barcode as key. * The barcode will be formatted (if needed) at the length corresponding to its data part in a * GS1 barcode (e.g.: 14 digits for a product's barcode) by padding with 0 the original barcode. * That makes it easier to find when a GS1 barcode is scanned. * If the formatted barcode is similar to an another barcode for the same model, it will show a * warning in the console (as a clue to find where issue could come from, not to alert the user) * * @param {string} barcode * @param {string} model * @param {Object} record */ _setBarcodeInCacheForGS1(barcode, model, record) { const length = this.gs1LengthsByModel[model]; if (!barcode || barcode.length >= length || isNaN(Number(barcode))) { // Barcode already has the good length, or is too long or isn't // fully numerical (and so, it doesn't make sense to adapt it). return; } const paddedBarcode = barcode.padStart(length, '0'); // Avoids to override or mix records if there is already a key for this // barcode (which means there is a conflict somewhere). if (!this.dbBarcodeCache[model][paddedBarcode]) { this.dbBarcodeCache[model][paddedBarcode] = [record.id]; } else if (!this.dbBarcodeCache[model][paddedBarcode].includes(record.id)) { const previousRecordId = this.dbBarcodeCache[model][paddedBarcode][0]; const previousRecord = this.getRecord(model, previousRecordId); console.log( `Conflict for barcode %c${paddedBarcode}%c:`, 'font-weight: bold', '', `it could refer for both ${record.display_name} and ${previousRecord.display_name}.`, `\nThe last one will be used but consider to edit those products barcode to avoid error due to ambiguities.` ); } } }