From e00cfc7c2afb9e2bfd40fb772c96f7546d7898da Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 28 Apr 2026 01:32:48 +0530 Subject: [PATCH 1/7] feat: add opening stock dialog box in Item form --- erpnext/stock/doctype/item/item.js | 176 ++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 6dfe5009a07..d857a02c313 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -193,10 +193,18 @@ frappe.ui.form.on("Item", { __("View") ); - frm.toggle_display( - ["opening_stock"], - frappe.model.can_create("Stock Entry") && frappe.model.can_write("Stock Entry") - ); + const can_create_stock_entry = + frappe.model.can_create("Stock Entry") && frappe.model.can_write("Stock Entry"); + + const has_existing_stock = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0; + + if (can_create_stock_entry && !has_existing_stock) { + frm.add_custom_button( + __("Set Opening Stock"), + () => erpnext.item.show_opening_stock_dialog(frm), + __("Actions") + ); + } } if (frm.doc.is_fixed_asset) { @@ -874,6 +882,166 @@ $.extend(erpnext.item, { }); }, + show_opening_stock_dialog: function (frm) { + const companies = (frm.doc.item_defaults || []).map((d) => d.company).filter(Boolean); + + if (!companies.length) { + frappe.msgprint({ + title: __("No Company Found"), + message: __( + "Please add at least one row in Item Defaults with a Company before setting opening stock." + ), + indicator: "orange", + }); + return; + } + + const get_warehouse_for_company = (company) => { + const row = (frm.doc.item_defaults || []).find((d) => d.company === company); + return (row && row.default_warehouse) || ""; + }; + + const has_serial = cint(frm.doc.has_serial_no); + const has_batch = cint(frm.doc.has_batch_no); + + const fields = [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Select", + options: companies.join("\n"), + default: companies[0], + reqd: 1, + onchange: function () { + const wh = get_warehouse_for_company(dialog.get_value("company")); + dialog.set_value("warehouse", wh); + dialog.set_df_property( + "warehouse", + "description", + wh + ? __("Default warehouse from Item Defaults.") + : __( + "No default warehouse set for this company. Entry will use Stock Settings default." + ) + ); + }, + }, + { + label: __("Default Warehouse"), + fieldname: "warehouse", + fieldtype: "Data", + read_only: 1, + description: __("Default warehouse from Item Defaults."), + }, + { fieldtype: "Column Break" }, + { + label: __("Opening Stock"), + fieldname: "qty", + fieldtype: "Float", + default: frm.doc.opening_stock || 1, + reqd: 1, + }, + { + label: __("Valuation Rate"), + fieldname: "valuation_rate", + fieldtype: "Currency", + default: frm.doc.valuation_rate || 0, + description: __("Leave as 0 to allow zero valuation rate."), + }, + ]; + + if (has_serial) { + fields.push( + { fieldtype: "Section Break", label: __("Serial / Batch") }, + { + label: __("Serial No Series"), + fieldname: "serial_no_series", + fieldtype: "Data", + default: frm.doc.serial_no_series || "", + reqd: 1, + description: __( + "Example: SN-.YYYY.-.##### - One serial number will be created per unit of qty." + ), + } + ); + } + + if (has_batch) { + if (!has_serial) { + fields.push({ fieldtype: "Section Break", label: __("Serial / Batch") }); + } + fields.push( + { + label: __("Automatically Create New Batch"), + fieldname: "create_new_batch", + fieldtype: "Check", + default: frm.doc.create_new_batch || 0, + description: __("Enable to auto-create a batch using the series below."), + onchange: function () { + const checked = dialog.get_value("create_new_batch"); + dialog.set_df_property("batch_number_series", "reqd", checked ? 1 : 0); + dialog.set_df_property("batch_number_series", "hidden", checked ? 0 : 1); + }, + }, + { + label: __("Batch Number Series"), + fieldname: "batch_number_series", + fieldtype: "Data", + default: frm.doc.batch_number_series || "", + reqd: frm.doc.create_new_batch ? 1 : 0, + hidden: frm.doc.create_new_batch ? 0 : 1, + description: __( + "Example: BATCH-.YYYY.-.##### - A new batch will be auto-created from this series." + ), + } + ); + } + + const dialog = new frappe.ui.Dialog({ + title: __("Set Opening Stock"), + fields: fields, + primary_action_label: __("Create Stock Entry"), + primary_action: function (values) { + frappe.call({ + method: "erpnext.stock.doctype.item.item.make_opening_stock_entry", + args: { + item_code: frm.doc.name, + company: values.company, + qty: values.qty, + valuation_rate: values.valuation_rate || 0, + warehouse: values.warehouse || null, + serial_no_series: values.serial_no_series || null, + create_new_batch: values.create_new_batch ? 1 : 0, + batch_number_series: values.batch_number_series || null, + }, + freeze: true, + freeze_message: __("Creating Opening Stock Entry..."), + callback: function (r) { + if (!r.exc && r.message) { + dialog.hide(); + frappe.show_alert( + { + message: __("Opening Stock entry created: {0}", [ + `${r.message}`, + ]), + indicator: "green", + }, + 8 + ); + frm.reload_doc(); + } + }, + }); + }, + }); + + dialog.set_value("warehouse", get_warehouse_for_company(companies[0])); + dialog.show(); + }, + weight_to_validate: function (frm) { if (frm.doc.weight_per_unit && !frm.doc.weight_uom) { frappe.msgprint({ From 60f528b5310f6f1ce65f93952ba64746c31515d4 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 29 Apr 2026 00:58:53 +0530 Subject: [PATCH 2/7] fix: minor UI and UX fixes --- erpnext/stock/doctype/item/item.js | 36 +++++++--------------------- erpnext/stock/doctype/item/item.json | 2 -- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index d857a02c313..362fa7a948b 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -913,12 +913,12 @@ $.extend(erpnext.item, { default: companies[0], reqd: 1, onchange: function () { - const wh = get_warehouse_for_company(dialog.get_value("company")); - dialog.set_value("warehouse", wh); + const warehouse = get_warehouse_for_company(dialog.get_value("company")); + dialog.set_value("warehouse", warehouse); dialog.set_df_property( "warehouse", "description", - wh + warehouse ? __("Default warehouse from Item Defaults.") : __( "No default warehouse set for this company. Entry will use Stock Settings default." @@ -960,7 +960,7 @@ $.extend(erpnext.item, { default: frm.doc.serial_no_series || "", reqd: 1, description: __( - "Example: SN-.YYYY.-.##### - One serial number will be created per unit of qty." + "Example: SN-.YYYY.-.#####. - One serial number will be created per unit of qty." ), } ); @@ -968,30 +968,24 @@ $.extend(erpnext.item, { if (has_batch) { if (!has_serial) { - fields.push({ fieldtype: "Section Break", label: __("Serial / Batch") }); + fields.push({ fieldtype: "Section Break", label: __("Serial Nos / Batches") }); } fields.push( { label: __("Automatically Create New Batch"), fieldname: "create_new_batch", fieldtype: "Check", - default: frm.doc.create_new_batch || 0, - description: __("Enable to auto-create a batch using the series below."), - onchange: function () { - const checked = dialog.get_value("create_new_batch"); - dialog.set_df_property("batch_number_series", "reqd", checked ? 1 : 0); - dialog.set_df_property("batch_number_series", "hidden", checked ? 0 : 1); - }, + default: 1, + read_only: 1, }, { label: __("Batch Number Series"), fieldname: "batch_number_series", fieldtype: "Data", default: frm.doc.batch_number_series || "", - reqd: frm.doc.create_new_batch ? 1 : 0, - hidden: frm.doc.create_new_batch ? 0 : 1, + reqd: 1, description: __( - "Example: BATCH-.YYYY.-.##### - A new batch will be auto-created from this series." + "Example: BATCH-.YYYY.-.#####. - A new batch will be auto-created from this series." ), } ); @@ -1019,18 +1013,6 @@ $.extend(erpnext.item, { callback: function (r) { if (!r.exc && r.message) { dialog.hide(); - frappe.show_alert( - { - message: __("Opening Stock entry created: {0}", [ - `${r.message}`, - ]), - indicator: "green", - }, - 8 - ); frm.reload_doc(); } }, diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index ed7886d6be3..8a458e8ea04 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -255,8 +255,6 @@ }, { "bold": 1, - "depends_on": "eval:(doc.__islocal&&doc.is_stock_item && !doc.has_serial_no && !doc.has_batch_no)", - "description": "Used to create an opening Stock Entry with the Valuation Rate when the item is saved", "fieldname": "opening_stock", "fieldtype": "Float", "hidden": 1, From e602cad39aca3331b98a6f2e8390ba8ad306017f Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 14 May 2026 03:28:07 +0530 Subject: [PATCH 3/7] fix: use Stock Reconciliation for opening stock entry --- erpnext/stock/doctype/item/item.js | 93 ++++++++++++++---------------- erpnext/stock/doctype/item/item.py | 58 +++++++++++-------- 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 362fa7a948b..ed6d4efe43d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -883,6 +883,29 @@ $.extend(erpnext.item, { }, show_opening_stock_dialog: function (frm) { + const has_serial = cint(frm.doc.has_serial_no); + const has_batch = cint(frm.doc.has_batch_no); + + if (has_serial || has_batch) { + const default_company = frappe.defaults.get_default("company"); + const row = (frm.doc.item_defaults || []).find((d) => d.company === default_company); + const default_warehouse = (row && row.default_warehouse) || ""; + + frappe.route_options = { + purpose: "Opening Stock", + company: default_company, + }; + + frappe.new_doc("Stock Reconciliation", null, (doc) => { + const child = doc.items[0]; + frappe.model.set_value(child.doctype, child.name, "item_code", frm.doc.name); + if (default_warehouse) { + frappe.model.set_value(child.doctype, child.name, "warehouse", default_warehouse); + } + }); + return; + } + const companies = (frm.doc.item_defaults || []).map((d) => d.company).filter(Boolean); if (!companies.length) { @@ -901,9 +924,6 @@ $.extend(erpnext.item, { return (row && row.default_warehouse) || ""; }; - const has_serial = cint(frm.doc.has_serial_no); - const has_batch = cint(frm.doc.has_batch_no); - const fields = [ { label: __("Company"), @@ -950,51 +970,10 @@ $.extend(erpnext.item, { }, ]; - if (has_serial) { - fields.push( - { fieldtype: "Section Break", label: __("Serial / Batch") }, - { - label: __("Serial No Series"), - fieldname: "serial_no_series", - fieldtype: "Data", - default: frm.doc.serial_no_series || "", - reqd: 1, - description: __( - "Example: SN-.YYYY.-.#####. - One serial number will be created per unit of qty." - ), - } - ); - } - - if (has_batch) { - if (!has_serial) { - fields.push({ fieldtype: "Section Break", label: __("Serial Nos / Batches") }); - } - fields.push( - { - label: __("Automatically Create New Batch"), - fieldname: "create_new_batch", - fieldtype: "Check", - default: 1, - read_only: 1, - }, - { - label: __("Batch Number Series"), - fieldname: "batch_number_series", - fieldtype: "Data", - default: frm.doc.batch_number_series || "", - reqd: 1, - description: __( - "Example: BATCH-.YYYY.-.#####. - A new batch will be auto-created from this series." - ), - } - ); - } - const dialog = new frappe.ui.Dialog({ - title: __("Set Opening Stock"), + title: __("Add Opening Stock"), fields: fields, - primary_action_label: __("Create Stock Entry"), + primary_action_label: __("Save"), primary_action: function (values) { frappe.call({ method: "erpnext.stock.doctype.item.item.make_opening_stock_entry", @@ -1004,9 +983,6 @@ $.extend(erpnext.item, { qty: values.qty, valuation_rate: values.valuation_rate || 0, warehouse: values.warehouse || null, - serial_no_series: values.serial_no_series || null, - create_new_batch: values.create_new_batch ? 1 : 0, - batch_number_series: values.batch_number_series || null, }, freeze: true, freeze_message: __("Creating Opening Stock Entry..."), @@ -1022,6 +998,25 @@ $.extend(erpnext.item, { dialog.set_value("warehouse", get_warehouse_for_company(companies[0])); dialog.show(); + dialog.add_custom_action(__("Edit Full Form"), function () { + const default_company = frappe.defaults.get_default("company"); + const row = (frm.doc.item_defaults || []).find((d) => d.company === default_company); + + frappe.route_options = { + purpose: "Opening Stock", + company: default_company, + }; + + frappe.new_doc("Stock Reconciliation", null, (doc) => { + const child = doc.items[0]; + frappe.model.set_value(child.doctype, child.name, "item_code", frm.doc.name); + if (row && row.default_warehouse) { + frappe.model.set_value(child.doctype, child.name, "warehouse", row.default_warehouse); + } + }); + + dialog.hide(); + }); }, weight_to_validate: function (frm) { diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 29e233a7bc8..8a397ac67ee 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -312,9 +312,6 @@ class Item(Document): if self.valuation_rate is None and not self.is_customer_provided_item: frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - # default warehouse, or Stores for default in self.item_defaults or [ frappe._dict({"company": frappe.defaults.get_defaults().company}) ]: @@ -329,39 +326,54 @@ class Item(Document): "Warehouse", {"warehouse_name": _("Stores"), "company": default.company} ) - if default_warehouse: - stock_entry = make_stock_entry( - item_code=self.name, - target=default_warehouse, - qty=self.opening_stock, - rate=self.valuation_rate, - company=default.company, - posting_date=getdate(), - posting_time=nowtime(), - do_not_save=True, + opening_account = frappe.db.get_value( + "Account", + {"company": default.company, "account_type": "Temporary", "is_group": 0}, + "name", + ) + + if not opening_account: + frappe.throw( + _( + "Please set a Temporary Opening account for company {0} to create an Opening Stock entry." + ).format(frappe.bold(default.company)) ) - if self.valuation_rate == 0: - for item in stock_entry.items: - item.allow_zero_valuation_rate = 1 + if default_warehouse: + stock_reco = frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "purpose": "Opening Stock", + "company": default.company, + "expense_account": opening_account, + "items": [ + { + "item_code": self.name, + "warehouse": default_warehouse, + "qty": self.opening_stock, + "valuation_rate": self.valuation_rate, + "allow_zero_valuation_rate": 1 if flt(self.valuation_rate) == 0 else 0, + } + ], + } + ) - stock_entry.insert() - stock_entry.submit() - stock_entry.load_from_db() - stock_entry.add_comment("Comment", _("Opening Stock")) + stock_reco.insert() + stock_reco.submit() + stock_reco.add_comment("Comment", _("Opening Stock")) - stock_entry_link = frappe.utils.get_link_to_form("Stock Entry", stock_entry.name) + stock_reco_link = frappe.utils.get_link_to_form("Stock Reconciliation", stock_reco.name) if self.valuation_rate == 0: frappe.msgprint( _("Opening Stock entry created with zero valuation rate: {0}").format( - stock_entry_link + stock_reco_link ), indicator="orange", alert=True, ) else: frappe.msgprint( - _("Opening Stock entry created: {0}").format(stock_entry_link), + _("Opening Stock entry created: {0}").format(stock_reco_link), indicator="green", alert=True, ) From 70086f92f5f336a61727442a6bde9f859c017813 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 14 May 2026 13:12:57 +0530 Subject: [PATCH 4/7] fix: test case --- erpnext/stock/doctype/item/item.py | 31 +++++++++++++----------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8a397ac67ee..516f02dc294 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -196,7 +196,7 @@ class Item(Document): ) frappe.msgprint( _( - "Opening stock creation has been queued and will be created in the background. Please check the stock entry after some time." + "Opening stock creation has been queued and will be created in the background. Please check the Stock Reconciliation after some time." ), indicator="orange", alert=True, @@ -302,11 +302,7 @@ class Item(Document): def set_opening_stock(self): """set opening stock""" - if ( - not self.is_stock_item - or (self.has_serial_no and not self.serial_no_series) - or (self.has_batch_no and (not self.create_new_batch or not self.batch_number_series)) - ): + if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return if self.valuation_rate is None and not self.is_customer_provided_item: @@ -326,20 +322,19 @@ class Item(Document): "Warehouse", {"warehouse_name": _("Stores"), "company": default.company} ) - opening_account = frappe.db.get_value( - "Account", - {"company": default.company, "account_type": "Temporary", "is_group": 0}, - "name", - ) - - if not opening_account: - frappe.throw( - _( - "Please set a Temporary Opening account for company {0} to create an Opening Stock entry." - ).format(frappe.bold(default.company)) + if default_warehouse: + opening_account = frappe.db.get_value( + "Account", + {"company": default.company, "account_type": "Temporary", "is_group": 0}, + "name", ) - if default_warehouse: + if not opening_account: + frappe.throw( + _( + "Please set a Temporary Opening account for company {0} to create an Opening Stock entry." + ).format(frappe.bold(default.company)) + ) stock_reco = frappe.get_doc( { "doctype": "Stock Reconciliation", From 03be975f2699377762d6f477fc8c570a2d1c3a4e Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 19 May 2026 14:20:07 +0530 Subject: [PATCH 5/7] fix(stock): create opening stock via Stock Reconciliation with serial/batch bundle support --- erpnext/stock/doctype/item/item.py | 39 ++++++++++--------------- erpnext/stock/doctype/item/test_item.py | 15 ++++++++-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 516f02dc294..8d5b4d107c2 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -31,6 +31,7 @@ from erpnext.controllers.item_variant import ( validate_item_variant_attributes, ) from erpnext.stock.doctype.item_default.item_default import ItemDefault +from erpnext.stock.serial_batch_bundle import SerialBatchCreation from erpnext.stock.utils import get_valuation_method @@ -302,7 +303,11 @@ class Item(Document): def set_opening_stock(self): """set opening stock""" - if not self.is_stock_item or self.has_serial_no or self.has_batch_no: + if ( + not self.is_stock_item + or (self.has_serial_no and not self.serial_no_series) + or (self.has_batch_no and (not self.create_new_batch or not self.batch_number_series)) + ): return if self.valuation_rate is None and not self.is_customer_provided_item: @@ -332,35 +337,23 @@ class Item(Document): if not opening_account: frappe.throw( _( - "Please set a Temporary Opening account for company {0} to create an Opening Stock entry." + "Please set a Temporary Opening account for company {0} to create an Opening Stock reconciliation." ).format(frappe.bold(default.company)) ) - stock_reco = frappe.get_doc( - { - "doctype": "Stock Reconciliation", - "purpose": "Opening Stock", - "company": default.company, - "expense_account": opening_account, - "items": [ - { - "item_code": self.name, - "warehouse": default_warehouse, - "qty": self.opening_stock, - "valuation_rate": self.valuation_rate, - "allow_zero_valuation_rate": 1 if flt(self.valuation_rate) == 0 else 0, - } - ], - } + stock_reco = create_opening_stock_reconciliation( + item_code=self.name, + company=default.company, + qty=self.opening_stock, + valuation_rate=self.valuation_rate, + warehouse=default_warehouse, + expense_account=opening_account, ) - - stock_reco.insert() - stock_reco.submit() stock_reco.add_comment("Comment", _("Opening Stock")) stock_reco_link = frappe.utils.get_link_to_form("Stock Reconciliation", stock_reco.name) if self.valuation_rate == 0: frappe.msgprint( - _("Opening Stock entry created with zero valuation rate: {0}").format( + _("Opening Stock reconciliation created with zero valuation rate: {0}").format( stock_reco_link ), indicator="orange", @@ -368,7 +361,7 @@ class Item(Document): ) else: frappe.msgprint( - _("Opening Stock entry created: {0}").format(stock_reco_link), + _("Opening Stock reconciliation created: {0}").format(stock_reco_link), indicator="green", alert=True, ) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 4c05765e65d..eb67a776f1d 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -997,13 +997,24 @@ class TestItem(ERPNextTestSuite): for item_code, properties in items.items(): make_item(item_code, properties) - serial_and_batch_bundle = frappe.db.get_value( + stock_entry_bundle = frappe.db.get_value( "Stock Entry Detail", {"docstatus": 1, "item_code": item_code}, "serial_and_batch_bundle" ) + self.assertFalse(stock_entry_bundle) + + serial_and_batch_bundle = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Stock Reconciliation", + "is_cancelled": 0, + "item_code": item_code, + }, + "serial_and_batch_bundle", + ) self.assertTrue(serial_and_batch_bundle) sabb_qty = frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "total_qty") - self.assertEqual(sabb_qty, properties["opening_stock"]) + self.assertEqual(abs(sabb_qty), properties["opening_stock"]) def set_item_variant_settings(fields): From c5b4a742b3b4597aa3b072a0cd4efe075ad03656 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 17 Jun 2026 17:22:44 +0530 Subject: [PATCH 6/7] fix: so many conflicts --- erpnext/stock/doctype/item/item.py | 162 +++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8d5b4d107c2..32901b0953a 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1584,3 +1584,165 @@ def get_item_prices(item_code: str): "prices": prices[:10], "has_more": has_more, } + + +@frappe.whitelist() +def make_opening_stock_entry( + item_code: str, + company: str, + qty: float, + valuation_rate: float, + warehouse: str | None = None, +): + if not frappe.has_permission("Item", "write", item_code): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + item = frappe.get_doc("Item", item_code) + + if not item.is_stock_item: + frappe.throw(_("Opening Stock can only be set for stock items.")) + if item.stock_ledger_created(): + frappe.throw( + _("Opening Stock cannot be created as stock transactions already exist for item {0}.").format( + frappe.bold(item_code) + ) + ) + + if flt(qty) <= 0: + frappe.throw(_("Quantity must be greater than zero.")) + + if flt(valuation_rate) < 0: + frappe.throw(_("Valuation Rate cannot be negative.")) + + if warehouse: + warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company") + if warehouse_company != company: + frappe.throw(_("Warehouse {0} does not belong to Company {1}.").format(warehouse, company)) + + target_warehouse = get_default_warehouse_for_opening_stock(item, company, warehouse) + + opening_account = frappe.db.get_value( + "Account", + {"company": company, "account_type": "Temporary", "is_group": 0}, + "name", + ) + + if not opening_account: + frappe.throw( + _( + "Please set a Temporary Opening account for company {0} to create an Opening Stock reconciliation." + ).format(frappe.bold(company)) + ) + + stock_reco = create_opening_stock_reconciliation( + item_code=item_code, + company=company, + qty=qty, + valuation_rate=valuation_rate, + warehouse=target_warehouse, + expense_account=opening_account, + ) + stock_reco.add_comment("Comment", _("Opening Stock")) + + frappe.msgprint( + _("Opening Stock reconciliation created: {0}").format( + get_link_to_form("Stock Reconciliation", stock_reco.name) + ), + indicator="green", + alert=True, + ) + + return stock_reco.name + + +def create_opening_stock_reconciliation( + item_code: str, + company: str, + qty: float, + valuation_rate: float, + warehouse: str, + expense_account: str, +): + stock_reco = frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "purpose": "Opening Stock", + "company": company, + "expense_account": expense_account, + "items": [ + { + "item_code": item_code, + "warehouse": warehouse, + "qty": flt(qty), + "valuation_rate": flt(valuation_rate), + "allow_zero_valuation_rate": 1 if flt(valuation_rate) == 0 else 0, + "reconcile_all_serial_batch": 1, + } + ], + } + ) + + stock_reco.insert() + set_opening_stock_serial_batch_bundle(stock_reco) + stock_reco.submit() + + return stock_reco + + +def set_opening_stock_serial_batch_bundle(stock_reco): + row = stock_reco.items[0] + item_details = frappe.get_cached_value( + "Item", row.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + + if not (item_details.has_serial_no or item_details.has_batch_no): + return + + bundle = SerialBatchCreation( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "voucher_type": stock_reco.doctype, + "voucher_no": stock_reco.name, + "voucher_detail_no": row.name, + "posting_date": stock_reco.posting_date, + "posting_time": stock_reco.posting_time, + "qty": row.qty, + "avg_rate": row.valuation_rate, + "type_of_transaction": "Inward", + "company": stock_reco.company, + "do_not_submit": True, + } + ).make_serial_and_batch_bundle() + + if not bundle: + return + + row.db_set("serial_and_batch_bundle", bundle.name, update_modified=False) + row.serial_and_batch_bundle = bundle.name + + +def get_default_warehouse_for_opening_stock(item, company: str, warehouse: str | None): + if warehouse: + return warehouse + + for default in item.item_defaults: + if default.company == company and default.default_warehouse: + return default.default_warehouse + + settings_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse") + if settings_warehouse: + warehouse_company = frappe.db.get_value("Warehouse", settings_warehouse, "company") + if warehouse_company == company: + return settings_warehouse + + stores_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores"), "company": company}) + + if stores_warehouse: + return stores_warehouse + + frappe.throw( + _( + "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Stock Settings." + ).format(frappe.bold(company)) + ) From e57593fcf86e9fc30d5c801ae5ce66974480f197 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 18 Jun 2026 02:25:13 +0530 Subject: [PATCH 7/7] fix(item): add server-side guard for serial/batch items in make_opening_stock_entry Co-Authored-By: Claude Sonnet 4.6 --- erpnext/stock/doctype/item/item.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 32901b0953a..74e8dde48af 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1601,6 +1601,10 @@ def make_opening_stock_entry( if not item.is_stock_item: frappe.throw(_("Opening Stock can only be set for stock items.")) + if item.has_serial_no or item.has_batch_no: + frappe.throw( + _("Opening Stock for serialised or batch items must be set via the Stock Reconciliation form.") + ) if item.stock_ledger_created(): frappe.throw( _("Opening Stock cannot be created as stock transactions already exist for item {0}.").format(