Merge pull request #54570 from khushi8112/item-opening-stock-dialog

feat: add opening stock dialog for stock items
This commit is contained in:
Khushi Rawat
2026-06-18 11:28:08 +05:30
committed by GitHub
4 changed files with 353 additions and 33 deletions

View File

@@ -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,143 @@ $.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) {
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 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."),
},
];
const dialog = new frappe.ui.Dialog({
title: __("Add Opening Stock"),
fields: fields,
primary_action_label: __("Save"),
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,
},
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();
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) {
if (frm.doc.weight_per_unit && !frm.doc.weight_uom) {
frappe.msgprint({

View File

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

View File

@@ -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
@@ -196,7 +197,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,
@@ -312,9 +313,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})
]:
@@ -330,38 +328,40 @@ class Item(Document):
)
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 self.valuation_rate == 0:
for item in stock_entry.items:
item.allow_zero_valuation_rate = 1
if not opening_account:
frappe.throw(
_(
"Please set a Temporary Opening account for company {0} to create an Opening Stock reconciliation."
).format(frappe.bold(default.company))
)
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.add_comment("Comment", _("Opening Stock"))
stock_entry.insert()
stock_entry.submit()
stock_entry.load_from_db()
stock_entry.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
_("Opening Stock reconciliation created with zero valuation rate: {0}").format(
stock_reco_link
),
indicator="orange",
alert=True,
)
else:
frappe.msgprint(
_("Opening Stock entry created: {0}").format(stock_entry_link),
_("Opening Stock reconciliation created: {0}").format(stock_reco_link),
indicator="green",
alert=True,
)
@@ -1584,3 +1584,169 @@ 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.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(
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))
)

View File

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