-
Notifications
You must be signed in to change notification settings - Fork 11.1k
feat: add opening stock dialog for stock items #54570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
Comment on lines
+714
to
+726
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't make Item Defaults mandatory for this workflow. The backend already falls back from Item Defaults to Stock Settings / "Stores", but the dialog exits immediately unless at least one 🤖 Prompt for AI Agents |
||
|
|
||
| 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, | ||
|
Comment on lines
+803
to
+838
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid permanently enabling auto batch creation from this one-off dialog. For batch items this always submits 🤖 Prompt for AI Agents |
||
| }, | ||
| 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({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
Comment on lines
256
to
+258
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep
🤖 Prompt for AI Agents |
||
| "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", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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.")) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1521
to
+1525
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject "opening stock" once ledger activity already exists. This endpoint trusts the client-side Suggested fix 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 can only be set before the first stock transaction."))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| 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)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1597
to
+1606
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the Stock Settings fallback company-safe.
Suggested fix- target = frappe.get_single_value("Stock Settings", "default_warehouse") or frappe.db.get_value(
- "Warehouse", {"warehouse_name": _("Stores"), "company": company}
- )
+ target = frappe.get_single_value("Stock Settings", "default_warehouse")
+ if target and frappe.db.get_value("Warehouse", target, "company") != company:
+ target = None
+
+ if not target:
+ target = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores"), "company": company})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.