Files
jikimo_sf/stock_barcode/static/src/models/barcode_quant_model.js
qihao.gong@jikimo.com 3c89404543 质量模块和库存扫码
2023-07-24 11:42:15 +08:00

598 lines
23 KiB
JavaScript

/** @odoo-module **/
import BarcodeModel from '@stock_barcode/models/barcode_model';
import {_t} from "web.core";
import { sprintf } from '@web/core/utils/strings';
export default class BarcodeQuantModel extends BarcodeModel {
constructor(params) {
super(...arguments);
this.lineModel = params.model;
this.validateMessage = _t("The inventory adjustment has been validated");
this.validateMethod = 'action_validate';
}
/**
* Validates only the quants of the current inventory page and don't close it.
*
* @returns {Promise}
*/
async apply() {
await this.save();
const linesToApply = this.pageLines.filter(line => line.inventory_quantity_set);
if (linesToApply.length === 0) {
const message = _t("There is nothing to apply in this page.");
return this.notification.add(message, { type: 'warning' });
}
const action = await this.orm.call('stock.quant', 'action_validate',
[linesToApply.map(quant => quant.id)]
);
const notifyAndGoAhead = res => {
if (res && res.special) { // Do nothing if come from a discarded wizard.
return this.trigger('refresh');
}
this.notification.add(_t("The inventory adjustment has been validated"), { type: 'success' });
this.trigger('history-back');
};
if (action && action.res_model) {
const options = { on_close: notifyAndGoAhead };
return this.trigger('do-action', { action, options });
}
notifyAndGoAhead();
}
get applyOn() {
return this.pageLines.filter(line => line.inventory_quantity_set).length;
}
get barcodeInfo() {
// Takes the parent line if the current line is part of a group.
let line = this._getParentLine(this.selectedLine) || this.selectedLine;
if (!line && this.lastScanned.packageId) {
const lines = this._moveEntirePackage() ? this.packageLines : this.pageLines;
line = lines.find(l => l.package_id && l.package_id.id === this.lastScanned.packageId);
}
if (line) { // Message depends of the selected line's state.
const { tracking } = line.product_id;
const trackingNumber = (line.lot_id && line.lot_id.name) || line.lot_name;
if (this._lineIsNotComplete(line)) {
if (tracking === 'none') {
this.messageType = 'scan_product';
} else {
this.messageType = tracking === 'lot' ? 'scan_lot' : 'scan_serial';
}
} else if (tracking !== 'none' && !trackingNumber) {
// Line's quantity is fulfilled but still waiting a tracking number.
this.messageType = tracking === 'lot' ? 'scan_lot' : 'scan_serial';
} else { // Line's quantity is fulfilled.
this.messageType = this.groups.group_stock_multi_locations && line.location_id.id === this.location.id ?
"scan_product_or_src" :
"scan_product";
}
} else { // Message depends if multilocation is enabled.
this.messageType = this.groups.group_stock_multi_locations && !this.lastScanned.sourceLocation ?
'scan_src' :
'scan_product';
}
const barcodeInformations = { class: this.messageType, warning: false, icon: 'barcode' };
switch (this.messageType) {
case 'scan_product':
barcodeInformations.message = this.groups.group_stock_multi_locations ?
sprintf(_t("Scan a product in %s or scan another location"), this.location.display_name) :
_t("Scan a product");
break;
case 'scan_src':
barcodeInformations.message = _t("Scan a location");
barcodeInformations.icon = 'sign-out';
break;
case 'scan_product_or_src':
barcodeInformations.message = sprintf(
_t("Scan more products in %s or scan another location"),
this.location.display_name);
break;
case 'scan_product_or_dest':
barcodeInformations.message = _t("Scan more products, or scan the destination location");
barcodeInformations.icon = 'sign-in';
break;
case 'scan_lot':
barcodeInformations.message = sprintf(
_t("Scan lot numbers for product %s to change their quantity"),
line.product_id.display_name
);
break;
case 'scan_serial':
barcodeInformations.message = sprintf(
_t("Scan serial numbers for product %s to change their quantity"),
line.product_id.display_name
);
break;
}
return barcodeInformations;
}
get displayByUnitButton () {
return true;
}
get displaySetButton() {
return true;
}
setData(data) {
this.userId = data.data.user_id;
super.setData(...arguments);
const companies = data.data.records['res.company'];
this.companyIds = companies.map(company => company.id);
this.lineFormViewId = data.data.line_view_id;
}
get displayApplyButton() {
return true;
}
getDisplayIncrementBtn(line) {
return line.product_id.tracking !== 'serial' && this.selectedLine &&
line.virtual_id === this.selectedLine.virtual_id;
}
getDisplayDecrementBtn(line) {
return this.getDisplayIncrementBtn(line);
}
getQtyDone(line) {
return line.inventory_quantity;
}
getQtyDemand(line) {
return line.quantity;
}
getActionRefresh(newId) {
const action = super.getActionRefresh(newId);
action.params.res_id = this.currentState.lines.map(l => l.id);
if (newId) {
action.params.res_id.push(newId);
}
return action;
}
get highlightValidateButton() {
return this.applyOn > 0 && this.applyOn === this.pageLines.length;
}
get incrementButtonsDisplayStyle() {
return "d-block my-3";
}
IsNotSet(line) {
return !line.inventory_quantity_set;
}
lineIsFaulty(line) {
return line.inventory_quantity_set && line.inventory_quantity !== line.quantity;
}
get printButtons() {
return [{
name: _t("Print Inventory"),
class: 'o_print_inventory',
action: 'stock.action_report_inventory',
}];
}
get recordIds() {
return this.currentState.lines.map(l => l.id);
}
/**
* Marks the line as set and set its inventory quantity if it was unset, or
* unset it if the line was already set.
*
* @param {Object} line
*/
setOnHandQuantity(line) {
if (line.product_id.tracking === 'serial') { // Special case for product tracked by SN.
const quantity = !(line.lot_name || line.lot_id) && line.quantity || 1;
if (line.inventory_quantity_set) {
line.inventory_quantity = line.inventory_quantity ? 0 : quantity;
line.inventory_quantity_set = line.inventory_quantity != quantity;
} else {
line.inventory_quantity = quantity;
line.inventory_quantity_set = true;
}
this._markLineAsDirty(line);
} else {
if (line.inventory_quantity_set) {
line.inventory_quantity = 0;
line.inventory_quantity_set = false;
this._markLineAsDirty(line);
} else {
const inventory_quantity = line.quantity - line.inventory_quantity;
this.updateLine(line, { inventory_quantity });
line.inventory_quantity_set = true;
}
}
this.trigger('update');
}
updateLineQty(virtualId, qty = 1) {
this.actionMutex.exec(() => {
const line = this.pageLines.find(l => l.virtual_id === virtualId);
this.updateLine(line, {inventory_quantity: qty});
this.trigger('update');
});
}
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
_getCommands() {
return Object.assign(super._getCommands(), {
'O-BTN.apply': this.apply.bind(this),
});
}
_getNewLineDefaultContext() {
return {
default_company_id: this.companyIds[0],
default_location_id: this._defaultLocation().id,
default_inventory_quantity: 1,
default_user_id: this.userId,
inventory_mode: true,
};
}
_createCommandVals(line) {
const values = {
dummy_id: line.virtual_id,
inventory_date: line.inventory_date,
inventory_quantity: line.inventory_quantity,
inventory_quantity_set: line.inventory_quantity_set,
location_id: line.location_id,
lot_id: line.lot_id,
lot_name: line.lot_name,
package_id: line.package_id,
product_id: line.product_id,
owner_id: line.owner_id,
user_id: this.userId,
};
for (const [key, value] of Object.entries(values)) {
values[key] = this._fieldToValue(value);
}
return values;
}
async _createNewLine(params) {
// When creating a new line, we need to know if a quant already exists
// for this line, and in this case, update the new line fields.
const product = params.fieldsParams.product_id;
if (product.detailed_type != 'product') {
const productName = (product.default_code ? `[${product.default_code}] ` : '') + product.display_name;
const message = sprintf(
_t("%s can't be inventoried. Only storable products can be inventoried."), productName);
this.notification.add(message, { type: 'warning' });
return false;
}
const domain = [
['location_id', '=', this.location.id],
['product_id', '=', product.id],
];
if (product.tracking !== 'none') {
if (params.fieldsParams.lot_name) { // Search for a quant with the exact same lot.
domain.push(['lot_id.name', '=', params.fieldsParams.lot_name]);
} else { // Search for a quant with no lot.
domain.push(['lot_id', '=', false]);
}
}
if (params.fieldsParams.package_id) {
domain.push(['package_id', '=', params.fieldsParams.package_id]);
}
const quant = await this.orm.searchRead(
'stock.quant',
domain,
['id', 'inventory_date', 'inventory_quantity', 'inventory_quantity_set', 'quantity', 'user_id'],
{ limit: 1 }
);
if (quant.length) {
Object.assign(params.fieldsParams, quant[0], { inventory_quantity: 1 });
}
const newLine = await super._createNewLine(params);
if (quant.length) {
// If the quant already exits, we add it into the `initialState` to
// avoid comparison issue with the `currentState` when the save occurs.
this.initialState.lines.push(Object.assign({}, newLine, quant[0]));
}
return newLine;
}
_convertDataToFieldsParams(args) {
const params = {
inventory_quantity: args.quantity,
lot_id: args.lot,
lot_name: args.lotName,
owner_id: args.owner,
package_id: args.package || args.resultPackage,
product_id: args.product,
product_uom_id: args.product && args.product.uom_id,
};
return params;
}
_getNewLineDefaultValues(fieldsParams) {
const defaultValues = super._getNewLineDefaultValues(...arguments);
return Object.assign(defaultValues, {
inventory_date: new Date().toISOString().slice(0, 10),
inventory_quantity: 0,
inventory_quantity_set: true,
quantity: (fieldsParams && fieldsParams.quantity) || 0,
user_id: this.userId,
});
}
_getFieldToWrite() {
return [
'inventory_date',
'inventory_quantity',
'inventory_quantity_set',
'user_id',
'location_id',
'lot_name',
'lot_id',
'package_id',
'owner_id',
];
}
_getSaveCommand() {
const commands = this._getSaveLineCommand();
if (commands.length) {
return {
route: '/stock_barcode/save_barcode_data',
params: {
model: this.params.model,
res_id: false,
write_field: false,
write_vals: commands,
},
};
}
return {};
}
_groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {
return Object.assign(super._groupSublines(...arguments), {
inventory_quantity: qtyDone,
quantity: qtyDemand,
});
}
_lineIsNotComplete(line) {
return line.inventory_quantity === 0;
}
async _processPackage(barcodeData) {
const { packageType, packageName } = barcodeData;
let recPackage = barcodeData.package;
this.lastScanned.packageId = false;
if (!recPackage && !packageType && !packageName) {
return; // No Package data to process.
}
// Scan a new package and/or a package type -> Create a new package with those parameters.
const currentLine = this.selectedLine || this.lastScannedLine;
if (currentLine.package_id && packageType &&
!recPackage && ! packageName &&
currentLine.package_id.id !== packageType) {
// Changes the package type for the scanned one.
await this.orm.write('stock.quant.package', [currentLine.package_id.id], {
package_type_id: packageType.id,
});
const message = sprintf(
_t("Package type %s was correctly applied to the package %s"),
packageType.name, currentLine.package_id.name
);
barcodeData.stopped = true;
return this.notification.add(message, { type: 'success' });
}
if (!recPackage) {
if (currentLine && !currentLine.package_id) {
const valueList = {};
if (packageName) {
valueList.name = packageName;
}
if (packageType) {
valueList.package_type_id = packageType.id;
}
const newPackageData = await this.orm.call(
'stock.quant.package',
'action_create_from_barcode',
[valueList]
);
this.cache.setCache(newPackageData);
recPackage = newPackageData['stock.quant.package'][0];
}
}
if (!recPackage && packageName) {
const currentLine = this.selectedLine || this.lastScannedLine;
if (currentLine && !currentLine.package_id) {
const newPackageData = await this.orm.call(
'stock.quant.package',
'action_create_from_barcode',
[{ name: packageName }]
);
this.cache.setCache(newPackageData);
recPackage = newPackageData['stock.quant.package'][0];
}
}
if (!recPackage || (
recPackage.location_id && recPackage.location_id != this.location.id
)) {
return;
}
// TODO: can check if quants already in cache to avoid to make a RPC if
// there is all in it (or make the RPC only on missing quants).
const res = await this.orm.call(
'stock.quant',
'get_stock_barcode_data_records',
[recPackage.quant_ids]
);
const quants = res.records['stock.quant'];
if (!quants.length) { // Empty package => Assigns it to the last scanned line.
const currentLine = this.selectedLine || this.lastScannedLine;
if (currentLine && !currentLine.package_id && !currentLine.result_package_id) {
const fieldsParams = this._convertDataToFieldsParams({
resultPackage: recPackage,
});
await this.updateLine(currentLine, fieldsParams);
barcodeData.stopped = true;
this.selectedLineVirtualId = false;
this.lastScanned.packageId = recPackage.id;
this.trigger('update');
}
return;
}
this.cache.setCache(res.records);
// Checks if the package is already scanned.
let alreadyExisting = 0;
for (const line of this.pageLines) {
if (line.package_id && line.package_id.id === recPackage.id &&
this.getQtyDone(line) > 0) {
alreadyExisting++;
}
}
if (alreadyExisting === quants.length) {
barcodeData.error = _t("This package is already scanned.");
return;
}
// For each quants, creates or increments a barcode line.
for (const quant of quants) {
const product = this.cache.getRecord('product.product', quant.product_id);
const searchLineParams = Object.assign({}, barcodeData, { product });
const currentLine = this._findLine(searchLineParams);
if (currentLine) { // Updates an existing line.
const fieldsParams = this._convertDataToFieldsParams({
quantity: quant.quantity,
lotName: barcodeData.lotName,
lot: barcodeData.lot,
package: recPackage,
owner: barcodeData.owner,
});
await this.updateLine(currentLine, fieldsParams);
} else { // Creates a new line.
const fieldsParams = this._convertDataToFieldsParams({
product,
quantity: quant.quantity,
lot: quant.lot_id,
package: quant.package_id,
resultPackage: quant.package_id,
owner: quant.owner_id,
});
const newLine = await this._createNewLine({ fieldsParams });
newLine.inventory_quantity = quant.quantity;
}
}
barcodeData.stopped = true;
this.selectedLineVirtualId = false;
this.lastScanned.packageId = recPackage.id;
this.trigger('update');
}
_updateLineQty(line, args) {
if (args.quantity) { // Set stock quantity.
line.quantity = args.quantity;
}
if (args.inventory_quantity) { // Increments inventory quantity.
if (args.uom) {
// An UoM was passed alongside the quantity, needs to check it's
// compatible with the product's UoM.
const productUOM = this.cache.getRecord('uom.uom', line.product_id.uom_id);
if (args.uom.category_id !== productUOM.category_id) {
// Not the same UoM's category -> Can't be converted.
const message = sprintf(
_t("Scanned quantity uses %s as Unit of Measure, but this UoM is not compatible with the product's one (%s)."),
args.uom.name, productUOM.name
);
return this.notification.add(message, { title: _t("Wrong Unit of Measure"), type: 'warning' });
} else if (args.uom.id !== productUOM.id) {
// Compatible but not the same UoM => Need a conversion.
args.inventory_quantity = (args.inventory_quantity / args.uom.factor) * productUOM.factor;
}
}
line.inventory_quantity += args.inventory_quantity;
line.inventory_quantity_set = true;
if (line.product_id.tracking === 'serial' && (line.lot_name || line.lot_id)) {
line.inventory_quantity = Math.max(0, Math.min(1, line.inventory_quantity));
}
}
}
async _updateLotName(line, lotName) {
if (line.lot_name === lotName) {
// No need to update the line's tracking number if it's already set.
return Promise.resolve();
}
line.lot_name = lotName;
// Checks if a quant exists for this line and updates the line in this case.
const domain = [
['location_id', '=', line.location_id.id],
['product_id', '=', line.product_id.id],
['lot_id.name', '=', lotName],
['owner_id', '=', line.owner_id && line.owner_id.id],
['package_id', '=', line.package_id && line.package_id.id],
];
const existingQuant = await this.orm.searchRead(
'stock.quant',
domain,
['id', 'quantity'],
{ limit: 1, load: false }
);
if (existingQuant.length) {
Object.assign(line, existingQuant[0]);
if (line.lot_id) {
line.lot_id = await this.cache.getRecordByBarcode(lotName, 'stock.lot');
}
}
}
_createLinesState() {
const today = new Date().toISOString().slice(0, 10);
const lines = [];
for (const id of Object.keys(this.cache.dbIdCache['stock.quant']).map(id => Number(id))) {
const quant = this.cache.getRecord('stock.quant', id);
if (quant.user_id !== this.userId || quant.inventory_date > today) {
// Doesn't take quants who must be counted by another user or in the future.
continue;
}
// Checks if this line is already in the quant state to get back
// its `virtual_id` (and so, avoid to set a new `virtual_id`).
const prevLine = this.currentState && this.currentState.lines.find(l => l.id === id);
const previousVirtualId = prevLine && prevLine.virtual_id;
quant.virtual_id = quant.dummy_id || previousVirtualId || this._uniqueVirtualId;
quant.product_id = this.cache.getRecord('product.product', quant.product_id);
quant.location_id = this.cache.getRecord('stock.location', quant.location_id);
quant.lot_id = quant.lot_id && this.cache.getRecord('stock.lot', quant.lot_id);
quant.package_id = quant.package_id && this.cache.getRecord('stock.quant.package', quant.package_id);
quant.owner_id = quant.owner_id && this.cache.getRecord('res.partner', quant.owner_id);
lines.push(Object.assign({}, quant));
}
return lines;
}
_getName() {
return _t("Inventory Adjustment");
}
_getPrintOptions() {
const options = super._getPrintOptions();
const quantsToPrint = this.pageLines.filter(quant => quant.inventory_quantity_set);
if (quantsToPrint.length === 0) {
return { warning: _t("There is nothing to print in this page.") };
}
options.additional_context = { active_ids: quantsToPrint.map(quant => quant.id) };
return options;
}
}