mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 14:11:46 +00:00
Merge pull request #54570 from khushi8112/item-opening-stock-dialog
feat: add opening stock dialog for stock items
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user