Merge pull request #32799 from rohitwaghchaure/fix-scan-qrcode-functionality

fix: Scan Barcode UX
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 3f85668..4bb1865 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -8,6 +8,7 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
+  "has_item_scanned",
   "item_code",
   "col_break1",
   "item_name",
@@ -808,11 +809,19 @@
    "fieldtype": "Check",
    "label": "Grant Commission",
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "barcode",
+   "fieldname": "has_item_scanned",
+   "fieldtype": "Check",
+   "label": "Has Item Scanned",
+   "read_only": 1
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2021-10-05 12:23:47.506290",
+ "modified": "2022-11-02 12:52:39.125295",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Invoice Item",
@@ -820,5 +829,6 @@
  "owner": "Administrator",
  "permissions": [],
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index 7f1a1ec..77055f9 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -8,6 +8,7 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
+  "has_item_scanned",
   "item_code",
   "col_break1",
   "item_name",
@@ -872,12 +873,20 @@
    "label": "Purchase Order Item",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "barcode",
+   "fieldname": "has_item_scanned",
+   "fieldtype": "Check",
+   "label": "Has Item Scanned",
+   "read_only": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-10-26 11:38:36.119339",
+ "modified": "2022-11-02 12:53:12.693217",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice Item",
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index dd957c7..677ca78 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -341,6 +341,7 @@
 		this.set_dynamic_labels();
 		this.setup_sms();
 		this.setup_quality_inspection();
+		this.validate_has_items();
 	}
 
 	scan_barcode() {
@@ -348,6 +349,12 @@
 		barcode_scanner.process_scan();
 	}
 
+	validate_has_items () {
+		let table = this.frm.doc.items;
+		this.frm.has_items = (table && table.length
+			&& table[0].qty && table[0].item_code);
+	}
+
 	apply_default_taxes() {
 		var me = this;
 		var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index 83b108b..a4f74bd 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -47,42 +47,49 @@
 				return;
 			}
 
-			frappe
-				.call({
-					method: this.scan_api,
-					args: {
-						search_value: input,
-					},
-				})
-				.then((r) => {
-					const data = r && r.message;
-					if (!data || Object.keys(data).length === 0) {
-						this.show_alert(__("Cannot find Item with this Barcode"), "red");
-						this.clean_up();
-						this.play_fail_sound();
-						reject();
-						return;
-					}
+			this.scan_api_call(input, (r) => {
+				const data = r && r.message;
+				if (!data || Object.keys(data).length === 0) {
+					this.show_alert(__("Cannot find Item with this Barcode"), "red");
+					this.clean_up();
+					this.play_fail_sound();
+					reject();
+					return;
+				}
 
-					me.update_table(data).then(row => {
-						this.play_success_sound();
-						resolve(row);
-					}).catch(() => {
-						this.play_fail_sound();
-						reject();
-					});
+				me.update_table(data).then(row => {
+					this.play_success_sound();
+					resolve(row);
+				}).catch(() => {
+					this.play_fail_sound();
+					reject();
 				});
+			});
 		});
 	}
 
