From 93a597410e79f7425a83a1bde4d656a9631cd6c1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 9 Feb 2026 14:51:20 +0530 Subject: [PATCH 1/2] feat: option to enable serial / batch features (cherry picked from commit 82c3da5b1efb0ee9043c03151176fd0a2f81e474) # Conflicts: # erpnext/patches.txt --- .../asset_capitalization.js | 1 + erpnext/patches.txt | 4 ++ .../v16_0/enable_serial_batch_setting.py | 9 +++ erpnext/public/js/controllers/transaction.js | 1 + erpnext/public/js/utils.js | 65 +++++++++++++++++++ erpnext/setup/utils.py | 2 + erpnext/stock/doctype/item/item.js | 18 +++++ erpnext/stock/doctype/item/item.json | 6 +- erpnext/stock/doctype/item/item.py | 13 ++++ erpnext/stock/doctype/pick_list/pick_list.js | 2 + .../serial_and_batch_bundle.py | 10 +++ .../stock/doctype/stock_entry/stock_entry.js | 1 + .../stock_reconciliation.js | 2 + .../stock_settings/stock_settings.json | 18 ++--- .../doctype/stock_settings/stock_settings.py | 22 +++++++ .../subcontracting_receipt.js | 1 + 16 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 erpnext/patches/v16_0/enable_serial_batch_setting.py diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 3857f6411f5..c7f17bc48a3 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -16,6 +16,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { this.show_general_ledger(); + erpnext.toggle_serial_batch_fields(this.frm); if ( (this.frm.doc.stock_items && this.frm.doc.stock_items.length) || diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 51c27e774ec..3b8c6acd836 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -468,4 +468,8 @@ erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v15_0.delete_quotation_lost_record_detail erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2 +<<<<<<< HEAD erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po +======= +erpnext.patches.v16_0.enable_serial_batch_setting +>>>>>>> 82c3da5b1e (feat: option to enable serial / batch features) diff --git a/erpnext/patches/v16_0/enable_serial_batch_setting.py b/erpnext/patches/v16_0/enable_serial_batch_setting.py new file mode 100644 index 00000000000..de46f591b99 --- /dev/null +++ b/erpnext/patches/v16_0/enable_serial_batch_setting.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + if not frappe.get_all("Serial No", limit=1) and not frappe.get_all("Batch", limit=1): + return + + frappe.db.set_single_value("Stock Settings", "enable_serial_and_batch_no_for_item", 1) + frappe.db.set_default("enable_serial_and_batch_no_for_item", 1) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 744a78222de..6ccf558f039 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -580,6 +580,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.validate_has_items(); erpnext.utils.view_serial_batch_nos(this.frm); this.set_route_options_for_new_doc(); + erpnext.toggle_serial_batch_fields(this.frm); } set_route_options_for_new_doc() { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 41e7a7cad81..07860741ece 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -19,6 +19,71 @@ $.extend(erpnext, { return currency_list; }, + toggle_serial_batch_fields(frm) { + let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0; + let fields = ["serial_and_batch_bundle", "use_serial_batch_fields", "serial_no", "batch_no"]; + + if ( + [ + "Stock Entry", + "Purchase Receipt", + "Purchase Invoice", + "Stock Reconciliation", + "Subcontracting Receipt", + ].includes(frm.doc.doctype) + ) { + fields.push("add_serial_batch_bundle"); + } + + if (["Stock Reconciliation"].includes(frm.doc.doctype)) { + fields.push("reconcile_all_serial_batch"); + } + + if (["Sales Invoice", "Delivery Note", "Pick List"].includes(frm.doc.doctype)) { + fields.push("pick_serial_and_batch"); + } + + if (["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(frm.doc.doctype)) { + fields.push("add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle"); + } + + let child_name = "items"; + if (frm.doc.doctype === "Pick List") { + child_name = "locations"; + } + + if (frm.doc.doctype === "Asset Capitalization") { + child_name = "stock_items"; + } + + fields.forEach((field) => { + frm.fields_dict[child_name].grid.update_docfield_property(field, "hidden", hide_fields); + + frm.fields_dict[child_name].grid.update_docfield_property( + field, + "in_list_view", + hide_fields ? 0 : 1 + ); + + if ( + frm.doc.doctype === "Subcontracting Receipt" && + !["add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle"].includes(field) + ) { + frm.fields_dict["supplied_items"].grid.update_docfield_property(field, "hidden", hide_fields); + + frm.fields_dict["supplied_items"].grid.update_docfield_property( + field, + "in_list_view", + hide_fields ? 0 : 1 + ); + + frm.fields_dict["supplied_items"].grid.reset_grid(); + } + }); + + frm.fields_dict[child_name].grid.reset_grid(); + }, + toggle_naming_series: function () { if ( cur_frm && diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 122503027b1..4a4ae55e85c 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -221,6 +221,8 @@ def set_defaults_for_tests(): frappe.db.set_default(key, value) frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + frappe.db.set_single_value("Stock Settings", "enable_serial_and_batch_no_for_item", 1) + def insert_record(records): from frappe.desk.page.setup_wizard.setup_wizard import make_records diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 6f13428c22b..af59eacd0df 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -84,7 +84,25 @@ frappe.ui.form.on("Item", { } }, + toggle_has_serial_batch_fields(frm) { + let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0; + + frm.toggle_display(["serial_no_series", "batch_number_series", "create_new_batch"], !hide_fields); + frm.toggle_enable(["has_serial_no", "has_batch_no"], !hide_fields); + + if (hide_fields) { + let description = __( + "To enable the Serial No and Batch No feature, please check the 'Enable Serial / Batch No for Item' checkbox in Stock Settings." + ); + + frm.set_df_property("has_serial_no", "description", description); + frm.set_df_property("has_batch_no", "description", description); + } + }, + refresh: function (frm) { + frm.trigger("toggle_has_serial_batch_fields"); + if (frm.doc.is_stock_item) { frm.add_custom_button( __("Stock Balance"), diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 820bf4b2730..444c2ec0e66 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -452,6 +452,7 @@ "fieldname": "batch_number_series", "fieldtype": "Data", "label": "Batch Number Series", + "show_description_on_click": 1, "translatable": 1 }, { @@ -493,7 +494,8 @@ "description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.", "fieldname": "serial_no_series", "fieldtype": "Data", - "label": "Serial Number Series" + "label": "Serial Number Series", + "show_description_on_click": 1 }, { "collapsible": 1, @@ -985,7 +987,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-02-05 17:20:35.605734", + "modified": "2026-03-05 16:29:31.653447", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 272595c8437..8dba7a1b7d1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -218,6 +218,7 @@ class Item(Document): self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() self.validate_item_tax_net_rate_range() + self.validate_allow_to_set_serial_batch() if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -226,6 +227,18 @@ class Item(Document): self.update_variants() self.update_item_price() + def validate_allow_to_set_serial_batch(self): + if not self.has_serial_no and not self.has_batch_no: + return + + if not frappe.db.get_single_value("Stock Settings", "enable_serial_and_batch_no_for_item"): + frappe.throw( + _( + "Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to set Serial No or Batch No for the item." + ).format(get_link_to_form("Stock Settings", "Stock Settings")), + title=_("Serial and Batch No for Item Disabled"), + ) + def validate_description(self): """Clean HTML description if set""" if ( diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 6e95243eb24..d1386c183e3 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -119,6 +119,8 @@ frappe.ui.form.on("Pick List", { refresh: (frm) => { frm.trigger("add_get_items_button"); frm.trigger("update_warehouse_property"); + erpnext.toggle_serial_batch_fields(frm); + if (frm.doc.docstatus === 1) { const status_completed = frm.doc.status === "Completed"; diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ba332231951..c987a364ade 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -107,6 +107,7 @@ class SerialandBatchBundle(Document): self.autoname() def validate(self): + self.validate_allow_to_set_serial_batch() if self.docstatus == 1 and self.voucher_detail_no: self.validate_voucher_detail_no() @@ -143,6 +144,15 @@ class SerialandBatchBundle(Document): self.calculate_qty_and_amount() self.set_child_details() + def validate_allow_to_set_serial_batch(self): + if not frappe.db.get_single_value("Stock Settings", "enable_serial_and_batch_no_for_item"): + frappe.throw( + _( + "Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to make Serial and Batch Bundle for the item." + ).format(get_link_to_form("Stock Settings", "Stock Settings")), + title=_("Serial and Batch No for Item Disabled"), + ) + def validate_serial_no_status(self): serial_nos = [d.serial_no for d in self.entries if d.serial_no] invalid_serial_nos = frappe.get_all( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 5d7704a6d03..3bdb5fb3596 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -245,6 +245,7 @@ frappe.ui.form.on("Stock Entry", { refresh: function (frm) { frm.trigger("get_items_from_transit_entry"); frm.trigger("toggle_warehouse_fields"); + erpnext.toggle_serial_batch_fields(frm); if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) { frm.trigger("validate_purpose_consumption"); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 450bc01a67c..ef4672899cc 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -76,6 +76,8 @@ frappe.ui.form.on("Stock Reconciliation", { }, refresh: function (frm) { + erpnext.toggle_serial_batch_fields(frm); + if (frm.doc.docstatus < 1) { frm.add_custom_button(__("Fetch Items from Warehouse"), function () { frm.events.get_items(frm); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index e2c55c3b8e3..ba195104564 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -38,6 +38,7 @@ "allow_internal_transfer_at_arms_length_price", "validate_material_transfer_warehouses", "serial_and_batch_item_settings_tab", + "enable_serial_and_batch_no_for_item", "section_break_7", "allow_existing_serial_no", "do_not_use_batchwise_valuation", @@ -48,9 +49,8 @@ "use_serial_batch_fields", "do_not_update_serial_batch_on_creation_of_auto_bundle", "allow_negative_stock_for_batch", - "serial_and_batch_bundle_section", - "set_serial_and_batch_bundle_naming_based_on_naming_series", "section_break_gnhq", + "set_serial_and_batch_bundle_naming_based_on_naming_series", "use_naming_series", "column_break_wslv", "naming_series_prefix", @@ -158,6 +158,7 @@ "label": "Convert Item Description to Clean HTML in Transactions" }, { + "depends_on": "enable_serial_and_batch_no_for_item", "fieldname": "section_break_7", "fieldtype": "Section Break", "label": "Serial & Batch Item Settings" @@ -487,11 +488,6 @@ "fieldtype": "Check", "label": "Auto Reserve Stock" }, - { - "fieldname": "serial_and_batch_bundle_section", - "fieldtype": "Section Break", - "label": "Serial and Batch Bundle" - }, { "default": "0", "fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series", @@ -499,6 +495,7 @@ "label": "Set Serial and Batch Bundle Naming Based on Naming Series" }, { + "depends_on": "enable_serial_and_batch_no_for_item", "fieldname": "section_break_gnhq", "fieldtype": "Section Break" }, @@ -554,6 +551,11 @@ "fieldname": "allow_negative_stock_for_batch", "fieldtype": "Check", "label": "Allow Negative Stock for Batch" + }, + { + "fieldname": "enable_serial_and_batch_no_for_item", + "fieldtype": "Check", + "label": "Enable Serial / Batch No for Item" } ], "hide_toolbar": 1, @@ -562,7 +564,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-02-25 09:56:34.105949", + "modified": "2026-02-25 10:56:34.105949", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 2ab8c93bcb7..8ec3e9865d9 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -47,6 +47,7 @@ class StockSettings(Document): disable_serial_no_and_batch_selector: DF.Check do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check do_not_use_batchwise_valuation: DF.Check + enable_serial_and_batch_no_for_item: DF.Check enable_stock_reservation: DF.Check item_group: DF.Link | None item_naming_by: DF.Literal["Item Code", "Naming Series"] @@ -82,6 +83,7 @@ class StockSettings(Document): "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input", "use_serial_batch_fields", + "enable_serial_and_batch_no_for_item", "set_serial_and_batch_bundle_naming_based_on_naming_series", ]: frappe.db.set_default(key, self.get(key, "")) @@ -104,6 +106,7 @@ class StockSettings(Document): ) self.validate_warehouses() + self.validate_serial_and_batch_no_settings() self.cant_change_valuation_method() self.validate_clean_description_html() self.validate_pending_reposts() @@ -112,6 +115,25 @@ class StockSettings(Document): self.change_precision_for_for_sales() self.change_precision_for_purchase() + def validate_serial_and_batch_no_settings(self): + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + + if doc_before_save.enable_serial_and_batch_no_for_item == self.enable_serial_and_batch_no_for_item: + return + + if ( + doc_before_save.enable_serial_and_batch_no_for_item + and not self.enable_serial_and_batch_no_for_item + ): + if frappe.get_all("Serial and Batch Bundle", filters={"docstatus": 1}, limit=1, pluck="name"): + frappe.throw( + _( + "Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch." + ) + ) + def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] for field in warehouse_fields: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 4e502793068..3339cff689c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -30,6 +30,7 @@ frappe.ui.form.on("Subcontracting Receipt", { refresh: (frm) => { frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" }; + erpnext.toggle_serial_batch_fields(frm); if (frm.doc.docstatus === 1) { frm.add_custom_button( __("Stock Ledger"), From 0685a9241e022d1997147c704a552a7726ddb7f4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Mar 2026 12:44:55 +0530 Subject: [PATCH 2/2] chore: fix conflicts Remove merge conflict markers and update patch list. --- erpnext/patches.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3b8c6acd836..dfff6af431a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -468,8 +468,5 @@ erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v15_0.delete_quotation_lost_record_detail erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2 -<<<<<<< HEAD erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po -======= erpnext.patches.v16_0.enable_serial_batch_setting ->>>>>>> 82c3da5b1e (feat: option to enable serial / batch features)