Merge pull request #53204 from frappe/mergify/bp/version-16-hotfix/pr-52549

feat: option to enable serial / batch feature (backport #52549)
This commit is contained in:
rohitwaghchaure
2026-03-06 13:07:31 +05:30
committed by GitHub
16 changed files with 162 additions and 10 deletions

View File

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

View File

@@ -469,3 +469,4 @@ 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
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.enable_serial_batch_setting

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();
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() {

View File

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

View File

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

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

View File

@@ -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",

View File

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

View File

@@ -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";

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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