+	scan_api_call(input, callback) {
+		frappe
+			.call({
+				method: this.scan_api,
+				args: {
+					search_value: input,
+				},
+			})
+			.then((r) => {
+				callback(r);
+			});
+	}
+
 	update_table(data) {
 		return new Promise((resolve, reject) => {
 			let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
 
 			const {item_code, barcode, batch_no, serial_no, uom} = data;
 
-			let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom);
+			let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode);
 
+			this.is_new_row = false;
 			if (!row) {
 				if (this.dont_allow_new_row) {
 					this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
@@ -90,11 +97,13 @@
 					reject();
 					return;
 				}
+				this.is_new_row = true;
 
 				// 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.
 				this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name);
+				this.frm.has_items = false;
 			}
 
 			if (this.is_duplicate_serial_no(row, serial_no)) {
@@ -105,7 +114,7 @@
 
 			frappe.run_serially([
 				() => this.set_selector_trigger_flag(data),
-				() => this.set_item(row, item_code).then(qty => {
+				() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
 					this.show_scan_message(row.idx, row.item_code, qty);
 				}),
 				() => this.set_barcode_uom(row, uom),
@@ -136,7 +145,7 @@
 		frappe.flags.hide_serial_batch_dialog = false;
 	}
 
-	set_item(row, item_code) {
+	set_item(row, item_code, barcode, batch_no, serial_no) {
 		return new Promise(resolve => {
 			const increment = async (value = 1) => {
 				const item_data = {item_code: item_code};
@@ -149,12 +158,186 @@
 				frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
 					increment(value).then((value) => resolve(value));
 				});
+			} else if (this.frm.has_items) {
+				this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
 			} else {
 				increment().then((value) => resolve(value));
 			}
 		});
 	}
 
+	prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no) {
+		var me = this;
+		this.dialog = new frappe.ui.Dialog({
+			title: __("Scan barcode for item {0}", [item_code]),
+			fields: me.get_fields_for_dialog(row, item_code, barcode, batch_no, serial_no),
+		})
+
+		this.dialog.set_primary_action(__("Update"), () => {
+			const item_data = {item_code: item_code};
+			item_data[this.qty_field] = this.dialog.get_value("scanned_qty");
+			item_data["has_item_scanned"] = 1;
+
+			this.remaining_qty = flt(this.dialog.get_value("qty")) - flt(this.dialog.get_value("scanned_qty"));
+			frappe.model.set_value(row.doctype, row.name, item_data);
+
+			frappe.run_serially([
+				() => this.set_batch_no(row, this.dialog.get_value("batch_no")),
+				() => this.set_barcode(row, this.dialog.get_value("barcode")),
+				() => this.set_serial_no(row, this.dialog.get_value("serial_no")),
+				() => this.add_child_for_remaining_qty(row),
+				() => this.clean_up()
+			]);
+
+			this.dialog.hide();
+		});
+
+		this.dialog.show();
+
+		this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
+		this.$scan_btn.css("display", "inline");
+	}
+
+	get_fields_for_dialog(row, item_code, barcode, batch_no, serial_no) {
+		let fields = [
+			{
+				fieldtype: "Data",
+				fieldname: "barcode_scanner",
+				options: "Barcode",
+				label: __("Scan Barcode"),
+				onchange: (e) => {
+					if (!e) {
+						return;
+					}
+
+					if (e.target.value) {
+						this.scan_api_call(e.target.value, (r) => {
+							if (r.message) {
+								this.update_dialog_values(item_code, r);
+							}
+						})
+					}
+				}
+			},
+			{
+				fieldtype: "Section Break",
+			},
+			{
+				fieldtype: "Float",
+				fieldname: "qty",
+				label: __("Quantity to Scan"),
+				default: row[this.qty_field] || 1,
+			},
+			{
+				fieldtype: "Column Break",
+				fieldname: "column_break_1",
+			},
+			{
+				fieldtype: "Float",
+				read_only: 1,
+				fieldname: "scanned_qty",
+				label: __("Scanned Quantity"),
+				default: 1,
+			},
+			{
+				fieldtype: "Section Break",
+			}
+		]
+
+		if (batch_no) {
+			fields.push({
+				fieldtype: "Link",
+				fieldname: "batch_no",
+				options: "Batch No",
+				label: __("Batch No"),
+				default: batch_no,
+				read_only: 1,
+				hidden: 1
+			});
+		}
+
+		if (serial_no) {
+			fields.push({
+				fieldtype: "Small Text",
+				fieldname: "serial_no",
+				label: __("Serial Nos"),
+				default: serial_no,
+				read_only: 1,
+			});
+		}
+
+		if (barcode) {
+			fields.push({
+				fieldtype: "Data",
+				fieldname: "barcode",
+				options: "Barcode",
+				label: __("Barcode"),
+				default: barcode,
+				read_only: 1,
+				hidden: 1
+			});
+		}
+
+		return fields;
+	}
+
+	update_dialog_values(scanned_item, r) {
+		const {item_code, barcode, batch_no, serial_no} = r.message;
+
+		this.dialog.set_value("barcode_scanner", "");
+		if (item_code === scanned_item &&
+			(this.dialog.get_value("barcode") === barcode || batch_no || serial_no)) {
+
+			if (batch_no) {
+				this.dialog.set_value("batch_no", batch_no);
+			}
+
+			if (serial_no) {
+
+				this.validate_duplicate_serial_no(serial_no);
+				let serial_nos = this.dialog.get_value("serial_no") + "\n" + serial_no;
+				this.dialog.set_value("serial_no", serial_nos);
+			}
+
+			let qty = flt(this.dialog.get_value("scanned_qty")) + 1.0;
+			this.dialog.set_value("scanned_qty", qty);
+		}
+	}
+
+	validate_duplicate_serial_no(serial_no) {
+		let serial_nos = this.dialog.get_value("serial_no") ?
+			this.dialog.get_value("serial_no").split("\n") : [];
+
+		if (in_list(serial_nos, serial_no)) {
+			frappe.throw(__("Serial No {0} already scanned", [serial_no]));
+		}
+	}
+
+	add_child_for_remaining_qty(prev_row) {
+		if (this.remaining_qty && this.remaining_qty >0) {
+			let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
+			let row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
+
+			let ignore_fields = ["name", "idx", "batch_no", "barcode",
+				"received_qty", "serial_no", "has_item_scanned"];
+
+			for (let key in prev_row) {
+				if (in_list(ignore_fields, key)) {
+					continue;
+				}
+
+				row[key] = prev_row[key];
+			}
+
+			row[this.qty_field] = this.remaining_qty;
+			if (this.qty_field == "qty" && frappe.meta.has_field(row.doctype, "stock_qty")) {
+				row["stock_qty"] = this.remaining_qty * row.conversion_factor;
+			}
+
+			this.frm.script_manager.trigger("item_code", row.doctype, row.name);
+		}
+	}
+
 	async set_serial_no(row, serial_no) {
 		if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
 			const existing_serial_nos = row[this.serial_no_field];
@@ -205,7 +388,7 @@
 		return is_duplicate;
 	}
 
