feat: option to enable serial / batch features

(cherry picked from commit 82c3da5b1e)

# Conflicts:
#	erpnext/patches.txt
This commit is contained in:
Rohit Waghchaure
2026-02-09 14:51:20 +05:30
committed by Mergify
parent 83f2fadbcf
commit 93a597410e
16 changed files with 165 additions and 10 deletions

View File

@@ -16,6 +16,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
refresh() { refresh() {
this.show_general_ledger(); this.show_general_ledger();
erpnext.toggle_serial_batch_fields(this.frm);
if ( if (
(this.frm.doc.stock_items && this.frm.doc.stock_items.length) || (this.frm.doc.stock_items && this.frm.doc.stock_items.length) ||

View File

@@ -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.v15_0.delete_quotation_lost_record_detail
erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2 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.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)

View File

@@ -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)

View File

@@ -580,6 +580,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.validate_has_items(); this.validate_has_items();
erpnext.utils.view_serial_batch_nos(this.frm); erpnext.utils.view_serial_batch_nos(this.frm);
this.set_route_options_for_new_doc(); this.set_route_options_for_new_doc();
erpnext.toggle_serial_batch_fields(this.frm);
} }
set_route_options_for_new_doc() { set_route_options_for_new_doc() {

View File

@@ -19,6 +19,71 @@ $.extend(erpnext, {
return currency_list; 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 () { toggle_naming_series: function () {
if ( if (
cur_frm && cur_frm &&

View File

@@ -221,6 +221,8 @@ def set_defaults_for_tests():
frappe.db.set_default(key, value) 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", "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): def insert_record(records):
from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.desk.page.setup_wizard.setup_wizard import make_records

View File

@@ -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) { refresh: function (frm) {
frm.trigger("toggle_has_serial_batch_fields");
if (frm.doc.is_stock_item) { if (frm.doc.is_stock_item) {
frm.add_custom_button( frm.add_custom_button(
__("Stock Balance"), __("Stock Balance"),

View File

@@ -452,6 +452,7 @@
"fieldname": "batch_number_series", "fieldname": "batch_number_series",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Batch Number Series", "label": "Batch Number Series",
"show_description_on_click": 1,
"translatable": 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.", "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", "fieldname": "serial_no_series",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Serial Number Series" "label": "Serial Number Series",
"show_description_on_click": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -985,7 +987,7 @@
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2026-02-05 17:20:35.605734", "modified": "2026-03-05 16:29:31.653447",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@@ -218,6 +218,7 @@ class Item(Document):
self.validate_auto_reorder_enabled_in_stock_settings() self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change() self.cant_change()
self.validate_item_tax_net_rate_range() self.validate_item_tax_net_rate_range()
self.validate_allow_to_set_serial_batch()
if not self.is_new(): if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") 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_variants()
self.update_item_price() 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): def validate_description(self):
"""Clean HTML description if set""" """Clean HTML description if set"""
if ( if (

View File

@@ -119,6 +119,8 @@ frappe.ui.form.on("Pick List", {
refresh: (frm) => { refresh: (frm) => {
frm.trigger("add_get_items_button"); frm.trigger("add_get_items_button");
frm.trigger("update_warehouse_property"); frm.trigger("update_warehouse_property");
erpnext.toggle_serial_batch_fields(frm);
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {
const status_completed = frm.doc.status === "Completed"; const status_completed = frm.doc.status === "Completed";

View File

@@ -107,6 +107,7 @@ class SerialandBatchBundle(Document):
self.autoname() self.autoname()
def validate(self): def validate(self):
self.validate_allow_to_set_serial_batch()
if self.docstatus == 1 and self.voucher_detail_no: if self.docstatus == 1 and self.voucher_detail_no:
self.validate_voucher_detail_no() self.validate_voucher_detail_no()
@@ -143,6 +144,15 @@ class SerialandBatchBundle(Document):
self.calculate_qty_and_amount() self.calculate_qty_and_amount()
self.set_child_details() 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): def validate_serial_no_status(self):
serial_nos = [d.serial_no for d in self.entries if d.serial_no] serial_nos = [d.serial_no for d in self.entries if d.serial_no]
invalid_serial_nos = frappe.get_all( invalid_serial_nos = frappe.get_all(

View File

@@ -245,6 +245,7 @@ frappe.ui.form.on("Stock Entry", {
refresh: function (frm) { refresh: function (frm) {
frm.trigger("get_items_from_transit_entry"); frm.trigger("get_items_from_transit_entry");
frm.trigger("toggle_warehouse_fields"); frm.trigger("toggle_warehouse_fields");
erpnext.toggle_serial_batch_fields(frm);
if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) { if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) {
frm.trigger("validate_purpose_consumption"); frm.trigger("validate_purpose_consumption");

View File

@@ -76,6 +76,8 @@ frappe.ui.form.on("Stock Reconciliation", {
}, },
refresh: function (frm) { refresh: function (frm) {
erpnext.toggle_serial_batch_fields(frm);
if (frm.doc.docstatus < 1) { if (frm.doc.docstatus < 1) {
frm.add_custom_button(__("Fetch Items from Warehouse"), function () { frm.add_custom_button(__("Fetch Items from Warehouse"), function () {
frm.events.get_items(frm); frm.events.get_items(frm);

View File

@@ -38,6 +38,7 @@
"allow_internal_transfer_at_arms_length_price", "allow_internal_transfer_at_arms_length_price",
"validate_material_transfer_warehouses", "validate_material_transfer_warehouses",
"serial_and_batch_item_settings_tab", "serial_and_batch_item_settings_tab",
"enable_serial_and_batch_no_for_item",
"section_break_7", "section_break_7",
"allow_existing_serial_no", "allow_existing_serial_no",
"do_not_use_batchwise_valuation", "do_not_use_batchwise_valuation",
@@ -48,9 +49,8 @@
"use_serial_batch_fields", "use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle", "do_not_update_serial_batch_on_creation_of_auto_bundle",
"allow_negative_stock_for_batch", "allow_negative_stock_for_batch",
"serial_and_batch_bundle_section",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
"section_break_gnhq", "section_break_gnhq",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
"use_naming_series", "use_naming_series",
"column_break_wslv", "column_break_wslv",
"naming_series_prefix", "naming_series_prefix",
@@ -158,6 +158,7 @@
"label": "Convert Item Description to Clean HTML in Transactions" "label": "Convert Item Description to Clean HTML in Transactions"
}, },
{ {
"depends_on": "enable_serial_and_batch_no_for_item",
"fieldname": "section_break_7", "fieldname": "section_break_7",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Serial & Batch Item Settings" "label": "Serial & Batch Item Settings"
@@ -487,11 +488,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Auto Reserve Stock" "label": "Auto Reserve Stock"
}, },
{
"fieldname": "serial_and_batch_bundle_section",
"fieldtype": "Section Break",
"label": "Serial and Batch Bundle"
},
{ {
"default": "0", "default": "0",
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series", "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" "label": "Set Serial and Batch Bundle Naming Based on Naming Series"
}, },
{ {
"depends_on": "enable_serial_and_batch_no_for_item",
"fieldname": "section_break_gnhq", "fieldname": "section_break_gnhq",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
@@ -554,6 +551,11 @@
"fieldname": "allow_negative_stock_for_batch", "fieldname": "allow_negative_stock_for_batch",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Negative Stock for Batch" "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, "hide_toolbar": 1,
@@ -562,7 +564,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-02-25 09:56:34.105949", "modified": "2026-02-25 10:56:34.105949",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -47,6 +47,7 @@ class StockSettings(Document):
disable_serial_no_and_batch_selector: DF.Check disable_serial_no_and_batch_selector: DF.Check
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
do_not_use_batchwise_valuation: DF.Check do_not_use_batchwise_valuation: DF.Check
enable_serial_and_batch_no_for_item: DF.Check
enable_stock_reservation: DF.Check enable_stock_reservation: DF.Check
item_group: DF.Link | None item_group: DF.Link | None
item_naming_by: DF.Literal["Item Code", "Naming Series"] item_naming_by: DF.Literal["Item Code", "Naming Series"]
@@ -82,6 +83,7 @@ class StockSettings(Document):
"default_warehouse", "default_warehouse",
"set_qty_in_transactions_based_on_serial_no_input", "set_qty_in_transactions_based_on_serial_no_input",
"use_serial_batch_fields", "use_serial_batch_fields",
"enable_serial_and_batch_no_for_item",
"set_serial_and_batch_bundle_naming_based_on_naming_series", "set_serial_and_batch_bundle_naming_based_on_naming_series",
]: ]:
frappe.db.set_default(key, self.get(key, "")) frappe.db.set_default(key, self.get(key, ""))
@@ -104,6 +106,7 @@ class StockSettings(Document):
) )
self.validate_warehouses() self.validate_warehouses()
self.validate_serial_and_batch_no_settings()
self.cant_change_valuation_method() self.cant_change_valuation_method()
self.validate_clean_description_html() self.validate_clean_description_html()
self.validate_pending_reposts() self.validate_pending_reposts()
@@ -112,6 +115,25 @@ class StockSettings(Document):
self.change_precision_for_for_sales() self.change_precision_for_for_sales()
self.change_precision_for_purchase() 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): def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
for field in warehouse_fields: for field in warehouse_fields:

View File

@@ -30,6 +30,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
refresh: (frm) => { refresh: (frm) => {
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" }; frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
erpnext.toggle_serial_batch_fields(frm);
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {
frm.add_custom_button( frm.add_custom_button(
__("Stock Ledger"), __("Stock Ledger"),