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

1180 lines
48 KiB
JavaScript

/** @odoo-module **/
import BarcodeModel from '@stock_barcode/models/barcode_model';
import {_t, _lt} from "web.core";
import { sprintf } from '@web/core/utils/strings';
import { session } from '@web/session';
export default class BarcodePickingModel extends BarcodeModel {
constructor(params) {
super(...arguments);
this.lineModel = 'stock.move.line';
this.validateMessage = _t("The transfer has been validated");
this.validateMethod = 'button_validate';
this.lastScanned.destLocation = false;
this.shouldShortenLocationName = true;
}
setData(data) {
super.setData(...arguments);
this._useReservation = this.initialState.lines.some(line => line.reserved_uom_qty);
this.config = data.data.config || {}; // Picking type's scan restrictions configuration.
if (!this.displayDestinationLocation) {
this.config.restrict_scan_dest_location = 'no';
}
this.lineFormViewId = data.data.line_view_id;
this.formViewId = data.data.form_view_id;
this.packageKanbanViewId = data.data.package_view_id;
}
askBeforeNewLinesCreation(product) {
return !this.record.immediate_transfer && product &&
!this.currentState.lines.some(line => line.product_id.id === product.id);
}
getDisplayIncrementBtn(line) {
if (this.config.restrict_scan_product && line.product_id.barcode && !this.getQtyDone(line) && (
!this.lastScanned.product || this.lastScanned.product.id != line.product_id.id
)) {
return false;
}
return super.getDisplayIncrementBtn(...arguments);
}
getDisplayIncrementBtnForSerial(line) {
return !this.config.restrict_scan_tracking_number && super.getDisplayIncrementBtnForSerial(...arguments);
}
getIncrementQuantity(line) {
return Math.max(this.getQtyDemand(line) - this.getQtyDone(line), 1);
}
getQtyDone(line) {
return line.qty_done;
}
getQtyDemand(line) {
return line.reserved_uom_qty;
}
getEditedLineParams(line) {
return Object.assign(super.getEditedLineParams(...arguments), { canBeDeleted: !line.reserved_uom_qty });
}
getDisplayIncrementPackagingBtn(line) {
const packagingQty = line.product_packaging_uom_qty;
return packagingQty &&
(!this.getQtyDemand(line) || this.getQtyDemand(line) >= this.getQtyDone(line) + packagingQty);
}
groupKey(line) {
return super.groupKey(...arguments) + `_${line.location_dest_id.id}`;
}
lineCanBeSelected(line) {
if (this.selectedLine && this.selectedLine.virtual_id === line.virtual_id) {
return true; // We consider an already selected line can always be re-selected.
}
if (this.config.restrict_scan_source_location && !this.lastScanned.sourceLocation && !line.qty_done) {
return false; // Can't select a line if source is mandatory and wasn't scanned yet.
}
if (line.isPackageLine) {
// The next conditions concern product, skips them in case of package line.
return super.lineCanBeSelected(...arguments);
}
const product = line.product_id;
if (this.config.restrict_put_in_pack === 'mandatory' && this.selectedLine &&
this.selectedLine.qty_done && !this.selectedLine.result_package_id &&
this.selectedLine.product_id.id != product.id) {
return false; // Can't select another product if a package must be scanned first.
}
if (this.config.restrict_scan_product && product.barcode) {
// If the product scan is mandatory, a line can't be selected if its product isn't
// scanned first (as we can't keep track of each line's product scanned state, we
// consider a product was scanned if the line has a qty. greater than zero).
if (product.tracking === 'none' || !this.config.restrict_scan_tracking_number) {
return !this.getQtyDemand(line) || this.getQtyDone(line) || (
this.lastScanned.product && this.lastScanned.product.id === line.product_id.id
);
} else if (product.tracking != 'none') {
return line.lot_name || (line.lot_id && line.qty_done);
}
}
return super.lineCanBeSelected(...arguments);
}
lineCanBeEdited(line) {
if (line.product_id.tracking !== 'none' && this.config.restrict_scan_tracking_number &&
!((line.lot_id && line.qty_done) || line.lot_name)) {
return false;
}
return this.lineCanBeSelected(line);
}
async updateLine(line, args) {
await super.updateLine(...arguments);
let { location_dest_id, result_package_id } = args;
if (result_package_id) {
if (typeof result_package_id === 'number') {
result_package_id = this.cache.getRecord('stock.quant.package', result_package_id);
if (result_package_id.package_type_id && typeof result_package_id === 'number') {
result_package_id.package_type_id = this.cache.getRecord('stock.package.type', result_package_id.package_type_id);
}
}
line.result_package_id = result_package_id;
}
if (location_dest_id) {
if (typeof location_dest_id === 'number') {
location_dest_id = this.cache.getRecord('stock.location', args.location_dest_id);
}
line.location_dest_id = location_dest_id;
}
}
updateLineQty(virtualId, qty = 1) {
this.actionMutex.exec(() => {
const line = this.pageLines.find(l => l.virtual_id === virtualId);
this.updateLine(line, {qty_done: qty});
this.trigger('update');
});
}
get barcodeInfo() {
if (this.isCancelled || this.isDone) {
return {
class: this.isDone ? 'picking_already_done' : 'picking_already_cancelled',
message: this.isDone ?
_t("This picking is already done") :
_t("This picking is cancelled"),
warning: true,
};
}
let barcodeInfo = super.barcodeInfo;
// Takes the parent line if the current line is part of a group.
const line = this._getParentLine(this.selectedLine) || this.selectedLine;
// Defines some messages who can appear in multiple cases.
const infos = {
scanScrLoc: {
message: this.considerPackageLines && !this.config.restrict_scan_source_location ?
_lt("Scan the source location or a package") :
_lt("Scan the source location"),
class: 'scan_src',
icon: 'sign-out',
},
scanDestLoc: {
message: _lt("Scan the destination location"),
class: 'scan_dest',
icon: 'sign-in',
},
scanProductOrDestLoc: {
message: this.considerPackageLines ?
_lt("Scan a product, a package or the destination location.") :
_lt("Scan a product or the destination location."),
class: 'scan_product_or_dest',
},
scanPackage: {
message: this._getScanPackageMessage(line),
class: "scan_package",
icon: 'archive',
},
scanLot: {
message: _lt("Scan a lot number"),
class: "scan_lot",
icon: "barcode",
},
scanSerial: {
message: _lt("Scan a serial number"),
class: "scan_serial",
icon: "barcode",
},
pressValidateBtn: {
message: _lt("Press Validate or scan another product"),
class: 'scan_validate',
icon: 'check-square',
},
};
if (!line && this._moveEntirePackage()) { // About package lines.
const packageLine = this.selectedPackageLine;
if (packageLine) {
if (this._lineIsComplete(packageLine)) {
if (this.config.restrict_scan_source_location && !this.lastScanned.sourceLocation) {
return infos.scanScrLoc;
} else if (this.config.restrict_scan_dest_location != 'no' && !this.lastScanned.destLocation) {
return this.config.restrict_scan_dest_location == 'mandatory' ?
infos.scanDestLoc :
infos.scanProductOrDestLoc;
} else if (this.pageIsDone) {
return infos.pressValidateBtn;
} else {
barcodeInfo.message = _lt("Scan a product or another package");
barcodeInfo.class = 'scan_product_or_package';
}
} else {
barcodeInfo.message = sprintf(_t("Scan the package %s"), packageLine.result_package_id.name);
barcodeInfo.icon = 'archive';
}
return barcodeInfo;
} else if (this.considerPackageLines && barcodeInfo.class == 'scan_product') {
barcodeInfo.message = _lt("Scan a product or a package");
barcodeInfo.class = 'scan_product_or_package';
}
}
if (this.messageType === "scan_product" && !line &&
this.config.restrict_scan_source_location && this.lastScanned.sourceLocation) {
barcodeInfo.message = sprintf(
_lt("Scan a product from %s"),
this.lastScanned.sourceLocation.name);
}
// About source location.
if (this.displaySourceLocation) {
if (!this.lastScanned.sourceLocation && !this.pageIsDone) {
return infos.scanScrLoc;
} else if (this.lastScanned.sourceLocation && this.lastScanned.destLocation == 'no' &&
line && this._lineIsComplete(line)) {
if (this.config.restrict_put_in_pack === 'mandatory' && !line.result_package_id) {
return {
message: _lt("Scan a package"),
class: 'scan_package',
icon: 'archive',
};
}
return infos.scanScrLoc;
}
}
if (!line) {
if (this.pageIsDone) { // All is done, says to validate the transfer.
return infos.pressValidateBtn;
} else if (this.config.lines_need_to_be_packed) {
const lines = new Array(...this.pageLines, ...this.packageLines);
if (lines.every(line => !this._lineIsNotComplete(line)) &&
lines.some(line => this._lineNeedsToBePacked(line))) {
return infos.scanPackage;
}
}
return barcodeInfo;
}
const product = line.product_id;
// About tracking numbers.
if (product.tracking !== 'none') {
const isLot = product.tracking === "lot";
if (this.getQtyDemand(line) && (line.lot_id || line.lot_name)) { // Reserved.
if (this.getQtyDone(line) === 0) { // Lot/SN not scanned yet.
return isLot ? infos.scanLot : infos.scanSerial;
} else if (this.getQtyDone(line) < this.getQtyDemand(line)) { // Lot/SN scanned but not enough.
barcodeInfo = isLot ? infos.scanLot : infos.scanSerial;
barcodeInfo.message = isLot ?
_t("Scan more lot numbers") :
_t("Scan another serial number");
return barcodeInfo;
}
} else if (!(line.lot_id || line.lot_name)) { // Not reserved.
return isLot ? infos.scanLot : infos.scanSerial;
}
}
// About package.
if (this._lineNeedsToBePacked(line)) {
if (this._lineIsComplete(line)) {
return infos.scanPackage;
}
if (product.tracking == 'serial') {
barcodeInfo.message = _t("Scan a serial number or a package");
} else if (product.tracking == 'lot') {
barcodeInfo.message = line.qty_done == 0 ?
_t("Scan a lot number") :
_t("Scan more lot numbers or a package");
barcodeInfo.class = "scan_lot";
} else {
barcodeInfo.message = _t("Scan more products or a package");
}
return barcodeInfo;
}
if (this.pageIsDone) {
Object.assign(barcodeInfo, infos.pressValidateBtn);
}
// About destination location.
const lineWaitingPackage = this.groups.group_tracking_lot && this.config.restrict_put_in_pack != "no" && !line.result_package_id;
if (this.config.restrict_scan_dest_location != 'no' && line.qty_done) {
if (this.pageIsDone) {
if (this.lastScanned.destLocation) {
return infos.pressValidateBtn;
} else {
return this.config.restrict_scan_dest_location == 'mandatory' && this._lineIsComplete(line) ?
infos.scanDestLoc :
infos.scanProductOrDestLoc;
}
} else if (this._lineIsComplete(line)) {
if (lineWaitingPackage) {
barcodeInfo.message = this.config.restrict_scan_dest_location == 'mandatory' ?
_t("Scan a package or the destination location") :
_t("Scan a package, the destination location or another product");
} else {
return this.config.restrict_scan_dest_location == 'mandatory' ?
infos.scanDestLoc :
infos.scanProductOrDestLoc;
}
} else {
if (product.tracking == 'serial') {
barcodeInfo.message = lineWaitingPackage ?
_t("Scan a serial number or a package then the destination location") :
_t("Scan a serial number then the destination location");
} else if (product.tracking == 'lot') {
barcodeInfo.message = lineWaitingPackage ?
_t("Scan a lot number or a packages then the destination location") :
_t("Scan a lot number then the destination location");
} else {
barcodeInfo.message = lineWaitingPackage ?
_t("Scan a product, a package or the destination location") :
_t("Scan a product then the destination location");
}
}
}
return barcodeInfo;
}
get canBeProcessed() {
return !['cancel', 'done'].includes(this.record.state);
}
/**
* Depending of the config, a transfer can be fully validate even if nothing was scanned (like
* with an immediate transfer) or if at least one product was scanned.
* @returns {boolean}
*/
get canBeValidate() {
if (this.record.immediate_transfer) {
return super.canBeValidate; // For immediate transfers, doesn't care about any special condition.
} else if (!this.config.barcode_validation_full && !this.currentState.lines.some(line => line.qty_done)) {
return false; // Can't be validate because "full validation" is forbidden and nothing was processed yet.
}
return super.canBeValidate;
}
get canCreateNewLot() {
return this.record.use_create_lots;
}
get canPutInPack() {
if (this.config.restrict_scan_product) {
return this.pageLines.some(line => line.qty_done && !line.result_package_id);
}
return true;
}
get canSelectLocation() {
return !(this.config.restrict_scan_source_location || this.config.restrict_scan_dest_location != 'optional');
}
/**
* Must be overridden to make something when the user selects a specific destination location.
*
* @param {int} id location's id
*/
changeDestinationLocation(id, selectedLine) {
selectedLine = this._getParentLine(selectedLine) || selectedLine;
if (selectedLine) {
if (selectedLine.lines) {
// Grouped lines, applies the location to all sublines with the
// same current location than the real selected line.
for (const line of selectedLine.lines) {
if (line.location_dest_id.id === selectedLine.location_dest_id.id &&
line.location_dest_id.id != id && line.qty_done) {
line.location_dest_id = this.cache.getRecord('stock.location', id);
this._markLineAsDirty(line);
}
}
} else if (selectedLine.location_dest_id.id != id) {
selectedLine.location_dest_id = this.cache.getRecord('stock.location', id);
this._markLineAsDirty(selectedLine);
}
// Clear selection and scan data.
this.selectedLineVirtualId = false;
this.location = false;
this.lastScanned.packageId = false;
this.lastScanned.product = false;
this.scannedLinesVirtualId = [];
}
}
get considerPackageLines() {
return this._moveEntirePackage() && this.packageLines.length;
}
get displayCancelButton() {
return !['done', 'cancel'].includes(this.record.state);
}
get displayDestinationLocation() {
return this.groups.group_stock_multi_locations &&
['incoming', 'internal'].includes(this.record.picking_type_code) &&
this.config.restrict_scan_dest_location != 'no';
}
get displayPutInPackButton() {
return this.groups.group_tracking_lot && this.config.restrict_put_in_pack != 'no';
}
get displayResultPackage() {
return true;
}
get displaySourceLocation() {
return super.displaySourceLocation && this.config.restrict_scan_source_location &&
['internal', 'outgoing'].includes(this.record.picking_type_code);
}
get displayValidateButton() {
return true;
}
get highlightValidateButton() {
if (!this.pageLines.length && !this.packageLines.length) {
return false;
}
if (this.config.restrict_scan_dest_location == 'mandatory' &&
!this.lastScanned.destLocation && this.selectedLine) {
return false;
}
for (let line of this.pageLines) {
line = this._getParentLine(line) || line;
if (this._lineIsNotComplete(line)) {
return false;
}
}
for (const packageLine of this.packageLines) {
if (this._lineIsNotComplete(packageLine)) {
return false;
}
}
return Boolean([...this.pageLines, ...this.packageLines].length);
}
get isDone() {
return this.record.state === 'done';
}
get isCancelled() {
return this.record.state === 'cancel';
}
lineIsFaulty(line) {
return this._useReservation && line.qty_done > line.reserved_uom_qty;
}
get packageLines() {
if (!this.record.picking_type_entire_packs) {
return [];
}
const linesWithPackage = this.currentState.lines.filter(line => line.package_id && line.result_package_id);
// Groups lines by package.
const groupedLines = {};
for (const line of linesWithPackage) {
const packageId = line.package_id.id;
if (!groupedLines[packageId]) {
groupedLines[packageId] = [];
}
groupedLines[packageId].push(line);
}
const packageLines = [];
for (const key in groupedLines) {
// Check if the package is reserved.
const reservedPackage = groupedLines[key].every(line => line.reserved_uom_qty);
groupedLines[key][0].reservedPackage = reservedPackage;
const packageLine = Object.assign({}, groupedLines[key][0], {
lines: groupedLines[key],
isPackageLine: true,
});
packageLines.push(packageLine);
}
return this._sortLine(packageLines);
}
get pageIsDone() {
for (const line of this.groupedLines) {
if (this._lineIsNotComplete(line) || this._lineNeedsToBePacked(line) ||
(line.product_id.tracking != 'none' && !(line.lot_id || line.lot_name))) {
return false;
}
}
for (const line of this.packageLines) {
if (this._lineIsNotComplete(line)) {
return false;
}
}
return Boolean([...this.groupedLines, ...this.packageLines].length);
}
/**
* Returns only the lines (filters out the package lines if relevant).
* @returns {Array<Object>}
*/
get pageLines() {
let lines = super.pageLines;
// If we show entire package, we don't return lines with package (they
// will be treated as "package lines").
if (this.record.picking_type_entire_packs) {
lines = lines.filter(line => !(line.package_id && line.result_package_id));
}
return this._sortLine(lines);
}
get previousScannedLinesByPackage() {
if (this.lastScanned.packageId) {
return this.currentState.lines.filter(l => l.result_package_id.id === this.lastScanned.packageId);
}
return [];
}
get printButtons() {
const buttons = [
{
name: _t("Print Picking Operations"),
class: 'o_print_picking',
method: 'do_print_picking',
}, {
name: _t("Print Delivery Slip"),
class: 'o_print_delivery_slip',
method: 'action_print_delivery_slip',
}, {
name: _t("Print Barcodes PDF"),
class: 'o_print_barcodes_pdf',
method: 'action_print_barcode_pdf',
},
];
if (this.groups.group_tracking_lot) {
buttons.push({
name: _t("Print Packages"),
class: 'o_print_packages',
method: 'action_print_packges',
});
}
const picking_type_code = this.record.picking_type_code;
const picking_state = this.record.state;
if ( (picking_type_code === 'incoming') && (picking_state === 'done') ||
(picking_type_code === 'outgoing') && (picking_state !== 'done') ||
(picking_type_code === 'internal')
) {
buttons.push({
name: _t("Scrap"),
class: 'o_scrap',
method: 'button_scrap',
});
}
return buttons;
}
get selectedPackageLine() {
return this.lastScanned.packageId && this.packageLines.find(pl => pl.result_package_id.id == this.lastScanned.packageId);
}
get useExistingLots() {
return this.record.use_existing_lots;
}
async validate() {
if (this.config.restrict_scan_dest_location == 'mandatory' &&
!this.lastScanned.destLocation && this.selectedLine) {
return this.notification.add(_t("Destination location must be scanned"), { type: 'danger' });
}
if (this.config.lines_need_to_be_packed &&
this.currentState.lines.some(line => this._lineNeedsToBePacked(line))) {
return this.notification.add(_t("All products need to be packed"), { type: 'danger' });
}
return await super.validate();
}
async displayProductPage() {
await this._setUser(); // Set current user as picking's responsible.
super.displayProductPage();
}
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
async _assignEmptyPackage(line, resultPackage) {
const fieldsParams = this._convertDataToFieldsParams({ resultPackage });
const parentLine = this._getParentLine(line);
if (parentLine) { // Assigns the result package on all sibling lines.
for (const subline of parentLine.lines) {
if (subline.qty_done && !subline.result_package_id) {
await this.updateLine(subline, fieldsParams);
}
}
} else {
await this.updateLine(line, fieldsParams);
}
}
_getNewLineDefaultContext() {
const picking = this.cache.getRecord(this.params.model, this.params.id);
return {
default_company_id: picking.company_id,
default_location_id: this._defaultLocation().id,
default_location_dest_id: this._defaultDestLocation().id,
default_picking_id: this.params.id,
default_qty_done: 1,
};
}
async _cancel() {
await this.save();
await this.orm.call(
this.params.model,
'action_cancel',
[[this.params.id]]
);
this._cancelNotification();
this.trigger('history-back');
}
_cancelNotification() {
this.notification.add(_t("The transfer has been cancelled"));
}
_checkBarcode(barcodeData) {
const check = { title: _lt("Not the expected scan") };
const { location, lot, product, destLocation, packageType } = barcodeData;
const resultPackage = barcodeData.package;
const packageWithQuant = (barcodeData.package && barcodeData.package.quant_ids || []).length;
if (this.config.restrict_scan_source_location && !barcodeData.location) {
// Special case where the user can not scan a destination but a source was already scanned.
// That means what is supposed to be a destination is in this case a source.
if (this.lastScanned.sourceLocation && barcodeData.destLocation &&
this.config.restrict_scan_dest_location == 'no') {
barcodeData.location = barcodeData.destLocation;
delete barcodeData.destLocation;
}
// Special case where the source is mandatory and the app's waiting for but none was
// scanned, get the previous scanned one if possible.
if (!this.lastScanned.sourceLocation && this._currentLocation) {
this.lastScanned.sourceLocation = this._currentLocation;
}
}
if (this.config.restrict_scan_source_location && !this._currentLocation && !this.selectedLine) { // Source Location.
if (location) {
this.location = location;
} else {
check.title = _t("Mandatory Source Location");
check.message = sprintf(
_t("You are supposed to scan %s or another source location"),
this.location.display_name,
);
}
} else if (this.config.restrict_scan_product && // Restriction on product.
!(product || packageWithQuant || this.selectedLine) && // A product/package was scanned.
!(this.config.restrict_scan_source_location && location && !this.selectedLine) // Maybe the user scanned the wrong location and trying to scan the right one
) {
check.message = lot ?
_t("Scan a product before scanning a tracking number") :
_t("You must scan a product");
} else if (this.config.restrict_put_in_pack == 'mandatory' && !(resultPackage || packageType) &&
this.selectedLine && !this.qty_done && !this.selectedLine.result_package_id &&
((product && product.id != this.selectedLine.product_id.id) || location || destLocation)) { // Package.
check.message = _t("You must scan a package or put in pack");
} else if (this.config.restrict_scan_dest_location == 'mandatory' && !this.lastScanned.destLocation) { // Destination Location.
if (destLocation) {
this.lastScanned.destLocation = destLocation;
} else if (product && this.selectedLine && this.selectedLine.product_id.id != product.id) {
// Cannot scan another product before a destination was scanned.
check.title = _t("Mandatory Destination Location");
check.message = sprintf(
_t("Please scan destination location for %s before scanning other product"),
this.selectedLine.product_id.display_name
);
}
}
check.error = Boolean(check.message);
return check;
}
async _closeValidate(ev) {
const record = await this.orm.read(this.params.model, [this.record.id], ["state"])
if (record[0].state === 'done') {
// If all is OK, displays a notification and goes back to the previous page.
this.notification.add(this.validateMessage, { type: 'success' });
this.trigger('history-back');
}
}
_convertDataToFieldsParams(args) {
const params = {
lot_name: args.lotName,
product_id: args.product,
qty_done: args.quantity,
};
if (args.lot) {
params.lot_id = args.lot;
}
if (args.package) {
params.package_id = args.package;
}
if (args.resultPackage) {
params.result_package_id = args.resultPackage;
}
if (args.owner) {
params.owner_id = args.owner;
}
if (args.destLocation) {
params.location_dest_id = args.destLocation.id;
}
return params;
}
_createCommandVals(line) {
const values = {
dummy_id: line.virtual_id,
location_id: line.location_id,
location_dest_id: line.location_dest_id,
lot_name: line.lot_name,
lot_id: line.lot_id,
package_id: line.package_id,
picking_id: line.picking_id,
product_id: line.product_id,
product_uom_id: line.product_uom_id,
owner_id: line.owner_id,
qty_done: line.qty_done,
result_package_id: line.result_package_id,
state: 'assigned',
};
for (const [key, value] of Object.entries(values)) {
values[key] = this._fieldToValue(value);
}
return values;
}
_createLinesState() {
const lines = [];
const picking = this.cache.getRecord(this.params.model, this.params.id);
for (const id of picking.move_line_ids) {
const smlData = this.cache.getRecord('stock.move.line', id);
// Checks if this line is already in the picking's 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;
smlData.virtual_id = Number(smlData.dummy_id) || previousVirtualId || this._uniqueVirtualId;
smlData.product_id = this.cache.getRecord('product.product', smlData.product_id);
smlData.product_uom_id = this.cache.getRecord('uom.uom', smlData.product_uom_id);
smlData.location_id = this.cache.getRecord('stock.location', smlData.location_id);
smlData.location_dest_id = this.cache.getRecord('stock.location', smlData.location_dest_id);
smlData.lot_id = smlData.lot_id && this.cache.getRecord('stock.lot', smlData.lot_id);
smlData.owner_id = smlData.owner_id && this.cache.getRecord('res.partner', smlData.owner_id);
smlData.package_id = smlData.package_id && this.cache.getRecord('stock.quant.package', smlData.package_id);
smlData.product_packaging_id = smlData.product_packaging_id && this.cache.getRecord('product.packaging', smlData.product_packaging_id);
const resultPackage = smlData.result_package_id && this.cache.getRecord('stock.quant.package', smlData.result_package_id);
if (resultPackage) { // Fetch the package type if needed.
smlData.result_package_id = resultPackage;
const packageType = resultPackage && resultPackage.package_type_id;
resultPackage.package_type_id = packageType && this.cache.getRecord('stock.package.type', packageType);
}
lines.push(smlData);
}
return lines;
}
_defaultLocation() {
return this.cache.getRecord('stock.location', this.record.location_id);
}
_defaultDestLocation() {
return this.cache.getRecord('stock.location', this.record.location_dest_id);
}
_getCommands() {
return Object.assign(super._getCommands(), {
'O-BTN.pack': this._putInPack.bind(this),
'O-CMD.cancel': this._cancel.bind(this),
});
}
_getDefaultMessageType() {
if (this.displaySourceLocation && !this.lastScanned.sourceLocation) {
return 'scan_src';
}
return 'scan_product';
}
_getLocationMessage() {
if (this.groups.group_stock_multi_locations) {
if (this.record.picking_type_code === 'outgoing') {
return 'scan_product_or_src';
} else if (this.config.restrict_scan_dest_location != 'no') {
return 'scan_product_or_dest';
}
}
return 'scan_product';
}
_getModelRecord() {
const record = this.cache.getRecord(this.params.model, this.params.id);
if (record.picking_type_id && record.state !== "cancel") {
record.picking_type_id = this.cache.getRecord('stock.picking.type', record.picking_type_id);
}
return record;
}
_getNewLineDefaultValues(fieldsParams) {
const defaultValues = super._getNewLineDefaultValues(...arguments);
return Object.assign(defaultValues, {
location_dest_id: this._defaultDestLocation(),
reserved_uom_qty: false,
qty_done: 0,
picking_id: this.params.id,
});
}
_getFieldToWrite() {
return [
'location_id',
'location_dest_id',
'lot_id',
'lot_name',
'package_id',
'owner_id',
'qty_done',
'result_package_id',
];
}
_getSaveCommand() {
const commands = this._getSaveLineCommand();
if (commands.length) {
return {
route: '/stock_barcode/save_barcode_data',
params: {
model: this.params.model,
res_id: this.params.id,
write_field: 'move_line_ids',
write_vals: commands,
},
};
}
return {};
}
_getScanPackageMessage() {
return _t("Scan a package or put in pack");
}
_groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {
return Object.assign(super._groupSublines(...arguments), {
reserved_uom_qty: qtyDemand,
qty_done: qtyDone,
});
}
_incrementTrackedLine() {
return !(this.record.use_create_lots || this.record.use_existing_lots);
}
_lineIsComplete(line) {
let isComplete = line.reserved_uom_qty && line.qty_done >= line.reserved_uom_qty;
if (line.isPackageLine && !line.reserved_uom_qty && line.qty_done) {
return true; // For package line, considers an unreserved package as a completed line.
}
if (isComplete && line.lines) { // Grouped lines/package lines have multiple sublines.
for (const subline of line.lines) {
// For tracked product, a line with `qty_done` but no tracking number is considered as not complete.
if (subline.product_id.tracking != 'none') {
if (subline.qty_done && !(subline.lot_id || subline.lot_name)) {
return false;
}
} else if (subline.reserved_uom_qty && subline.qty_done < subline.reserved_uom_qty) {
return false;
}
}
}
return isComplete;
}
_lineIsNotComplete(line) {
const isNotComplete = line.reserved_uom_qty && line.qty_done < line.reserved_uom_qty;
if (!isNotComplete && line.lines) { // Grouped lines/package lines have multiple sublines.
for (const subline of line.lines) {
// For tracked product, a line with `qty_done` but no tracking number is considered as not complete.
if (subline.product_id.tracking != 'none') {
if (subline.qty_done && !(subline.lot_id || subline.lot_name)) {
return true;
}
} else if (subline.reserved_uom_qty && subline.qty_done < subline.reserved_uom_qty) {
return true;
}
}
}
return isNotComplete;
}
_lineNeedsToBePacked(line) {
return Boolean(
this.config.lines_need_to_be_packed && line.qty_done && !line.result_package_id);
}
_moveEntirePackage() {
return this.record.picking_type_entire_packs;
}
async _processLocation(barcodeData) {
super._processLocation(...arguments);
if (barcodeData.destLocation) {
this._processLocationDestination(barcodeData);
this.trigger('update');
}
}
_processLocationDestination(barcodeData) {
const selectedLine = this.selectedLine || this.selectedPackageLine;
if (this.config.restrict_scan_dest_location == 'no' || !selectedLine) {
return;
}
this.changeDestinationLocation(barcodeData.destLocation.id, selectedLine);
this.trigger('update');
barcodeData.stopped = true;
}
async _processPackage(barcodeData) {
const { packageName } = barcodeData;
const recPackage = barcodeData.package;
this.lastScanned.packageId = false;
if (barcodeData.packageType && !recPackage) {
// Scanned a package type and no existing package: make a put in pack (forced package type).
barcodeData.stopped = true;
return await this._processPackageType(barcodeData);
} else if (packageName && !recPackage) {
// Scanned a non-existing package: make a put in pack.
barcodeData.stopped = true;
return await this._putInPack({ default_name: packageName });
} else if (!recPackage || (
recPackage.location_id && recPackage.location_id != this.location.id
)) {
return; // No package, package's type or package's name => Nothing to do.
}
// If move entire package, checks if the scanned package matches a package line.
if (this._moveEntirePackage()) {
for (const packageLine of this.packageLines) {
if (packageLine.package_id.name !== (packageName || recPackage.name)) {
continue;
}
barcodeData.stopped = true;
if (packageLine.qty_done) {
this.lastScanned.packageId = packageLine.package_id.id;
const message = _t("This package is already scanned.");
this.notification.add(message, { type: 'danger' });
return this.trigger('update');
}
for (const line of packageLine.lines) {
await this._updateLineQty(line, { qty_done: line.reserved_uom_qty });
this._markLineAsDirty(line);
}
return this.trigger('update');
}
}
// Scanned a package: fetches package's quant and creates a line for
// each of them, except if the package is already scanned.
// 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.result_package_id) {
await this._assignEmptyPackage(currentLine, recPackage);
barcodeData.stopped = true;
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,
});
await this._createNewLine({ fieldsParams });
}
}
barcodeData.stopped = true;
this.selectedLineVirtualId = false;
this.lastScanned.packageId = recPackage.id;
this.trigger('update');
}
async _processPackageType(barcodeData) {
const { packageType } = barcodeData;
const line = this.selectedLine;
if (!line || !line.qty_done) {
barcodeData.stopped = true;
const message = _t("You can't apply a package type. First, scan product or select a line");
return this.notification.add(message, { type: 'warning' });
}
const resultPackage = line.result_package_id;
if (!resultPackage) { // No package on the line => Do a put in pack.
const additionalContext = { default_package_type_id: packageType.id };
if (barcodeData.packageName) {
additionalContext.default_name = barcodeData.packageName;
}
await this._putInPack(additionalContext);
} else if (resultPackage.package_type_id.id !== packageType.id) {
// Changes the package type for the scanned one.
await this.save();
await this.orm.write('stock.quant.package', [resultPackage.id], {
package_type_id: packageType.id,
});
const message = sprintf(
_t("Package type %s was correctly applied to the package %s"),
packageType.name, resultPackage.name
);
this.notification.add(message, { type: 'success' });
this.trigger('refresh');
}
}
async _putInPack(additionalContext = {}) {
const context = Object.assign({ barcode_view: true }, additionalContext);
if (!this.groups.group_tracking_lot) {
return this.notification.add(
_t("To use packages, enable 'Packages' in the settings"),
{ type: 'danger'}
);
}
await this.save();
const result = await this.orm.call(
this.params.model,
'action_put_in_pack',
[[this.params.id]],
{ context }
);
if (typeof result === 'object') {
this.trigger('process-action', result);
} else {
this.trigger('refresh');
}
}
/**
* Set the pickings's responsible if not assigned to active user.
*/
async _setUser() {
if (this.record.user_id != session.uid) {
this.record.user_id = session.uid;
await this.orm.write(this.params.model, [this.record.id], { user_id: session.uid });
}
}
_setLocationFromBarcode(result, location) {
if (this.record.picking_type_code === 'outgoing') {
result.location = location;
} else if (this.record.picking_type_code === 'incoming') {
result.destLocation = location;
} else if (this.previousScannedLines.length || this.previousScannedLinesByPackage.length) {
if (this.config.restrict_scan_source_location && this.config.restrict_scan_dest_location === 'no') {
result.location = location;
} else {
result.destLocation = location;
}
} else {
result.location = location;
}
return result;
}
_sortingMethod(l1, l2) {
const l1IsCompleted = this._lineIsComplete(l1);
const l2IsCompleted = this._lineIsComplete(l2);
// Complete lines always on the bottom.
if (!l1IsCompleted && l2IsCompleted) {
return -1;
} else if (l1IsCompleted && !l2IsCompleted) {
return 1;
}
return super._sortingMethod(...arguments);
}
_updateLineQty(line, args) {
if (line.product_id.tracking === 'serial' && line.qty_done > 0 && (this.record.use_create_lots || this.record.use_existing_lots)) {
return;
}
if (args.qty_done) {
if (args.uom) {
// An UoM was passed alongside the quantity, needs to check it's
// compatible with the product's UoM.
const lineUOM = line.product_uom_id;
if (args.uom.category_id !== lineUOM.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 line's one (%s)."),
args.uom.name, lineUOM.name
);
return this.notification.add(message, { title: _t("Wrong Unit of Measure"), type: 'danger' });
} else if (args.uom.id !== lineUOM.id) {
// Compatible but not the same UoM => Need a conversion.
args.qty_done = (args.qty_done / args.uom.factor) * lineUOM.factor;
args.uom = lineUOM;
}
}
line.qty_done += args.qty_done;
this._setUser();
}
}
_updateLotName(line, lotName) {
line.lot_name = lotName;
}
async _processGs1Data(data) {
const result = await super._processGs1Data(...arguments);
const { rule } = data;
if (result.location && (rule.type === 'location_dest' || this.messageType === 'scan_product_or_dest')) {
result.destLocation = result.location;
result.location = undefined;
}
return result;
}
}