-	get_row_to_modify_on_scan(item_code, batch_no, uom) {
+	get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
 		let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
 
 		// Check if batch is scanned and table has batch no field
@@ -214,12 +397,14 @@
 
 		const matching_row = (row) => {
 			const item_match = row.item_code == item_code;
-			const batch_match = row[this.batch_no_field] == batch_no;
+			const batch_match = (!row[this.batch_no_field] || row[this.batch_no_field] == batch_no);
 			const uom_match = !uom || row[this.uom_field] == uom;
 			const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
+			const item_scanned = row.has_item_scanned;
 
 			return item_match
 				&& uom_match
+				&& !item_scanned
 				&& (!is_batch_no_scan || batch_match)
 				&& (!check_max_qty || qty_in_limit)
 		}
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 77c3253..3229463 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -8,6 +8,7 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
+  "has_item_scanned",
   "item_code",
   "item_name",
   "col_break1",
@@ -809,13 +810,21 @@
    "label": "Purchase Order Item",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "barcode",
+   "fieldname": "has_item_scanned",
+   "fieldtype": "Check",
+   "label": "Has Item Scanned",
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-10-26 16:05:17.720768",
+ "modified": "2022-11-02 12:54:07.225623",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 474ee92..557bb59 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -8,6 +8,7 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
+  "has_item_scanned",
   "section_break_2",
   "item_code",
   "product_bundle",
@@ -996,12 +997,20 @@
   {
    "fieldname": "column_break_102",
    "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "depends_on": "barcode",
+   "fieldname": "has_item_scanned",
+   "fieldtype": "Check",
+   "label": "Has Item Scanned",
+   "read_only": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-10-26 16:06:02.524435",
+ "modified": "2022-11-02 12:49:28.746701",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 5fe11a2..95f4f5f 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -8,6 +8,7 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
+  "has_item_scanned",
   "section_break_2",
   "s_warehouse",
   "col_break1",
@@ -498,14 +499,14 @@
    "read_only": 1
   },
   {
-    "fieldname": "sco_rm_detail",
-    "fieldtype": "Data",
-    "hidden": 1,
-    "label": "SCO Supplied Item",
-    "no_copy": 1,
-    "print_hide": 1,
-    "read_only": 1
-   },
+   "fieldname": "sco_rm_detail",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "SCO Supplied Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
+  },
   {
    "default": "0",
    "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",
@@ -563,13 +564,21 @@
    "fieldname": "is_process_loss",
    "fieldtype": "Check",
    "label": "Is Process Loss"
+  },
+  {
+   "default": "0",
+   "depends_on": "barcode",
+   "fieldname": "has_item_scanned",
+   "fieldtype": "Check",
+   "label": "Has Item Scanned",
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-06-17 05:06:33.621264",
+ "modified": "2022-11-02 13:00:34.258828",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index 79c2fcc..7c3e151 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -7,6 +7,7 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
+  "has_item_scanned",
   "item_code",
   "item_name",
   "warehouse",
@@ -177,11 +178,18 @@
    "label": "Allow Zero Valuation Rate",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "depends_on": "barcode",
+   "fieldname": "has_item_scanned",
+   "fieldtype": "Data",
+   "label": "Has Item Scanned",
+   "read_only": 1
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2022-04-02 04:19:40.380587",
+ "modified": "2022-11-02 13:01:23.580937",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Reconciliation Item",