diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b96e29979a8..5870cd9c9da 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -62,6 +62,7 @@ "items_section", "update_stock", "scan_barcode", + "last_scanned_warehouse", "items", "pricing_rule_details", "pricing_rules", @@ -1569,6 +1570,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 83e704734ce..ca31f1de4ec 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -47,6 +47,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "col_break_warehouse", "update_stock", "set_warehouse", @@ -1644,6 +1645,13 @@ "label": "Select Dispatch Address ", "options": "Address", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 07b97920ae2..816a6bfeded 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -45,6 +45,7 @@ "items_section", "scan_barcode", "update_stock", + "last_scanned_warehouse", "column_break_39", "set_warehouse", "set_target_warehouse", @@ -2177,6 +2178,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 59b44a22e61..cfea482d217 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -41,8 +41,9 @@ "ignore_pricing_rule", "before_items_section", "scan_barcode", - "set_from_warehouse", + "last_scanned_warehouse", "items_col_break", + "set_from_warehouse", "set_warehouse", "items_section", "items", @@ -1294,6 +1295,13 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, @@ -1301,7 +1309,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-04-09 16:54:08.836106", + "modified": "2025-07-31 17:19:40.816883", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c9c589b6599..cab3d48c7b7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -6,6 +6,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup() { super.setup(); let me = this; + this.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm }); this.set_fields_onload_for_line_item(); this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; @@ -473,8 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe scan_barcode() { frappe.flags.dialog_set = false; - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.process_scan(); + this.barcode_scanner.process_scan(); } barcode(doc, cdt, cdn) { diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index d6ef7944cee..0719e5ed99f 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -12,6 +12,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.batch_no_field = opts.batch_no_field || "batch_no"; this.uom_field = opts.uom_field || "uom"; this.qty_field = opts.qty_field || "qty"; + this.warehouse_field = opts.warehouse_field || "warehouse"; // 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. @@ -20,7 +21,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { 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]; // optional sound name to play when scan either fails or passes. // see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds @@ -34,8 +34,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch_no: "LOT12", // present if batch was scanned // serial_no: "987XYZ", // present if serial no was scanned // uom: "Kg", // present if barcode UOM is different from default + // warehouse: "Store-001", // present if warehouse was found (location-first scanning) // } this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode"; + this.has_last_scanned_warehouse = frappe.meta.has_field(this.frm.doctype, "last_scanned_warehouse"); } process_scan() { @@ -50,14 +52,31 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { 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"); + if ( + !data || + Object.keys(data).length === 0 || + (data.warehouse && !this.has_last_scanned_warehouse) + ) { + this.show_alert( + this.has_last_scanned_warehouse + ? __("Cannot find Item or Warehouse with this Barcode") + : __("Cannot find Item with this Barcode"), + "red" + ); this.clean_up(); this.play_fail_sound(); reject(); return; } + // Handle warehouse scanning + if (data.warehouse) { + this.handle_warehouse_scan(data); + this.play_success_sound(); + resolve(); + return; + } + me.update_table(data) .then((row) => { this.play_success_sound(); @@ -77,6 +96,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { method: this.scan_api, args: { search_value: input, + ctx: { + set_warehouse: this.frm.doc.set_warehouse, + company: this.frm.doc.company, + }, }, }) .then((r) => { @@ -89,11 +112,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; frappe.flags.trigger_from_barcode_scanner = true; - const { item_code, barcode, batch_no, serial_no, uom } = data; + const { item_code, barcode, batch_no, serial_no, uom, default_warehouse } = data; - let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode); + const warehouse = this.has_last_scanned_warehouse + ? this.frm.doc.last_scanned_warehouse || default_warehouse + : null; - this.is_new_row = false; + let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse); + const is_new_row = !row?.item_code; if (!row) { if (this.dont_allow_new_row) { this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); @@ -101,7 +127,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { 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); @@ -120,12 +145,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_selector_trigger_flag(data), () => this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => { - this.show_scan_message(row.idx, row.item_code, qty); + this.show_scan_message(row.idx, !is_new_row, qty); }), () => this.set_barcode_uom(row, uom), () => this.set_serial_no(row, serial_no), () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), + () => this.set_warehouse(row, warehouse), () => this.clean_up(), () => this.revert_selector_flag(), () => resolve(row), @@ -386,9 +412,17 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - show_scan_message(idx, exist = null, qty = 1) { + async set_warehouse(row, warehouse) { + const warehouse_field = this.get_warehouse_field(); + + if (warehouse && frappe.meta.has_field(row.doctype, warehouse_field)) { + await frappe.model.set_value(row.doctype, row.name, warehouse_field, warehouse); + } + } + + show_scan_message(idx, is_existing_row = false, qty = 1) { // show new row or qty increase toast - if (exist) { + if (is_existing_row) { this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green"); } else { this.show_alert(__("Row #{0}: Item added", [idx]), "green"); @@ -404,13 +438,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return is_duplicate; } - get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) { + get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; // 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 warehouse_field = this.get_warehouse_field(); + let has_warehouse_field = frappe.meta.has_field(cur_grid.doctype, warehouse_field); + const matching_row = (row) => { const item_match = row.item_code == item_code; const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no; @@ -418,20 +455,94 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); const item_scanned = row.has_item_scanned; + let warehouse_match = true; + if (has_warehouse_field) { + if (warehouse) { + warehouse_match = row[warehouse_field] === warehouse; + } else { + warehouse_match = !row[warehouse_field]; + } + } + return ( item_match && uom_match && + warehouse_match && !item_scanned && (!is_batch_no_scan || batch_match) && (!check_max_qty || qty_in_limit) ); }; - return this.items_table.find(matching_row) || this.get_existing_blank_row(); + const items_table = this.frm.doc[this.items_table_name] || []; + + return items_table.find(matching_row) || items_table.find((d) => !d.item_code); } - get_existing_blank_row() { - return this.items_table.find((d) => !d.item_code); + setup_last_scanned_warehouse() { + this.frm.set_df_property("last_scanned_warehouse", "options", "Warehouse"); + this.frm.set_df_property("last_scanned_warehouse", "fieldtype", "Link"); + this.frm.set_df_property("last_scanned_warehouse", "formatter", function (value, df, options, doc) { + const link_formatter = frappe.form.get_formatter(df.fieldtype); + const link_value = link_formatter(value, df, options, doc); + + if (!value) { + return link_value; + } + + const clear_btn = ` + + ${frappe.utils.icon("close", "xs", "es-icon")} + + `; + + return link_value + clear_btn; + }); + + this.frm.$wrapper.on("click", ".btn-clear-last-scanned-warehouse", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.clear_warehouse_context(); + }); + } + + handle_warehouse_scan(data) { + const warehouse = data.warehouse; + const warehouse_field = this.get_warehouse_field(); + const warehouse_field_label = frappe.meta.get_label(this.items_table_name, warehouse_field); + + if (!this.last_scanned_warehouse_initialized) { + this.setup_last_scanned_warehouse(); + this.last_scanned_warehouse_initialized = true; + } + + this.frm.set_value("last_scanned_warehouse", warehouse); + this.show_alert( + __("{0} will be set as the {1} in subsequently scanned items", [ + __(warehouse).bold(), + __(warehouse_field_label).bold(), + ]), + "green", + 6 + ); + } + + clear_warehouse_context() { + this.frm.set_value("last_scanned_warehouse", null); + this.show_alert( + __( + "The last scanned warehouse has been cleared and won't be set in the subsequently scanned items" + ), + "blue", + 6 + ); + } + + get_warehouse_field() { + if (typeof this.warehouse_field === "function") { + return this.warehouse_field(this.frm.doc); + } + return this.warehouse_field; } play_success_sound() { diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 29a2696470f..d17e4f416a4 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -593,3 +593,11 @@ body[data-route="pos"] { .frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { white-space: normal; } + +.btn-clear-last-scanned-warehouse { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + z-index: 1; +} diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index b3aaae99c4d..ae5b980bb9b 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -33,6 +33,7 @@ "ignore_pricing_rule", "items_section", "scan_barcode", + "last_scanned_warehouse", "items", "sec_break23", "total_qty", @@ -1094,13 +1095,20 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2025-05-27 16:04:39.208077", + "modified": "2025-07-31 17:23:48.875382", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1199,4 +1207,4 @@ "states": [], "timeline_field": "party_name", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a3219cac8f0..1542721d117 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -41,6 +41,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "column_break_28", "set_warehouse", "reserve_stock", @@ -1657,6 +1658,13 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 3a9cca1c418..bdfbbb93916 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -38,6 +38,7 @@ "ignore_pricing_rule", "items_section", "scan_barcode", + "last_scanned_warehouse", "col_break_warehouse", "set_warehouse", "set_target_warehouse", @@ -1390,6 +1391,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-truck", diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 60725b9ce9c..0c467df381c 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -20,9 +20,9 @@ "amended_from", "warehouse_section", "scan_barcode", - "column_break_13", - "set_from_warehouse", + "last_scanned_warehouse", "column_break5", + "set_from_warehouse", "set_warehouse", "items_section", "items", @@ -350,22 +350,25 @@ "fieldname": "column_break_35", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List" + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-28 15:13:49.000037", + "modified": "2025-07-31 17:19:01.166208", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index cef346a6d85..603f4a121d1 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -40,6 +40,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "column_break_31", "set_warehouse", "set_from_warehouse", @@ -1285,6 +1286,13 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 51455ef0d24..67918ee1dfd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1003,6 +1003,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle setup() { var me = this; + this.barcode_scanner = new erpnext.utils.BarcodeScanner({ + frm: this.frm, + warehouse_field: (doc) => { + return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse"; + }, + }); + this.setup_posting_date_time_check(); this.frm.fields_dict.bom_no.get_query = function () { @@ -1130,8 +1137,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle scan_barcode() { frappe.flags.dialog_set = false; - const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm }); - barcode_scanner.process_scan(); + this.barcode_scanner.process_scan(); } on_submit() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index adec80dcab2..023dca5bdf2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -50,6 +50,7 @@ "target_address_display", "sb0", "scan_barcode", + "last_scanned_warehouse", "items_section", "items", "get_stock_and_rate", @@ -691,6 +692,13 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 44dd2952409..d8dd2a7560a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -7,6 +7,7 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { setup(frm) { frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; + frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm }); }, onload: function (frm) { @@ -96,8 +97,7 @@ frappe.ui.form.on("Stock Reconciliation", { }, scan_barcode: function (frm) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm }); - barcode_scanner.process_scan(); + frm.barcode_scanner.process_scan(); }, scan_mode: function (frm) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 4712b8aeb16..76f2691be72 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -18,6 +18,7 @@ "set_warehouse", "section_break_22", "scan_barcode", + "last_scanned_warehouse", "column_break_12", "scan_mode", "sb9", @@ -178,6 +179,13 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-upload-alt", diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index bc646fae45c..6e66c100466 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -81,3 +81,44 @@ class TestStockUtilities(FrappeTestCase, StockTestMixin): self.assertEqual(serial_scan["serial_no"], serial.name) self.assertEqual(serial_scan["has_batch_no"], 0) self.assertEqual(serial_scan["has_serial_no"], 1) + + def test_barcode_scanning_of_warehouse(self): + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Warehouse for Barcode", + "company": "_Test Company", + } + ).insert() + + warehouse_2 = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Warehouse for Barcode 2", + "company": "_Test Company", + } + ).insert() + + warehouse_scan = scan_barcode(warehouse.name) + self.assertEqual(warehouse_scan["warehouse"], warehouse.name) + + item_with_warehouse = self.make_item( + properties={ + "item_defaults": [{"company": "_Test Company", "default_warehouse": warehouse.name}], + "barcodes": [{"barcode": "w12345"}], + } + ) + + item_scan = scan_barcode("w12345") + self.assertEqual(item_scan["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan.get("default_warehouse"), None) + + ctx = {"company": "_Test Company"} + item_scan_with_ctx = scan_barcode("w12345", ctx=ctx) + self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse.name) + + ctx = {"company": "_Test Company", "set_warehouse": warehouse_2.name} + item_scan_with_ctx = scan_barcode("w12345", ctx=ctx) + self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse_2.name) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 637d56f093a..c863960d7a0 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -584,13 +584,24 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool @frappe.whitelist() -def scan_barcode(search_value: str) -> BarcodeScanResult: +def scan_barcode(search_value: str, ctx: dict | str | None = None) -> BarcodeScanResult: def set_cache(data: BarcodeScanResult): frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120) + _update_item_info(data, ctx) def get_cache() -> BarcodeScanResult | None: - if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"): - return data + data = frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}") + if not data: + return + + _update_item_info(data, ctx) + return data + + if ctx is None: + ctx = frappe._dict() + + else: + ctx = frappe.parse_json(ctx) if scan_data := get_cache(): return scan_data @@ -603,7 +614,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: as_dict=True, ) if barcode_data: - _update_item_info(barcode_data) set_cache(barcode_data) return barcode_data @@ -615,7 +625,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: as_dict=True, ) if serial_no_data: - _update_item_info(serial_no_data) set_cache(serial_no_data) return serial_no_data @@ -634,22 +643,36 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: ).format(search_value, batch_no_data.item_code) ) - _update_item_info(batch_no_data) set_cache(batch_no_data) return batch_no_data + warehouse = frappe.get_cached_value("Warehouse", search_value, ("name", "disabled"), as_dict=True) + if warehouse and not warehouse.disabled: + warehouse_data = {"warehouse": warehouse.name} + set_cache(warehouse_data) + return warehouse_data + return {} -def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]: - if item_code := scan_result.get("item_code"): - if item_info := frappe.get_cached_value( - "Item", - item_code, - ["has_batch_no", "has_serial_no"], - as_dict=True, - ): - scan_result.update(item_info) +def _update_item_info(scan_result: dict[str, str | None], ctx: dict | None = None) -> dict[str, str | None]: + from erpnext.stock.get_item_details import get_item_warehouse + + item_code = scan_result.get("item_code") + if not item_code: + return scan_result + + if item_info := frappe.get_cached_value( + "Item", + item_code, + ("has_batch_no", "has_serial_no"), + as_dict=True, + ): + scan_result.update(item_info) + + if ctx and (warehouse := get_item_warehouse(frappe._dict(name=item_code), ctx, overwrite_warehouse=True)): + scan_result["default_warehouse"] = warehouse + return scan_result