Merge pull request #30950 from marination/actual-qty-total-js-reactive

fix: Set actual qty and basic rate in SE on warehouse triggers (`get_warehouse_details`)
diff --git a/.eslintrc b/.eslintrc
index 46fb354..276d6ff 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 f72b85c..3ae1234 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -10,6 +10,12 @@
 		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 @@
 			.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 @@
 
 	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 @@
 			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 @@
 	}
 
 	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 @@
 		}
 	}
 
-	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 @@
 		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 13b74b5..799406c 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -158,6 +158,19 @@
 				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 e984c08..ff20909 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 d552c09..7dc3ba0 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -41,8 +41,15 @@
 	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 a96ebfc..a6f8c0d 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 e545b8e..9a85431 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",