Merge pull request #26157 from nextchamp-saqib/pos-fixes-9

refactor(pos): use pos invoice item name as unique identifier
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ae3f9e3..c827368 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -241,8 +241,8 @@
 			events: {
 				get_frm: () => this.frm,
 
-				cart_item_clicked: (item_code, batch_no, uom, rate) => {
-					const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
+				cart_item_clicked: (item) => {
+					const item_row = this.get_item_from_frm(item);
 					this.item_details.toggle_item_details_section(item_row);
 				},
 
@@ -273,17 +273,15 @@
 					this.cart.toggle_numpad(minimize);
 				},
 
-				form_updated: (cdt, cdn, fieldname, value) => {
-					const item_row = frappe.model.get_doc(cdt, cdn);
-					if (item_row && item_row[fieldname] != value) {
-
-						const { item_code, batch_no, uom, rate } = this.item_details.current_item;
-						const event = {
-							field: fieldname,
+				form_updated: (item, field, value) => {
+					const item_row = frappe.model.get_doc(item.doctype, item.name);
+					if (item_row && item_row[field] != value) {
+						const args = {
+							field,
 							value,
-							item: { item_code, batch_no, uom, rate }
-						}
-						return this.on_cart_update(event)
+							item: this.item_details.current_item
+						};
+						return this.on_cart_update(args);
 					}
 
 					return Promise.resolve();
@@ -300,19 +298,18 @@
 				set_value_in_current_cart_item: (selector, value) => {
 					this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item);
 				},
-				clone_new_batch_item_in_frm: (batch_serial_map, current_item) => {
+				clone_new_batch_item_in_frm: (batch_serial_map, item) => {
 					// called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches
 					// for each unique batch new item row is added in the form & cart
 					Object.keys(batch_serial_map).forEach(batch => {
-						const { item_code, batch_no } = current_item;
-						const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no);
+						const item_to_clone = this.frm.doc.items.find(i => i.name == item.name);
 						const new_row = this.frm.add_child("items", { ...item_to_clone });
 						// update new serialno and batch
 						new_row.batch_no = batch;
 						new_row.serial_no = batch_serial_map[batch].join(`\n`);
 						new_row.qty = batch_serial_map[batch].length;
 						this.frm.doc.items.forEach(row => {
-							if (item_code === row.item_code) {
+							if (item.item_code === row.item_code) {
 								this.update_cart_html(row);
 							}
 						});
@@ -321,8 +318,8 @@
 				remove_item_from_cart: () => this.remove_item_from_cart(),
 				get_item_stock_map: () => this.item_stock_map,
 				close_item_details: () => {
-					this.item_details.toggle_item_details_section(undefined);
-					this.cart.prev_action = undefined;
+					this.item_details.toggle_item_details_section(null);
+					this.cart.prev_action = null;
 					this.cart.toggle_item_highlight();
 				},
 				get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse)
@@ -506,50 +503,47 @@
 		let item_row = undefined;
 		try {
 			let { field, value, item } = args;
-			const { item_code, batch_no, serial_no, uom, rate } = item;
-			item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
+			item_row = this.get_item_from_frm(item);
+			const item_row_exists = !$.isEmptyObject(item_row);
 
-			const item_selected_from_selector = field === 'qty' && value === "+1"
+			const from_selector = field === 'qty' && value === "+1";
+			if (from_selector)
+				value = flt(item_row.qty) + flt(value);
 
-			if (item_row) {
-				item_selected_from_selector && (value = item_row.qty + flt(value))
-
-				field === 'qty' && (value = flt(value));
+			if (item_row_exists) {
+				if (field === 'qty')
+					value = flt(value);
 
 				if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) {
 					const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value;
 					await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
 				}
 
-				if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
+				if (this.is_current_item_being_edited(item_row) || from_selector) {
 					await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
 					this.update_cart_html(item_row);
 				}
 
 			} else {
-				if (!this.frm.doc.customer) {
-					frappe.dom.unfreeze();
-					frappe.show_alert({
-						message: __('You must select a customer before adding an item.'),
-						indicator: 'orange'
-					});
-					frappe.utils.play_sound("error");
+				if (!this.frm.doc.customer) 
+					return this.raise_customer_selection_alert();
+
+				const { item_code, batch_no, serial_no, rate } = item;
+
+				if (!item_code)
 					return;
-				}
-				if (!item_code) return;
 
-				item_selected_from_selector && (value = flt(value))
-
-				const args = { item_code, batch_no, rate, [field]: value };
+				const new_item = { item_code, batch_no, rate, [field]: value };
 
 				if (serial_no) {
 					await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
-					args['serial_no'] = serial_no;
+					new_item['serial_no'] = serial_no;
 				}
 
-				if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0;
+				if (field === 'serial_no')
+					new_item['qty'] = value.split(`\n`).length || 0;
 
-				item_row = this.frm.add_child('items', args);
+				item_row = this.frm.add_child('items', new_item);
 
 				if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
 					await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
@@ -558,8 +552,11 @@
 				
 				this.update_cart_html(item_row);
 
-				this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row);
-				this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
+				if (this.item_details.$component.is(':visible'))
+					this.edit_item_details_of(item_row);
+
+				if (this.check_serial_batch_selection_needed(item_row))
+					this.edit_item_details_of(item_row);
 			}
 
 		} catch (error) {
@@ -570,14 +567,33 @@
 		}
 	}
 
-	get_item_from_frm(item_code, batch_no, uom, rate) {
-		const has_batch_no = batch_no;
-		return this.frm.doc.items.find(
-			i => i.item_code === item_code
-				&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
-				&& (i.uom === uom)
-				&& (i.rate == rate)
-		);
+	raise_customer_selection_alert() {
+		frappe.dom.unfreeze();
+		frappe.show_alert({
+			message: __('You must select a customer before adding an item.'),
+			indicator: 'orange'
+		});
+		frappe.utils.play_sound("error");
+	}
+
+	get_item_from_frm({ name, item_code, batch_no, uom, rate }) {
+		let item_row = null;
+		if (name) {
+			item_row = this.frm.doc.items.find(i => i.name == name);
+		} else {
+			// if item is clicked twice from item selector
+			// then "item_code, batch_no, uom, rate" will help in getting the exact item
+			// to increase the qty by one
+			const has_batch_no = batch_no;
+			item_row = this.frm.doc.items.find(
+				i => i.item_code === item_code
+					&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
+					&& (i.uom === uom)
+					&& (i.rate == rate)
+			);
+		}
+
+		return item_row || {};
 	}
 
 	edit_item_details_of(item_row) {
@@ -585,9 +601,7 @@
 	}
 
 	is_current_item_being_edited(item_row) {
-		const { item_code, batch_no } = this.item_details.current_item;
-
-		return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true;
+		return item_row.name == this.item_details.current_item.name;
 	}
 
 	update_cart_html(item_row, remove_item) {
@@ -669,7 +683,7 @@
 
 	update_item_field(value, field_or_action) {
 		if (field_or_action === 'checkout') {
-			this.item_details.toggle_item_details_section(undefined);
+			this.item_details.toggle_item_details_section(null);
 		} else if (field_or_action === 'remove') {
 			this.remove_item_from_cart();
 		} else {
@@ -688,7 +702,7 @@
 			.then(() => {
 				frappe.model.clear_doc(doctype, name);
 				this.update_cart_html(current_item, true);
-				this.item_details.toggle_item_details_section(undefined);
+				this.item_details.toggle_item_details_section(null);
 				frappe.dom.unfreeze();
 			})
 			.catch(e => console.log(e));
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index f5019f5..7cae0e4 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -181,11 +181,8 @@
 				me.$totals_section.find(".edit-cart-btn").click();
 			}
 
-			const item_code = unescape($cart_item.attr('data-item-code'));
-			const batch_no = unescape($cart_item.attr('data-batch-no'));
-			const uom = unescape($cart_item.attr('data-uom'));
-			const rate = unescape($cart_item.attr('data-rate'));
-			me.events.cart_item_clicked(item_code, batch_no, uom, rate);
+			const item_row_name = unescape($cart_item.attr('data-row-name'));
+			me.events.cart_item_clicked({ name: item_row_name });
 			this.numpad_value = '';
 		});
 
@@ -521,25 +518,14 @@
 		}
 	}
 
-	get_cart_item({ item_code, batch_no, uom, rate }) {
-		const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
-		const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
-		const uom_attr = `[data-uom="${escape(uom)}"]`;
-		const rate_attr = `[data-rate="${escape(rate)}"]`;
-
-		const item_selector = batch_no ?
-			`.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`;
-
+	get_cart_item({ name }) {
+		const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
 		return this.$cart_items_wrapper.find(item_selector);
 	}
 
 	get_item_from_frm(item) {
 		const doc = this.events.get_frm().doc;
-		const { item_code, batch_no, uom, rate } = item;
-		const search_field = batch_no ? 'batch_no' : 'item_code';
-		const search_value = batch_no || item_code;
-
-		return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate);
+		return doc.items.find(i => i.name == item.name);
 	}
 
 	update_item_html(item, remove_item) {
@@ -564,10 +550,7 @@
 
 		if (!$item_to_update.length) {
 			this.$cart_items_wrapper.append(
-				`<div class="cart-item-wrapper"
-						data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
-						data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
-				</div>
+				`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
 				<div class="seperator"></div>`
 			)
 			$item_to_update = this.get_cart_item(item_data);
@@ -642,7 +625,7 @@
 
 		function get_item_image_html() {
 			const { image, item_name } = item_data;
-			if (image) {
+			if (!me.hide_images && image) {
 				return `
 					<div class="item-image">
 						<img
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index 5e09df8..6a4d3d5 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -2,6 +2,7 @@
 	constructor({ wrapper, events, settings }) {
 		this.wrapper = wrapper;
 		this.events = events;
+		this.hide_images = settings.hide_images;
 		this.allow_rate_change = settings.allow_rate_change;
 		this.allow_discount_change = settings.allow_discount_change;
 		this.current_item = {};
@@ -54,36 +55,28 @@
 		this.$dicount_section = this.$component.find('.discount-section');
 	}
 
-	has_item_has_changed(item) {
-		const { item_code, batch_no, uom, rate } = this.current_item;
-		const item_code_is_same = item && item_code === item.item_code;
-		const batch_is_same = item && batch_no == item.batch_no;
-		const uom_is_same = item && uom === item.uom;
-		const rate_is_same = item && rate === item.rate;
-		
-		if (!item)
-			return false;
-
-		if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same)
-			return false;
-
-		return true;
+	compare_with_current_item(item) {
+		// returns true if `item` is currently being edited
+		return item && item.name == this.current_item.name;
 	}
 
 	toggle_item_details_section(item) {
-		this.item_has_changed = this.has_item_has_changed(item);
+		const current_item_changed = !this.compare_with_current_item(item);
 
-		this.events.toggle_item_selector(this.item_has_changed);
-		this.toggle_component(this.item_has_changed);
+		// if item is null or highlighted cart item is clicked twice
+		const hide_item_details = !Boolean(item) || !current_item_changed;
+		
+		this.events.toggle_item_selector(!hide_item_details);
+		this.toggle_component(!hide_item_details);
 
-		if (this.item_has_changed) {
+		if (item && current_item_changed) {
 			this.doctype = item.doctype;
 			this.item_meta = frappe.get_meta(this.doctype);
 			this.name = item.name;
 			this.item_row = item;
 			this.currency = this.events.get_frm().doc.currency;
 
-			this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate };
+			this.current_item = item;
 
 			this.render_dom(item);
 			this.render_discount_dom(item);
@@ -132,7 +125,7 @@
 		this.$item_name.html(item_name);
 		this.$item_description.html(get_description_html());
 		this.$item_price.html(format_currency(price_list_rate, this.currency));
-		if (image) {
+		if (!this.hide_images && image) {
 			this.$item_image.html(
 				`<img 
 					onerror="cur_pos.item_details.handle_broken_image(this)"
@@ -180,7 +173,7 @@
 				df: {
 					...field_meta,
 					onchange: function() {
-						me.events.form_updated(me.doctype, me.name, fieldname, this.value);
+						me.events.form_updated(me.current_item, fieldname, this.value);
 					}
 				},
 				parent: this.$form_container.find(`.${fieldname}-control`),
@@ -218,22 +211,17 @@
 	bind_custom_control_change_event() {
 		const me = this;
 		if (this.rate_control) {
-			if (this.allow_rate_change) {
-				this.rate_control.df.onchange = function() {
-					if (this.value || flt(this.value) === 0) {
-						me.events.set_value_in_current_cart_item('rate', this.value);
-						me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
-							const item_row = frappe.get_doc(me.doctype, me.name);
-							const doc = me.events.get_frm().doc;
-							me.$item_price.html(format_currency(item_row.rate, doc.currency));
-							me.render_discount_dom(item_row);
-						});
-						me.current_item.rate = this.value;
-					}
-				};
-			} else {
-				this.rate_control.df.read_only = 1;
-			}
+			this.rate_control.df.onchange = function() {
+				if (this.value || flt(this.value) === 0) {
+					me.events.form_updated(me.current_item, 'rate', this.value).then(() => {
+						const item_row = frappe.get_doc(me.doctype, me.name);
+						const doc = me.events.get_frm().doc;
+						me.$item_price.html(format_currency(item_row.rate, doc.currency));
+						me.render_discount_dom(item_row);
+					});
+				}
+			};
+			this.rate_control.df.read_only = !this.allow_rate_change;
 			this.rate_control.refresh();
 		}
 
@@ -246,7 +234,7 @@
 			this.warehouse_control.df.reqd = 1;
 			this.warehouse_control.df.onchange = function() {
 				if (this.value) {
-					me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => {
+					me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
 						me.item_stock_map = me.events.get_item_stock_map();
 						const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
 						if (available_qty === undefined) {
@@ -278,7 +266,7 @@
 			this.serial_no_control.df.reqd = 1;
 			this.serial_no_control.df.onchange = async function() {
 				!me.current_item.batch_no && await me.auto_update_batch_no();
-				me.events.form_updated(me.doctype, me.name, 'serial_no', this.value);
+				me.events.form_updated(me.current_item, 'serial_no', this.value);
 			}
 			this.serial_no_control.refresh();
 		}
@@ -295,19 +283,12 @@
 					}
 				}
 			};
-			this.batch_no_control.df.onchange = function() {
-				me.events.set_value_in_current_cart_item('batch-no', this.value);
-				me.events.form_updated(me.doctype, me.name, 'batch_no', this.value);
-				me.current_item.batch_no = this.value;
-			}
 			this.batch_no_control.refresh();
 		}
 
 		if (this.uom_control) {
 			this.uom_control.df.onchange = function() {
-				me.events.set_value_in_current_cart_item('uom', this.value);
-				me.events.form_updated(me.doctype, me.name, 'uom', this.value);
-				me.current_item.uom = this.value;
+				me.events.form_updated(me.current_item, 'uom', this.value);
 
 				const item_row = frappe.get_doc(me.doctype, me.name);
 				me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value);
@@ -317,9 +298,9 @@
 
 		frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
 			const field_control = this[`${fieldname}_control`];
-			const item_is_same = !this.has_item_has_changed(item_row);
+			const item_row_is_being_edited = this.compare_with_current_item(item_row);
 
-			if (item_is_same && field_control && field_control.get_value() !== value) {
+			if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
 				field_control.set_value(value);
 				cur_pos.update_cart_html(item_row);
 			}
@@ -337,7 +318,9 @@
 				fields: ["batch_no", "name"]
 			});
 			const batch_serial_map = serials_with_batch_no.reduce((acc, r) => {
-				acc[r.batch_no] || (acc[r.batch_no] = []);
+				if (!acc[r.batch_no]) {
+					acc[r.batch_no] = [];
+				}
 				acc[r.batch_no] = [...acc[r.batch_no], r.name];
 				return acc;
 			}, {});
@@ -353,12 +336,10 @@
 			if (serial_nos_belongs_to_other_batch) {
 				this.serial_no_control.set_value(batch_serial_nos);
 				this.qty_control.set_value(batch_serial_map[batch_no].length);
-			}
 
-			delete batch_serial_map[batch_no];
-
-			if (serial_nos_belongs_to_other_batch)
+				delete batch_serial_map[batch_no];
 				this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
+			}
 		}
 	}
 
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 64c529e..dd7f143 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -232,7 +232,11 @@
 			uom = uom === "undefined" ? undefined : uom;
 			rate = rate === "undefined" ? undefined : rate;
 
-			me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }});
+			me.events.item_selected({
+				field: 'qty',
+				value: "+1",
+				item: { item_code, batch_no, serial_no, uom, rate }
+			});
 			me.set_search_value('');
 		});