diff --git a/.eslintrc b/.eslintrc index 46fb354c11c..276d6ff3725 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 9, + "ecmaVersion": 11, "sourceType": "module" }, "extends": "eslint:recommended", diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index f72b85c0f6e..3ae1234767c 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -10,6 +10,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.serial_no_field = opts.serial_no_field || "serial_no"; this.batch_no_field = opts.batch_no_field || "batch_no"; this.qty_field = opts.qty_field || "qty"; + // field name on row which defines max quantity to be scanned e.g. picklist + this.max_qty_field = opts.max_qty_field; + // scanner won't add a new row if this flag is set. + this.dont_allow_new_row = opts.dont_allow_new_row; + // scanner will ask user to type the quantity instead of incrementing by 1 + this.prompt_qty = opts.prompt_qty; this.items_table_name = opts.items_table_name || "items"; this.items_table = this.frm.doc[this.items_table_name]; @@ -42,10 +48,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { .then((r) => { const data = r && r.message; if (!data || Object.keys(data).length === 0) { - frappe.show_alert({ - message: __("Cannot find Item with this Barcode"), - indicator: "red", - }); + this.show_alert(__("Cannot find Item with this Barcode"), "red"); this.clean_up(); return; } @@ -56,22 +59,18 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { update_table(data) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; - let row = null; const {item_code, barcode, batch_no, serial_no} = data; - // Check if batch is scanned and table has batch no field - let batch_no_scan = - Boolean(batch_no) && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field); - - if (batch_no_scan) { - row = this.get_batch_row_to_modify(batch_no); - } else { - // serial or barcode scan - row = this.get_row_to_modify_on_scan(item_code); - } + let row = this.get_row_to_modify_on_scan(item_code, batch_no); if (!row) { + if (this.dont_allow_new_row) { + this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); + this.clean_up(); + return; + } + // add new row if new item/batch is scanned row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); // trigger any row add triggers defined on child table. @@ -83,9 +82,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return; } - this.show_scan_message(row.idx, row.item_code); this.set_selector_trigger_flag(row, data); - this.set_item(row, item_code); + this.set_item(row, item_code).then(qty => { + this.show_scan_message(row.idx, row.item_code, qty); + }); this.set_serial_no(row, serial_no); this.set_batch_no(row, batch_no); this.set_barcode(row, barcode); @@ -106,9 +106,23 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } set_item(row, item_code) { - const item_data = { item_code: item_code }; - item_data[this.qty_field] = (row[this.qty_field] || 0) + 1; - frappe.model.set_value(row.doctype, row.name, item_data); + return new Promise(resolve => { + const increment = (value = 1) => { + const item_data = {item_code: item_code}; + item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); + frappe.model.set_value(row.doctype, row.name, item_data); + }; + + if (this.prompt_qty) { + frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => { + increment(value); + resolve(value); + }); + } else { + increment(); + resolve(); + } + }); } set_serial_no(row, serial_no) { @@ -137,53 +151,42 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - show_scan_message(idx, exist = null) { + show_scan_message(idx, exist = null, qty = 1) { // show new row or qty increase toast if (exist) { - frappe.show_alert( - { - message: __("Row #{0}: Qty increased by 1", [idx]), - indicator: "green", - }, - 5 - ); + this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green"); } else { - frappe.show_alert( - { - message: __("Row #{0}: Item added", [idx]), - indicator: "green", - }, - 5 - ); + this.show_alert(__("Row #{0}: Item added", [idx]), "green") } } is_duplicate_serial_no(row, serial_no) { - const is_duplicate = !!serial_no && !!row[this.serial_no_field] - && row[this.serial_no_field].includes(serial_no); + const is_duplicate = row[this.serial_no_field]?.includes(serial_no); if (is_duplicate) { - frappe.show_alert( - { - message: __("Serial No {0} is already added", [serial_no]), - indicator: "orange", - }, - 5 - ); + this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); } return is_duplicate; } - get_batch_row_to_modify(batch_no) { - // get row if batch already exists in table - const existing_batch_row = this.items_table.find((d) => d.batch_no === batch_no); - return existing_batch_row || this.get_existing_blank_row(); - } + get_row_to_modify_on_scan(item_code, batch_no) { + let cur_grid = this.frm.fields_dict[this.items_table_name].grid; - get_row_to_modify_on_scan(item_code) { - // get an existing item row to increment or blank row to modify - const existing_item_row = this.items_table.find((d) => d.item_code === item_code); - return existing_item_row || this.get_existing_blank_row(); + // Check if batch is scanned and table has batch no field + let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field); + let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field); + + const matching_row = (row) => { + const item_match = row.item_code == item_code; + const batch_match = row.batch_no == batch_no; + const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); + + return item_match + && (!is_batch_no_scan || batch_match) + && (!check_max_qty || qty_in_limit) + } + + return this.items_table.find(matching_row) || this.get_existing_blank_row(); } get_existing_blank_row() { @@ -194,4 +197,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.scan_barcode_field.set_value(""); refresh_field(this.items_table_name); } + show_alert(msg, indicator, duration=3) { + frappe.show_alert({message: msg, indicator: indicator}, duration); + } }; diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 13b74b5eb16..799406cd79e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -158,6 +158,19 @@ frappe.ui.form.on('Pick List', { get_query_filters: get_query_filters }); }); + }, + scan_barcode: (frm) => { + const opts = { + frm, + items_table_name: 'locations', + qty_field: 'picked_qty', + max_qty_field: 'qty', + dont_allow_new_row: true, + prompt_qty: frm.doc.prompt_qty, + serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field. + }; + const barcode_scanner = new erpnext.utils.BarcodeScanner(opts); + barcode_scanner.process_scan(); } }); diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index e984c082d48..ff209097a01 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -17,6 +17,11 @@ "parent_warehouse", "get_item_locations", "section_break_6", + "scan_barcode", + "column_break_13", + "scan_mode", + "prompt_qty", + "section_break_15", "locations", "amended_from", "print_settings_section", @@ -36,6 +41,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.docstatus", "fieldname": "section_break_6", "fieldtype": "Section Break" }, @@ -126,11 +132,38 @@ "fieldtype": "Check", "label": "Group Same Items", "print_hide": 1 + }, + { + "fieldname": "section_break_15", + "fieldtype": "Section Break" + }, + { + "fieldname": "scan_barcode", + "fieldtype": "Data", + "label": "Scan Barcode", + "options": "Barcode" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If checked, picked qty won't automatically be fulfilled on submit of pick list.", + "fieldname": "scan_mode", + "fieldtype": "Check", + "label": "Scan Mode" + }, + { + "default": "0", + "fieldname": "prompt_qty", + "fieldtype": "Check", + "label": "Prompt Qty" } ], "is_submittable": 1, "links": [], - "modified": "2022-04-21 07:56:40.646473", + "modified": "2022-05-11 09:09:53.029312", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 70d2f23070c..6b0e928baee 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -41,8 +41,15 @@ class PickList(Document): def before_submit(self): update_sales_orders = set() for item in self.locations: - # if the user has not entered any picked qty, set it to stock_qty, before submit - if item.picked_qty == 0: + if self.scan_mode and item.picked_qty < item.stock_qty: + frappe.throw( + _( + "Row {0} picked quantity is less than the required quantity, additional {1} {2} required." + ).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom), + title=_("Pick List Incomplete"), + ) + elif not self.scan_mode and item.picked_qty == 0: + # if the user has not entered any picked qty, set it to stock_qty, before submit item.picked_qty = item.stock_qty if item.sales_order_item: diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index a96ebfcdee6..a6f8c0db458 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -202,4 +202,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index e545b8ea5c3..9a854311100 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -170,6 +170,7 @@ "options": "Warehouse" }, { + "depends_on": "eval:!doc.docstatus", "fieldname": "section_break_22", "fieldtype": "Section Break" }, @@ -182,7 +183,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-27 08:57:47.161959", + "modified": "2022-05-11 09:10:26.327652", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation",