diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index c03199e43482..7aa6ebbe6af8 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -175,10 +175,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) { @@ -703,6 +711,148 @@ $.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 warehouse = get_warehouse_for_company(dialog.get_value("company")); + dialog.set_value("warehouse", warehouse); + dialog.set_df_property( + "warehouse", + "description", + warehouse + ? __("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 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"), + 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(); + 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({ diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 1f2581a49810..c1f269e1e150 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -16,6 +16,7 @@ "item_name", "item_group", "stock_uom", + "image", "column_break0", "disabled", "is_stock_item", @@ -40,7 +41,6 @@ "over_delivery_receipt_allowance", "column_break_wugd", "over_billing_allowance", - "image", "section_break_11", "brand", "description", @@ -253,10 +253,9 @@ }, { "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, "label": "Opening Stock" }, { @@ -1077,7 +1076,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-04-28 17:31:47.613279", + "modified": "2026-04-28 17:31:48.613279", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index e06b6714d5a8..39db9b71e748 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1502,3 +1502,127 @@ def get_child_warehouses(warehouse): from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses return get_child_warehouses(warehouse) + + +@frappe.whitelist() +def make_opening_stock_entry( + item_code: str, + company: str, + qty: float, + valuation_rate: float, + warehouse: str | None = None, + serial_no_series: str | None = None, + create_new_batch: int = 0, + batch_number_series: str | None = None, +) -> str: + 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 flt(qty) <= 0: + frappe.throw(_("Quantity must be greater than zero.")) + + if flt(valuation_rate) < 0: + frappe.throw(_("Valuation Rate cannot be negative.")) + + if item.has_serial_no and not serial_no_series: + frappe.throw(_("Serial No Series is required for serialised items.")) + + if item.has_batch_no and cint(create_new_batch) and not batch_number_series: + frappe.throw(_("Batch Number Series is required when auto-creating a batch.")) + + 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( + frappe.bold(warehouse), frappe.bold(company) + ) + ) + + target_warehouse = get_default_warehouse_for_opening_stock(item, company, warehouse) + + persist_serial_batch_fields_for_opening_stock( + item_code, + serial_no_series=serial_no_series, + create_new_batch=cint(create_new_batch), + batch_number_series=batch_number_series, + ) + + stock_entry = frappe.get_doc( + { + "doctype": "Stock Entry", + "stock_entry_type": "Material Receipt", + "company": company, + "items": [ + { + "item_code": item_code, + "t_warehouse": target_warehouse, + "qty": flt(qty), + "basic_rate": flt(valuation_rate), + "allow_zero_valuation_rate": 1 if not flt(valuation_rate) else 0, + "use_serial_batch_fields": 1, + "serial_no_series": serial_no_series, + "batch_number_series": batch_number_series if cint(create_new_batch) else None, + } + ], + } + ) + + stock_entry.insert() + stock_entry.submit() + stock_entry.add_comment("Comment", _("Opening Stock")) + + frappe.msgprint( + _("Opening Stock entry created: {0}").format(get_link_to_form("Stock Entry", stock_entry.name)), + indicator="green", + alert=True, + ) + + return stock_entry.name + + +def get_default_warehouse_for_opening_stock(item, company: str, warehouse: str | None) -> str: + if warehouse: + return warehouse + + for default in item.item_defaults: + if default.company == company and default.default_warehouse: + return default.default_warehouse + + target = frappe.get_single_value("Stock Settings", "default_warehouse") or frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores"), "company": company} + ) + + if not target: + frappe.throw( + _( + "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Stock Settings." + ).format(frappe.bold(company)) + ) + + return target + + +def persist_serial_batch_fields_for_opening_stock( + item_code: str, + serial_no_series: str | None, + create_new_batch: int, + batch_number_series: str | None, +) -> None: + fields_to_update = {} + + if serial_no_series: + fields_to_update["serial_no_series"] = serial_no_series + + if create_new_batch: + fields_to_update["create_new_batch"] = 1 + if batch_number_series: + fields_to_update["batch_number_series"] = batch_number_series + + if fields_to_update: + frappe.db.set_value("Item", item_code, fields_to_update)