1180 lines
48 KiB
JavaScript
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;
|
|
}
|
|
}
|