598 lines
23 KiB
JavaScript
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;
|
|
}
|
|
}
|