From 90aba582ece00cc7463039fc9923056c202f31b9 Mon Sep 17 00:00:00 2001 From: sokumon Date: Fri, 12 Jun 2026 13:08:48 +0530 Subject: [PATCH 001/195] chore: export workspaces with new schema --- .../accounts/workspace/banking/banking.json | 222 +++++ .../financial_reports/financial_reports.json | 416 +++++++++- .../workspace/invoicing/invoicing.json | 351 +++++++- .../accounts/workspace/payments/payments.json | 240 ++++++ .../share_management/share_management.json | 86 ++ erpnext/accounts/workspace/taxes/taxes.json | 188 +++++ erpnext/assets/workspace/assets/assets.json | 291 ++++++- erpnext/crm/workspace/crm/crm.json | 763 +++++++++++++++++- erpnext/hooks.py | 2 +- .../manufacturing/manufacturing.json | 462 ++++++++++- .../projects/workspace/projects/projects.json | 231 +++++- .../workspace/quality/quality.json | 157 +++- .../selling/workspace/selling/selling.json | 759 ++++++++++++++++- erpnext/setup/doctype/company/company_list.js | 5 - erpnext/setup/workspace/home/home.json | 65 +- .../workspace/organization/organization.json | 126 +++ erpnext/stock/workspace/stock/stock.json | 679 +++++++++++++++- .../subcontracting/subcontracting.json | 250 +++++- .../support/workspace/support/support.json | 178 +++- 19 files changed, 5430 insertions(+), 41 deletions(-) create mode 100644 erpnext/accounts/workspace/banking/banking.json create mode 100644 erpnext/accounts/workspace/payments/payments.json create mode 100644 erpnext/accounts/workspace/share_management/share_management.json create mode 100644 erpnext/accounts/workspace/taxes/taxes.json create mode 100644 erpnext/setup/workspace/organization/organization.json diff --git a/erpnext/accounts/workspace/banking/banking.json b/erpnext/accounts/workspace/banking/banking.json new file mode 100644 index 00000000000..45d3af0767d --- /dev/null +++ b/erpnext/accounts/workspace/banking/banking.json @@ -0,0 +1,222 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:22.767176", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "circle-dollar-sign", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Banking", + "link_type": "DocType", + "links": [], + "modified": "2026-06-11 11:51:22.781813", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Banking", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 49.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "book-open-check", + "indent": 0, + "keep_closed": 0, + "label": "Bank Clearance", + "link_to": "Bank Clearance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "tool", + "indent": 0, + "keep_closed": 0, + "label": "Bank Reconciliation", + "link_to": "Bank Reconciliation Tool", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "clipboard-check", + "indent": 0, + "keep_closed": 0, + "label": "Reconciliation Statement", + "link_to": "Bank Reconciliation Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "split", + "indent": 0, + "keep_closed": 0, + "label": "Unreconcile Payment", + "link_to": "Unreconcile Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "link", + "indent": 0, + "keep_closed": 0, + "label": "Process Payment Reconciliation", + "link_to": "Process Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Bank", + "link_to": "Bank", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Bank Account", + "link_to": "Bank Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bank Account Type", + "link_to": "Bank Account Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bank Account Subtype", + "link_to": "Bank Account Subtype", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bank Guarantee", + "link_to": "Bank Guarantee", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Plaid Settings", + "link_to": "Plaid Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "scroll-text", + "indent": 1, + "keep_closed": 1, + "label": "Dunning", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Dunning", + "link_to": "Dunning", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Dunning Type", + "link_to": "Dunning Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Banking", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/financial_reports/financial_reports.json b/erpnext/accounts/workspace/financial_reports/financial_reports.json index 5ce20bed642..0c542093cc7 100644 --- a/erpnext/accounts/workspace/financial_reports/financial_reports.json +++ b/erpnext/accounts/workspace/financial_reports/financial_reports.json @@ -13,7 +13,7 @@ "doctype": "Workspace", "for_user": "", "hide_custom": 0, - "icon": "table", + "icon": "sheet", "idx": 1, "indicator_color": "", "is_hidden": 0, @@ -266,9 +266,10 @@ "type": "Link" } ], - "modified": "2026-05-18 09:49:45.138296", + "modified": "2026-06-11 11:51:23.876824", "modified_by": "Administrator", "module": "Accounts", + "module_onboarding": "Accounting Onboarding", "name": "Financial Reports", "number_cards": [], "owner": "Administrator", @@ -279,6 +280,417 @@ "roles": [], "sequence_id": 5.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "accounting", + "indent": 1, + "keep_closed": 0, + "label": "Financial Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Balance Sheet", + "link_to": "Balance Sheet", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Profit and Loss", + "link_to": "Profit and Loss Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Cash Flow", + "link_to": "Cash Flow", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance", + "link_to": "Trial Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Consolidated Report", + "link_to": "Consolidated Financial Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Custom Financial Statement", + "link_to": "Custom Financial Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Financial Report Template", + "link_to": "Financial Report Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "book-text", + "indent": 1, + "keep_closed": 0, + "label": "Ledgers", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "General Ledger", + "link_to": "General Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Ledger", + "link_to": "Customer Ledger Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Ledger", + "link_to": "Supplier Ledger Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "indent": 1, + "keep_closed": 1, + "label": "Registers", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Receivable", + "link_to": "Accounts Receivable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Payable", + "link_to": "Accounts Payable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "AR Summary", + "link_to": "Accounts Receivable Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "AP Summary", + "link_to": "Accounts Payable Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Register", + "link_to": "Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Register", + "link_to": "Purchase Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise sales Register", + "link_to": "Item-wise Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Purchase Register", + "link_to": "Item-wise Purchase Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "dollar-sign", + "indent": 1, + "keep_closed": 1, + "label": "Profitability", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Gross Profit", + "link_to": "Gross Profit", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Profitability Analysis", + "link_to": "Profitability Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice Trends", + "link_to": "Sales Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice Trends", + "link_to": "Purchase Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "scroll-text", + "indent": 1, + "keep_closed": 1, + "label": "Other Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance for Party", + "link_to": "Trial Balance for Party", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Period Based On Invoice Date", + "link_to": "Payment Period Based On Invoice Date", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partners Commission", + "link_to": "Sales Partners Commission", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Credit Balance", + "link_to": "Customer Credit Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Payment Summary", + "link_to": "Sales Payment Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Address And Contacts", + "link_to": "Address And Contacts", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "UAE VAT 201", + "link_to": "UAE VAT 201", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Financial Reports", "type": "Workspace" } diff --git a/erpnext/accounts/workspace/invoicing/invoicing.json b/erpnext/accounts/workspace/invoicing/invoicing.json index 30ffe9a8f16..094ee9c4a5d 100644 --- a/erpnext/accounts/workspace/invoicing/invoicing.json +++ b/erpnext/accounts/workspace/invoicing/invoicing.json @@ -587,9 +587,10 @@ "type": "Link" } ], - "modified": "2026-01-23 11:05:47.246213", + "modified": "2026-06-11 11:51:24.435013", "modified_by": "Administrator", "module": "Accounts", + "module_onboarding": "Accounting Onboarding", "name": "Invoicing", "number_cards": [ { @@ -617,6 +618,354 @@ "roles": [], "sequence_id": 2.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Invoicing", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Accounts", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "list-tree", + "indent": 0, + "keep_closed": 0, + "label": "Chart of Accounts", + "link_to": "Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-left-to-line", + "indent": 1, + "keep_closed": 0, + "label": "Receivables", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Credit Note", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "route_options": "{\"is_return\": 1}", + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Receivable", + "link_to": "Accounts Receivable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-right-from-line", + "indent": 1, + "keep_closed": 0, + "label": "Payables", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice", + "link_to": "Purchase Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Debit Note", + "link_to": "Purchase Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "route_options": "{\"is_return\": 1}", + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Payable", + "link_to": "Accounts Payable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "money-coins-1", + "indent": 1, + "keep_closed": 0, + "label": "Payments", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Entry", + "link_to": "Payment Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Journal Entry", + "link_to": "Journal Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Request", + "link_to": "Payment Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Order", + "link_to": "Payment Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Unreconcile Payment", + "link_to": "Unreconcile Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Process Payment Reconciliation", + "link_to": "Process Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Accounting Ledger", + "link_to": "Repost Accounting Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Payment Ledger", + "link_to": "Repost Payment Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 0, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "General Ledger", + "link_to": "General Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance", + "link_to": "Trial Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Financial Reports", + "link_to": "Financial Reports", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Invoicing", "type": "Workspace" } diff --git a/erpnext/accounts/workspace/payments/payments.json b/erpnext/accounts/workspace/payments/payments.json new file mode 100644 index 00000000000..a443aa8dc66 --- /dev/null +++ b/erpnext/accounts/workspace/payments/payments.json @@ -0,0 +1,240 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:21.886461", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "receipt-text", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Payments", + "link_type": "DocType", + "links": [], + "modified": "2026-06-11 11:51:21.898721", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Payments", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 47.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Payments", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "money-coins-1", + "indent": 1, + "keep_closed": 0, + "label": "Payments", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Entry", + "link_to": "Payment Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Journal Entry", + "link_to": "Journal Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Request", + "link_to": "Payment Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Order", + "link_to": "Payment Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Unreconcile Payment", + "link_to": "Unreconcile Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Process Payment Reconciliation", + "link_to": "Process Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Accounting Ledger", + "link_to": "Repost Accounting Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Payment Ledger", + "link_to": "Repost Payment Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Receivable", + "link_to": "Accounts Receivable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Payable", + "link_to": "Accounts Payable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "General Ledger", + "link_to": "General Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance", + "link_to": "Trial Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Financial Reports", + "link_to": "Financial Reports", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Payments", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/share_management/share_management.json b/erpnext/accounts/workspace/share_management/share_management.json new file mode 100644 index 00000000000..9771dd468ec --- /dev/null +++ b/erpnext/accounts/workspace/share_management/share_management.json @@ -0,0 +1,86 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:22.831729", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "money-coins-1", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Share Management", + "link_type": "DocType", + "links": [], + "modified": "2026-06-11 14:06:17.396069", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Share Management", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 50.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 1, + "collapsible": 1, + "icon": "customer", + "indent": 0, + "keep_closed": 0, + "label": "Shareholder", + "link_to": "Shareholder", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "move-horizontal", + "indent": 0, + "keep_closed": 0, + "label": "Share Transfer", + "link_to": "Share Transfer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "list", + "indent": 0, + "keep_closed": 0, + "label": "Share Ledger", + "link_to": "Share Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Share Balance", + "link_to": "Share Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Share Management", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/taxes/taxes.json b/erpnext/accounts/workspace/taxes/taxes.json new file mode 100644 index 00000000000..32f9c13025e --- /dev/null +++ b/erpnext/accounts/workspace/taxes/taxes.json @@ -0,0 +1,188 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:22.649582", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "money-coins-1", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Taxes", + "link_type": "DocType", + "links": [], + "modified": "2026-06-11 11:51:22.661660", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Taxes", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 48.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "panel-bottom-close", + "indent": 0, + "keep_closed": 0, + "label": "Sales Tax Template", + "link_to": "Sales Taxes and Charges Template", + "link_type": "DocType", + "navigate_to_tab": "", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "panel-top-close", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Tax Template", + "link_to": "Purchase Taxes and Charges Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Item Tax Template", + "link_to": "Item Tax Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "triangle", + "indent": 0, + "keep_closed": 0, + "label": "Tax Category", + "link_to": "Tax Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "book-open-text", + "indent": 0, + "keep_closed": 0, + "label": "Tax Rule", + "link_to": "Tax Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "book-text", + "indent": 0, + "keep_closed": 0, + "label": "Tax Withholding Category", + "link_to": "Tax Withholding Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Tax Withholding Group", + "link_to": "Tax Withholding Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notebook-text", + "indent": 0, + "keep_closed": 0, + "label": "Deduction Certificate", + "link_to": "Lower Deduction Certificate", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_to": "", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "TDS Computation Summary", + "link_to": "TDS Computation Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Tax Withholding Details", + "link_to": "Tax Withholding Details", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Taxes", + "type": "Workspace" +} diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index 82c22dac559..a54d6e1c37e 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -199,9 +199,10 @@ "type": "Link" } ], - "modified": "2025-12-31 16:22:38.132729", + "modified": "2026-06-11 11:51:24.332555", "modified_by": "Administrator", "module": "Assets", + "module_onboarding": "Asset Onboarding", "name": "Assets", "number_cards": [], "owner": "Administrator", @@ -212,6 +213,294 @@ "roles": [], "sequence_id": 7.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Assets", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Asset", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "laptop", + "indent": 0, + "keep_closed": 0, + "label": "Asset", + "link_to": "Asset", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "trending-down", + "indent": 0, + "keep_closed": 0, + "label": "Depreciation Schedule", + "link_to": "Asset Depreciation Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sprout", + "indent": 0, + "keep_closed": 0, + "label": "Asset Capitalization", + "link_to": "Asset Capitalization", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "move-horizontal", + "indent": 0, + "keep_closed": 0, + "label": "Asset Movement", + "link_to": "Asset Movement", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Maintenance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance Team", + "link_to": "Asset Maintenance Team", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance", + "link_to": "Asset Maintenance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance Log", + "link_to": "Asset Maintenance Log", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Value Adjustment", + "link_to": "Asset Value Adjustment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Repair", + "link_to": "Asset Repair", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Fixed Asset Register", + "link_to": "Fixed Asset Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Depreciation Ledger", + "link_to": "Asset Depreciation Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Depreciations and Balances", + "link_to": "Asset Depreciations and Balances", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance", + "link_to": "Asset Maintenance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Activity", + "link_to": "Asset Activity", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Category", + "link_to": "Asset Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Location", + "link_to": "Location", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "navigate_to_tab": "assets_tab", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link", + "url": "" + } + ], + "standard": 1, "title": "Assets", "type": "Workspace" } diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index 59814c87e4a..6851bf6d10a 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -2,11 +2,11 @@ "app": "erpnext", "charts": [ { - "chart_name": "Won Opportunities", - "label": "Won Opportunities" + "chart_name": "Territory Wise Sales", + "label": "Territory Wise Sales" } ], - "content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe CRM instead.\",\"col\":12}},{\"id\":\"-bzBQ_IbL9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Won Opportunities\",\"col\":12}},{\"id\":\"LdM1QgUnqU\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"New Lead (Last 1 Month)\",\"col\":4}},{\"id\":\"X23-SXBcYG\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"New Opportunity (Last 1 Month)\",\"col\":4}},{\"id\":\"3rm7fH52M-\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Won Opportunity (Last 1 Month)\",\"col\":4}},{\"id\":\"K6a2Kh5Zav\",\"type\":\"spacer\",\"data\":{\"col\":12}}]", + "content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe CRM instead.\",\"col\":12}},{\"id\":\"Cj2TyhgiWy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"id\":\"LAKRmpYMRA\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"69RN0XsiJK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"id\":\"t6PQ0vY-Iw\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"id\":\"VOFE0hqXRD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"0ik53fuemG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"id\":\"wdROEmB_XG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"-I9HhcgUKE\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"ttpROKW9vk\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"-76QPdbBHy\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"id\":\"_YmGwzVWRr\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"Bma1PxoXk3\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"80viA0R83a\",\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"id\":\"Buo5HtKRFN\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"sLS_x4FMK2\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}}]", "creation": "2020-01-23 14:48:30.183272", "custom_blocks": [], "docstatus": 0, @@ -18,6 +18,14 @@ "is_hidden": 0, "label": "CRM", "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "Lead", "hidden": 0, @@ -115,6 +123,14 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Maintenance", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -148,6 +164,183 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Sales Pipeline", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Lead", + "link_count": 0, + "link_to": "Lead", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Opportunity", + "link_count": 0, + "link_to": "Opportunity", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customer", + "link_count": 0, + "link_to": "Customer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Contract", + "link_count": 0, + "link_to": "Contract", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Appointment", + "link_count": 0, + "link_to": "Appointment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 0, + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Communication", + "link_count": 0, + "link_to": "Communication", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 2, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "CRM Settings", + "link_count": 0, + "link_to": "CRM Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Settings", + "link_count": 0, + "link_to": "SMS Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Campaign", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Campaign", + "link_count": 0, + "link_to": "Campaign", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Campaign", + "link_count": 0, + "link_to": "Email Campaign", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Center", + "link_count": 0, + "link_to": "SMS Center", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Log", + "link_count": 0, + "link_to": "SMS Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Group", + "link_count": 0, + "link_to": "Email Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -228,24 +421,11 @@ "type": "Link" } ], - "modified": "2026-01-03 15:05:23.983099", + "modified": "2026-06-11 11:51:24.168713", "modified_by": "Administrator", "module": "CRM", "name": "CRM", - "number_cards": [ - { - "label": "New Lead (Last 1 Month)", - "number_card_name": "New Lead (Last 1 Month)" - }, - { - "label": "New Opportunity (Last 1 Month)", - "number_card_name": "New Opportunity (Last 1 Month)" - }, - { - "label": "Won Opportunity (Last 1 Month)", - "number_card_name": "Won Opportunity (Last 1 Month)" - } - ], + "number_cards": [], "owner": "Administrator", "parent_page": "", "public": 1, @@ -253,7 +433,552 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 17.0, - "shortcuts": [], + "shortcuts": [ + { + "color": "Blue", + "format": "{} Open", + "label": "Lead", + "link_to": "Lead", + "stats_filter": "{\"status\":\"Open\"}", + "type": "DocType" + }, + { + "color": "Blue", + "format": "{} Assigned", + "label": "Opportunity", + "link_to": "Opportunity", + "stats_filter": "{\"_assign\": [\"like\", '%' + frappe.session.user + '%']}", + "type": "DocType" + }, + { + "label": "Customer", + "link_to": "Customer", + "type": "DocType" + }, + { + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "report_ref_doctype": "Sales Order", + "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "CRM", + "type": "Dashboard" + } + ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "CRM", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "users-round", + "indent": 0, + "keep_closed": 0, + "label": "Lead", + "link_to": "Lead", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "lightbulb", + "indent": 0, + "keep_closed": 0, + "label": "Opportunity", + "link_to": "Opportunity", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "customer", + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead Details", + "link_to": "Lead Details", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Pipeline Analytics", + "link_to": "Sales Pipeline Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Opportunity Summary by Sales Stage", + "link_to": "Opportunity Summary by Sales Stage", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Funnel", + "link_to": "sales-funnel", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Prospects Engaged But Not Converted", + "link_to": "Prospects Engaged But Not Converted", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "First Response Time for Opportunity", + "link_to": "First Response Time for Opportunity", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Campaign Efficiency", + "link_to": "Campaign Efficiency", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead Owner Efficiency", + "link_to": "Lead Owner Efficiency", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Maintenance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Schedule", + "link_to": "Maintenance Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Visit", + "link_to": "Maintenance Visit", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Warranty Claim", + "link_to": "Warranty Claim", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "funnel", + "indent": 1, + "keep_closed": 1, + "label": "Sales Pipeline", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead", + "link_to": "Lead", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Opportunity", + "link_to": "Opportunity", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contract", + "link_to": "Contract", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Appointment", + "link_to": "Appointment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Communication", + "link_to": "Communication", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sell", + "indent": 1, + "keep_closed": 1, + "label": "Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Campaign", + "link_to": "Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Email Campaign", + "link_to": "Email Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "SMS Center", + "link_to": "SMS Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "SMS Log", + "link_to": "SMS Log", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Email Group", + "link_to": "Email Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Territory", + "link_to": "Territory", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Group", + "link_to": "Customer Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contact", + "link_to": "Contact", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Prospect", + "link_to": "Prospect", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person", + "link_to": "Sales Person", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Stage", + "link_to": "Sales Stage", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead Source", + "link_to": "UTM Source", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 1, + "keep_closed": 1, + "label": "Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "CRM Settings", + "link_to": "CRM Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "SMS Settings", + "link_to": "SMS Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "CRM", "type": "Workspace" } diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ddc267e543d..7b6e1efd324 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -8,7 +8,7 @@ app_email = "hello@frappe.io" app_license = "GNU General Public License (v3)" source_link = "https://github.com/frappe/erpnext" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" -app_home = "/desk" +app_home = "/desk/home" add_to_apps_screen = [ { diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 35781672835..86a4568cdfc 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -432,9 +432,10 @@ "type": "Link" } ], - "modified": "2026-05-05 11:00:26.131777", + "modified": "2026-06-11 11:51:22.955115", "modified_by": "Administrator", "module": "Manufacturing", + "module_onboarding": "Manufacturing Onboarding", "name": "Manufacturing", "number_cards": [ { @@ -458,6 +459,465 @@ "roles": [], "sequence_id": 8.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Manufacturing", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Manufacturing", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "list-tree", + "indent": 0, + "keep_closed": 0, + "label": "BOM", + "link_to": "BOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "factory", + "indent": 0, + "keep_closed": 0, + "label": "Work Order", + "link_to": "Work Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "person-standing", + "indent": 0, + "keep_closed": 0, + "label": "Job Card", + "link_to": "Job Card", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Stock Entry", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Material Planning", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item Lead Time", + "link_to": "Item Lead Time", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Plan", + "link_to": "Production Plan", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Forecasting", + "link_to": "Exponential Smoothing Forecasting", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Master Production Schedule", + "link_to": "Master Production Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Forecast", + "link_to": "Sales Forecast", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Planning Report", + "link_to": "Production Planning Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "tool", + "indent": 1, + "keep_closed": 1, + "label": "Tools", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Creator", + "link_to": "BOM Creator", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Update Tool", + "link_to": "BOM Update Tool", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Comparison Tool", + "link_to": "bom-comparison-tool", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Downtime Entry", + "link_to": "Downtime Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Planning Report", + "link_to": "Production Planning Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Work Order Summary", + "link_to": "Work Order Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection Summary", + "link_to": "Quality Inspection Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Downtime Analysis", + "link_to": "Downtime Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Job Card Summary", + "link_to": "Job Card Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Search", + "link_to": "BOM Search", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Analytics", + "link_to": "Production Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Operations Time", + "link_to": "BOM Operations Time", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Work Order Consumed Materials", + "link_to": "Work Order Consumed Materials", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Warehouse", + "link_to": "Warehouse", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Operation", + "link_to": "Operation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Workstation", + "link_to": "Workstation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Workstation Type", + "link_to": "Workstation Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Plant Floor", + "link_to": "Plant Floor", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Routing", + "link_to": "Routing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Manufacturing Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Manufacturing", "type": "Workspace" } diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index aa0f2cf2e71..9e71b05d9aa 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -6,7 +6,7 @@ "label": "Completed Projects" } ], - "content": "[{\"id\":\"7Mbx6I5JUf\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"fVYsju6dB9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Completed Projects\",\"col\":12}},{\"id\":\"67w8up7H_0\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Open Projects\",\"col\":4}},{\"id\":\"IFEYSadaYc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Non Completed Tasks\",\"col\":4}},{\"id\":\"VqbqxA0YL1\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Working Hours\",\"col\":4}},{\"id\":\"oGhjvYjfv-\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"TdsgJyG3EI\",\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"id\":\"nIc0iyvf1T\",\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"id\":\"8G1if4jsQ7\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"o7qTNRXZI8\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"7Mbx6I5JUf\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"QYGHTcrCDB\",\"type\":\"header\",\"data\":{\"text\":\"Hello\",\"col\":12}},{\"id\":\"fVYsju6dB9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Completed Projects\",\"col\":12}},{\"id\":\"67w8up7H_0\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Open Projects\",\"col\":4}},{\"id\":\"IFEYSadaYc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Non Completed Tasks\",\"col\":4}},{\"id\":\"VqbqxA0YL1\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Working Hours\",\"col\":4}},{\"id\":\"oGhjvYjfv-\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"TdsgJyG3EI\",\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"id\":\"nIc0iyvf1T\",\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"id\":\"8G1if4jsQ7\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"o7qTNRXZI8\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 15:46:04.874669", "custom_blocks": [], "docstatus": 0, @@ -193,9 +193,10 @@ "type": "Link" } ], - "modified": "2026-01-02 17:26:44.644507", + "modified": "2026-06-11 11:51:23.641700", "modified_by": "Administrator", "module": "Projects", + "module_onboarding": "Projects Onboarding", "name": "Projects", "number_cards": [ { @@ -219,6 +220,232 @@ "roles": [], "sequence_id": 11.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Projects", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Project", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "projects", + "indent": 0, + "keep_closed": 0, + "label": "Project", + "link_to": "Project", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "list-todo", + "indent": 0, + "keep_closed": 0, + "label": "Task", + "link_to": "Task", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "calendar-clock", + "indent": 0, + "keep_closed": 0, + "label": "Timesheet", + "link_to": "Timesheet", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Activity Type", + "link_to": "Activity Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Activity Cost", + "link_to": "Activity Cost", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Project Template", + "link_to": "Project Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Project Type", + "link_to": "Project Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Project Update", + "link_to": "Project Update", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Project Summary", + "link_to": "Project Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Daily Timesheet Summary", + "link_to": "Daily Timesheet Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Timesheet Billing Summary", + "link_to": "Timesheet Billing Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Project wise Stock Tracking", + "link_to": "Project wise Stock Tracking", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Delayed Tasks Summary", + "link_to": "Delayed Tasks Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Projects Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Projects", "type": "Workspace" } diff --git a/erpnext/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json index 43a9ca18759..76c02e2667f 100644 --- a/erpnext/quality_management/workspace/quality/quality.json +++ b/erpnext/quality_management/workspace/quality/quality.json @@ -161,7 +161,7 @@ "type": "Link" } ], - "modified": "2026-01-02 17:32:47.522875", + "modified": "2026-06-11 11:51:23.514170", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality", @@ -174,6 +174,161 @@ "roles": [], "sequence_id": 9.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Quality", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "inspection-panel", + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection", + "link_to": "Quality Inspection", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "goal", + "indent": 0, + "keep_closed": 0, + "label": "Quality Goal", + "link_to": "Quality Goal", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "review", + "indent": 0, + "keep_closed": 0, + "label": "Quality Review", + "link_to": "Quality Review", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "square-activity", + "indent": 0, + "keep_closed": 0, + "label": "Quality Action", + "link_to": "Quality Action", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "grid-2x2-check", + "indent": 0, + "keep_closed": 0, + "label": "Non Conformance", + "link_to": "Non Conformance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "thumbs-up", + "indent": 0, + "keep_closed": 0, + "label": "Quality Feedback", + "link_to": "Quality Feedback", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "users", + "indent": 0, + "keep_closed": 0, + "label": "Quality Meeting", + "link_to": "Quality Meeting", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Procedure", + "link_to": "Quality Procedure", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Feedback Template", + "link_to": "Quality Feedback Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection Template", + "link_to": "Quality Inspection Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Quality", "type": "Workspace" } diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index 141b3634708..d1b20dfb734 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -622,9 +622,10 @@ "type": "Link" } ], - "modified": "2026-02-19 13:01:26.893303", + "modified": "2026-06-11 11:51:23.367918", "modified_by": "Administrator", "module": "Selling", + "module_onboarding": "Selling Onboarding", "name": "Selling", "number_cards": [ { @@ -648,6 +649,762 @@ "roles": [], "sequence_id": 6.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Selling", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Selling", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Quotation", + "link_to": "Quotation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sell", + "indent": 0, + "keep_closed": 0, + "label": "Sales Order", + "link_to": "Sales Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt", + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "computer", + "indent": 1, + "keep_closed": 1, + "label": "POS", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "POS", + "link_to": "point-of-sale", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Profile", + "link_to": "POS Profile", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Invoice", + "link_to": "POS Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Opening Entry", + "link_to": "POS Opening Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Closing Entry", + "link_to": "POS Closing Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Invoice Merge Log", + "link_to": "POS Invoice Merge Log", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Settings", + "link_to": "POS Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Loyalty Program", + "link_to": "Loyalty Program", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Loyalty Point Entry", + "link_to": "Loyalty Point Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 1, + "keep_closed": 1, + "label": "Items & Pricing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Group", + "link_to": "Item Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Price List", + "link_to": "Price List", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Price", + "link_to": "Item Price", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Pricing Rule", + "link_to": "Pricing Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Promotional Scheme", + "link_to": "Promotional Scheme", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Coupon Code", + "link_to": "Coupon Code", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Blanket Order", + "link_to": "Blanket Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Group", + "link_to": "Customer Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Address", + "link_to": "Address", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contact", + "link_to": "Contact", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Territory", + "link_to": "Territory", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Campaign", + "link_to": "Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person", + "link_to": "Sales Person", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partner", + "link_to": "Sales Partner", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Monthly Distribution", + "link_to": "Monthly Distribution", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Terms Template", + "link_to": "Terms and Conditions", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Tax Template", + "link_to": "Sales Taxes and Charges Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Product Bundle", + "link_to": "Product Bundle", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "UTM Source", + "link_to": "UTM Source", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Shipping Rule", + "link_to": "Shipping Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Register", + "link_to": "Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Sales Register", + "link_to": "Item-wise Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Addresses And Contacts", + "link_to": "Address And Contacts", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Inactive Customers", + "link_to": "Inactive Customers", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice Trends", + "link_to": "Sales Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Credit Balance", + "link_to": "Customer Credit Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customers Without Any Sales Transactions", + "link_to": "Customers Without Any Sales Transactions", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partners Commission", + "link_to": "Sales Partners Commission", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Available Stock for Packing Items", + "link_to": "Available Stock for Packing Items", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Territory Target Variance Based On Item Group", + "link_to": "Territory Target Variance Based On Item Group", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person Target Variance Based On Item Group", + "link_to": "Sales Person Target Variance Based On Item Group", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partner Target Variance Based On Item Group", + "link_to": "Sales Partner Target Variance based on Item Group", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Pending SO Items For Purchase Request", + "link_to": "Pending SO Items For Purchase Request", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Funnel", + "link_to": "sales-funnel", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Order Analysis", + "link_to": "Sales Order Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Acquisition and Loyalty", + "link_to": "Customer Acquisition and Loyalty", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quotation Trends", + "link_to": "Quotation Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Order Trends", + "link_to": "Sales Order Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Sales History", + "link_to": "Item-wise Sales History", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person-wise Transaction Summary", + "link_to": "Sales Person-wise Transaction Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Selling Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Selling", "type": "Workspace" } diff --git a/erpnext/setup/doctype/company/company_list.js b/erpnext/setup/doctype/company/company_list.js index 558e7500e3a..e69de29bb2d 100644 --- a/erpnext/setup/doctype/company/company_list.js +++ b/erpnext/setup/doctype/company/company_list.js @@ -1,5 +0,0 @@ -frappe.listview_settings["Company"] = { - onload() { - frappe.breadcrumbs.add("Accounts"); - }, -}; diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 8c011f32d29..bf6b32c5af4 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -233,7 +233,7 @@ "type": "Link" } ], - "modified": "2025-07-02 14:12:28.407612", + "modified": "2026-06-11 11:51:22.225174", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -267,6 +267,69 @@ "type": "DocType" } ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Home", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Home", "type": "Workspace" } diff --git a/erpnext/setup/workspace/organization/organization.json b/erpnext/setup/workspace/organization/organization.json new file mode 100644 index 00000000000..92298a440c6 --- /dev/null +++ b/erpnext/setup/workspace/organization/organization.json @@ -0,0 +1,126 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:21.789012", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "organization", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Organization", + "link_type": "DocType", + "links": [], + "modified": "2026-06-11 11:51:21.800588", + "modified_by": "Administrator", + "module": "Setup", + "module_onboarding": "Organization Onboarding", + "name": "Organization", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 46.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "organization", + "indent": 0, + "keep_closed": 0, + "label": "Company", + "link_to": "Company", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "book-text", + "indent": 0, + "keep_closed": 0, + "label": "Letter Head", + "link_to": "Letter Head", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "file-user", + "indent": 0, + "keep_closed": 0, + "label": "Department", + "link_to": "Department", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "book-user", + "indent": 0, + "keep_closed": 0, + "label": "Branch", + "link_to": "Branch", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "users", + "indent": 0, + "keep_closed": 0, + "label": "User", + "link_to": "User", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "user-round-check", + "indent": 0, + "keep_closed": 0, + "label": "Role Permissions", + "link_to": "permission-manager", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "mail", + "indent": 0, + "keep_closed": 0, + "label": "Email Account", + "link_to": "Email Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Organization", + "type": "Workspace" +} diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 0a084146d29..f2e067d621f 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -789,9 +789,10 @@ "type": "Link" } ], - "modified": "2026-01-02 12:38:50.043198", + "modified": "2026-06-11 11:51:22.112805", "modified_by": "Administrator", "module": "Stock", + "module_onboarding": "Stock Onboarding", "name": "Stock", "number_cards": [ { @@ -815,6 +816,682 @@ "roles": [], "sequence_id": 7.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Stock", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Stock", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Stock Entry", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Receipt", + "link_to": "Purchase Receipt", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "truck", + "indent": 0, + "keep_closed": 0, + "label": "Delivery Note", + "link_to": "Delivery Note", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-left-to-line", + "indent": 0, + "keep_closed": 0, + "label": "Material Request", + "link_to": "Material Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "caravan", + "indent": 0, + "keep_closed": 0, + "label": "Pick List", + "link_to": "Pick List", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "tool", + "indent": 1, + "keep_closed": 1, + "label": "Tools", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Stock Reconciliation", + "link_to": "Stock Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Landed Cost Voucher", + "link_to": "Landed Cost Voucher", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Item Valuation", + "link_to": "Repost Item Valuation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Packing Slip", + "link_to": "Packing Slip", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection", + "link_to": "Quality Inspection", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_to": "", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Group", + "link_to": "Item Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Attribute", + "link_to": "Item Attribute", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Brand", + "link_to": "Brand", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Warehouse", + "link_to": "Warehouse", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Unit of Measure (UOM)", + "link_to": "UOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "UOM Conversion Factor", + "link_to": "UOM Conversion Factor", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Serial No", + "link_to": "Serial No", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Batch No", + "link_to": "Batch", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Serial and Batch Bundle", + "link_to": "Serial and Batch Bundle", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Inventory Dimension", + "link_to": "Inventory Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Shipping Rule", + "link_to": "Shipping Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Alternative", + "link_to": "Item Alternative", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection Template", + "link_to": "Quality Inspection Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Trip", + "link_to": "Delivery Trip", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Ledger", + "link_to": "Stock Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Balance", + "link_to": "Stock Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quick Stock Balance", + "link_to": "Quick Stock Balance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Projected Qty", + "link_to": "Stock Projected Qty", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Analytics", + "link_to": "Stock Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Ageing", + "link_to": "Stock Ageing", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Receipt Trends", + "link_to": "Purchase Receipt Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Note Trends", + "link_to": "Delivery Note Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Price Stock", + "link_to": "Item Price Stock", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Warehouse Wise Stock Balance", + "link_to": "Warehouse Wise Stock Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Shortage Report", + "link_to": "Item Shortage Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Serial No and Batch Traceability", + "link_to": "Serial No and Batch Traceability", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Serial No Status", + "link_to": "Serial No Status", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Serial No Ledger", + "link_to": "Serial No Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Serial No Warranty Expiry", + "link_to": "Serial No Warranty Expiry", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Batch-Wise Balance History", + "link_to": "Batch-Wise Balance History", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Batch Item Expiry Status", + "link_to": "Batch Item Expiry Status", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Requested Items To Be Transferred", + "link_to": "Requested Items To Be Transferred", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Itemwise Recommended Reorder Level", + "link_to": "Itemwise Recommended Reorder Level", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Variant Details", + "link_to": "Item Variant Details", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 1, + "keep_closed": 1, + "label": "Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Stock Settings", + "link_to": "Stock Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Variant Settings", + "link_to": "Item Variant Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Reposting Settings", + "link_to": "Stock Reposting Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Settings", + "link_to": "Delivery Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Stock", "type": "Workspace" } diff --git a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json index f0d703e0798..1995e64db6d 100644 --- a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json +++ b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json @@ -13,7 +13,7 @@ "doctype": "Workspace", "for_user": "", "hide_custom": 0, - "icon": "organization", + "icon": "getting-started", "idx": 2, "is_hidden": 0, "label": "Subcontracting", @@ -138,9 +138,10 @@ "type": "Link" } ], - "modified": "2025-12-19 16:50:25.976741", + "modified": "2026-06-11 11:51:22.033578", "modified_by": "Administrator", "module": "Subcontracting", + "module_onboarding": "Subcontracting Onboarding", "name": "Subcontracting", "number_cards": [ { @@ -164,6 +165,251 @@ "roles": [], "sequence_id": 8.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Subcontracting", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "folder-tree", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting BOM", + "link_to": "Subcontracting BOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "move-horizontal", + "indent": 0, + "keep_closed": 0, + "label": "Stock Entry", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-left-to-line", + "indent": 1, + "keep_closed": 0, + "label": "Inward Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Sales Order", + "link_to": "Sales Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Inward Order", + "link_to": "Subcontracting Inward Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Delivery", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-right-from-line", + "indent": 1, + "keep_closed": 0, + "label": "Outward Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order", + "link_to": "Purchase Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Order", + "link_to": "Subcontracting Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Receipt", + "link_to": "Subcontracting Receipt", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 0, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bill of Materials", + "link_to": "BOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontract Order Summary", + "link_to": "Subcontract Order Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Materials To Be Transferred", + "link_to": "Subcontracted Raw Materials To Be Transferred", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Items To Be Received", + "link_to": "Subcontracted Item To Be Received", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Buying Settings", + "link_type": "DocType", + "navigate_to_tab": "subcontract", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Subcontracting", "type": "Workspace" } diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json index 8c6647b8d55..a188ea6abe9 100644 --- a/erpnext/support/workspace/support/support.json +++ b/erpnext/support/workspace/support/support.json @@ -1,7 +1,7 @@ { "app": "erpnext", "charts": [], - "content": "[{\"id\":\"HOEnlt9aR9\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe Helpdesk instead.\",\"col\":12}},{\"id\":\"oxhWhXp9b2\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"Ff8Ab3nLLN\",\"type\":\"card\",\"data\":{\"card_name\":\"Issues\",\"col\":4}},{\"id\":\"_lndiuJTVP\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"id\":\"R_aNO5ESzJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Service Level Agreement\",\"col\":4}},{\"id\":\"N8aA2afWfi\",\"type\":\"card\",\"data\":{\"card_name\":\"Warranty\",\"col\":4}},{\"id\":\"M5fxGuFwUR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"xKH0kO9q4P\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "content": "[{\"id\":\"HOEnlt9aR9\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe Helpdesk instead.\",\"col\":12}},{\"id\":\"qzP2mZrGOu\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"Fkdjo6bJ7A\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Issue\",\"col\":3}},{\"id\":\"OTS8kx2f3x\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Maintenance Visit\",\"col\":3}},{\"id\":\"smDTSjBR3Z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Service Level Agreement\",\"col\":3}},{\"id\":\"WCqL_gBYGU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oxhWhXp9b2\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"Ff8Ab3nLLN\",\"type\":\"card\",\"data\":{\"card_name\":\"Issues\",\"col\":4}},{\"id\":\"_lndiuJTVP\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"id\":\"R_aNO5ESzJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Service Level Agreement\",\"col\":4}},{\"id\":\"N8aA2afWfi\",\"type\":\"card\",\"data\":{\"card_name\":\"Warranty\",\"col\":4}},{\"id\":\"M5fxGuFwUR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"xKH0kO9q4P\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:23.224699", "custom_blocks": [], "docstatus": 0, @@ -172,7 +172,7 @@ "type": "Link" } ], - "modified": "2026-01-02 17:45:04.203273", + "modified": "2026-06-11 11:51:23.316581", "modified_by": "Administrator", "module": "Support", "name": "Support", @@ -184,7 +184,179 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 12.0, - "shortcuts": [], + "shortcuts": [ + { + "color": "Yellow", + "format": "{} Assigned", + "label": "Issue", + "link_to": "Issue", + "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}", + "type": "DocType" + }, + { + "label": "Maintenance Visit", + "link_to": "Maintenance Visit", + "type": "DocType" + }, + { + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", + "type": "DocType" + } + ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Support", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "file-question-mark", + "indent": 0, + "keep_closed": 0, + "label": "Issue", + "link_to": "Issue", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "calendar-days", + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Schedule", + "link_to": "Maintenance Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "calendar-check-2", + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Visit", + "link_to": "Maintenance Visit", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "grid-2x2-check", + "indent": 0, + "keep_closed": 0, + "label": "Warranty Claim", + "link_to": "Warranty Claim", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Issue Type", + "link_to": "Issue Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Issue Priority", + "link_to": "Issue Priority", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "First Response Time for Issues", + "link_to": "First Response Time for Issues", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Support Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Support", "type": "Workspace" } From 16c71fa102989ed1ace956fb9be60fdbcab6b900 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:16:41 +0530 Subject: [PATCH 002/195] test: add coverage for Item-wise Sales History report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_item_wise_sales_history.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py diff --git a/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py new file mode 100644 index 00000000000..5eb4468b710 --- /dev/null +++ b/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py @@ -0,0 +1,119 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import ( + create_dn_against_so, + make_sales_order, +) +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestItemWiseSalesHistory(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + **extra, + } + ) + return execute(filters) + + def so_row(self, so_name, **extra): + data = self.run_report(**extra)[1] + return next(row for row in data if row["sales_order"] == so_name) + + def test_sales_order_line_shown_with_values(self): + so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01") + + row = self.so_row(so.name) + self.assertEqual(row["item_code"], "_Test Item") + self.assertEqual(row["quantity"], 10) + self.assertEqual(row["rate"], 100) + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["customer"], "_Test Customer") + + def test_draft_sales_order_excluded(self): + so = make_sales_order(transaction_date="2026-06-01", do_not_submit=True) + + names = {row["sales_order"] for row in self.run_report()[1]} + self.assertNotIn(so.name, names) + + def test_date_range_filters_on_transaction_date(self): + so = make_sales_order(transaction_date="2026-06-01") + + in_range = {row["sales_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1]} + self.assertIn(so.name, in_range) + + out_of_range = { + row["sales_order"] for row in self.run_report(from_date="2026-01-01", to_date="2026-03-01")[1] + } + self.assertNotIn(so.name, out_of_range) + + def test_item_code_filter(self): + so = make_sales_order( + transaction_date="2026-06-01", + item_list=[ + {"item_code": "_Test Item", "qty": 5, "rate": 100, "warehouse": "_Test Warehouse - _TC"}, + {"item_code": "_Test Item 2", "qty": 3, "rate": 200, "warehouse": "_Test Warehouse - _TC"}, + ], + ) + + item_codes = {row["item_code"] for row in self.run_report(item_code="_Test Item 2")[1]} + self.assertEqual(item_codes, {"_Test Item 2"}) + # the filtered-out line of the same order must not leak in + self.assertTrue(all(row["sales_order"] == so.name for row in self.run_report(item_code="_Test Item 2")[1])) + + def test_customer_filter(self): + so = make_sales_order(customer="_Test Customer 1", transaction_date="2026-06-01") + make_sales_order(customer="_Test Customer 2", transaction_date="2026-06-01") + + customers = {row["customer"] for row in self.run_report(customer="_Test Customer 1")[1]} + self.assertEqual(customers, {"_Test Customer 1"}) + + def test_delivered_quantity_reflects_delivery(self): + so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01") + create_dn_against_so(so.name, delivered_qty=4) + + self.assertEqual(self.so_row(so.name)["delivered_quantity"], 4) + + def test_billed_amount_reflects_invoice(self): + so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01") + si = make_sales_invoice(so.name) + si.insert() + si.submit() + + self.assertEqual(self.so_row(so.name)["billed_amount"], 1000) + + def test_amounts_reported_in_company_currency(self): + # a USD order must report rate/amount converted to the company's currency (base_* fields) + so = make_sales_order( + do_not_save=True, + currency="USD", + qty=10, + rate=100, + transaction_date="2026-06-01", + ) + so.conversion_rate = 80 + so.insert() + so.submit() + + row = self.so_row(so.name) + self.assertEqual(row["rate"], 8000) # 100 USD * 80 + self.assertEqual(row["amount"], 80000) # 10 * 100 USD * 80 + + def test_chart_aggregates_amount_per_item(self): + make_sales_order(item_code="_Test Item", qty=2, rate=100, transaction_date="2026-06-01") + make_sales_order(item_code="_Test Item", qty=3, rate=100, transaction_date="2026-06-01") + + chart = self.run_report(item_code="_Test Item")[3] + labels = chart["data"]["labels"] + values = chart["data"]["datasets"][0]["values"] + self.assertIn("_Test Item", labels) + # 2*100 + 3*100 aggregated for the item + self.assertEqual(values[labels.index("_Test Item")], 500) From 34fbcc95144ea9416da338fe32a1d58aa8e213c9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:20:33 +0530 Subject: [PATCH 003/195] test: add coverage for Item-wise Purchase History report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_item_wise_purchase_history.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py diff --git a/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py new file mode 100644 index 00000000000..775c13ee9d0 --- /dev/null +++ b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py @@ -0,0 +1,129 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice +from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, +) +from erpnext.buying.report.item_wise_purchase_history.item_wise_purchase_history import execute +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + + +class TestItemWisePurchaseHistory(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + **extra, + } + ) + return execute(filters) + + def po_row(self, po_name, **extra): + data = self.run_report(**extra)[1] + return next(row for row in data if row["purchase_order"] == po_name) + + def test_purchase_order_line_shown_with_values(self): + po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01") + + row = self.po_row(po.name) + self.assertEqual(row["item_code"], "_Test Item") + self.assertEqual(row["quantity"], 10) + self.assertEqual(row["rate"], 500) + self.assertEqual(row["amount"], 5000) + self.assertEqual(row["supplier"], "_Test Supplier") + + def test_draft_purchase_order_excluded(self): + po = create_purchase_order(transaction_date="2026-06-01", do_not_submit=True) + + names = {row["purchase_order"] for row in self.run_report()[1]} + self.assertNotIn(po.name, names) + + def test_date_range_filters_on_transaction_date(self): + po = create_purchase_order(transaction_date="2026-06-01") + + in_range = {row["purchase_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1]} + self.assertIn(po.name, in_range) + + out_of_range = { + row["purchase_order"] for row in self.run_report(from_date="2026-01-01", to_date="2026-03-01")[1] + } + self.assertNotIn(po.name, out_of_range) + + def test_item_code_filter(self): + po = create_purchase_order( + transaction_date="2026-06-01", + rm_items=[ + {"item_code": "_Test Item", "qty": 5, "rate": 500, "warehouse": "_Test Warehouse - _TC"}, + {"item_code": "_Test Item 2", "qty": 3, "rate": 200, "warehouse": "_Test Warehouse - _TC"}, + ], + ) + + rows = self.run_report(item_code="_Test Item 2")[1] + self.assertEqual({row["item_code"] for row in rows}, {"_Test Item 2"}) + # the filtered-out line of the same order must not leak in + self.assertTrue(all(row["purchase_order"] == po.name for row in rows)) + + def test_item_group_filter(self): + other_item = make_item("_Test IWPH Products Item", {"item_group": "Products"}).name + po_test_group = create_purchase_order(item_code="_Test Item", transaction_date="2026-06-01") + po_products = create_purchase_order(item_code=other_item, transaction_date="2026-06-01") + + names = {row["purchase_order"] for row in self.run_report(item_group="_Test Item Group")[1]} + self.assertIn(po_test_group.name, names) + self.assertNotIn(po_products.name, names) + + def test_supplier_filter(self): + po = create_purchase_order(supplier="_Test Supplier", transaction_date="2026-06-01") + create_purchase_order(supplier="_Test Supplier 1", transaction_date="2026-06-01") + + suppliers = {row["supplier"] for row in self.run_report(supplier="_Test Supplier")[1]} + self.assertEqual(suppliers, {"_Test Supplier"}) + + def test_received_quantity_reflects_receipt(self): + po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01") + create_pr_against_po(po.name, received_qty=4) + + self.assertEqual(self.po_row(po.name)["received_qty"], 4) + + def test_billed_amount_reflects_invoice(self): + po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01") + pi = make_purchase_invoice(po.name) + pi.insert() + pi.submit() + + self.assertEqual(self.po_row(po.name)["billed_amt"], 5000) + + def test_amounts_reported_in_company_currency(self): + # a USD order must report rate/amount converted to the company's currency (base_* fields) + po = create_purchase_order( + do_not_save=True, + currency="USD", + qty=10, + rate=100, + transaction_date="2026-06-01", + ) + po.conversion_rate = 80 + po.insert() + po.submit() + + row = self.po_row(po.name) + self.assertEqual(row["rate"], 8000) # 100 USD * 80 + self.assertEqual(row["amount"], 80000) # 10 * 100 USD * 80 + + def test_chart_aggregates_amount_per_item(self): + create_purchase_order(item_code="_Test Item", qty=2, rate=500, transaction_date="2026-06-01") + create_purchase_order(item_code="_Test Item", qty=3, rate=500, transaction_date="2026-06-01") + + chart = self.run_report(item_code="_Test Item")[3] + labels = chart["data"]["labels"] + values = chart["data"]["datasets"][0]["values"] + self.assertIn("_Test Item", labels) + # 2*500 + 3*500 aggregated for the item + self.assertEqual(values[labels.index("_Test Item")], 2500) From 8b28aa8992635203feda2a8f36b7b07866d6a96a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:23:28 +0530 Subject: [PATCH 004/195] test: add coverage for Stock Ledger report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/stock_ledger/test_stock_ledger.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 erpnext/stock/report/stock_ledger/test_stock_ledger.py diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger.py b/erpnext/stock/report/stock_ledger/test_stock_ledger.py new file mode 100644 index 00000000000..b80f1440a01 --- /dev/null +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger.py @@ -0,0 +1,129 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.stock_ledger.stock_ledger import execute +from erpnext.tests.utils import ERPNextTestSuite + +WH = "_Test Warehouse - _TC" +WH2 = "Stores - _TC" + + +class TestStockLedgerReport(ERPNextTestSuite): + def make_item(self, valuation_method="Moving Average"): + return make_item( + properties={"is_stock_item": 1, "valuation_method": valuation_method} + ).name + + def run_report(self, item_code, warehouse=None, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + "valuation_field_type": "Currency", + "item_code": [item_code] if isinstance(item_code, str) else item_code, + **extra, + } + ) + if warehouse: + filters["warehouse"] = [warehouse] if isinstance(warehouse, str) else warehouse + return execute(filters)[1] + + def sle_rows(self, item_code, **extra): + # drop the synthetic "'Opening'" row, keep only this item's ledger lines + return [row for row in self.run_report(item_code, **extra) if row.get("item_code") == item_code] + + def test_receipt_shows_in_qty_and_balance(self): + item = self.make_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + + (row,) = self.sle_rows(item) + self.assertEqual(row["in_qty"], 10) + self.assertEqual(row["out_qty"], 0) + self.assertEqual(row["qty_after_transaction"], 10) + self.assertEqual(row["incoming_rate"], 100) + self.assertEqual(row["valuation_rate"], 100) + self.assertEqual(row["stock_value"], 1000) + self.assertEqual(row["stock_value_difference"], 1000) + + def test_issue_shows_out_qty_and_outgoing_rate(self): + item = self.make_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WH, qty=4, posting_date="2026-06-02") + + issue = self.sle_rows(item)[-1] + self.assertEqual(issue["in_qty"], 0) + self.assertEqual(issue["out_qty"], -4) + self.assertEqual(issue["qty_after_transaction"], 6) + self.assertEqual(issue["in_out_rate"], 100) # stock_value_difference / actual_qty + self.assertEqual(issue["stock_value"], 600) + + def test_running_balance_across_transactions(self): + item = self.make_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH, qty=5, rate=100, posting_date="2026-06-02") + make_stock_entry(item_code=item, from_warehouse=WH, qty=3, posting_date="2026-06-03") + + balances = [row["qty_after_transaction"] for row in self.sle_rows(item)] + self.assertEqual(balances, [10, 15, 12]) + + def test_moving_average_valuation(self): + item = self.make_item(valuation_method="Moving Average") + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=200, posting_date="2026-06-02") + + latest = self.sle_rows(item)[-1] + # (10*100 + 10*200) / 20 = 150 + self.assertEqual(latest["valuation_rate"], 150) + self.assertEqual(latest["stock_value"], 3000) + + def test_item_code_filter_excludes_other_items(self): + item_a = self.make_item() + item_b = self.make_item() + make_stock_entry(item_code=item_a, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item_b, to_warehouse=WH, qty=7, rate=100, posting_date="2026-06-01") + + item_codes = {row["item_code"] for row in self.run_report(item_a)} + self.assertEqual(item_codes, {item_a}) + + def test_warehouse_filter(self): + item = self.make_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH2, qty=5, rate=100, posting_date="2026-06-01") + + warehouses = {row["warehouse"] for row in self.sle_rows(item, warehouse=WH2)} + self.assertEqual(warehouses, {WH2}) + + def test_voucher_no_filter(self): + item = self.make_item() + se = make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH, qty=5, rate=100, posting_date="2026-06-02") + + rows = self.sle_rows(item, voucher_no=se.name) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0]["voucher_no"], se.name) + + def test_date_range_excludes_out_of_range_entries(self): + item = self.make_item() + se = make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2025-12-01") + + # 2026 window must not include the 2025 entry + self.assertEqual(self.sle_rows(item), []) + # widening the window back to 2025 brings it in + in_window = self.run_report(item, from_date="2025-01-01", to_date="2025-12-31") + self.assertIn(se.name, {row.get("voucher_no") for row in in_window}) + + def test_opening_balance_row(self): + item = self.make_item() + # stock received before the reporting window should surface as the opening balance + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2025-12-01") + + data = self.run_report(item, warehouse=WH) + opening = data[0] + self.assertEqual(opening["item_code"], "'Opening'") + self.assertEqual(opening["qty_after_transaction"], 10) + self.assertEqual(opening["stock_value"], 1000) From e2dc38433e788903d21bee9dfb6627c29c087279 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:34:39 +0530 Subject: [PATCH 005/195] test: cover Enable Serial/Batch Bundle filter in Stock Ledger report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/stock_ledger/test_stock_ledger.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger.py b/erpnext/stock/report/stock_ledger/test_stock_ledger.py index b80f1440a01..2e3118f5647 100644 --- a/erpnext/stock/report/stock_ledger/test_stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger.py @@ -18,6 +18,21 @@ class TestStockLedgerReport(ERPNextTestSuite): properties={"is_stock_item": 1, "valuation_method": valuation_method} ).name + def make_serial_item(self): + return make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SL-SN-.#####"} + ).name + + def make_batch_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SL-BCH-.#####", + } + ).name + def run_report(self, item_code, warehouse=None, **extra): filters = frappe._dict( { @@ -127,3 +142,44 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(opening["item_code"], "'Opening'") self.assertEqual(opening["qty_after_transaction"], 10) self.assertEqual(opening["stock_value"], 1000) + + def test_bundle_not_segregated_by_default(self): + item = self.make_serial_item() + # a single receipt of 3 serials is one ledger line when the filter is off + make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") + + (row,) = self.sle_rows(item) + self.assertEqual(row["in_qty"], 3) + self.assertEqual(row["qty_after_transaction"], 3) + + def test_serial_bundle_segregated_into_per_serial_rows(self): + item = self.make_serial_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") + + rows = self.sle_rows(item, segregate_serial_batch_bundle=1) + # the one receipt is split into one row per serial number + self.assertEqual(len(rows), 3) + self.assertTrue(all(row["in_qty"] == 1 for row in rows)) + self.assertEqual(len({row["serial_no"] for row in rows}), 3) + # running balance accumulates across the segregated rows + self.assertEqual([row["qty_after_transaction"] for row in rows], [1, 2, 3]) + + def test_segregated_issue_rows_show_out_qty_per_serial(self): + item = self.make_serial_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WH, qty=2, posting_date="2026-06-02") + + rows = self.sle_rows(item, segregate_serial_batch_bundle=1) + issue_rows = [row for row in rows if row["out_qty"]] + self.assertEqual(len(issue_rows), 2) + self.assertTrue(all(row["out_qty"] == -1 for row in issue_rows)) + self.assertTrue(all(row["in_out_rate"] == 100 for row in issue_rows)) + + def test_batch_bundle_segregated_shows_batch_no(self): + item = self.make_batch_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + + (row,) = self.sle_rows(item, segregate_serial_batch_bundle=1) + self.assertTrue(row["batch_no"]) + self.assertEqual(row["in_qty"], 10) + self.assertEqual(row["qty_after_transaction"], 10) From d4ec544b25adb9defb0df6563d9537555795075f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:38:45 +0530 Subject: [PATCH 006/195] test: add value-level coverage for Budget Variance report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_budget_variance_report.py | 82 +++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py index a3a652cb658..8d404b1c994 100644 --- a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py @@ -2,26 +2,98 @@ # For license information, please see license.txt import frappe +from frappe.utils import nowdate +from erpnext.accounts.doctype.budget.test_budget import make_budget +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute +from erpnext.accounts.utils import get_fiscal_year from erpnext.tests.utils import ERPNextTestSuite +ACCOUNT = "_Test Account Cost for Goods Sold - _TC" +COST_CENTER = "_Test Cost Center - _TC" +COST_CENTER_2 = "_Test Cost Center 2 - _TC" + class TestBudgetVarianceReport(ERPNextTestSuite): + def setUp(self): + self.fy = get_fiscal_year(nowdate())[0] + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_fiscal_year": self.fy, + "to_fiscal_year": self.fy, + "period": "Yearly", + "budget_against": "Cost Center", + **extra, + } + ) + return execute(filters)[1] + + def report_row(self, data, dimension, account=ACCOUNT): + return next( + row for row in data if row["budget_against"] == dimension and row["account"] == account + ) + + def field(self, label): + return frappe.scrub(f"{label} {self.fy}") + def test_report_executes(self): # Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on # both MariaDB and postgres. - company = frappe.db.get_value("Company", {}, "name") - fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc") columns, *_rest = execute( frappe._dict( { - "company": company, - "from_fiscal_year": fy, - "to_fiscal_year": fy, + "company": "_Test Company", + "from_fiscal_year": self.fy, + "to_fiscal_year": self.fy, "period": "Yearly", "budget_against": "Cost Center", } ) ) self.assertTrue(columns) + + def test_budget_amount_shown_with_zero_actual(self): + make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + + row = self.report_row(self.run_report(), COST_CENTER) + self.assertEqual(row[self.field("Budget")], 120000) + self.assertEqual(row[self.field("Actual")], 0) + self.assertEqual(row[self.field("Variance")], 120000) + + def test_actual_expense_updates_actual_and_variance(self): + make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + # book an actual expense well within the annual budget so the "Stop" action does not block it + make_journal_entry(ACCOUNT, "_Test Bank - _TC", 50000, cost_center=COST_CENTER, submit=True) + + row = self.report_row(self.run_report(), COST_CENTER) + self.assertEqual(row[self.field("Actual")], 50000) + self.assertEqual(row[self.field("Variance")], 70000) # 120000 - 50000 + + def test_budget_against_filter_limits_dimensions(self): + make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER_2, budget_amount=80000, submit_budget=1 + ) + + data = self.run_report(budget_against_filter=[COST_CENTER]) + dimensions = {row["budget_against"] for row in data} + self.assertEqual(dimensions, {COST_CENTER}) + + def test_monthly_period_totals(self): + make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + make_journal_entry(ACCOUNT, "_Test Bank - _TC", 50000, cost_center=COST_CENTER, submit=True) + + row = self.report_row(self.run_report(period="Monthly"), COST_CENTER) + # totals roll up the per-month columns across the year + self.assertEqual(row["total_budget"], 120000) + self.assertEqual(row["total_actual"], 50000) + self.assertEqual(row["total_variance"], 70000) + + def test_no_budget_returns_no_rows(self): + # a dimension without any budget produces no report rows + data = self.run_report(budget_against_filter=["_Test Write Off Cost Center - _TC"]) + self.assertEqual(data, []) From 55c6d16d6969990df9cacae3c2f1e9c5ac363680 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:49:40 +0530 Subject: [PATCH 007/195] test: add coverage for Profitability Analysis report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_profitability_analysis.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py diff --git a/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py new file mode 100644 index 00000000000..453587dbf9f --- /dev/null +++ b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py @@ -0,0 +1,102 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.profitability_analysis.profitability_analysis import execute +from erpnext.tests.utils import ERPNextTestSuite + +INCOME = "Sales - _TC" +EXPENSE = "_Test Account Cost for Goods Sold - _TC" +BANK = "_Test Bank - _TC" + + +class TestProfitabilityAnalysis(ERPNextTestSuite): + def run_report(self, fiscal_year="_Test Fiscal Year 2026", **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "based_on": "Cost Center", + "fiscal_year": fiscal_year, + "from_date": "2026-01-01", + "to_date": "2026-12-31", + **extra, + } + ) + return execute(filters)[1] + + def make_cc(self, name, **args): + create_cost_center(cost_center_name=name, **args) + return name + " - _TC" + + def row(self, data, account): + return next(r for r in data if r.get("account") == account) + + def book_income(self, cost_center, amount, posting_date="2026-06-01"): + create_sales_invoice( + cost_center=cost_center, income_account=INCOME, rate=amount, qty=1, posting_date=posting_date + ) + + def book_expense(self, cost_center, amount, posting_date="2026-06-01"): + make_journal_entry(EXPENSE, BANK, amount, cost_center=cost_center, posting_date=posting_date, submit=True) + + def test_income_expense_and_gross_profit(self): + cc = self.make_cc("_Test PA Leaf") + self.book_income(cc, 10000) + self.book_expense(cc, 4000) + + row = self.row(self.run_report(), cc) + self.assertEqual(row["income"], 10000) + self.assertEqual(row["expense"], 4000) + self.assertEqual(row["gross_profit_loss"], 6000) + + def test_parent_cost_center_accumulates_children(self): + parent = self.make_cc("_Test PA Parent", is_group=1) + child_1 = self.make_cc("_Test PA Child 1", parent_cost_center=parent) + child_2 = self.make_cc("_Test PA Child 2", parent_cost_center=parent) + + self.book_income(child_1, 10000) + self.book_expense(child_2, 3000) + + data = self.run_report() + self.assertEqual(self.row(data, child_1)["income"], 10000) + self.assertEqual(self.row(data, child_2)["expense"], 3000) + + parent_row = self.row(data, parent) + self.assertEqual(parent_row["income"], 10000) + self.assertEqual(parent_row["expense"], 3000) + self.assertEqual(parent_row["gross_profit_loss"], 7000) + + def test_date_range_excludes_out_of_period_entries(self): + cc = self.make_cc("_Test PA Date") + self.book_income(cc, 10000, posting_date="2025-06-01") + + # the 2025 income must not appear in a 2026 report (zero-value rows are dropped) + accounts_2026 = {r.get("account") for r in self.run_report()} + self.assertNotIn(cc, accounts_2026) + + row_2025 = self.row( + self.run_report( + fiscal_year="_Test Fiscal Year 2025", from_date="2025-01-01", to_date="2025-12-31" + ), + cc, + ) + self.assertEqual(row_2025["income"], 10000) + + def test_total_row_sums_income_and_expense(self): + cc = self.make_cc("_Test PA Total") + self.book_income(cc, 10000) + self.book_expense(cc, 4000) + + data = self.run_report() + # the report appends a blank separator row and a totals row at the end + total_row = data[-1] + self.assertEqual(total_row["account"], "'Total'") + # total is built from direct (non-accumulated) values, so it stays internally consistent + self.assertEqual(total_row["gross_profit_loss"], total_row["income"] - total_row["expense"]) + # and it includes this test's bookings + self.assertGreaterEqual(total_row["income"], 10000) + self.assertGreaterEqual(total_row["expense"], 4000) From 7688a7653e06b43e15c7e6ec3ad014ebed0c3851 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 09:52:14 +0530 Subject: [PATCH 008/195] test: add coverage for Gross and Net Profit report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_gross_and_net_profit_report.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py diff --git a/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py new file mode 100644 index 00000000000..fb571dfc4e6 --- /dev/null +++ b/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py @@ -0,0 +1,81 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.gross_and_net_profit_report.gross_and_net_profit_report import execute +from erpnext.tests.utils import ERPNextTestSuite + +BANK = "_Test Bank - _TC" +INCOME_PARENT = "Income - _TC" +EXPENSE_PARENT = "Expenses - _TC" +# an isolated fiscal year so other accounts contribute nothing to the totals +FY = "_Test Fiscal Year 2049" +DATE = "2049-06-01" + + +class TestGrossAndNetProfitReport(ERPNextTestSuite): + def run_report(self, from_fiscal_year=FY, to_fiscal_year=FY): + filters = frappe._dict( + { + "company": "_Test Company", + "filter_based_on": "Fiscal Year", + "from_fiscal_year": from_fiscal_year, + "to_fiscal_year": to_fiscal_year, + "period_start_date": "2049-01-01", + "period_end_date": "2049-12-31", + "periodicity": "Yearly", + "accumulated_values": 0, + "presentation_currency": None, + } + ) + return execute(filters)[1] + + def make_account(self, name, parent, include_in_gross): + account = create_account(account_name=name, parent_account=parent, company="_Test Company") + frappe.db.set_value("Account", account, "include_in_gross", include_in_gross) + return account + + def book_income(self, account, amount): + make_journal_entry(BANK, account, amount, posting_date=DATE, submit=True) + + def book_expense(self, account, amount): + make_journal_entry(account, BANK, amount, posting_date=DATE, submit=True) + + def report_row(self, data, account): + return next(row for row in data if row.get("account") == account) + + def test_gross_profit_excludes_non_gross_accounts(self): + gross_income = self.make_account("_Test GNP Gross Income", INCOME_PARENT, include_in_gross=1) + non_gross_income = self.make_account("_Test GNP Other Income", INCOME_PARENT, include_in_gross=0) + gross_expense = self.make_account("_Test GNP Gross Expense", EXPENSE_PARENT, include_in_gross=1) + non_gross_expense = self.make_account("_Test GNP Other Expense", EXPENSE_PARENT, include_in_gross=0) + + self.book_income(gross_income, 10000) + self.book_income(non_gross_income, 2000) + self.book_expense(gross_expense, 4000) + self.book_expense(non_gross_expense, 1000) + + data = self.run_report() + # gross profit only counts include_in_gross accounts: 10000 - 4000 + self.assertEqual(self.report_row(data, "'Gross Profit'")["total"], 6000) + # net profit counts everything: (10000 + 2000) - (4000 + 1000) + self.assertEqual(self.report_row(data, "'Net Profit'")["total"], 7000) + + def test_net_profit_equals_gross_when_all_included(self): + income = self.make_account("_Test GNP All Income", INCOME_PARENT, include_in_gross=1) + expense = self.make_account("_Test GNP All Expense", EXPENSE_PARENT, include_in_gross=1) + + self.book_income(income, 9000) + self.book_expense(expense, 5000) + + data = self.run_report() + self.assertEqual(self.report_row(data, "'Gross Profit'")["total"], 4000) + self.assertEqual(self.report_row(data, "'Net Profit'")["total"], 4000) + + def test_nothing_included_in_gross_when_no_entries(self): + # a fiscal year with no income/expense entries yields the placeholder row + data = self.run_report(from_fiscal_year="_Test Fiscal Year 2048", to_fiscal_year="_Test Fiscal Year 2048") + self.assertEqual(data[0]["account"], "'Nothing is included in gross'") From c7d6b6c0c4631c34664250a0ccd19a2cfd2495d2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:18:48 +0530 Subject: [PATCH 009/195] test: add coverage for Warehouse Wise Stock Balance report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_warehouse_wise_stock_balance.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py b/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py new file mode 100644 index 00000000000..527baee591f --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py @@ -0,0 +1,31 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.report.warehouse_wise_stock_balance.warehouse_wise_stock_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWarehouseWiseStockBalance(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company", **extra}) + return execute(filters)[1] + + def row(self, data, warehouse): + return next(w for w in data if w["name"] == warehouse) + + def test_balance_and_parent_accumulation(self): + parent = create_warehouse("_Test WWSB Parent", properties={"is_group": 1}) + child = create_warehouse("_Test WWSB Child", properties={"parent_warehouse": parent}) + + make_stock_entry(item_code=make_item().name, to_warehouse=child, qty=10, rate=100) + + data = self.run_report() + # stock balance = sum of stock value difference (10 * 100) + self.assertEqual(self.row(data, child)["stock_balance"], 1000) + # the group warehouse rolls up its children + self.assertEqual(self.row(data, parent)["stock_balance"], 1000) From 3c749ec7852b30ea69edcb3fad8043b6dabfdfb5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:18:57 +0530 Subject: [PATCH 010/195] test: add coverage for Total Stock Summary report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_total_stock_summary.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 erpnext/stock/report/total_stock_summary/test_total_stock_summary.py diff --git a/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py b/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py new file mode 100644 index 00000000000..1301789d2fa --- /dev/null +++ b/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.report.total_stock_summary.total_stock_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestTotalStockSummary(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company", "group_by": "Warehouse", **extra}) + return execute(filters)[1] + + def test_warehouse_wise_quantity(self): + item = make_item().name + warehouse = create_warehouse("_Test TSS Warehouse") + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100) + + # rows are (warehouse, item_code, description, actual_qty) + row = next(r for r in self.run_report() if r[0] == warehouse and r[1] == item) + self.assertEqual(row[3], 10) + + def test_only_non_zero_bins_are_listed(self): + item = make_item().name + warehouse = create_warehouse("_Test TSS Empty Warehouse") + # receive then issue everything -> bin actual_qty back to zero + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=5, rate=100) + make_stock_entry(item_code=item, from_warehouse=warehouse, qty=5) + + self.assertFalse([r for r in self.run_report() if r[0] == warehouse and r[1] == item]) From 752aefbdfd96b20e1e9a832afa25ae4e02a3575c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:11 +0530 Subject: [PATCH 011/195] test: add coverage for Product Bundle Balance report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_product_bundle_balance.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py diff --git a/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py new file mode 100644 index 00000000000..865403b8528 --- /dev/null +++ b/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py @@ -0,0 +1,43 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.product_bundle_balance.product_bundle_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + +WH = "_Test Warehouse - _TC" + + +class TestProductBundleBalance(ERPNextTestSuite): + def make_bundle(self, parent, child_qty): + bundle = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": parent}) + for item_code, qty in child_qty.items(): + bundle.append("items", {"item_code": item_code, "qty": qty}) + bundle.insert() + bundle.submit() + + def run_report(self, item_code): + filters = frappe._dict({"company": "_Test Company", "item_code": item_code}) + return execute(filters)[1] + + def test_bundle_qty_is_limited_by_scarcest_child(self): + parent = make_item(properties={"is_stock_item": 0, "is_sales_item": 1}).name + child_a = make_item(properties={"is_stock_item": 1}).name + child_b = make_item(properties={"is_stock_item": 1}).name + self.make_bundle(parent, {child_a: 2, child_b: 1}) + + make_stock_entry(item_code=child_a, to_warehouse=WH, qty=10, rate=100) + make_stock_entry(item_code=child_b, to_warehouse=WH, qty=3, rate=100) + + data = self.run_report(parent) + parent_row = next(r for r in data if r["item_code"] == parent and r["indent"] == 0) + # min(10 // 2, 3 // 1) = min(5, 3) = 3 buildable bundles + self.assertEqual(parent_row["bundle_qty"], 3) + + row_a = next(r for r in data if r["item_code"] == child_a and r["indent"] == 1) + row_b = next(r for r in data if r["item_code"] == child_b and r["indent"] == 1) + self.assertEqual((row_a["actual_qty"], row_a["minimum_qty"], row_a["bundle_qty"]), (10, 2, 5)) + self.assertEqual((row_b["actual_qty"], row_b["minimum_qty"], row_b["bundle_qty"]), (3, 1, 3)) From 119e0caafb3b35dec79797f40f7f32236e52dfb7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:17 +0530 Subject: [PATCH 012/195] test: add coverage for Item-wise Consumption report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_item_wise_consumption.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py diff --git a/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py new file mode 100644 index 00000000000..1b417e712c2 --- /dev/null +++ b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py @@ -0,0 +1,46 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.item_wise_consumption.item_wise_consumption import execute +from erpnext.tests.utils import ERPNextTestSuite + +WH = "_Test Warehouse - _TC" +# row: 0 item, 1 name, 2 desc, 3 uom, 4 consumed_qty, 5 consumed_amt, 6 delivered_qty, +# 7 delivered_amt, 8 total_qty, 9 total_amt, 10 suppliers + + +class TestItemWiseConsumption(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "from_date": "2026-01-01", "to_date": "2026-12-31", **extra} + ) + return execute(filters)[1] + + def test_consumed_vs_delivered_split(self): + item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1, "is_sales_item": 1}).name + # purchase receipt gives the supplier mapping and stocks the item + make_purchase_receipt( + item_code=item, + qty=10, + rate=100, + warehouse=WH, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + # a material issue counts as "consumed", a delivery note counts as "delivered" + make_stock_entry(item_code=item, from_warehouse=WH, qty=4, posting_date="2026-06-02") + create_delivery_note(item_code=item, qty=3, warehouse=WH, posting_date="2026-06-03") + + row = next(r for r in self.run_report() if r[0] == item) + self.assertEqual(row[4], 4) # consumed qty + self.assertEqual(row[5], 400) # consumed amount + self.assertEqual(row[6], 3) # delivered qty + self.assertEqual(row[7], 300) # delivered amount + self.assertEqual(row[8], 7) # total qty + self.assertIn("_Test Supplier", row[10]) From ec739b213dd7b4b2f9f1bb264eaa06024b23672f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:24 +0530 Subject: [PATCH 013/195] test: add coverage for FIFO Queue vs Qty After Transaction Comparison report Co-Authored-By: Claude Opus 4.8 (1M context) --- ...eue_vs_qty_after_transaction_comparison.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py new file mode 100644 index 00000000000..27bf999c7f3 --- /dev/null +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py @@ -0,0 +1,33 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.fifo_queue_vs_qty_after_transaction_comparison.fifo_queue_vs_qty_after_transaction_comparison import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestFifoQueueVsQtyAfterTransactionComparison(ERPNextTestSuite): + def run_report(self, filters: dict) -> list: + return execute(frappe._dict(filters))[1] + + def test_healthy_fifo_item_no_mismatch(self): + item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name + warehouse = "_Test Warehouse - _TC" + + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=5, rate=120, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=warehouse, qty=4, posting_date="2026-06-02") + + data = self.run_report({"company": "_Test Company", "item_code": item}) + + item_codes = [row.get("item_code") for row in data if row] + self.assertNotIn(item, item_codes) + + def test_requires_a_filter(self): + with self.assertRaises(frappe.ValidationError): + self.run_report({"company": "_Test Company"}) From 7c8ef4cfc6e6dcd601617ff11b52fff550e3579d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:31 +0530 Subject: [PATCH 014/195] test: add coverage for Incorrect Balance Qty After Transaction report Co-Authored-By: Claude Opus 4.8 (1M context) --- ...incorrect_balance_qty_after_transaction.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py new file mode 100644 index 00000000000..15ac648988b --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py @@ -0,0 +1,41 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_balance_qty_after_transaction.incorrect_balance_qty_after_transaction import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + +WAREHOUSE = "_Test Warehouse - _TC" +COMPANY = "_Test Company" + + +class TestIncorrectBalanceQtyAfterTransaction(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE}) + filters.update(extra) + return execute(filters)[1] + + def test_healthy_stock_not_flagged(self): + item = make_item(properties={"is_stock_item": 1}).name + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=4, rate=100, posting_date="2026-06-02") + + data = self.run_report(item_code=item) + flagged = [row for row in data if row.get("item_code") == item] + self.assertEqual(flagged, []) + + def test_sequence_of_movements_not_flagged(self): + item = make_item(properties={"is_stock_item": 1}).name + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=20, rate=50, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=5, rate=50, posting_date="2026-06-02") + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=8, rate=50, posting_date="2026-06-03") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=3, rate=50, posting_date="2026-06-04") + + data = self.run_report(item_code=item) + flagged = [row for row in data if row.get("item_code") == item] + self.assertEqual(flagged, []) From 047014f2b569a8fbcf9ede9bd6c93d21830d1392 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:38 +0530 Subject: [PATCH 015/195] test: add coverage for Incorrect Serial and Batch Bundle report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_serial_and_batch_bundle.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py new file mode 100644 index 00000000000..e0bca85a1b4 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestIncorrectSerialAndBatchBundle(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def test_healthy_bundles_not_flagged(self): + batch_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "ISBB-.#####", + } + ).name + serial_item = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "ISBS-.#####", + } + ).name + + make_stock_entry( + item_code=batch_item, + qty=10, + rate=100, + to_warehouse="_Test Warehouse - _TC", + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=serial_item, + qty=3, + rate=100, + to_warehouse="_Test Warehouse - _TC", + posting_date="2026-06-01", + ) + + data = self.run_report() + + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={"item_code": ["in", [batch_item, serial_item]]}, + pluck="name", + ) + + flagged_names = {row.get("name") for row in data} + self.assertFalse( + flagged_names.intersection(bundles), + msg="Healthy serial/batch bundles should not be flagged as incorrect.", + ) From 06592a49c85e45ddbb8a2f062dfccb9dffe7768d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:45 +0530 Subject: [PATCH 016/195] test: add coverage for Incorrect Serial No Valuation report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_serial_no_valuation.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py new file mode 100644 index 00000000000..96b5ca11451 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -0,0 +1,66 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestIncorrectSerialNoValuation(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def make_serial_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "ISV-.#####", + } + ).name + + def test_healthy_serial_item_not_flagged(self): + item = self.make_serial_item() + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=3, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + from_warehouse="_Test Warehouse - _TC", + qty=1, + posting_date="2026-06-02", + ) + + data = self.run_report(item_code=item) + + flagged_items = {row.get("item_code") for row in data if isinstance(row, dict)} + self.assertNotIn(item, flagged_items) + + def test_only_balance_row_when_filtered_to_healthy_item(self): + item = self.make_serial_item() + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=3, + rate=100, + posting_date="2026-06-01", + ) + + data = self.run_report(item_code=item) + + # The report always appends a single "Balance" summary row. A healthy + # serial item contributes no detail rows, so only that summary remains. + self.assertEqual(len(data), 1) + self.assertEqual(data[-1].get("qty"), 0) + self.assertEqual(data[-1].get("valuation_rate"), 0) From ddb07bcc0afeacef7499461ead43ff5f6993c6d0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:52 +0530 Subject: [PATCH 017/195] test: add coverage for Negative Batch Report report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_negative_batch_report.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 erpnext/stock/report/negative_batch_report/test_negative_batch_report.py diff --git a/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py new file mode 100644 index 00000000000..ccb80e340f2 --- /dev/null +++ b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py @@ -0,0 +1,54 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestNegativeBatchReport(ERPNextTestSuite): + def run_report(self, item_code): + from erpnext.stock.report.negative_batch_report.negative_batch_report import execute + + return execute( + frappe._dict( + { + "company": "_Test Company", + "warehouse": "_Test Warehouse - _TC", + "item_code": item_code, + } + ) + )[1] + + def test_healthy_batch_not_negative(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "NBR-.#####", + } + ).name + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + from_warehouse="_Test Warehouse - _TC", + qty=4, + posting_date="2026-06-02", + ) + + data = self.run_report(item) + + # The batch was only received (10) before being issued (4), so its running + # balance never goes negative; the report must not list this item's batch. + self.assertFalse([row for row in data if row.get("item_code") == item]) From e9d4e2ceddf9e0dff8e946f28a729242c30af5f1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:20:07 +0530 Subject: [PATCH 018/195] test: add coverage for Stock Ledger Variance report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_stock_ledger_variance.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py diff --git a/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py new file mode 100644 index 00000000000..e561a2fce5a --- /dev/null +++ b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py @@ -0,0 +1,83 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestStockLedgerVariance(ERPNextTestSuite): + def run_report(self, **extra): + from erpnext.stock.report.stock_ledger_variance.stock_ledger_variance import execute + + filters = {"company": "_Test Company"} + filters.update(extra) + + return execute(frappe._dict(filters))[1] + + def test_healthy_stock_has_no_variance(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + from_warehouse="_Test Warehouse - _TC", + qty=4, + posting_date="2026-06-02", + ) + + # A clean receipt followed by a clean issue keeps the ledger consistent, + # so the corruption detector must not flag any entry for this item. + data = self.run_report(item_code=item) + self.assertFalse([row for row in data if row.get("item_code") == item]) + + qty_data = self.run_report(item_code=item, difference_in="Qty") + self.assertFalse([row for row in qty_data if row.get("item_code") == item]) + + def test_multiple_clean_movements_no_variance(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=5, + rate=120, + posting_date="2026-06-02", + ) + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=8, + rate=90, + posting_date="2026-06-03", + ) + make_stock_entry( + item_code=item, + from_warehouse="_Test Warehouse - _TC", + qty=6, + posting_date="2026-06-04", + ) + + # Several receipts at different rates plus an issue still produce a + # self-consistent ledger, so no variance rows are expected. + data = self.run_report(item_code=item) + self.assertFalse([row for row in data if row.get("item_code") == item]) From 655dea37dd2cb33609b5a5d564ae57b13ebe7a89 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:20:15 +0530 Subject: [PATCH 019/195] test: add coverage for Stock Qty vs Batch Qty report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_stock_qty_vs_batch_qty.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py new file mode 100644 index 00000000000..e68365c491f --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py @@ -0,0 +1,67 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.stock_qty_vs_batch_qty.stock_qty_vs_batch_qty import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestStockQtyVsBatchQty(ERPNextTestSuite): + def run_report(self, **extra): + return execute(frappe._dict({"company": "_Test Company", **extra}))[1] + + def make_batch_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SQB-.#####", + } + ).name + + def rows_for_item(self, data, item): + return [row for row in data if row["item_code"] == item] + + def test_stock_qty_matches_batch_qty(self): + item = self.make_batch_item() + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + + # The report only lists batches where stock qty and batch qty differ. + # A healthy item has difference == 0, so it must be absent from results. + data = self.run_report(item=item) + self.assertEqual(self.rows_for_item(data, item), []) + + def test_mismatch_reports_difference(self): + item = self.make_batch_item() + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + batch_no = frappe.db.get_value("Batch", {"item": item}, "name") + self.assertTrue(batch_no) + + # Corrupt the stored batch qty so it no longer matches actual stock qty (10). + frappe.db.set_value("Batch", batch_no, "batch_qty", 7) + + data = self.run_report(item=item, batch=batch_no) + rows = self.rows_for_item(data, item) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row["batch"], batch_no) + self.assertEqual(row["batch_qty"], 7) + self.assertEqual(row["stock_qty"], 10) + self.assertEqual(row["difference"], 3) From 495677ceb7f76b89fa4a7b03fd0f6ea53d2be73f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:25:26 +0530 Subject: [PATCH 020/195] test: add coverage for Warehouse Wise Item Balance Age and Value report Co-Authored-By: Claude Opus 4.8 (1M context) --- ...rehouse_wise_item_balance_age_and_value.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py new file mode 100644 index 00000000000..cb50892290c --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py @@ -0,0 +1,56 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.warehouse_wise_item_balance_age_and_value.warehouse_wise_item_balance_age_and_value import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWarehouseWiseItemBalanceAgeAndValue(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + "warehouse": "_Test Warehouse - _TC", + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_balance_qty_and_value(self): + item_code = make_item(properties={"is_stock_item": 1}).name + warehouse = "_Test Warehouse - _TC" + + make_stock_entry( + item_code=item_code, + to_warehouse=warehouse, + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item_code, + from_warehouse=warehouse, + qty=4, + posting_date="2026-06-02", + ) + + data = self.run_report(item_code=item_code) + + # With a single (leaf) warehouse filter the row shape is: + # [item, item_name, item_group, brand, value, age, bal_qty] + rows = [row for row in data if row[0] == item_code] + self.assertEqual(len(rows), 1) + + row = rows[0] + # index 6 -> balance qty in the filtered warehouse + self.assertEqual(row[6], 6) + # index 4 -> total stock value (6 units @ 100) + self.assertEqual(row[4], 600) From 6d97a5d543d8f9fe5c07fe0d5ea9ff8b8a3d9a06 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:25:33 +0530 Subject: [PATCH 021/195] test: add coverage for Item Prices report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/item_prices/test_item_prices.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 erpnext/stock/report/item_prices/test_item_prices.py diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py new file mode 100644 index 00000000000..d77b5f60716 --- /dev/null +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -0,0 +1,63 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.report.item_prices.item_prices import execute +from erpnext.tests.utils import ERPNextTestSuite + +# Positional columns returned by the report (it returns string-format columns, +# not dicts, so rows are plain lists indexed by position). +ITEM_CODE = 0 +SALES_PRICE_LIST = 8 +PURCHASE_PRICE_LIST = 9 + + +class TestItemPrices(ERPNextTestSuite): + """Correctness tests for the Item Prices report.""" + + def run_report(self, **extra): + filters = frappe._dict({"items": "Enabled Items only", **extra}) + return execute(filters)[1] + + def row_for(self, data, item_code): + for row in data: + if row[ITEM_CODE] == item_code: + return row + self.fail(f"No report row found for item {item_code}") + + def test_item_selling_price_listed(self): + """A Standard Selling Item Price shows up in the Sales Price List column.""" + item = make_item(properties={"is_stock_item": 1}).name + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item, + "price_list": "Standard Selling", + "price_list_rate": 250, + } + ).insert() + + row = self.row_for(self.run_report(), item) + self.assertIn("250.0", row[SALES_PRICE_LIST]) + self.assertIn("Standard Selling", row[SALES_PRICE_LIST]) + # A selling price must not leak into the buying column. + self.assertFalse(row[PURCHASE_PRICE_LIST]) + + def test_item_buying_price_listed(self): + """A Standard Buying Item Price shows up in the Purchase Price List column.""" + item = make_item(properties={"is_stock_item": 1}).name + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item, + "price_list": "Standard Buying", + "price_list_rate": 175, + } + ).insert() + + row = self.row_for(self.run_report(), item) + self.assertIn("175.0", row[PURCHASE_PRICE_LIST]) + self.assertIn("Standard Buying", row[PURCHASE_PRICE_LIST]) + self.assertFalse(row[SALES_PRICE_LIST]) From 993578dc2f6cdab12e62a011ccdc43d905fd9740 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:25:48 +0530 Subject: [PATCH 022/195] test: add coverage for Itemwise Recommended Reorder Level report Co-Authored-By: Claude Opus 4.8 (1M context) --- ...test_itemwise_recommended_reorder_level.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py new file mode 100644 index 00000000000..9483f0f6f18 --- /dev/null +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py @@ -0,0 +1,66 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.itemwise_recommended_reorder_level.itemwise_recommended_reorder_level import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + +WAREHOUSE = "_Test Warehouse - _TC" + + +class TestItemwiseRecommendedReorderLevel(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"from_date": "2026-06-01", "to_date": "2026-06-10"}) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, item_code): + for row in data: + if row[0] == item_code: + return row + return None + + def test_consumption_drives_recommendation(self): + item = make_item(properties={"is_stock_item": 1, "lead_time_days": 3, "safety_stock": 5}).name + + # Receive stock, then issue a known total across dates inside the report window. + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=100, rate=10, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=20, posting_date="2026-06-03") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=30, posting_date="2026-06-07") + + data = self.run_report(item_group="All Item Groups") + row = self.find_row(data, item) + self.assertIsNotNone(row, msg=f"Item {item} not found in report") + + float_precision = frappe.db.get_default("float_precision") + # Window 2026-06-01..2026-06-10 inclusive => diff = 10 days. + diff = 10 + total_outgoing = 50.0 # 20 + 30 issued + expected_avg = flt(total_outgoing / diff, float_precision) # 5.0 + expected_reorder = (expected_avg * 3) + 5 # avg * lead_time_days + safety_stock = 20 + + # Row shape: [item, item_name, item_group, brand, description, + # safety_stock, lead_time_days, consumed, delivered, total_outgoing, + # avg_daily_outgoing, reorder_level] + self.assertEqual(flt(row[7]), total_outgoing) # consumed + self.assertEqual(flt(row[8]), 0.0) # delivered + self.assertEqual(flt(row[9]), total_outgoing) # total outgoing + self.assertEqual(flt(row[10]), expected_avg) # avg daily outgoing + self.assertEqual(flt(row[11]), expected_reorder) # reorder level + + def test_no_consumption_yields_zero_outgoing(self): + item = make_item(properties={"is_stock_item": 1, "lead_time_days": 3, "safety_stock": 5}).name + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=100, rate=10, posting_date="2026-06-01") + + row = self.find_row(self.run_report(), item) + self.assertIsNotNone(row) + self.assertEqual(flt(row[9]), 0.0) # total outgoing + self.assertEqual(flt(row[10]), 0.0) # avg daily outgoing + # With no consumption, reorder level falls back to safety_stock only. + self.assertEqual(flt(row[11]), 5.0) From 68330843d8d04eb29d629973cef976f7ea88a9e6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:32:43 +0530 Subject: [PATCH 023/195] test: add coverage for Purchase Receipt Trends report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_purchase_receipt_trends.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py diff --git a/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py new file mode 100644 index 00000000000..87511222754 --- /dev/null +++ b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py @@ -0,0 +1,40 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.report.purchase_receipt_trends.purchase_receipt_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestPurchaseReceiptTrends(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": "_Test Fiscal Year 2026", + "period": "Yearly", + "based_on": "Item", + "group_by": "", + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_receipt_qty_in_trend(self): + item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1}).name + make_purchase_receipt( + item_code=item, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + + data = self.run_report() + + # Row layout for based_on="Item", Yearly, no group_by: + # [item_code, item_name, currency, FY(Qty), FY(Amt), Total(Qty), Total(Amt)] + row = next(r for r in data if r[0] == item) + self.assertEqual(row[3], 10) # fiscal-year qty + self.assertEqual(row[4], 1000) # fiscal-year amount (10 * 100) + self.assertEqual(row[5], 10) # Total(Qty) + self.assertEqual(row[6], 1000) # Total(Amt) From 25bcd12e923039c64e34bd4a7f8317907d2a90b0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:32:51 +0530 Subject: [PATCH 024/195] test: add coverage for Delivery Note Trends report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_delivery_note_trends.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py diff --git a/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py new file mode 100644 index 00000000000..003b481f85e --- /dev/null +++ b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py @@ -0,0 +1,59 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDeliveryNoteTrends(ERPNextTestSuite): + def run_report(self, **extra): + from erpnext.stock.report.delivery_note_trends.delivery_note_trends import execute + + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": "_Test Fiscal Year 2026", + "period": "Yearly", + "based_on": "Item", + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_delivery_qty_in_trend(self): + # based_on="Item" + period="Yearly": each row is + # [item_code, item_name, currency, yearly_qty, yearly_amt, total_qty, total_amt]. + # A submitted Delivery Note of qty 5 @ rate 200 should sum to qty 5 / amount 1000 + # (base_net_amount) in both the yearly bucket and the Total columns. + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=20, + rate=100, + posting_date="2026-06-01", + ) + create_delivery_note( + item_code=item, + qty=5, + rate=200, + company="_Test Company", + posting_date="2026-06-01", + ) + + data = self.run_report() + + item_rows = [row for row in data if row[0] == item] + self.assertEqual(len(item_rows), 1) + + row = item_rows[0] + self.assertEqual(row[3], 5) # yearly qty bucket + self.assertEqual(row[4], 1000) # yearly amount bucket (base_net_amount) + self.assertEqual(row[5], 5) # Total(Qty) + self.assertEqual(row[6], 1000) # Total(Amt) From 2092909f213bf062d14901999d4925f90be60b43 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:39:51 +0530 Subject: [PATCH 025/195] test: add coverage for Serial No and Batch Traceability report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_serial_no_and_batch_traceability.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py b/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py new file mode 100644 index 00000000000..3b145d672ff --- /dev/null +++ b/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py @@ -0,0 +1,99 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.serial_no_and_batch_traceability.serial_no_and_batch_traceability import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestSerialNoAndBatchTraceability(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def make_serial_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SNT-.#####", + } + ).name + + def test_serial_movements_traced(self): + """Backward trace should surface the receipt voucher the serial came in through.""" + item = self.make_serial_item() + receipt = make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=2, + rate=100, + posting_date="2026-06-01", + company="_Test Company", + ) + serial_no = frappe.get_all("Serial No", {"item_code": item}, pluck="name")[0] + + rows = self.run_report( + item_code=item, + serial_nos=[serial_no], + traceability_direction="Backward", + ) + + traced = {row["reference_name"]: row for row in rows if row.get("reference_name")} + self.assertIn(receipt.name, traced) + + receipt_row = traced[receipt.name] + self.assertEqual(receipt_row["serial_no"], serial_no) + self.assertEqual(receipt_row["item_code"], item) + self.assertEqual(receipt_row["reference_doctype"], "Stock Entry") + self.assertEqual(receipt_row["warehouse"], "_Test Warehouse - _TC") + self.assertEqual(receipt_row["direction"], "Backward") + self.assertGreater(receipt_row["qty"], 0) + + def test_forward_and_backward_directions(self): + """'Both' should trace backward to the receipt and forward to the outward delivery.""" + item = self.make_serial_item() + receipt = make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=2, + rate=100, + posting_date="2026-06-01", + company="_Test Company", + ) + serial_no = frappe.get_all("Serial No", {"item_code": item}, pluck="name")[0] + + delivery_note = create_delivery_note( + item_code=item, + qty=1, + serial_no=[serial_no], + warehouse="_Test Warehouse - _TC", + posting_date="2026-06-03", + company="_Test Company", + ) + + rows = self.run_report( + item_code=item, + serial_nos=[serial_no], + traceability_direction="Both", + ) + + traced = {row["reference_name"]: row for row in rows if row.get("reference_name")} + + self.assertIn(receipt.name, traced) + self.assertEqual(traced[receipt.name]["direction"], "Backward") + + self.assertIn(delivery_note.name, traced) + forward_row = traced[delivery_note.name] + self.assertEqual(forward_row["reference_doctype"], "Delivery Note") + self.assertEqual(forward_row["serial_no"], serial_no) + self.assertEqual(forward_row["direction"], "Forward") + self.assertEqual(forward_row["customer"], delivery_note.customer) + self.assertLess(forward_row["qty"], 0) From 6595a32d909bc7a9d976223bec051664739b17e5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:40:07 +0530 Subject: [PATCH 026/195] test: add coverage for COGS By Item Group report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_cogs_by_item_group.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py diff --git a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py new file mode 100644 index 00000000000..eb5dc7a434d --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py @@ -0,0 +1,67 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.cogs_by_item_group.cogs_by_item_group import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCogsByItemGroup(ERPNextTestSuite): + def run_report(self, **extra) -> list: + filters = frappe._dict( + company="_Test Company with perpetual inventory", + from_date="2026-01-01", + to_date="2026-12-31", + ) + filters.update(extra) + return execute(filters)[1] + + def test_cogs_for_item_group(self): + frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": "_Test COGS Group", + "parent_item_group": "All Item Groups", + "is_group": 0, + } + ).insert(ignore_if_duplicate=True) + + item = make_item( + properties={"is_stock_item": 1, "is_sales_item": 1, "item_group": "_Test COGS Group"} + ).name + + make_stock_entry( + item_code=item, + to_warehouse="Stores - TCP1", + qty=10, + rate=100, + company="_Test Company with perpetual inventory", + posting_date="2026-06-01", + ) + + # A Sales Invoice with update_stock delivers the goods and books the COGS + # against the company's default expense account, which the report keys on. + create_sales_invoice( + item_code=item, + qty=4, + rate=150, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + update_stock=1, + cost_center="Main - TCP1", + parent_cost_center="Main - TCP1", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + posting_date="2026-06-02", + ) + + data = self.run_report() + rows = [row for row in data if "_Test COGS Group" in row.get("item_group")] + self.assertTrue(rows, "No row found for _Test COGS Group") + # 4 units delivered at 100 valuation rate -> 400 COGS. + self.assertEqual(rows[0].get("cogs_debit"), 400) From 403788324a7f823c5ec068b84585faa319d4112d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:40:24 +0530 Subject: [PATCH 027/195] test: add coverage for Stock and Account Value Comparison report Co-Authored-By: Claude Opus 4.8 (1M context) --- ...test_stock_and_account_value_comparison.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py diff --git a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py new file mode 100644 index 00000000000..ffff5411e90 --- /dev/null +++ b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py @@ -0,0 +1,44 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company with perpetual inventory" + + +class TestStockAndAccountValueComparison(ERPNextTestSuite): + def test_balanced_warehouse_not_flagged(self): + warehouse = create_warehouse("_Test SAVC WH", company=COMPANY) + account = frappe.get_value("Warehouse", warehouse, "account") + item = make_item(properties={"is_stock_item": 1}).name + + make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=10, + rate=100, + company=COMPANY, + posting_date="2026-06-01", + ) + + # Filtering by the isolated account restricts both the stock-ledger and GL + # scans to this fresh warehouse's account only. + rows = self.run_report(account=account) + + # The report lists only mismatches (rows where abs(difference_value) > 0.1), + # keyed per voucher. A balanced perpetual warehouse posts equal stock-ledger + # and GL values for the receipt voucher, so nothing should be flagged. + self.assertEqual(rows, []) + + def run_report(self, **extra): + filters = {"company": COMPANY, "as_on_date": "2026-12-31"} + filters.update(extra) + return execute(frappe._dict(filters))[1] From 364250467f11172af3a33180b70e9e76d1a49ef4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:19 +0530 Subject: [PATCH 028/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_cogs_by_item_group.py | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py index eb5dc7a434d..36b9d4ad1da 100644 --- a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py @@ -4,7 +4,6 @@ import frappe from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.cogs_by_item_group.cogs_by_item_group import execute from erpnext.tests.utils import ERPNextTestSuite @@ -21,18 +20,9 @@ class TestCogsByItemGroup(ERPNextTestSuite): return execute(filters)[1] def test_cogs_for_item_group(self): - frappe.get_doc( - { - "doctype": "Item Group", - "item_group_name": "_Test COGS Group", - "parent_item_group": "All Item Groups", - "is_group": 0, - } - ).insert(ignore_if_duplicate=True) - - item = make_item( - properties={"is_stock_item": 1, "is_sales_item": 1, "item_group": "_Test COGS Group"} - ).name + # Reuse the bootstrap item `_Test Item` (item group `_Test Item Group`). + # It has zero stock in `Stores - TCP1`, so this receipt starts from a clean balance. + item = "_Test Item" make_stock_entry( item_code=item, @@ -61,7 +51,7 @@ class TestCogsByItemGroup(ERPNextTestSuite): ) data = self.run_report() - rows = [row for row in data if "_Test COGS Group" in row.get("item_group")] - self.assertTrue(rows, "No row found for _Test COGS Group") + rows = [row for row in data if "_Test Item Group" in row.get("item_group")] + self.assertTrue(rows, "No row found for _Test Item Group") # 4 units delivered at 100 valuation rate -> 400 COGS. self.assertEqual(rows[0].get("cogs_debit"), 400) From 78de0c976a9761fab9ae1b187239807cecd0093f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:32 +0530 Subject: [PATCH 029/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../delivery_note_trends/test_delivery_note_trends.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py index 003b481f85e..f08fe643648 100644 --- a/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py @@ -27,22 +27,23 @@ class TestDeliveryNoteTrends(ERPNextTestSuite): # A submitted Delivery Note of qty 5 @ rate 200 should sum to qty 5 / amount 1000 # (base_net_amount) in both the yearly bucket and the Total columns. from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - item = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name + item = "_Test Item" make_stock_entry( item_code=item, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", qty=20, rate=100, posting_date="2026-06-01", ) create_delivery_note( item_code=item, + warehouse="Stores - _TC", qty=5, rate=200, + customer="_Test Customer", company="_Test Company", posting_date="2026-06-01", ) From 04617b40b41d1322451303d551d38721a05d5614 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:37 +0530 Subject: [PATCH 030/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- ...test_fifo_queue_vs_qty_after_transaction_comparison.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py index 27bf999c7f3..8f1b32f1209 100644 --- a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.fifo_queue_vs_qty_after_transaction_comparison.fifo_queue_vs_qty_after_transaction_comparison import ( execute, @@ -16,14 +15,15 @@ class TestFifoQueueVsQtyAfterTransactionComparison(ERPNextTestSuite): return execute(frappe._dict(filters))[1] def test_healthy_fifo_item_no_mismatch(self): - item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name - warehouse = "_Test Warehouse - _TC" + item = "_Test Item" + warehouse = "Stores - _TC" + frappe.db.set_value("Item", item, "valuation_method", "FIFO") make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, to_warehouse=warehouse, qty=5, rate=120, posting_date="2026-06-01") make_stock_entry(item_code=item, from_warehouse=warehouse, qty=4, posting_date="2026-06-02") - data = self.run_report({"company": "_Test Company", "item_code": item}) + data = self.run_report({"company": "_Test Company", "item_code": item, "warehouse": warehouse}) item_codes = [row.get("item_code") for row in data if row] self.assertNotIn(item, item_codes) From abb7fec598b3269f0199c16b16fd5211c7080e71 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:41 +0530 Subject: [PATCH 031/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_balance_qty_after_transaction.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py index 15ac648988b..db95dd96ff6 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py @@ -3,14 +3,13 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.incorrect_balance_qty_after_transaction.incorrect_balance_qty_after_transaction import ( execute, ) from erpnext.tests.utils import ERPNextTestSuite -WAREHOUSE = "_Test Warehouse - _TC" +WAREHOUSE = "Stores - _TC" COMPANY = "_Test Company" @@ -21,7 +20,7 @@ class TestIncorrectBalanceQtyAfterTransaction(ERPNextTestSuite): return execute(filters)[1] def test_healthy_stock_not_flagged(self): - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item" make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=4, rate=100, posting_date="2026-06-02") @@ -30,7 +29,7 @@ class TestIncorrectBalanceQtyAfterTransaction(ERPNextTestSuite): self.assertEqual(flagged, []) def test_sequence_of_movements_not_flagged(self): - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item 2" make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=20, rate=50, posting_date="2026-06-01") make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=5, rate=50, posting_date="2026-06-02") make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=8, rate=50, posting_date="2026-06-03") From 851dfb16be9bb5e63250f7dbd0fc33f34d800530 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:48 +0530 Subject: [PATCH 032/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_serial_and_batch_bundle.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py index e0bca85a1b4..806a8d24ad1 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py @@ -26,26 +26,20 @@ class TestIncorrectSerialAndBatchBundle(ERPNextTestSuite): "batch_number_series": "ISBB-.#####", } ).name - serial_item = make_item( - properties={ - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "ISBS-.#####", - } - ).name + serial_item = "_Test Serialized Item With Series" make_stock_entry( item_code=batch_item, qty=10, rate=100, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", posting_date="2026-06-01", ) make_stock_entry( item_code=serial_item, qty=3, rate=100, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", posting_date="2026-06-01", ) From 7da4bc46bfa34cf00de716d20cc7cfab1e15e940 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:54 +0530 Subject: [PATCH 033/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_serial_no_valuation.py | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py index 96b5ca11451..9015c06cd1d 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -3,11 +3,13 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute from erpnext.tests.utils import ERPNextTestSuite +SERIAL_ITEM = "_Test Serialized Item With Series" +WAREHOUSE = "Stores - _TC" + class TestIncorrectSerialNoValuation(ERPNextTestSuite): def run_report(self, **extra): @@ -15,49 +17,36 @@ class TestIncorrectSerialNoValuation(ERPNextTestSuite): filters.update(extra) return execute(filters)[1] - def make_serial_item(self): - return make_item( - properties={ - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "ISV-.#####", - } - ).name - def test_healthy_serial_item_not_flagged(self): - item = self.make_serial_item() - make_stock_entry( - item_code=item, - to_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + to_warehouse=WAREHOUSE, qty=3, rate=100, posting_date="2026-06-01", ) make_stock_entry( - item_code=item, - from_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + from_warehouse=WAREHOUSE, qty=1, posting_date="2026-06-02", ) - data = self.run_report(item_code=item) + data = self.run_report(item_code=SERIAL_ITEM) flagged_items = {row.get("item_code") for row in data if isinstance(row, dict)} - self.assertNotIn(item, flagged_items) + self.assertNotIn(SERIAL_ITEM, flagged_items) def test_only_balance_row_when_filtered_to_healthy_item(self): - item = self.make_serial_item() - make_stock_entry( - item_code=item, - to_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + to_warehouse=WAREHOUSE, qty=3, rate=100, posting_date="2026-06-01", ) - data = self.run_report(item_code=item) + data = self.run_report(item_code=SERIAL_ITEM) # The report always appends a single "Balance" summary row. A healthy # serial item contributes no detail rows, so only that summary remains. From 79bd6a9b7d9dc16fe5e2d2638f2c760be77ac20b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:09 +0530 Subject: [PATCH 034/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/stock/report/item_prices/test_item_prices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index d77b5f60716..bbc21396418 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.report.item_prices.item_prices import execute from erpnext.tests.utils import ERPNextTestSuite @@ -29,7 +28,7 @@ class TestItemPrices(ERPNextTestSuite): def test_item_selling_price_listed(self): """A Standard Selling Item Price shows up in the Sales Price List column.""" - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item" frappe.get_doc( { "doctype": "Item Price", @@ -43,11 +42,12 @@ class TestItemPrices(ERPNextTestSuite): self.assertIn("250.0", row[SALES_PRICE_LIST]) self.assertIn("Standard Selling", row[SALES_PRICE_LIST]) # A selling price must not leak into the buying column. - self.assertFalse(row[PURCHASE_PRICE_LIST]) + self.assertNotIn("250.0", row[PURCHASE_PRICE_LIST] or "") + self.assertNotIn("Standard Selling", row[PURCHASE_PRICE_LIST] or "") def test_item_buying_price_listed(self): """A Standard Buying Item Price shows up in the Purchase Price List column.""" - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item 2" frappe.get_doc( { "doctype": "Item Price", @@ -60,4 +60,6 @@ class TestItemPrices(ERPNextTestSuite): row = self.row_for(self.run_report(), item) self.assertIn("175.0", row[PURCHASE_PRICE_LIST]) self.assertIn("Standard Buying", row[PURCHASE_PRICE_LIST]) - self.assertFalse(row[SALES_PRICE_LIST]) + # A buying price must not leak into the selling column. + self.assertNotIn("175.0", row[SALES_PRICE_LIST] or "") + self.assertNotIn("Standard Buying", row[SALES_PRICE_LIST] or "") From 0b35d394c507991c922b82939d6b3c422ac7356b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:22 +0530 Subject: [PATCH 035/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../item_wise_consumption/test_item_wise_consumption.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py index 1b417e712c2..6ce53d3dd99 100644 --- a/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py +++ b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py @@ -4,13 +4,12 @@ import frappe from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.item_wise_consumption.item_wise_consumption import execute from erpnext.tests.utils import ERPNextTestSuite -WH = "_Test Warehouse - _TC" +WH = "Stores - _TC" # row: 0 item, 1 name, 2 desc, 3 uom, 4 consumed_qty, 5 consumed_amt, 6 delivered_qty, # 7 delivered_amt, 8 total_qty, 9 total_amt, 10 suppliers @@ -23,7 +22,7 @@ class TestItemWiseConsumption(ERPNextTestSuite): return execute(filters)[1] def test_consumed_vs_delivered_split(self): - item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1, "is_sales_item": 1}).name + item = "_Test Item" # purchase receipt gives the supplier mapping and stocks the item make_purchase_receipt( item_code=item, From 9aef148a444272cceb7407f69c61e7ba17ef8ce1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:27 +0530 Subject: [PATCH 036/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_itemwise_recommended_reorder_level.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py index 9483f0f6f18..858ec9aa0ba 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py @@ -4,14 +4,13 @@ import frappe from frappe.utils import flt -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.itemwise_recommended_reorder_level.itemwise_recommended_reorder_level import ( execute, ) from erpnext.tests.utils import ERPNextTestSuite -WAREHOUSE = "_Test Warehouse - _TC" +WAREHOUSE = "Stores - _TC" class TestItemwiseRecommendedReorderLevel(ERPNextTestSuite): @@ -27,7 +26,8 @@ class TestItemwiseRecommendedReorderLevel(ERPNextTestSuite): return None def test_consumption_drives_recommendation(self): - item = make_item(properties={"is_stock_item": 1, "lead_time_days": 3, "safety_stock": 5}).name + item = "_Test Item" + frappe.db.set_value("Item", item, {"lead_time_days": 3, "safety_stock": 5}) # Receive stock, then issue a known total across dates inside the report window. make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=100, rate=10, posting_date="2026-06-01") @@ -55,7 +55,8 @@ class TestItemwiseRecommendedReorderLevel(ERPNextTestSuite): self.assertEqual(flt(row[11]), expected_reorder) # reorder level def test_no_consumption_yields_zero_outgoing(self): - item = make_item(properties={"is_stock_item": 1, "lead_time_days": 3, "safety_stock": 5}).name + item = "_Test Item 2" + frappe.db.set_value("Item", item, {"lead_time_days": 3, "safety_stock": 5}) make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=100, rate=10, posting_date="2026-06-01") row = self.find_row(self.run_report(), item) From 286ac77a05dc6787fc02051abceb5479791e310e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:35 +0530 Subject: [PATCH 037/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../negative_batch_report/test_negative_batch_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py index ccb80e340f2..16d50fa27c3 100644 --- a/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py +++ b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py @@ -14,7 +14,7 @@ class TestNegativeBatchReport(ERPNextTestSuite): frappe._dict( { "company": "_Test Company", - "warehouse": "_Test Warehouse - _TC", + "warehouse": "Stores - _TC", "item_code": item_code, } ) @@ -35,14 +35,14 @@ class TestNegativeBatchReport(ERPNextTestSuite): make_stock_entry( item_code=item, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", qty=10, rate=100, posting_date="2026-06-01", ) make_stock_entry( item_code=item, - from_warehouse="_Test Warehouse - _TC", + from_warehouse="Stores - _TC", qty=4, posting_date="2026-06-02", ) From 3398e051903e2a742ad18fe3f0f2ee7e0d03da9f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:39 +0530 Subject: [PATCH 038/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_product_bundle_balance.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py index 865403b8528..4403db6fcd7 100644 --- a/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py @@ -8,7 +8,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.product_bundle_balance.product_bundle_balance import execute from erpnext.tests.utils import ERPNextTestSuite -WH = "_Test Warehouse - _TC" +WH = "Stores - _TC" class TestProductBundleBalance(ERPNextTestSuite): @@ -24,20 +24,28 @@ class TestProductBundleBalance(ERPNextTestSuite): return execute(filters)[1] def test_bundle_qty_is_limited_by_scarcest_child(self): + # Reuse the bootstrap stock items as children. They start at zero in `Stores - _TC`, + # so transacting there gives a clean, deterministic balance for this warehouse's row. parent = make_item(properties={"is_stock_item": 0, "is_sales_item": 1}).name - child_a = make_item(properties={"is_stock_item": 1}).name - child_b = make_item(properties={"is_stock_item": 1}).name + child_a = "_Test Item" + child_b = "_Test Item 2" self.make_bundle(parent, {child_a: 2, child_b: 1}) make_stock_entry(item_code=child_a, to_warehouse=WH, qty=10, rate=100) make_stock_entry(item_code=child_b, to_warehouse=WH, qty=3, rate=100) data = self.run_report(parent) - parent_row = next(r for r in data if r["item_code"] == parent and r["indent"] == 0) + parent_row = next( + r for r in data if r["item_code"] == parent and r["indent"] == 0 and r["warehouse"] == WH + ) # min(10 // 2, 3 // 1) = min(5, 3) = 3 buildable bundles self.assertEqual(parent_row["bundle_qty"], 3) - row_a = next(r for r in data if r["item_code"] == child_a and r["indent"] == 1) - row_b = next(r for r in data if r["item_code"] == child_b and r["indent"] == 1) + row_a = next( + r for r in data if r["item_code"] == child_a and r["indent"] == 1 and r["warehouse"] == WH + ) + row_b = next( + r for r in data if r["item_code"] == child_b and r["indent"] == 1 and r["warehouse"] == WH + ) self.assertEqual((row_a["actual_qty"], row_a["minimum_qty"], row_a["bundle_qty"]), (10, 2, 5)) self.assertEqual((row_b["actual_qty"], row_b["minimum_qty"], row_b["bundle_qty"]), (3, 1, 3)) From 04fd425fb65a772038f46fc2f047564331b55627 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:43 +0530 Subject: [PATCH 039/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_purchase_receipt_trends.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py index 87511222754..bd11379ec29 100644 --- a/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.report.purchase_receipt_trends.purchase_receipt_trends import execute from erpnext.tests.utils import ERPNextTestSuite @@ -23,18 +22,25 @@ class TestPurchaseReceiptTrends(ERPNextTestSuite): filters.update(extra) return execute(filters)[1] + def get_item_totals(self, data, item): + # Row layout for based_on="Item", Yearly, no group_by: + # [item_code, item_name, currency, FY(Qty), FY(Amt), Total(Qty), Total(Amt)] + row = next((r for r in data if r[0] == item), None) + if row is None: + return 0, 0 + return row[3], row[4] + def test_receipt_qty_in_trend(self): - item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1}).name + item = "_Test Item" + + # The report sums ALL purchase receipts for the item in the fiscal year, so capture + # any pre-existing committed baseline and assert only this receipt's contribution. + base_qty, base_amt = self.get_item_totals(self.run_report(), item) + make_purchase_receipt( item_code=item, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" ) - data = self.run_report() - - # Row layout for based_on="Item", Yearly, no group_by: - # [item_code, item_name, currency, FY(Qty), FY(Amt), Total(Qty), Total(Amt)] - row = next(r for r in data if r[0] == item) - self.assertEqual(row[3], 10) # fiscal-year qty - self.assertEqual(row[4], 1000) # fiscal-year amount (10 * 100) - self.assertEqual(row[5], 10) # Total(Qty) - self.assertEqual(row[6], 1000) # Total(Amt) + qty, amt = self.get_item_totals(self.run_report(), item) + self.assertEqual(qty - base_qty, 10) # fiscal-year qty + self.assertEqual(amt - base_amt, 1000) # fiscal-year amount (10 * 100) From db76533c1651a1954845cdcead586e0de7fd8a88 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:52 +0530 Subject: [PATCH 040/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_serial_no_and_batch_traceability.py | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py b/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py index 3b145d672ff..09e68e7ab0a 100644 --- a/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py +++ b/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py @@ -4,13 +4,14 @@ import frappe from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.serial_no_and_batch_traceability.serial_no_and_batch_traceability import ( execute, ) from erpnext.tests.utils import ERPNextTestSuite +SERIAL_ITEM = "_Test Serialized Item With Series" + class TestSerialNoAndBatchTraceability(ERPNextTestSuite): def run_report(self, **extra): @@ -18,30 +19,28 @@ class TestSerialNoAndBatchTraceability(ERPNextTestSuite): filters.update(extra) return execute(filters)[1] - def make_serial_item(self): - return make_item( - properties={ - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "SNT-.#####", - } - ).name + def get_received_serial_no(self, receipt): + bundle = frappe.db.get_value( + "Stock Entry Detail", + {"parent": receipt.name, "item_code": SERIAL_ITEM}, + "serial_and_batch_bundle", + ) + return frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "serial_no") def test_serial_movements_traced(self): """Backward trace should surface the receipt voucher the serial came in through.""" - item = self.make_serial_item() receipt = make_stock_entry( - item_code=item, - to_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + to_warehouse="Stores - _TC", qty=2, rate=100, posting_date="2026-06-01", company="_Test Company", ) - serial_no = frappe.get_all("Serial No", {"item_code": item}, pluck="name")[0] + serial_no = self.get_received_serial_no(receipt) rows = self.run_report( - item_code=item, + item_code=SERIAL_ITEM, serial_nos=[serial_no], traceability_direction="Backward", ) @@ -51,36 +50,36 @@ class TestSerialNoAndBatchTraceability(ERPNextTestSuite): receipt_row = traced[receipt.name] self.assertEqual(receipt_row["serial_no"], serial_no) - self.assertEqual(receipt_row["item_code"], item) + self.assertEqual(receipt_row["item_code"], SERIAL_ITEM) self.assertEqual(receipt_row["reference_doctype"], "Stock Entry") - self.assertEqual(receipt_row["warehouse"], "_Test Warehouse - _TC") + self.assertEqual(receipt_row["warehouse"], "Stores - _TC") self.assertEqual(receipt_row["direction"], "Backward") self.assertGreater(receipt_row["qty"], 0) def test_forward_and_backward_directions(self): """'Both' should trace backward to the receipt and forward to the outward delivery.""" - item = self.make_serial_item() receipt = make_stock_entry( - item_code=item, - to_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + to_warehouse="Stores - _TC", qty=2, rate=100, posting_date="2026-06-01", company="_Test Company", ) - serial_no = frappe.get_all("Serial No", {"item_code": item}, pluck="name")[0] + serial_no = self.get_received_serial_no(receipt) delivery_note = create_delivery_note( - item_code=item, + item_code=SERIAL_ITEM, qty=1, serial_no=[serial_no], - warehouse="_Test Warehouse - _TC", + warehouse="Stores - _TC", + customer="_Test Customer", posting_date="2026-06-03", company="_Test Company", ) rows = self.run_report( - item_code=item, + item_code=SERIAL_ITEM, serial_nos=[serial_no], traceability_direction="Both", ) From e005d7021b2d25bf4f9bb398bf9f490923ef688c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:28:00 +0530 Subject: [PATCH 041/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_stock_and_account_value_comparison.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py index ffff5411e90..314a1aa16f3 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import ( @@ -18,7 +17,7 @@ class TestStockAndAccountValueComparison(ERPNextTestSuite): def test_balanced_warehouse_not_flagged(self): warehouse = create_warehouse("_Test SAVC WH", company=COMPANY) account = frappe.get_value("Warehouse", warehouse, "account") - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item" make_stock_entry( item_code=item, From 5d217295e546e82ea213dd68a7fbd54884c3e745 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:28:09 +0530 Subject: [PATCH 042/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_stock_ledger_variance.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py index e561a2fce5a..2459204ca8f 100644 --- a/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py @@ -16,21 +16,21 @@ class TestStockLedgerVariance(ERPNextTestSuite): return execute(frappe._dict(filters))[1] def test_healthy_stock_has_no_variance(self): - from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - item = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name + item = "_Test Item" + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") make_stock_entry( item_code=item, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", qty=10, rate=100, posting_date="2026-06-01", ) make_stock_entry( item_code=item, - from_warehouse="_Test Warehouse - _TC", + from_warehouse="Stores - _TC", qty=4, posting_date="2026-06-02", ) @@ -44,35 +44,35 @@ class TestStockLedgerVariance(ERPNextTestSuite): self.assertFalse([row for row in qty_data if row.get("item_code") == item]) def test_multiple_clean_movements_no_variance(self): - from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - item = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name + item = "_Test Item" + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") make_stock_entry( item_code=item, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", qty=10, rate=100, posting_date="2026-06-01", ) make_stock_entry( item_code=item, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", qty=5, rate=120, posting_date="2026-06-02", ) make_stock_entry( item_code=item, - to_warehouse="_Test Warehouse - _TC", + to_warehouse="Stores - _TC", qty=8, rate=90, posting_date="2026-06-03", ) make_stock_entry( item_code=item, - from_warehouse="_Test Warehouse - _TC", + from_warehouse="Stores - _TC", qty=6, posting_date="2026-06-04", ) From 5a62746dd3816482167b8240923a8f98dcd6f967 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:28:19 +0530 Subject: [PATCH 043/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../total_stock_summary/test_total_stock_summary.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py b/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py index 1301789d2fa..62f32465894 100644 --- a/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py @@ -3,9 +3,7 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.report.total_stock_summary.total_stock_summary import execute from erpnext.tests.utils import ERPNextTestSuite @@ -16,8 +14,8 @@ class TestTotalStockSummary(ERPNextTestSuite): return execute(filters)[1] def test_warehouse_wise_quantity(self): - item = make_item().name - warehouse = create_warehouse("_Test TSS Warehouse") + item = "_Test Item" + warehouse = "Stores - _TC" # clean zero baseline for _Test Item make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100) # rows are (warehouse, item_code, description, actual_qty) @@ -25,8 +23,8 @@ class TestTotalStockSummary(ERPNextTestSuite): self.assertEqual(row[3], 10) def test_only_non_zero_bins_are_listed(self): - item = make_item().name - warehouse = create_warehouse("_Test TSS Empty Warehouse") + item = "_Test Item 2" + warehouse = "Stores - _TC" # clean zero baseline for _Test Item 2 # receive then issue everything -> bin actual_qty back to zero make_stock_entry(item_code=item, to_warehouse=warehouse, qty=5, rate=100) make_stock_entry(item_code=item, from_warehouse=warehouse, qty=5) From 51bd2727a0e32288a9c34c9b527a8ea19f3380e9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:28:23 +0530 Subject: [PATCH 044/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_warehouse_wise_item_balance_age_and_value.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py index cb50892290c..a94cd21f02a 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.warehouse_wise_item_balance_age_and_value.warehouse_wise_item_balance_age_and_value import ( execute, @@ -18,15 +17,15 @@ class TestWarehouseWiseItemBalanceAgeAndValue(ERPNextTestSuite): "company": "_Test Company", "from_date": "2026-01-01", "to_date": "2026-12-31", - "warehouse": "_Test Warehouse - _TC", + "warehouse": "Stores - _TC", } ) filters.update(extra) return execute(filters)[1] def test_balance_qty_and_value(self): - item_code = make_item(properties={"is_stock_item": 1}).name - warehouse = "_Test Warehouse - _TC" + item_code = "_Test Item" + warehouse = "Stores - _TC" make_stock_entry( item_code=item_code, From d46b3f362703309e2cfc2cd55e99ea7e6729b1e3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:28:28 +0530 Subject: [PATCH 045/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_warehouse_wise_stock_balance.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py b/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py index 527baee591f..16bcc181d87 100644 --- a/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py +++ b/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.report.warehouse_wise_stock_balance.warehouse_wise_stock_balance import execute @@ -22,7 +21,7 @@ class TestWarehouseWiseStockBalance(ERPNextTestSuite): parent = create_warehouse("_Test WWSB Parent", properties={"is_group": 1}) child = create_warehouse("_Test WWSB Child", properties={"parent_warehouse": parent}) - make_stock_entry(item_code=make_item().name, to_warehouse=child, qty=10, rate=100) + make_stock_entry(item_code="_Test Item", to_warehouse=child, qty=10, rate=100) data = self.run_report() # stock balance = sum of stock value difference (10 * 100) From 31ee3f1923458e939c5cd8c0df05541119e2af0a Mon Sep 17 00:00:00 2001 From: ervishnucs Date: Fri, 26 Jun 2026 13:33:30 +0530 Subject: [PATCH 046/195] fix: set conversion_rate on quotation created from customer --- erpnext/selling/doctype/customer/mapper.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/selling/doctype/customer/mapper.py b/erpnext/selling/doctype/customer/mapper.py index be69e7e5d6c..f6c41ee31a6 100644 --- a/erpnext/selling/doctype/customer/mapper.py +++ b/erpnext/selling/doctype/customer/mapper.py @@ -6,6 +6,8 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from erpnext.setup.utils import get_exchange_rate + @frappe.whitelist() def make_quotation(source_name: str, target_doc: str | Document | None = None): @@ -32,6 +34,11 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): target_doc.selling_price_list = price_list if currency: target_doc.currency = currency + company_currency = frappe.get_cached_value("Company", target_doc.company, "default_currency") + if target_doc.company and target_doc.currency != company_currency: + target_doc.conversion_rate = get_exchange_rate( + target_doc.currency, company_currency, target_doc.transaction_date, "for_selling" + ) return target_doc From 18c4a20ad4182dcced6f902a915fe01ab8328a59 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:36:30 +0530 Subject: [PATCH 047/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_item_wise_purchase_history.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py index 775c13ee9d0..568fa1e5446 100644 --- a/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py +++ b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py @@ -9,7 +9,6 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import ( create_purchase_order, ) from erpnext.buying.report.item_wise_purchase_history.item_wise_purchase_history import execute -from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestSuite @@ -48,7 +47,9 @@ class TestItemWisePurchaseHistory(ERPNextTestSuite): def test_date_range_filters_on_transaction_date(self): po = create_purchase_order(transaction_date="2026-06-01") - in_range = {row["purchase_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1]} + in_range = { + row["purchase_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1] + } self.assertIn(po.name, in_range) out_of_range = { @@ -71,13 +72,13 @@ class TestItemWisePurchaseHistory(ERPNextTestSuite): self.assertTrue(all(row["purchase_order"] == po.name for row in rows)) def test_item_group_filter(self): - other_item = make_item("_Test IWPH Products Item", {"item_group": "Products"}).name + # _Test Item is in _Test Item Group; _Test FG Item is in _Test Item Group Desktops po_test_group = create_purchase_order(item_code="_Test Item", transaction_date="2026-06-01") - po_products = create_purchase_order(item_code=other_item, transaction_date="2026-06-01") + po_other_group = create_purchase_order(item_code="_Test FG Item", transaction_date="2026-06-01") names = {row["purchase_order"] for row in self.run_report(item_group="_Test Item Group")[1]} self.assertIn(po_test_group.name, names) - self.assertNotIn(po_products.name, names) + self.assertNotIn(po_other_group.name, names) def test_supplier_filter(self): po = create_purchase_order(supplier="_Test Supplier", transaction_date="2026-06-01") From a90db9a223223cbbd4f66488f51a260baccfdf45 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:36:35 +0530 Subject: [PATCH 048/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/stock_ledger/test_stock_ledger.py | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger.py b/erpnext/stock/report/stock_ledger/test_stock_ledger.py index 2e3118f5647..0b384630d10 100644 --- a/erpnext/stock/report/stock_ledger/test_stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger.py @@ -8,21 +8,14 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.stock_ledger.stock_ledger import execute from erpnext.tests.utils import ERPNextTestSuite -WH = "_Test Warehouse - _TC" -WH2 = "Stores - _TC" +WH = "Stores - _TC" +WH2 = "Finished Goods - _TC" +ITEM = "_Test Item" +ITEM2 = "_Test Item 2" +SERIAL_ITEM = "_Test Serialized Item With Series" class TestStockLedgerReport(ERPNextTestSuite): - def make_item(self, valuation_method="Moving Average"): - return make_item( - properties={"is_stock_item": 1, "valuation_method": valuation_method} - ).name - - def make_serial_item(self): - return make_item( - properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SL-SN-.#####"} - ).name - def make_batch_item(self): return make_item( properties={ @@ -48,12 +41,15 @@ class TestStockLedgerReport(ERPNextTestSuite): filters["warehouse"] = [warehouse] if isinstance(warehouse, str) else warehouse return execute(filters)[1] - def sle_rows(self, item_code, **extra): - # drop the synthetic "'Opening'" row, keep only this item's ledger lines - return [row for row in self.run_report(item_code, **extra) if row.get("item_code") == item_code] + def sle_rows(self, item_code, warehouse=WH, **extra): + # scope to the clean warehouse so the committed baseline stock of reused master + # items (in `_Test Warehouse - _TC`) does not leak in; drop the synthetic + # "'Opening'" row and keep only this item's ledger lines + rows = self.run_report(item_code, warehouse=warehouse, **extra) + return [row for row in rows if row.get("item_code") == item_code] def test_receipt_shows_in_qty_and_balance(self): - item = self.make_item() + item = ITEM make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") (row,) = self.sle_rows(item) @@ -66,7 +62,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(row["stock_value_difference"], 1000) def test_issue_shows_out_qty_and_outgoing_rate(self): - item = self.make_item() + item = ITEM make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, from_warehouse=WH, qty=4, posting_date="2026-06-02") @@ -78,7 +74,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(issue["stock_value"], 600) def test_running_balance_across_transactions(self): - item = self.make_item() + item = ITEM make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, to_warehouse=WH, qty=5, rate=100, posting_date="2026-06-02") make_stock_entry(item_code=item, from_warehouse=WH, qty=3, posting_date="2026-06-03") @@ -87,7 +83,8 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(balances, [10, 15, 12]) def test_moving_average_valuation(self): - item = self.make_item(valuation_method="Moving Average") + item = ITEM + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=200, posting_date="2026-06-02") @@ -97,8 +94,8 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(latest["stock_value"], 3000) def test_item_code_filter_excludes_other_items(self): - item_a = self.make_item() - item_b = self.make_item() + item_a = ITEM + item_b = ITEM2 make_stock_entry(item_code=item_a, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item_b, to_warehouse=WH, qty=7, rate=100, posting_date="2026-06-01") @@ -106,7 +103,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(item_codes, {item_a}) def test_warehouse_filter(self): - item = self.make_item() + item = ITEM make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, to_warehouse=WH2, qty=5, rate=100, posting_date="2026-06-01") @@ -114,7 +111,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(warehouses, {WH2}) def test_voucher_no_filter(self): - item = self.make_item() + item = ITEM se = make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, to_warehouse=WH, qty=5, rate=100, posting_date="2026-06-02") @@ -123,7 +120,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(rows[0]["voucher_no"], se.name) def test_date_range_excludes_out_of_range_entries(self): - item = self.make_item() + item = ITEM se = make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2025-12-01") # 2026 window must not include the 2025 entry @@ -133,7 +130,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertIn(se.name, {row.get("voucher_no") for row in in_window}) def test_opening_balance_row(self): - item = self.make_item() + item = ITEM # stock received before the reporting window should surface as the opening balance make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2025-12-01") @@ -144,7 +141,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(opening["stock_value"], 1000) def test_bundle_not_segregated_by_default(self): - item = self.make_serial_item() + item = SERIAL_ITEM # a single receipt of 3 serials is one ledger line when the filter is off make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") @@ -153,7 +150,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual(row["qty_after_transaction"], 3) def test_serial_bundle_segregated_into_per_serial_rows(self): - item = self.make_serial_item() + item = SERIAL_ITEM make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") rows = self.sle_rows(item, segregate_serial_batch_bundle=1) @@ -165,7 +162,7 @@ class TestStockLedgerReport(ERPNextTestSuite): self.assertEqual([row["qty_after_transaction"] for row in rows], [1, 2, 3]) def test_segregated_issue_rows_show_out_qty_per_serial(self): - item = self.make_serial_item() + item = SERIAL_ITEM make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") make_stock_entry(item_code=item, from_warehouse=WH, qty=2, posting_date="2026-06-02") From 03ecd2fd3a21ef8f5d7085861967ce21b1cb59a2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:36:39 +0530 Subject: [PATCH 049/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_profitability_analysis.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py index 453587dbf9f..19e0c57ceb2 100644 --- a/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py @@ -41,10 +41,13 @@ class TestProfitabilityAnalysis(ERPNextTestSuite): ) def book_expense(self, cost_center, amount, posting_date="2026-06-01"): - make_journal_entry(EXPENSE, BANK, amount, cost_center=cost_center, posting_date=posting_date, submit=True) + make_journal_entry( + EXPENSE, BANK, amount, cost_center=cost_center, posting_date=posting_date, submit=True + ) def test_income_expense_and_gross_profit(self): - cc = self.make_cc("_Test PA Leaf") + # bootstrap leaf cost center; clean of committed GL so exact assertions hold + cc = "_Test Cost Center - _TC" self.book_income(cc, 10000) self.book_expense(cc, 4000) @@ -71,7 +74,7 @@ class TestProfitabilityAnalysis(ERPNextTestSuite): self.assertEqual(parent_row["gross_profit_loss"], 7000) def test_date_range_excludes_out_of_period_entries(self): - cc = self.make_cc("_Test PA Date") + cc = "_Test Cost Center 2 - _TC" self.book_income(cc, 10000, posting_date="2025-06-01") # the 2025 income must not appear in a 2026 report (zero-value rows are dropped) @@ -87,7 +90,7 @@ class TestProfitabilityAnalysis(ERPNextTestSuite): self.assertEqual(row_2025["income"], 10000) def test_total_row_sums_income_and_expense(self): - cc = self.make_cc("_Test PA Total") + cc = "_Test Cost Center - _TC" self.book_income(cc, 10000) self.book_expense(cc, 4000) From ba88667d99abda22c36848750b104d997254fe49 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:36:42 +0530 Subject: [PATCH 050/195] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_gross_and_net_profit_report.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py index fb571dfc4e6..d7a46d778b3 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py @@ -11,6 +11,9 @@ from erpnext.tests.utils import ERPNextTestSuite BANK = "_Test Bank - _TC" INCOME_PARENT = "Income - _TC" EXPENSE_PARENT = "Expenses - _TC" +# bootstrap leaf accounts that already have include_in_gross = 0 (no creation needed) +NON_GROSS_INCOME = "_Test Account Sales - _TC" +NON_GROSS_EXPENSE = "_Test Account Cost for Goods Sold - _TC" # an isolated fiscal year so other accounts contribute nothing to the totals FY = "_Test Fiscal Year 2049" DATE = "2049-06-01" @@ -48,15 +51,14 @@ class TestGrossAndNetProfitReport(ERPNextTestSuite): return next(row for row in data if row.get("account") == account) def test_gross_profit_excludes_non_gross_accounts(self): + # reuse bootstrap accounts for the non-gross (include_in_gross = 0) side gross_income = self.make_account("_Test GNP Gross Income", INCOME_PARENT, include_in_gross=1) - non_gross_income = self.make_account("_Test GNP Other Income", INCOME_PARENT, include_in_gross=0) gross_expense = self.make_account("_Test GNP Gross Expense", EXPENSE_PARENT, include_in_gross=1) - non_gross_expense = self.make_account("_Test GNP Other Expense", EXPENSE_PARENT, include_in_gross=0) self.book_income(gross_income, 10000) - self.book_income(non_gross_income, 2000) + self.book_income(NON_GROSS_INCOME, 2000) self.book_expense(gross_expense, 4000) - self.book_expense(non_gross_expense, 1000) + self.book_expense(NON_GROSS_EXPENSE, 1000) data = self.run_report() # gross profit only counts include_in_gross accounts: 10000 - 4000 @@ -77,5 +79,7 @@ class TestGrossAndNetProfitReport(ERPNextTestSuite): def test_nothing_included_in_gross_when_no_entries(self): # a fiscal year with no income/expense entries yields the placeholder row - data = self.run_report(from_fiscal_year="_Test Fiscal Year 2048", to_fiscal_year="_Test Fiscal Year 2048") + data = self.run_report( + from_fiscal_year="_Test Fiscal Year 2048", to_fiscal_year="_Test Fiscal Year 2048" + ) self.assertEqual(data[0]["account"], "'Nothing is included in gross'") From d2abb569d4adc13088b15834cc21dd7e5e197e5e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 14:49:27 +0530 Subject: [PATCH 051/195] test: cover period, based_on and group_by filters in Purchase Receipt Trends --- .../test_purchase_receipt_trends.py | 120 +++++++++++++++--- 1 file changed, 103 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py index bd11379ec29..220f80616ed 100644 --- a/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py @@ -7,9 +7,16 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu from erpnext.stock.report.purchase_receipt_trends.purchase_receipt_trends import execute from erpnext.tests.utils import ERPNextTestSuite +ITEM = "_Test Item" +ITEM_GROUP = "_Test Item Group" +SUPPLIER = "_Test Supplier" + class TestPurchaseReceiptTrends(ERPNextTestSuite): def run_report(self, **extra): + return self.run_report_full(**extra)[1] + + def run_report_full(self, **extra): filters = frappe._dict( { "company": "_Test Company", @@ -20,27 +27,106 @@ class TestPurchaseReceiptTrends(ERPNextTestSuite): } ) filters.update(extra) - return execute(filters)[1] + columns, data = execute(filters)[:2] + return columns, data - def get_item_totals(self, data, item): - # Row layout for based_on="Item", Yearly, no group_by: - # [item_code, item_name, currency, FY(Qty), FY(Amt), Total(Qty), Total(Amt)] - row = next((r for r in data if r[0] == item), None) - if row is None: - return 0, 0 - return row[3], row[4] + # trend columns are "Label:fieldtype:width" strings; assert by label so the index + # stays correct across period / based_on / group_by combinations. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + + def find_row(self, columns, data, match): + labels = self.labels(columns) + for row in data: + if all(row[labels.index(label)] == value for label, value in match.items()): + return row + return None + + def value(self, columns, row, label): + if not row: + return 0 + return row[self.labels(columns).index(label)] or 0 + + def values(self, match, wanted_labels, **extra): + columns, data = self.run_report_full(**extra) + row = self.find_row(columns, data, match) + return {label: self.value(columns, row, label) for label in wanted_labels} def test_receipt_qty_in_trend(self): - item = "_Test Item" - # The report sums ALL purchase receipts for the item in the fiscal year, so capture - # any pre-existing committed baseline and assert only this receipt's contribution. - base_qty, base_amt = self.get_item_totals(self.run_report(), item) - + # any pre-existing baseline and assert only this receipt's contribution. + cols = ["_Test Fiscal Year 2026 (Qty)", "_Test Fiscal Year 2026 (Amt)"] + before = self.values({"Item": ITEM}, cols) make_purchase_receipt( - item_code=item, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" ) + after = self.values({"Item": ITEM}, cols) + self.assertEqual(after[cols[0]] - before[cols[0]], 10) + self.assertEqual(after[cols[1]] - before[cols[1]], 1000) - qty, amt = self.get_item_totals(self.run_report(), item) - self.assertEqual(qty - base_qty, 10) # fiscal-year qty - self.assertEqual(amt - base_amt, 1000) # fiscal-year amount (10 * 100) + def test_monthly_period_buckets(self): + cols = ["Jun (Qty)", "Jun (Amt)", "Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, period="Monthly") + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item": ITEM}, cols, period="Monthly") + # the June receipt lands only in the June bucket, and rolls up into the Total columns + self.assertEqual(after["Jun (Qty)"] - before["Jun (Qty)"], 10) + self.assertEqual(after["Jun (Amt)"] - before["Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_quarterly_period_buckets(self): + # 2026-06-01 falls in the Apr-Jun quarter + cols = ["Apr-Jun (Qty)", "Apr-Jun (Amt)", "Total(Qty)"] + before = self.values({"Item": ITEM}, cols, period="Quarterly") + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item": ITEM}, cols, period="Quarterly") + self.assertEqual(after["Apr-Jun (Qty)"] - before["Apr-Jun (Qty)"], 10) + self.assertEqual(after["Apr-Jun (Amt)"] - before["Apr-Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + + def test_based_on_supplier(self): + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Supplier": SUPPLIER}, cols, based_on="Supplier") + make_purchase_receipt( + item_code=ITEM, + qty=10, + rate=100, + supplier=SUPPLIER, + company="_Test Company", + posting_date="2026-06-01", + ) + after = self.values({"Supplier": SUPPLIER}, cols, based_on="Supplier") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_based_on_item_group(self): + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Item Group": ITEM_GROUP}, cols, based_on="Item Group") + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item Group": ITEM_GROUP}, cols, based_on="Item Group") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_group_by_item_under_supplier(self): + # based_on=Supplier with group_by=Item produces an item-wise breakdown row + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, based_on="Supplier", group_by="Item") + make_purchase_receipt( + item_code=ITEM, + qty=10, + rate=100, + supplier=SUPPLIER, + company="_Test Company", + posting_date="2026-06-01", + ) + after = self.values({"Item": ITEM}, cols, based_on="Supplier", group_by="Item") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) From f2adb64f3bc87979053c73d3327ab18c288b0c9b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 14:50:54 +0530 Subject: [PATCH 052/195] test: cover period, based_on and group_by filters in Delivery Note Trends --- .../test_delivery_note_trends.py | 128 +++++++++++++----- 1 file changed, 97 insertions(+), 31 deletions(-) diff --git a/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py index f08fe643648..f5f62c5bde2 100644 --- a/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py @@ -3,58 +3,124 @@ import frappe +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.delivery_note_trends.delivery_note_trends import execute from erpnext.tests.utils import ERPNextTestSuite +ITEM = "_Test Item" +WAREHOUSE = "Stores - _TC" +CUSTOMER = "_Test Customer" + class TestDeliveryNoteTrends(ERPNextTestSuite): - def run_report(self, **extra): - from erpnext.stock.report.delivery_note_trends.delivery_note_trends import execute - + def run_report_full(self, **extra): filters = frappe._dict( { "company": "_Test Company", "fiscal_year": "_Test Fiscal Year 2026", "period": "Yearly", "based_on": "Item", + "group_by": "", } ) filters.update(extra) - return execute(filters)[1] + columns, data = execute(filters)[:2] + return columns, data - def test_delivery_qty_in_trend(self): - # based_on="Item" + period="Yearly": each row is - # [item_code, item_name, currency, yearly_qty, yearly_amt, total_qty, total_amt]. - # A submitted Delivery Note of qty 5 @ rate 200 should sum to qty 5 / amount 1000 - # (base_net_amount) in both the yearly bucket and the Total columns. - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + # trend columns are "Label:fieldtype:width" strings; assert by label so the index + # stays correct across period / based_on / group_by combinations. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] - item = "_Test Item" + def find_row(self, columns, data, match): + labels = self.labels(columns) + for row in data: + if all(row[labels.index(label)] == value for label, value in match.items()): + return row + return None + def value(self, columns, row, label): + if not row: + return 0 + return row[self.labels(columns).index(label)] or 0 + + def values(self, match, wanted_labels, **extra): + columns, data = self.run_report_full(**extra) + row = self.find_row(columns, data, match) + return {label: self.value(columns, row, label) for label in wanted_labels} + + def deliver(self, qty=5, rate=200, customer=CUSTOMER, posting_date="2026-06-01"): + # stock the item first so the delivery note can ship, then deliver make_stock_entry( - item_code=item, - to_warehouse="Stores - _TC", - qty=20, - rate=100, - posting_date="2026-06-01", + item_code=ITEM, to_warehouse=WAREHOUSE, qty=qty + 10, rate=100, posting_date=posting_date ) create_delivery_note( - item_code=item, - warehouse="Stores - _TC", - qty=5, - rate=200, - customer="_Test Customer", + item_code=ITEM, + warehouse=WAREHOUSE, + qty=qty, + rate=rate, + customer=customer, company="_Test Company", - posting_date="2026-06-01", + posting_date=posting_date, ) - data = self.run_report() + def test_delivery_qty_in_trend(self): + # A Delivery Note of qty 5 @ rate 200 sums to qty 5 / amount 1000 (base_net_amount) + # in the yearly bucket and the Total columns. + cols = ["_Test Fiscal Year 2026 (Qty)", "_Test Fiscal Year 2026 (Amt)", "Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols) + self.deliver() + after = self.values({"Item": ITEM}, cols) + self.assertEqual(after[cols[0]] - before[cols[0]], 5) + self.assertEqual(after[cols[1]] - before[cols[1]], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) - item_rows = [row for row in data if row[0] == item] - self.assertEqual(len(item_rows), 1) + def test_monthly_period_buckets(self): + cols = ["Jun (Qty)", "Jun (Amt)", "Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, period="Monthly") + self.deliver(posting_date="2026-06-01") + after = self.values({"Item": ITEM}, cols, period="Monthly") + # the June delivery lands only in the June bucket, and rolls up into the Total columns + self.assertEqual(after["Jun (Qty)"] - before["Jun (Qty)"], 5) + self.assertEqual(after["Jun (Amt)"] - before["Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) - row = item_rows[0] - self.assertEqual(row[3], 5) # yearly qty bucket - self.assertEqual(row[4], 1000) # yearly amount bucket (base_net_amount) - self.assertEqual(row[5], 5) # Total(Qty) - self.assertEqual(row[6], 1000) # Total(Amt) + def test_quarterly_period_buckets(self): + # 2026-06-01 falls in the Apr-Jun quarter + cols = ["Apr-Jun (Qty)", "Apr-Jun (Amt)", "Total(Qty)"] + before = self.values({"Item": ITEM}, cols, period="Quarterly") + self.deliver(posting_date="2026-06-01") + after = self.values({"Item": ITEM}, cols, period="Quarterly") + self.assertEqual(after["Apr-Jun (Qty)"] - before["Apr-Jun (Qty)"], 5) + self.assertEqual(after["Apr-Jun (Amt)"] - before["Apr-Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + + def test_based_on_customer(self): + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Customer": CUSTOMER}, cols, based_on="Customer") + self.deliver(customer=CUSTOMER) + after = self.values({"Customer": CUSTOMER}, cols, based_on="Customer") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_based_on_territory(self): + territory = frappe.db.get_value("Customer", CUSTOMER, "territory") + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Territory": territory}, cols, based_on="Territory") + self.deliver(customer=CUSTOMER) + after = self.values({"Territory": territory}, cols, based_on="Territory") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_group_by_item_under_customer(self): + # based_on=Customer with group_by=Item produces an item-wise breakdown row + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, based_on="Customer", group_by="Item") + self.deliver(customer=CUSTOMER) + after = self.values({"Item": ITEM}, cols, based_on="Customer", group_by="Item") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) From 87af67febe34509196f75ad1f0a073a32d835c79 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 15:00:39 +0530 Subject: [PATCH 053/195] test: cover last purchase rate and valuation rate in Item Prices report --- .../report/item_prices/test_item_prices.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index bbc21396418..6c521c9e025 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -3,12 +3,16 @@ import frappe +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.item_prices.item_prices import execute from erpnext.tests.utils import ERPNextTestSuite # Positional columns returned by the report (it returns string-format columns, # not dicts, so rows are plain lists indexed by position). ITEM_CODE = 0 +LAST_PURCHASE_RATE = 6 +VALUATION_RATE = 7 SALES_PRICE_LIST = 8 PURCHASE_PRICE_LIST = 9 @@ -63,3 +67,24 @@ class TestItemPrices(ERPNextTestSuite): # A buying price must not leak into the selling column. self.assertNotIn("175.0", row[SALES_PRICE_LIST] or "") self.assertNotIn("Standard Buying", row[SALES_PRICE_LIST] or "") + + def test_last_purchase_rate_from_receipt(self): + """The latest purchase rate (from a Purchase Receipt) shows in the Last Purchase Rate column.""" + item = "_Test Item" + make_purchase_receipt( + item_code=item, qty=5, rate=500, company="_Test Company", posting_date="2026-06-01" + ) + + row = self.row_for(self.run_report(), item) + self.assertEqual(row[LAST_PURCHASE_RATE], 500) + + def test_valuation_rate_from_stock(self): + """The Bin valuation rate shows in the Valuation Rate column.""" + # _Test FG Item has no opening-stock baseline, so its valuation reflects only this receipt + item = "_Test FG Item" + make_stock_entry( + item_code=item, to_warehouse="Stores - _TC", qty=10, rate=250, posting_date="2026-06-01" + ) + + row = self.row_for(self.run_report(), item) + self.assertEqual(row[VALUATION_RATE], 250) From e61d299e63436390920c0c765273aa0d84639b1d Mon Sep 17 00:00:00 2001 From: ervishnucs Date: Sun, 28 Jun 2026 09:53:38 +0530 Subject: [PATCH 054/195] fix: recalculate totals after setting quotation conversion rate --- erpnext/selling/doctype/customer/mapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/customer/mapper.py b/erpnext/selling/doctype/customer/mapper.py index f6c41ee31a6..1cff9fbac79 100644 --- a/erpnext/selling/doctype/customer/mapper.py +++ b/erpnext/selling/doctype/customer/mapper.py @@ -39,6 +39,7 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): target_doc.conversion_rate = get_exchange_rate( target_doc.currency, company_currency, target_doc.transaction_date, "for_selling" ) + target_doc.run_method("calculate_taxes_and_totals") return target_doc From 8446be6518b513db39bf544e74564ddf8ba78fd2 Mon Sep 17 00:00:00 2001 From: ervishnucs Date: Sun, 28 Jun 2026 20:13:53 +0530 Subject: [PATCH 055/195] fix: set currency and price list before computing quotation totals --- erpnext/selling/doctype/customer/mapper.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/customer/mapper.py b/erpnext/selling/doctype/customer/mapper.py index 1cff9fbac79..6d1ed14662d 100644 --- a/erpnext/selling/doctype/customer/mapper.py +++ b/erpnext/selling/doctype/customer/mapper.py @@ -23,9 +23,6 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): ) target_doc.quotation_to = "Customer" - target_doc.run_method("set_missing_values") - target_doc.run_method("set_other_charges") - target_doc.run_method("calculate_taxes_and_totals") price_list, currency = frappe.db.get_value( "Customer", {"name": source_name}, ["default_price_list", "default_currency"] @@ -39,7 +36,9 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): target_doc.conversion_rate = get_exchange_rate( target_doc.currency, company_currency, target_doc.transaction_date, "for_selling" ) - target_doc.run_method("calculate_taxes_and_totals") + target_doc.run_method("set_missing_values") + target_doc.run_method("set_other_charges") + target_doc.run_method("calculate_taxes_and_totals") return target_doc From dead28e50ea2496cf2d099c17147a5d25563b1a4 Mon Sep 17 00:00:00 2001 From: ervishnucs Date: Sun, 28 Jun 2026 20:49:56 +0530 Subject: [PATCH 056/195] test: assert quotation from customer uses actual exchange rate --- .../selling/doctype/customer/test_customer.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index e4b2ccae4d6..a1b15a1e867 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -5,7 +5,7 @@ import json import frappe -from frappe.utils import flt +from frappe.utils import flt, nowdate from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen @@ -14,12 +14,53 @@ from erpnext.selling.doctype.customer.customer import ( get_customer_outstanding, ) from erpnext.selling.doctype.customer.mapper import ( + make_quotation, parse_full_name, ) from erpnext.tests.utils import ERPNextTestSuite class TestCustomer(ERPNextTestSuite): + def test_quotation_from_customer_uses_actual_exchange_rate(self): + company = "_Test Company" + company_currency = frappe.get_cached_value("Company", company, "default_currency") + foreign_currency = "USD" if company_currency != "USD" else "EUR" + + frappe.defaults.set_user_default("company", company) + self.addCleanup(frappe.defaults.clear_user_default, "company") + + # Seed a deterministic rate so the test does not depend on the live exchange-rate API. + rate = 83.0 + exchange = frappe.get_doc( + { + "doctype": "Currency Exchange", + "date": nowdate(), + "from_currency": foreign_currency, + "to_currency": company_currency, + "exchange_rate": rate, + "for_selling": 1, + "for_buying": 1, + } + ).insert(ignore_if_duplicate=True) + self.addCleanup(frappe.delete_doc, "Currency Exchange", exchange.name, force=1) + + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Customer FX Quotation", + "customer_type": "Company", + "default_currency": foreign_currency, + } + ).insert() + self.addCleanup(frappe.delete_doc, "Customer", customer.name, force=1) + + quotation = make_quotation(customer.name) + + self.assertEqual(quotation.currency, foreign_currency) + self.assertNotEqual(flt(quotation.conversion_rate), 1.0) + self.assertNotEqual(flt(quotation.conversion_rate), 0.0) + self.assertEqual(flt(quotation.conversion_rate), rate) + def test_get_customer_name_dedupes_with_numeric_suffix(self): # When a customer name already exists, get_customer_name appends "- ". The # Postgres branch extracts the suffix with regexp_replace/NULLIF/CAST (pypika's Substring cannot From 5a32866b938ca80b772584e777383f6e567b287b Mon Sep 17 00:00:00 2001 From: sokumon Date: Tue, 30 Jun 2026 17:00:54 +0530 Subject: [PATCH 057/195] chore: export more workspaces --- .../accounts_setup/accounts_setup.json | 329 ++++++++++++++ .../accounts/workspace/banking/banking.json | 2 +- .../workspace/budgeting/budgeting.json | 178 ++++++++ .../financial_reports/financial_reports.json | 2 +- .../workspace/invoicing/invoicing.json | 2 +- .../accounts/workspace/payments/payments.json | 2 +- .../share_management/share_management.json | 2 +- .../subscriptions/subscriptions.json | 121 ++++++ erpnext/accounts/workspace/taxes/taxes.json | 2 +- erpnext/assets/workspace/assets/assets.json | 2 +- erpnext/buying/workspace/buying/buying.json | 411 +++++++++++++++++- erpnext/crm/workspace/crm/crm.json | 2 +- .../manufacturing/manufacturing.json | 2 +- .../projects/workspace/projects/projects.json | 2 +- .../workspace/quality/quality.json | 2 +- .../selling/workspace/selling/selling.json | 2 +- .../erpnext_settings/erpnext_settings.json | 232 +++++++++- erpnext/setup/workspace/home/home.json | 299 ++++++++++++- .../workspace/organization/organization.json | 80 +++- erpnext/stock/workspace/stock/stock.json | 131 +++++- .../subcontracting/subcontracting.json | 2 +- .../support/workspace/support/support.json | 2 +- erpnext/workspace_sidebar/accounts_setup.json | 37 +- .../{budget.json => budgeting.json} | 9 +- .../workspace_sidebar/erpnext_settings.json | 31 +- erpnext/workspace_sidebar/organization.json | 16 +- .../{subscription.json => subscriptions.json} | 11 +- 27 files changed, 1852 insertions(+), 61 deletions(-) create mode 100644 erpnext/accounts/workspace/accounts_setup/accounts_setup.json create mode 100644 erpnext/accounts/workspace/budgeting/budgeting.json create mode 100644 erpnext/accounts/workspace/subscriptions/subscriptions.json rename erpnext/workspace_sidebar/{budget.json => budgeting.json} (89%) rename erpnext/workspace_sidebar/{subscription.json => subscriptions.json} (88%) diff --git a/erpnext/accounts/workspace/accounts_setup/accounts_setup.json b/erpnext/accounts/workspace/accounts_setup/accounts_setup.json new file mode 100644 index 00000000000..88dd071b131 --- /dev/null +++ b/erpnext/accounts/workspace/accounts_setup/accounts_setup.json @@ -0,0 +1,329 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-14 12:44:31.994274", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "database", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Accounts Setup", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 13:43:50.138704", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Accounts Setup", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 55.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 0, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Chart of Accounts", + "link_to": "Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Chart of Cost Centers", + "link_to": "Cost Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Account Category", + "link_to": "Account Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounting Dimension", + "link_to": "Accounting Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency", + "link_to": "Currency", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency Exchange", + "link_to": "Currency Exchange", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Finance Book", + "link_to": "Finance Book", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Mode of Payment", + "link_to": "Mode of Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Term", + "link_to": "Payment Term", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Journal Entry Template", + "link_to": "Journal Entry Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Terms and Conditions", + "link_to": "Terms and Conditions", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Company", + "link_to": "Company", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Fiscal Year", + "link_to": "Fiscal Year", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Taxes", + "link_to": "Sales Taxes and Charges Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "lock-keyhole-open", + "indent": 1, + "keep_closed": 0, + "label": "Opening & Closing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "COA Importer", + "link_to": "Chart of Accounts Importer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Opening Invoice Tool", + "link_to": "Opening Invoice Creation Tool", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Accounting Period", + "link_to": "Accounting Period", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "FX Revaluation", + "link_to": "Exchange Rate Revaluation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Period Closing Voucher", + "link_to": "Period Closing Voucher", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 1, + "keep_closed": 0, + "label": "Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Accounts Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency Exchange Settings", + "link_to": "Currency Exchange Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Accounts Setup", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/banking/banking.json b/erpnext/accounts/workspace/banking/banking.json index 45d3af0767d..072af4a7193 100644 --- a/erpnext/accounts/workspace/banking/banking.json +++ b/erpnext/accounts/workspace/banking/banking.json @@ -15,7 +15,7 @@ "label": "Banking", "link_type": "DocType", "links": [], - "modified": "2026-06-11 11:51:22.781813", + "modified": "2026-06-14 13:43:50.924019", "modified_by": "Administrator", "module": "Accounts", "name": "Banking", diff --git a/erpnext/accounts/workspace/budgeting/budgeting.json b/erpnext/accounts/workspace/budgeting/budgeting.json new file mode 100644 index 00000000000..d330c74cd01 --- /dev/null +++ b/erpnext/accounts/workspace/budgeting/budgeting.json @@ -0,0 +1,178 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-14 14:38:20.315394", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "accounting", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Budgeting", + "link_type": "DocType", + "links": [], + "modified": "2026-06-28 21:04:03.108867", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Budgeting", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [ + { + "role": "Academics User" + } + ], + "sequence_id": 57.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "briefcase-business", + "indent": 0, + "keep_closed": 0, + "label": "Budget", + "link_to": "Budget", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "briefcase-business", + "indent": 0, + "keep_closed": 0, + "label": "Budget", + "link_to": "Budget", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "badge-cent", + "indent": 0, + "keep_closed": 0, + "label": "Cost Center", + "link_to": "Cost Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "badge-cent", + "indent": 0, + "keep_closed": 0, + "label": "Cost Center", + "link_to": "Cost Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "accounting", + "indent": 0, + "keep_closed": 0, + "label": "Accounting Dimension", + "link_to": "Accounting Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "accounting", + "indent": 0, + "keep_closed": 0, + "label": "Accounting Dimension", + "link_to": "Accounting Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Cost Center Allocation", + "link_to": "Cost Center Allocation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Cost Center Allocation", + "link_to": "Cost Center Allocation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "sheet", + "indent": 0, + "keep_closed": 0, + "label": "Budget Variance", + "link_to": "Budget Variance Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "sheet", + "indent": 0, + "keep_closed": 0, + "label": "Budget Variance", + "link_to": "Budget Variance Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Budgeting", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/financial_reports/financial_reports.json b/erpnext/accounts/workspace/financial_reports/financial_reports.json index 0c542093cc7..3ad09d26e52 100644 --- a/erpnext/accounts/workspace/financial_reports/financial_reports.json +++ b/erpnext/accounts/workspace/financial_reports/financial_reports.json @@ -266,7 +266,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:23.876824", + "modified": "2026-06-14 13:44:08.095321", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", diff --git a/erpnext/accounts/workspace/invoicing/invoicing.json b/erpnext/accounts/workspace/invoicing/invoicing.json index 094ee9c4a5d..f34ea417b25 100644 --- a/erpnext/accounts/workspace/invoicing/invoicing.json +++ b/erpnext/accounts/workspace/invoicing/invoicing.json @@ -587,7 +587,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:24.435013", + "modified": "2026-06-14 13:44:08.471142", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", diff --git a/erpnext/accounts/workspace/payments/payments.json b/erpnext/accounts/workspace/payments/payments.json index a443aa8dc66..118e2961298 100644 --- a/erpnext/accounts/workspace/payments/payments.json +++ b/erpnext/accounts/workspace/payments/payments.json @@ -15,7 +15,7 @@ "label": "Payments", "link_type": "DocType", "links": [], - "modified": "2026-06-11 11:51:21.898721", + "modified": "2026-06-14 13:43:50.184761", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", diff --git a/erpnext/accounts/workspace/share_management/share_management.json b/erpnext/accounts/workspace/share_management/share_management.json index 9771dd468ec..6766b4ea9a4 100644 --- a/erpnext/accounts/workspace/share_management/share_management.json +++ b/erpnext/accounts/workspace/share_management/share_management.json @@ -15,7 +15,7 @@ "label": "Share Management", "link_type": "DocType", "links": [], - "modified": "2026-06-11 14:06:17.396069", + "modified": "2026-06-14 13:43:51.040978", "modified_by": "Administrator", "module": "Accounts", "name": "Share Management", diff --git a/erpnext/accounts/workspace/subscriptions/subscriptions.json b/erpnext/accounts/workspace/subscriptions/subscriptions.json new file mode 100644 index 00000000000..750573eb38c --- /dev/null +++ b/erpnext/accounts/workspace/subscriptions/subscriptions.json @@ -0,0 +1,121 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-14 14:08:36.817393", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "accounting", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Subscriptions", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 14:08:36.999272", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Subscriptions", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 56.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "circle-dollar-sign", + "indent": 0, + "keep_closed": 0, + "label": "Subscription", + "link_to": "Subscription", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Subscription Plan", + "link_to": "Subscription Plan", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Subscription Settings", + "link_to": "Subscription Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Subscriptions", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/taxes/taxes.json b/erpnext/accounts/workspace/taxes/taxes.json index 32f9c13025e..f2ccf3aa7e7 100644 --- a/erpnext/accounts/workspace/taxes/taxes.json +++ b/erpnext/accounts/workspace/taxes/taxes.json @@ -15,7 +15,7 @@ "label": "Taxes", "link_type": "DocType", "links": [], - "modified": "2026-06-11 11:51:22.661660", + "modified": "2026-06-14 13:43:50.894825", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index a54d6e1c37e..fae323faad2 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -199,7 +199,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:24.332555", + "modified": "2026-06-14 13:44:08.417956", "modified_by": "Administrator", "module": "Assets", "module_onboarding": "Asset Onboarding", diff --git a/erpnext/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json index 6bc20901467..268501949a7 100644 --- a/erpnext/buying/workspace/buying/buying.json +++ b/erpnext/buying/workspace/buying/buying.json @@ -341,17 +341,6 @@ "onboard": 1, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 1, - "label": "Item Wise Consumption", - "link_count": 0, - "link_to": "Item Wise Consumption", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, { "dependencies": "", "hidden": 0, @@ -512,9 +501,10 @@ "type": "Link" } ], - "modified": "2026-01-02 14:55:59.078773", + "modified": "2026-06-14 13:43:50.509039", "modified_by": "Administrator", "module": "Buying", + "module_onboarding": "Buying Onboarding", "name": "Buying", "number_cards": [ { @@ -538,6 +528,403 @@ "roles": [], "sequence_id": 5.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Buying", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Buying", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Material Request", + "link_to": "Material Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "git-pull-request-arrow", + "indent": 0, + "keep_closed": 0, + "label": "Request for Quotation", + "link_to": "Request for Quotation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "book-open-text", + "indent": 0, + "keep_closed": 0, + "label": "Supplier Quotation", + "link_to": "Supplier Quotation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order", + "link_to": "Purchase Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "liabilities", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice", + "link_to": "Purchase Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Group", + "link_to": "Supplier Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Price List", + "link_to": "Price List", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Address", + "link_to": "Address", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contacts", + "link_to": "Contact", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard", + "link_to": "Supplier Scorecard", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard Criteria", + "link_to": "Supplier Scorecard Criteria", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard Variable", + "link_to": "Supplier Scorecard Variable", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard Standing", + "link_to": "Supplier Scorecard Standing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Analytics", + "link_to": "Purchase Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order Analysis", + "link_to": "Purchase Order Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Requested Items to Order and Receive", + "link_to": "Requested Items to Order and Receive", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Items To Be Requested", + "link_to": "Items To Be Requested", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Purchase History", + "link_to": "Item-wise Purchase History", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Receipt Trends ", + "link_to": "Purchase Receipt Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice Trends", + "link_to": "Purchase Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order Trends", + "link_to": "Purchase Order Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Procurement Tracker", + "link_to": "Procurement Tracker", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Wise Consumption", + "link_to": "Item Wise Consumption", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Quotation Comparison", + "link_to": "Supplier Quotation Comparison", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Addresses And Contacts", + "link_to": "Address And Contacts", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Buying Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Buying", "type": "Workspace" } diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index 6851bf6d10a..52e1a1acbfb 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -421,7 +421,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:24.168713", + "modified": "2026-06-14 13:44:08.297053", "modified_by": "Administrator", "module": "CRM", "name": "CRM", diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 86a4568cdfc..4586876bbc6 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -432,7 +432,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:22.955115", + "modified": "2026-06-14 13:44:07.420267", "modified_by": "Administrator", "module": "Manufacturing", "module_onboarding": "Manufacturing Onboarding", diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index 9e71b05d9aa..488ae012483 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -193,7 +193,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:23.641700", + "modified": "2026-06-14 13:44:07.964218", "modified_by": "Administrator", "module": "Projects", "module_onboarding": "Projects Onboarding", diff --git a/erpnext/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json index 76c02e2667f..adde0e308dc 100644 --- a/erpnext/quality_management/workspace/quality/quality.json +++ b/erpnext/quality_management/workspace/quality/quality.json @@ -161,7 +161,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:23.514170", + "modified": "2026-06-14 13:44:07.920643", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality", diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index d1b20dfb734..b2b81a6e07c 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -622,7 +622,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:23.367918", + "modified": "2026-06-14 13:44:07.820564", "modified_by": "Administrator", "module": "Selling", "module_onboarding": "Selling Onboarding", diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 745d0ad4d24..7398a65b56e 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -69,7 +69,7 @@ "type": "Link" } ], - "modified": "2026-01-09 13:05:08.007297", + "modified": "2026-06-14 13:43:50.429297", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -128,6 +128,236 @@ "type": "DocType" } ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "earth", + "indent": 0, + "keep_closed": 0, + "label": "Global Defaults", + "link_to": "Global Defaults", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "washing-machine", + "indent": 0, + "keep_closed": 0, + "label": "System Settings", + "link_to": "System Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "accounting", + "indent": 0, + "keep_closed": 0, + "label": "Accounts Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "computer", + "indent": 0, + "keep_closed": 0, + "label": "POS Settings", + "link_to": "POS Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sell", + "indent": 0, + "keep_closed": 0, + "label": "Selling Settings", + "link_to": "Selling Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "buying", + "indent": 0, + "keep_closed": 0, + "label": "Buying Settings", + "link_to": "Buying Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Stock Settings", + "link_to": "Stock Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "building-2", + "indent": 0, + "keep_closed": 0, + "label": "Manufacturing Settings", + "link_to": "Manufacturing Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "projects", + "indent": 0, + "keep_closed": 0, + "label": "Projects Settings", + "link_to": "Projects Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "crm", + "indent": 0, + "keep_closed": 0, + "label": "CRM Settings", + "link_to": "CRM Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "support", + "indent": 0, + "keep_closed": 0, + "label": "Support Settings", + "link_to": "Support Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Other Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Subscription Settings", + "link_to": "Subscription Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Variant Settings", + "link_to": "Item Variant Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Settings", + "link_to": "Delivery Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency Exchange Settings", + "link_to": "Currency Exchange Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Appointment Booking Settings", + "link_to": "Appointment Booking Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Reposting Settings", + "link_to": "Stock Reposting Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "ERPNext Settings", "type": "Workspace" } diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index bf6b32c5af4..1cbb1fb7292 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -1,7 +1,7 @@ { "app": "erpnext", "charts": [], - "content": "[{\"id\":\"aCk49ShVRs\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"WU4F-HUcIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", + "content": "[{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", "creation": "2020-01-23 13:46:38.833076", "custom_blocks": [], "docstatus": 0, @@ -13,6 +13,14 @@ "is_hidden": 0, "label": "Home", "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Accounting", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "hidden": 0, "is_query_report": 0, @@ -32,6 +40,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Chart of Accounts", + "link_count": 0, + "link_to": "Account", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Company", + "link_count": 0, + "link_to": "Company", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -54,6 +84,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customer", + "link_count": 0, + "link_to": "Customer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Supplier", + "link_count": 0, + "link_to": "Supplier", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -73,6 +125,14 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Stock", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -84,6 +144,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Item", + "link_count": 0, + "link_to": "Item", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Warehouse", + "link_count": 0, + "link_to": "Warehouse", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -106,6 +188,17 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Brand", + "link_count": 0, + "link_to": "Brand", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -117,6 +210,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Unit of Measure (UOM)", + "link_count": 0, + "link_to": "UOM", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Stock Reconciliation", + "link_count": 0, + "link_to": "Stock Reconciliation", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -136,6 +251,25 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "CRM", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Lead", + "link_count": 0, + "link_to": "Lead", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -158,6 +292,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customer Group", + "link_count": 0, + "link_to": "Customer Group", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Territory", + "link_count": 0, + "link_to": "Territory", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -177,6 +333,14 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Data Import and Settings", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -188,6 +352,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Import Data", + "link_count": 0, + "link_to": "Data Import", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Opening Invoice Creation Tool", + "link_count": 0, + "link_to": "Opening Invoice Creation Tool", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -210,6 +396,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Chart of Accounts Importer", + "link_count": 0, + "link_to": "Chart of Accounts Importer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Letter Head", + "link_count": 0, + "link_to": "Letter Head", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -221,6 +429,17 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Account", + "link_count": 0, + "link_to": "Email Account", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -233,7 +452,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:22.225174", + "modified": "2026-06-29 13:27:45.986650", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -243,7 +462,11 @@ "public": 1, "quick_lists": [], "restrict_to_domain": "", - "roles": [], + "roles": [ + { + "role": "Desk User" + } + ], "sequence_id": 1.0, "shortcuts": [ { @@ -271,6 +494,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item", @@ -283,6 +507,20 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Home", @@ -295,6 +533,20 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Home", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Customer", @@ -307,6 +559,20 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Supplier", @@ -319,6 +585,33 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Sales Invoice", diff --git a/erpnext/setup/workspace/organization/organization.json b/erpnext/setup/workspace/organization/organization.json index 92298a440c6..50cab83acdb 100644 --- a/erpnext/setup/workspace/organization/organization.json +++ b/erpnext/setup/workspace/organization/organization.json @@ -1,4 +1,75 @@ { + "allowed_users": [ + { + "user": "Administrator" + }, + { + "user": "Guest" + }, + { + "user": "accounts@test.com" + }, + { + "user": "ankush@erpnext.com" + }, + { + "user": "faris@erpnext.com" + }, + { + "user": "mention_test_user@example.com" + }, + { + "user": "project@frappe.io" + }, + { + "user": "rushabh@erpnext.com" + }, + { + "user": "saqib@erpnext.com" + }, + { + "user": "soham@frappe.io" + }, + { + "user": "sohamengineer123@gmail.com" + }, + { + "user": "sohamkulkarns9@gmail.com" + }, + { + "user": "sydel@frappe.io" + }, + { + "user": "test'5@example.com" + }, + { + "user": "test1@example.com" + }, + { + "user": "test2@example.com" + }, + { + "user": "test3@example.com" + }, + { + "user": "test4@example.com" + }, + { + "user": "test@example.com" + }, + { + "user": "test@portal.com" + }, + { + "user": "testpassword@example.com" + }, + { + "user": "testperm@example.com" + }, + { + "user": "web@web.com" + } + ], "app": "erpnext", "charts": [], "content": "[]", @@ -15,7 +86,7 @@ "label": "Organization", "link_type": "DocType", "links": [], - "modified": "2026-06-11 11:51:21.800588", + "modified": "2026-06-16 00:45:57.595188", "modified_by": "Administrator", "module": "Setup", "module_onboarding": "Organization Onboarding", @@ -31,6 +102,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 1, "icon": "organization", "indent": 0, "keep_closed": 0, @@ -44,6 +116,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-text", "indent": 0, "keep_closed": 0, @@ -57,6 +130,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "file-user", "indent": 0, "keep_closed": 0, @@ -70,6 +144,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-user", "indent": 0, "keep_closed": 0, @@ -83,6 +158,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "users", "indent": 0, "keep_closed": 0, @@ -96,6 +172,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "user-round-check", "indent": 0, "keep_closed": 0, @@ -109,6 +186,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "mail", "indent": 0, "keep_closed": 0, diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index f2e067d621f..123ec24e6b4 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -1,4 +1,78 @@ { + "allowed_users": [ + { + "user": "Administrator" + }, + { + "user": "Guest" + }, + { + "user": "accounts@test.com" + }, + { + "user": "ankush@erpnext.com" + }, + { + "user": "faris@erpnext.com" + }, + { + "user": "mention_test_user@example.com" + }, + { + "user": "project@frappe.io" + }, + { + "user": "rushabh@erpnext.com" + }, + { + "user": "saqib@erpnext.com" + }, + { + "user": "soham@frappe.io" + }, + { + "user": "sohamengineer123@gmail.com" + }, + { + "user": "sohamkulkarns9@gmail.com" + }, + { + "user": "stock@xyz.com" + }, + { + "user": "sydel@frappe.io" + }, + { + "user": "test'5@example.com" + }, + { + "user": "test1@example.com" + }, + { + "user": "test2@example.com" + }, + { + "user": "test3@example.com" + }, + { + "user": "test4@example.com" + }, + { + "user": "test@example.com" + }, + { + "user": "test@portal.com" + }, + { + "user": "testpassword@example.com" + }, + { + "user": "testperm@example.com" + }, + { + "user": "web@web.com" + } + ], "app": "erpnext", "charts": [ { @@ -789,7 +863,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:22.112805", + "modified": "2026-06-17 12:11:34.739020", "modified_by": "Administrator", "module": "Stock", "module_onboarding": "Stock Onboarding", @@ -820,6 +894,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "home", "indent": 0, "keep_closed": 0, @@ -833,6 +908,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "chart", "indent": 0, "keep_closed": 0, @@ -846,6 +922,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "stock", "indent": 0, "keep_closed": 0, @@ -859,6 +936,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "receipt-text", "indent": 0, "keep_closed": 0, @@ -872,6 +950,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "truck", "indent": 0, "keep_closed": 0, @@ -885,6 +964,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "arrow-left-to-line", "indent": 0, "keep_closed": 0, @@ -898,6 +978,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "caravan", "indent": 0, "keep_closed": 0, @@ -911,6 +992,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "tool", "indent": 1, "keep_closed": 1, @@ -923,6 +1005,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -936,6 +1019,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -949,6 +1033,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Repost Item Valuation", @@ -961,6 +1046,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Packing Slip", @@ -973,6 +1059,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Quality Inspection", @@ -985,6 +1072,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "database", "indent": 1, "keep_closed": 1, @@ -998,6 +1086,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 1, "icon": "", "indent": 0, "keep_closed": 0, @@ -1011,6 +1100,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Group", @@ -1023,6 +1113,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Attribute", @@ -1035,6 +1126,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Brand", @@ -1047,6 +1139,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -1060,6 +1153,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Unit of Measure (UOM)", @@ -1072,6 +1166,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "UOM Conversion Factor", @@ -1084,6 +1179,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Serial No", @@ -1096,6 +1192,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Batch No", @@ -1108,6 +1205,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Serial and Batch Bundle", @@ -1120,6 +1218,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Inventory Dimension", @@ -1132,6 +1231,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Shipping Rule", @@ -1144,6 +1244,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Alternative", @@ -1156,6 +1257,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Quality Inspection Template", @@ -1168,6 +1270,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Delivery Trip", @@ -1180,6 +1283,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "sheet", "indent": 1, "keep_closed": 1, @@ -1192,6 +1296,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Stock Ledger", @@ -1204,6 +1309,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Stock Balance", @@ -1216,6 +1322,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Quick Stock Balance", @@ -1228,6 +1335,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Stock Projected Qty", @@ -1240,6 +1348,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Stock Analytics", @@ -1252,6 +1361,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Stock Ageing", @@ -1264,6 +1374,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Purchase Receipt Trends", @@ -1276,6 +1387,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Delivery Note Trends", @@ -1288,6 +1400,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Price Stock", @@ -1300,6 +1413,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Warehouse Wise Stock Balance", @@ -1312,6 +1426,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Shortage Report", @@ -1324,6 +1439,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Serial No and Batch Traceability", @@ -1336,6 +1452,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Serial No Status", @@ -1348,6 +1465,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Serial No Ledger", @@ -1360,6 +1478,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Serial No Warranty Expiry", @@ -1372,6 +1491,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Batch-Wise Balance History", @@ -1384,6 +1504,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Batch Item Expiry Status", @@ -1396,6 +1517,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Requested Items To Be Transferred", @@ -1408,6 +1530,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Itemwise Recommended Reorder Level", @@ -1420,6 +1543,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Variant Details", @@ -1432,6 +1556,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "settings", "indent": 1, "keep_closed": 1, @@ -1444,6 +1569,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -1457,6 +1583,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Item Variant Settings", @@ -1469,6 +1596,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Stock Reposting Settings", @@ -1481,6 +1609,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Delivery Settings", diff --git a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json index 1995e64db6d..1044614fe25 100644 --- a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json +++ b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json @@ -138,7 +138,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:22.033578", + "modified": "2026-06-14 13:43:50.289920", "modified_by": "Administrator", "module": "Subcontracting", "module_onboarding": "Subcontracting Onboarding", diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json index a188ea6abe9..900b9b42001 100644 --- a/erpnext/support/workspace/support/support.json +++ b/erpnext/support/workspace/support/support.json @@ -172,7 +172,7 @@ "type": "Link" } ], - "modified": "2026-06-11 11:51:23.316581", + "modified": "2026-06-14 13:44:07.764547", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/workspace_sidebar/accounts_setup.json b/erpnext/workspace_sidebar/accounts_setup.json index df28f57238a..93a436ee15b 100644 --- a/erpnext/workspace_sidebar/accounts_setup.json +++ b/erpnext/workspace_sidebar/accounts_setup.json @@ -14,6 +14,7 @@ "keep_closed": 0, "label": "Setup", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -25,6 +26,7 @@ "label": "Chart of Accounts", "link_to": "Account", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -36,6 +38,7 @@ "label": "Chart of Cost Centers", "link_to": "Cost Center", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -47,6 +50,7 @@ "label": "Account Category", "link_to": "Account Category", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -58,6 +62,7 @@ "label": "Accounting Dimension", "link_to": "Accounting Dimension", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -69,6 +74,7 @@ "label": "Currency", "link_to": "Currency", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -80,6 +86,7 @@ "label": "Currency Exchange", "link_to": "Currency Exchange", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -91,6 +98,7 @@ "label": "Finance Book", "link_to": "Finance Book", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -102,6 +110,7 @@ "label": "Mode of Payment", "link_to": "Mode of Payment", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -113,6 +122,7 @@ "label": "Payment Term", "link_to": "Payment Term", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -124,6 +134,7 @@ "label": "Journal Entry Template", "link_to": "Journal Entry Template", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -135,6 +146,7 @@ "label": "Terms and Conditions", "link_to": "Terms and Conditions", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -146,6 +158,7 @@ "label": "Company", "link_to": "Company", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -157,6 +170,7 @@ "label": "Fiscal Year", "link_to": "Fiscal Year", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -168,6 +182,7 @@ "label": "Sales Taxes", "link_to": "Sales Taxes and Charges Template", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -179,6 +194,7 @@ "keep_closed": 0, "label": "Opening & Closing", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -191,6 +207,7 @@ "label": "COA Importer", "link_to": "Chart of Accounts Importer", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -203,6 +220,7 @@ "label": "Opening Invoice Tool", "link_to": "Opening Invoice Creation Tool", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -215,6 +233,7 @@ "label": "Accounting Period", "link_to": "Accounting Period", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -227,6 +246,7 @@ "label": "FX Revaluation", "link_to": "Exchange Rate Revaluation", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -239,6 +259,7 @@ "label": "Period Closing Voucher", "link_to": "Period Closing Voucher", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -250,6 +271,7 @@ "keep_closed": 0, "label": "Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -262,6 +284,7 @@ "label": "Accounts Settings", "link_to": "Accounts Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -273,22 +296,12 @@ "label": "Currency Exchange Settings", "link_to": "Currency Exchange Settings", "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Repost Accounting Ledger Settings", - "link_to": "Repost Accounting Ledger Settings", - "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-02-23 22:20:51.043478", + "modified": "2026-06-12 14:50:50.262533", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", diff --git a/erpnext/workspace_sidebar/budget.json b/erpnext/workspace_sidebar/budgeting.json similarity index 89% rename from erpnext/workspace_sidebar/budget.json rename to erpnext/workspace_sidebar/budgeting.json index dd9b6f87311..e98e6ca36ce 100644 --- a/erpnext/workspace_sidebar/budget.json +++ b/erpnext/workspace_sidebar/budgeting.json @@ -15,6 +15,7 @@ "label": "Budget", "link_to": "Budget", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "Cost Center", "link_to": "Cost Center", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Accounting Dimension", "link_to": "Accounting Dimension", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -51,6 +54,7 @@ "label": "Cost Center Allocation", "link_to": "Cost Center Allocation", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -63,6 +67,7 @@ "label": "Budget Variance", "link_to": "Budget Variance Report", "link_type": "Report", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } @@ -70,8 +75,8 @@ "modified": "2026-01-10 00:06:13.032297", "modified_by": "Administrator", "module": "Accounts", - "name": "Budget", + "name": "Budgeting", "owner": "Administrator", "standard": 1, - "title": "Budget" + "title": "Budgeting" } diff --git a/erpnext/workspace_sidebar/erpnext_settings.json b/erpnext/workspace_sidebar/erpnext_settings.json index 117a36b666e..c89270ce56f 100644 --- a/erpnext/workspace_sidebar/erpnext_settings.json +++ b/erpnext/workspace_sidebar/erpnext_settings.json @@ -15,6 +15,7 @@ "label": "Global Defaults", "link_to": "Global Defaults", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "System Settings", "link_to": "System Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Accounts Settings", "link_to": "Accounts Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -51,6 +54,7 @@ "label": "POS Settings", "link_to": "POS Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -63,6 +67,7 @@ "label": "Selling Settings", "link_to": "Selling Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -75,6 +80,7 @@ "label": "Buying Settings", "link_to": "Buying Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -87,6 +93,7 @@ "label": "Stock Settings", "link_to": "Stock Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -99,6 +106,7 @@ "label": "Manufacturing Settings", "link_to": "Manufacturing Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -111,6 +119,7 @@ "label": "Projects Settings", "link_to": "Projects Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -123,6 +132,7 @@ "label": "CRM Settings", "link_to": "CRM Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -135,6 +145,7 @@ "label": "Support Settings", "link_to": "Support Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -146,6 +157,7 @@ "keep_closed": 1, "label": "Other Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -157,6 +169,7 @@ "label": "Subscription Settings", "link_to": "Subscription Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -168,6 +181,7 @@ "label": "Item Variant Settings", "link_to": "Item Variant Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -179,6 +193,7 @@ "label": "Delivery Settings", "link_to": "Delivery Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -190,6 +205,7 @@ "label": "Currency Exchange Settings", "link_to": "Currency Exchange Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -201,6 +217,7 @@ "label": "Appointment Booking Settings", "link_to": "Appointment Booking Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -212,22 +229,12 @@ "label": "Stock Reposting Settings", "link_to": "Stock Reposting Settings", "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Repost Accounting Ledger Settings", - "link_to": "Repost Accounting Ledger Settings", - "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-01-10 00:06:12.956275", + "modified": "2026-06-12 14:51:11.333051", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", diff --git a/erpnext/workspace_sidebar/organization.json b/erpnext/workspace_sidebar/organization.json index 8ea0a44faca..06802556abe 100644 --- a/erpnext/workspace_sidebar/organization.json +++ b/erpnext/workspace_sidebar/organization.json @@ -9,89 +9,103 @@ { "child": 0, "collapsible": 1, + "default_workspace": 1, "icon": "organization", "indent": 0, "keep_closed": 0, "label": "Company", "link_to": "Company", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-text", "indent": 0, "keep_closed": 0, "label": "Letter Head", "link_to": "Letter Head", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "file-user", "indent": 0, "keep_closed": 0, "label": "Department", "link_to": "Department", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-user", "indent": 0, "keep_closed": 0, "label": "Branch", "link_to": "Branch", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "users", "indent": 0, "keep_closed": 0, "label": "User", "link_to": "User", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "user-round-check", "indent": 0, "keep_closed": 0, "label": "Role Permissions", "link_to": "permission-manager", "link_type": "Page", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "mail", "indent": 0, "keep_closed": 0, "label": "Email Account", "link_to": "Email Account", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-02-24 18:08:00.796746", + "modified": "2026-06-16 00:37:22.942285", "modified_by": "Administrator", "module": "Setup", "module_onboarding": "Organization Onboarding", diff --git a/erpnext/workspace_sidebar/subscription.json b/erpnext/workspace_sidebar/subscriptions.json similarity index 88% rename from erpnext/workspace_sidebar/subscription.json rename to erpnext/workspace_sidebar/subscriptions.json index ca42736b27c..ec188edf169 100644 --- a/erpnext/workspace_sidebar/subscription.json +++ b/erpnext/workspace_sidebar/subscriptions.json @@ -15,6 +15,7 @@ "label": "Subscription", "link_to": "Subscription", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "Subscription Plan", "link_to": "Subscription Plan", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Subscription Settings", "link_to": "Subscription Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -50,6 +53,7 @@ "keep_closed": 1, "label": "Setup", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -61,6 +65,7 @@ "label": "Customer", "link_to": "Customer", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -72,6 +77,7 @@ "label": "Supplier", "link_to": "Supplier", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -83,6 +89,7 @@ "label": "Item", "link_to": "Item", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } @@ -90,8 +97,8 @@ "modified": "2026-01-10 00:06:13.048591", "modified_by": "Administrator", "module": "Accounts", - "name": "Subscription", + "name": "Subscriptions", "owner": "Administrator", "standard": 1, - "title": "Subscription" + "title": "Subscriptions" } From 55afd95b20f3fa628f3c27aa2c45fa2ff49205f9 Mon Sep 17 00:00:00 2001 From: sokumon Date: Tue, 30 Jun 2026 17:24:58 +0530 Subject: [PATCH 058/195] fix: remove duplicate links from export --- erpnext/setup/workspace/home/home.json | 67 +------------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 1cbb1fb7292..5d1e56c89a1 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -452,7 +452,7 @@ "type": "Link" } ], - "modified": "2026-06-29 13:27:45.986650", + "modified": "2026-06-30 17:24:11.542162", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -504,32 +504,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "indent": 0, - "keep_closed": 0, - "label": "Item", - "link_to": "Item", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "indent": 0, - "keep_closed": 0, - "label": "Home", - "link_to": "Home", - "link_type": "Workspace", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, { "child": 0, "collapsible": 1, @@ -556,19 +530,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "indent": 0, - "keep_closed": 0, - "label": "Customer", - "link_to": "Customer", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, { "child": 0, "collapsible": 1, @@ -582,32 +543,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "indent": 0, - "keep_closed": 0, - "label": "Supplier", - "link_to": "Supplier", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "indent": 0, - "keep_closed": 0, - "label": "Sales Invoice", - "link_to": "Sales Invoice", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, { "child": 0, "collapsible": 1, From ba7b6a47c56f62c885191bf1a12faeace2967cb7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Jul 2026 13:27:21 +0530 Subject: [PATCH 059/195] refactor: rename execute_synced_report to execute_snapshot_report Match the framework rename of the standard report entry point in the trial balance, P&L, balance sheet, and general ledger reports. Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/accounts/report/balance_sheet/balance_sheet.py | 2 +- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- .../profit_and_loss_statement/profit_and_loss_statement.py | 2 +- erpnext/accounts/report/trial_balance/trial_balance.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 756d0c2ebbb..1090b7f4b9c 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -277,7 +277,7 @@ def get_chart_data(filters, chart_columns, asset, liability, equity, currency): return chart -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if not (conn := get_latest_sync("GL Entry")): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 43383cf6b36..ca06baf4a08 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -820,7 +820,7 @@ def get_columns(filters): return columns -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if conn := get_latest_sync("GL Entry"): diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 297aa961058..25eca6f4c79 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -207,7 +207,7 @@ def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, cur return chart -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if not (conn := get_latest_sync("GL Entry")): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d8fce97263e..15862f5746e 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -583,7 +583,7 @@ def hide_group_accounts(data): return non_group_accounts_data -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if conn := get_latest_sync("GL Entry"): From 3b25878d71389bc21a0820a07246b78caedbdec2 Mon Sep 17 00:00:00 2001 From: sokumon Date: Wed, 1 Jul 2026 13:29:25 +0530 Subject: [PATCH 060/195] fix: remove text from projects workspace --- .../projects/workspace/projects/projects.json | 196 +++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index 488ae012483..55a296d4d0d 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -6,7 +6,7 @@ "label": "Completed Projects" } ], - "content": "[{\"id\":\"7Mbx6I5JUf\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"QYGHTcrCDB\",\"type\":\"header\",\"data\":{\"text\":\"Hello\",\"col\":12}},{\"id\":\"fVYsju6dB9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Completed Projects\",\"col\":12}},{\"id\":\"67w8up7H_0\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Open Projects\",\"col\":4}},{\"id\":\"IFEYSadaYc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Non Completed Tasks\",\"col\":4}},{\"id\":\"VqbqxA0YL1\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Working Hours\",\"col\":4}},{\"id\":\"oGhjvYjfv-\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"TdsgJyG3EI\",\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"id\":\"nIc0iyvf1T\",\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"id\":\"8G1if4jsQ7\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"o7qTNRXZI8\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"7Mbx6I5JUf\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"fVYsju6dB9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Completed Projects\",\"col\":12}},{\"id\":\"67w8up7H_0\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Open Projects\",\"col\":4}},{\"id\":\"IFEYSadaYc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Non Completed Tasks\",\"col\":4}},{\"id\":\"VqbqxA0YL1\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Working Hours\",\"col\":4}},{\"id\":\"oGhjvYjfv-\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"TdsgJyG3EI\",\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"id\":\"nIc0iyvf1T\",\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"id\":\"8G1if4jsQ7\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"o7qTNRXZI8\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 15:46:04.874669", "custom_blocks": [], "docstatus": 0, @@ -18,6 +18,14 @@ "is_hidden": 0, "label": "Projects", "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Projects", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "hidden": 0, "is_query_report": 0, @@ -37,6 +45,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Project", + "link_count": 0, + "link_to": "Project", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Task", + "link_count": 0, + "link_to": "Task", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -59,6 +89,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Project Template", + "link_count": 0, + "link_to": "Project Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -70,6 +111,28 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Project Type", + "link_count": 0, + "link_to": "Project Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Project", + "hidden": 0, + "is_query_report": 0, + "label": "Project Update", + "link_count": 0, + "link_to": "Project Update", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Project", "hidden": 0, @@ -89,6 +152,14 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Time Tracking", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -100,6 +171,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Timesheet", + "link_count": 0, + "link_to": "Timesheet", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Activity Type", + "link_count": 0, + "link_to": "Activity Type", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -122,6 +215,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Activity Type", + "hidden": 0, + "is_query_report": 0, + "label": "Activity Cost", + "link_count": 0, + "link_to": "Activity Cost", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -130,6 +234,25 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Timesheet", + "hidden": 0, + "is_query_report": 1, + "label": "Daily Timesheet Summary", + "link_count": 0, + "link_to": "Daily Timesheet Summary", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, { "dependencies": "Timesheet", "hidden": 0, @@ -152,6 +275,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Project", + "hidden": 0, + "is_query_report": 1, + "label": "Project wise Stock Tracking", + "link_count": 0, + "link_to": "Project wise Stock Tracking", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Project", "hidden": 0, @@ -163,6 +297,28 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Project", + "hidden": 0, + "is_query_report": 1, + "label": "Timesheet Billing Summary", + "link_count": 0, + "link_to": "Timesheet Billing Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Task", + "hidden": 0, + "is_query_report": 1, + "label": "Delayed Tasks Summary", + "link_count": 0, + "link_to": "Delayed Tasks Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Task", "hidden": 0, @@ -182,6 +338,24 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 1, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Projects Settings", + "link_count": 0, + "link_to": "Projects Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -193,7 +367,7 @@ "type": "Link" } ], - "modified": "2026-06-14 13:44:07.964218", + "modified": "2026-07-01 13:20:50.651608", "modified_by": "Administrator", "module": "Projects", "module_onboarding": "Projects Onboarding", @@ -224,6 +398,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "home", "indent": 0, "keep_closed": 0, @@ -237,6 +412,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "chart", "indent": 0, "keep_closed": 0, @@ -250,6 +426,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "projects", "indent": 0, "keep_closed": 0, @@ -263,6 +440,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "list-todo", "indent": 0, "keep_closed": 0, @@ -276,6 +454,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "calendar-clock", "indent": 0, "keep_closed": 0, @@ -289,6 +468,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "database", "indent": 1, "keep_closed": 1, @@ -301,6 +481,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Activity Type", @@ -313,6 +494,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Activity Cost", @@ -325,6 +507,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Project Template", @@ -337,6 +520,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Project Type", @@ -349,6 +533,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Project Update", @@ -361,6 +546,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "sheet", "indent": 1, "keep_closed": 1, @@ -373,6 +559,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -386,6 +573,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Daily Timesheet Summary", @@ -398,6 +586,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Timesheet Billing Summary", @@ -410,6 +599,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Project wise Stock Tracking", @@ -422,6 +612,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Delayed Tasks Summary", @@ -434,6 +625,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "settings", "indent": 0, "keep_closed": 0, From 088b8ff69b5f745013aae130a27112c330b88bb4 Mon Sep 17 00:00:00 2001 From: sokumon Date: Wed, 1 Jul 2026 14:22:43 +0530 Subject: [PATCH 061/195] fix: remove roles from home workspace --- erpnext/setup/workspace/home/home.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 5d1e56c89a1..9b1f186e934 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -452,7 +452,7 @@ "type": "Link" } ], - "modified": "2026-06-30 17:24:11.542162", + "modified": "2026-07-01 14:22:16.927245", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -462,11 +462,7 @@ "public": 1, "quick_lists": [], "restrict_to_domain": "", - "roles": [ - { - "role": "Desk User" - } - ], + "roles": [], "sequence_id": 1.0, "shortcuts": [ { From 27d5165755d2c56996304191d69bb4778bbf2e33 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Wed, 1 Jul 2026 15:44:26 +0530 Subject: [PATCH 062/195] feat(stock): support partial transfer from pick list Creating a Stock Entry from a Pick List blocked any further entry (stock_entry_exists) and flipped the pick list to Completed as soon as one entry existed, so picked stock could not be transferred in parts. Track transferred_qty per Pick List Item (summed from submitted Stock Entry rows via a new pick_list_item link, mirroring delivered_qty), add a Partially Transferred status, and map each new Stock Entry from the remaining qty so transfers can continue until fully transferred. --- erpnext/controllers/status_updater.py | 3 +- erpnext/stock/doctype/pick_list/mapper.py | 24 +++++++++++--- .../stock/doctype/pick_list/pick_list.json | 4 +-- erpnext/stock/doctype/pick_list/pick_list.py | 32 ++++++++++++++++++- .../stock/doctype/pick_list/pick_list_list.js | 1 + .../pick_list_item/pick_list_item.json | 13 +++++++- .../doctype/pick_list_item/pick_list_item.py | 1 + .../stock/doctype/stock_entry/stock_entry.py | 13 ++++++++ .../stock_entry_detail.json | 13 +++++++- .../stock_entry_detail/stock_entry_detail.py | 1 + 10 files changed, 94 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 9e46598768f..eed56008547 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -167,7 +167,8 @@ status_map = { "Pick List": [ ["Draft", None], ["Open", "eval:self.docstatus == 1"], - ["Completed", "stock_entry_exists"], + ["Completed", "is_fully_transferred"], + ["Partially Transferred", "is_partially_transferred"], [ "Partly Delivered", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'", diff --git a/erpnext/stock/doctype/pick_list/mapper.py b/erpnext/stock/doctype/pick_list/mapper.py index f9f5fdc8e08..df2c3f4e2c2 100644 --- a/erpnext/stock/doctype/pick_list/mapper.py +++ b/erpnext/stock/doctype/pick_list/mapper.py @@ -285,9 +285,6 @@ def create_stock_entry(pick_list: str | dict): pick_list = frappe.get_doc(frappe.parse_json(pick_list)) validate_item_locations(pick_list) - if stock_entry_exists(pick_list.get("name")): - return frappe.msgprint(_("Stock Entry has already been created against this Pick List")) - stock_entry = frappe.new_doc("Stock Entry") stock_entry.pick_list = pick_list.get("name") stock_entry.purpose = pick_list.get("purpose") @@ -301,6 +298,9 @@ def create_stock_entry(pick_list: str | dict): else: stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry) + if not stock_entry.get("items"): + return frappe.msgprint(_("All picked items have already been transferred against this Pick List")) + stock_entry.set_missing_values() return stock_entry.as_dict() @@ -366,6 +366,8 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.project = work_order.project for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue item = frappe._dict() update_common_item_properties(item, location) item.t_warehouse = wip_warehouse @@ -377,6 +379,8 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): def update_stock_entry_based_on_material_request(pick_list, stock_entry): for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue target_warehouse = None if location.material_request_item: target_warehouse = frappe.get_value( @@ -392,6 +396,8 @@ def update_stock_entry_based_on_material_request(pick_list, stock_entry): def update_stock_entry_items_with_no_reference(pick_list, stock_entry): for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue item = frappe._dict() update_common_item_properties(item, location) @@ -400,11 +406,18 @@ def update_stock_entry_items_with_no_reference(pick_list, stock_entry): return stock_entry +def get_pending_transfer_stock_qty(location): + """Stock qty of this pick list row still to be moved into a Stock Entry.""" + return flt(location.picked_qty) - flt(location.transferred_qty) + + def update_common_item_properties(item, location): + pending_stock_qty = get_pending_transfer_stock_qty(location) item.item_code = location.item_code + item.item_name = location.item_name item.s_warehouse = location.warehouse - item.transfer_qty = location.picked_qty - item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty")) + item.transfer_qty = pending_stock_qty + item.qty = flt(pending_stock_qty / (location.conversion_factor or 1), location.precision("qty")) item.uom = location.uom item.conversion_factor = location.conversion_factor item.stock_uom = location.stock_uom @@ -412,3 +425,4 @@ def update_common_item_properties(item, location): item.serial_no = location.serial_no item.batch_no = location.batch_no item.material_request_item = location.material_request_item + item.pick_list_item = location.name diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 9ee1b7a1922..55e66f74b3c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -190,7 +190,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled", + "options": "Draft\nOpen\nPartly Delivered\nPartially Transferred\nCompleted\nCancelled", "print_hide": 1, "read_only": 1, "report_hide": 1, @@ -278,7 +278,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2026-02-06 18:14:18.361039", + "modified": "2026-07-01 14:27:50.617011", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a25770351e4..846c020de72 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -71,7 +71,9 @@ class PickList(TransactionBase): purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"] scan_barcode: DF.Data | None scan_mode: DF.Check - status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"] + status: DF.Literal[ + "Draft", "Open", "Partly Delivered", "Partially Transferred", "Completed", "Cancelled" + ] work_order: DF.Link | None # end: auto-generated types @@ -417,6 +419,34 @@ class PickList(TransactionBase): return stock_entry_exists(self.name) + def get_transfer_status(self): + """Return the pick list's transfer progress based on how much of the picked qty has been + moved into submitted Stock Entries (tracked on Pick List Item.transferred_qty). + + Only applies to purposes that move stock via Stock Entry; the Delivery purpose is tracked + via delivery_status instead. Returns "Completed", "Partially Transferred" or None.""" + if self.purpose == "Delivery": + return None + + total_picked = sum(flt(row.picked_qty) for row in self.locations) + if not total_picked: + return None + + total_transferred = sum(flt(row.transferred_qty) for row in self.locations) + if total_transferred <= 0: + return None + + if total_transferred >= total_picked: + return "Completed" + + return "Partially Transferred" + + def is_fully_transferred(self): + return self.get_transfer_status() == "Completed" + + def is_partially_transferred(self): + return self.get_transfer_status() == "Partially Transferred" + def update_reference_qty(self): packed_items = [] so_items = [] diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js index a675c95f973..5bc4f2f3eef 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -7,6 +7,7 @@ frappe.listview_settings["Pick List"] = { Draft: "red", Open: "orange", "Partly Delivered": "orange", + "Partially Transferred": "yellow", Completed: "green", Cancelled: "red", }; diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 01630278168..658dff42d7f 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -22,6 +22,7 @@ "conversion_factor", "stock_uom", "delivered_qty", + "transferred_qty", "available_quantity_section", "actual_qty", "column_break_kyek", @@ -255,6 +256,16 @@ "read_only": 1, "report_hide": 1 }, + { + "default": "0", + "fieldname": "transferred_qty", + "fieldtype": "Float", + "label": "Transferred Qty (in Stock UOM)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, { "fieldname": "available_quantity_section", "fieldtype": "Section Break", @@ -285,7 +296,7 @@ ], "istable": 1, "links": [], - "modified": "2026-03-17 16:25:10.358013", + "modified": "2026-07-01 14:27:50.617011", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py index bdba97f4056..97e6525c97b 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py @@ -39,6 +39,7 @@ class PickListItem(Document): stock_qty: DF.Float stock_reserved_qty: DF.Float stock_uom: DF.Link | None + transferred_qty: DF.Float uom: DF.Link | None use_serial_batch_fields: DF.Check warehouse: DF.Link | None diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e575e8eedb4..18ee63f6abf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -165,6 +165,15 @@ class StockEntry(StockController, SubcontractingInwardController): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._configure_purpose_class() + self.status_updater = [ + { + "source_dt": "Stock Entry Detail", + "target_dt": "Pick List Item", + "join_field": "pick_list_item", + "target_field": "transferred_qty", + "source_field": "transfer_qty", + } + ] if self.subcontracting_inward_order: self.subcontract_data = frappe._dict( @@ -350,6 +359,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.delink_asset_repair_sabb() self.validate_closed_subcontracting_order() self.update_subcontracting_order_status() + self.update_pick_list_status() self.cancel_stock_reserve_for_wip_and_fg() if self.work_order and self.purpose == "Material Consumption for Manufacture": @@ -1485,6 +1495,9 @@ class StockEntry(StockController, SubcontractingInwardController): def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + if self.pick_list: + self.update_qty() + update_pick_list_status(self.pick_list) def set_missing_values(self): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index ea9d3b75b51..75f45275de1 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -72,6 +72,7 @@ "col_break6", "material_request", "material_request_item", + "pick_list_item", "original_item", "reference_section", "against_stock_entry", @@ -424,6 +425,16 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "pick_list_item", + "fieldtype": "Link", + "hidden": 1, + "label": "Pick List Item", + "no_copy": 1, + "options": "Pick List Item", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "original_item", "fieldtype": "Link", @@ -679,7 +690,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-06-30 12:18:34.132425", + "modified": "2026-07-01 14:27:50.617011", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index 3224ea905c7..4e690d4d8ec 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -58,6 +58,7 @@ class StockEntryDetail(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + pick_list_item: DF.Link | None po_detail: DF.Data | None project: DF.Link | None putaway_rule: DF.Link | None From a1daad8d4f755dca705a6b2fcb94e51267a7d674 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Wed, 1 Jul 2026 15:44:35 +0530 Subject: [PATCH 063/195] test(stock): add test for partial transfer status from pick list --- .../stock/doctype/pick_list/test_pick_list.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index c442539f1f5..e37cb0a3532 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -13,6 +13,7 @@ from erpnext.stock.doctype.pick_list.mapper import ( create_delivery, create_delivery_note, create_dn_for_pick_lists, + create_stock_entry, ) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( @@ -1221,6 +1222,64 @@ class TestPickList(ERPNextTestSuite): pl.reload() self.assertEqual(pl.status, "Cancelled") + def test_pick_list_partial_transfer_status(self): + """Partial Stock Entries from a Pick List should track transferred_qty and drive the + Partially Transferred / Completed status, and allow further transfers for the remainder.""" + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + item = make_item(properties={"is_stock_item": 1}).name + source_warehouse = "_Test Warehouse - _TC" + target_warehouse = create_warehouse("_Test Transfer Target Warehouse") + make_stock_entry(item=item, to_warehouse=source_warehouse, qty=10) + + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "pick_manually": 1, + "locations": [ + { + "item_code": item, + "qty": 10, + "stock_qty": 10, + "conversion_factor": 1, + "warehouse": source_warehouse, + "picked_qty": 10, + } + ], + } + ) + pick_list.submit() + self.assertEqual(pick_list.status, "Open") + + # Transfer 4 of the 10 picked units. + se1 = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(se1.items[0].qty, 10) + se1.items[0].qty = 4 + se1.items[0].t_warehouse = target_warehouse + se1.submit() + + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 4) + self.assertEqual(pick_list.status, "Partially Transferred") + + # The next Stock Entry should only offer the remaining 6 units. + se2 = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(se2.items[0].qty, 6) + se2.items[0].t_warehouse = target_warehouse + se2.submit() + + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 10) + self.assertEqual(pick_list.status, "Completed") + + # Cancelling the last entry rolls transferred_qty and status back. + se2.cancel() + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 4) + self.assertEqual(pick_list.status, "Partially Transferred") + def test_pick_list_validation(self): warehouse = "_Test Warehouse - _TC" item = make_item("Test Non Serialized Pick List Item", properties={"is_stock_item": 1}).name From 497ca14747bf932ff897b16b23bf627df56b54bd Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 15:55:33 +0530 Subject: [PATCH 064/195] test: detect a real stock/account value mismatch in comparison report --- ...test_stock_and_account_value_comparison.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py index 314a1aa16f3..82266f25354 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py @@ -37,6 +37,37 @@ class TestStockAndAccountValueComparison(ERPNextTestSuite): # and GL values for the receipt voucher, so nothing should be flagged. self.assertEqual(rows, []) + def test_stock_account_gl_mismatch_is_flagged(self): + warehouse = create_warehouse("_Test SAVC Mismatch WH", company=COMPANY) + account = frappe.get_value("Warehouse", warehouse, "account") + + receipt = make_stock_entry( + item_code="_Test Item", + to_warehouse=warehouse, + qty=10, + rate=100, + company=COMPANY, + posting_date="2026-06-01", + ) + + # Simulate corruption: the stock-account GL entry for this receipt drifts out of sync + # with the stock ledger (stock value stays 1000, but the account only shows 600). + frappe.db.set_value( + "GL Entry", + {"voucher_no": receipt.name, "account": account, "is_cancelled": 0}, + "debit_in_account_currency", + 600, + update_modified=False, + ) + + rows = self.run_report(account=account) + + row = next(r for r in rows if r["voucher_no"] == receipt.name) + self.assertEqual(row["ledger_type"], "Stock Ledger Entry") + self.assertEqual(row["stock_value"], 1000) # unchanged stock ledger value + self.assertEqual(row["account_value"], 600) # tampered GL value + self.assertEqual(row["difference_value"], 400) # 1000 - 600, above the 0.1 threshold + def run_report(self, **extra): filters = {"company": COMPANY, "as_on_date": "2026-12-31"} filters.update(extra) From d072909451dd3c12ec7f8e89793b47d008f81ffc Mon Sep 17 00:00:00 2001 From: pandiyan Date: Tue, 30 Jun 2026 21:49:47 +0530 Subject: [PATCH 065/195] fix: recompute transferred qty before deciding work order status work order status was decided using a stale transferred-qty value, computed before the current stock entry's transfer got recomputed. this left work orders stuck at "not started" for pick-list-driven transfers, since those entries never set fg_completed_qty and their transferred qty can only be known from actual item-level transfers. an earlier attempt fixed this by setting fg_completed_qty from the pick list's for_qty, but that broke two things tied to fg_completed_qty being zero: the excess-transfer guard, and the partial-transfer fraction logic used to avoid marking a work order as fully supplied too early. recompute the transferred qty first, then decide status from the fresh value. revert the fg_completed_qty change since it's no longer needed. --- .../doctype/work_order/services/required_items.py | 10 ++++++++++ .../doctype/work_order/services/status.py | 6 ++++++ erpnext/manufacturing/doctype/work_order/work_order.py | 3 +++ 3 files changed, 19 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/services/required_items.py b/erpnext/manufacturing/doctype/work_order/services/required_items.py index a8a415ca4fc..c43d1a43e4f 100644 --- a/erpnext/manufacturing/doctype/work_order/services/required_items.py +++ b/erpnext/manufacturing/doctype/work_order/services/required_items.py @@ -149,6 +149,16 @@ class RequiredItemsService: self.recompute_material_transferred_for_manufacturing(transferred_items) + def refresh_material_transferred_for_manufacturing(self): + """Recompute material_transferred_for_manufacturing only, without touching per-row + transferred_qty or stock reservations. Used to get a status decision (Not Started vs + In Process) based on fresh data, ahead of the fuller update_required_items() pass. + """ + if self.doc.skip_transfer: + return + transferred_items = self._material_transfer_qty_by_item(is_return=0) + self.recompute_material_transferred_for_manufacturing(transferred_items) + def recompute_material_transferred_for_manufacturing(self, transferred_items): """Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty.""" # When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py index cb9e49a2cce..ce67978afd7 100644 --- a/erpnext/manufacturing/doctype/work_order/services/status.py +++ b/erpnext/manufacturing/doctype/work_order/services/status.py @@ -87,6 +87,12 @@ class StatusService: def update_status(self, status=None): """Update status of work order if unknown""" + if self.doc.docstatus == 1: + # Refresh material_transferred_for_manufacturing before deciding status so pick-list- + # driven transfers (where this qty is derived from item transfers, not fg_completed_qty) + # are reflected immediately, instead of only after the next status update call. + self.doc.refresh_material_transferred_for_manufacturing() + if self.doc.status != "Closed": if status not in ["Stopped", "Closed"]: status = self.get_status(status) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index a7af812fd34..22da1a1d989 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1003,6 +1003,9 @@ class WorkOrder(Document): def update_transferred_qty_for_required_items(self): return RequiredItemsService(self).update_transferred_qty_for_required_items() + def refresh_material_transferred_for_manufacturing(self): + return RequiredItemsService(self).refresh_material_transferred_for_manufacturing() + def update_returned_qty(self): return RequiredItemsService(self).update_returned_qty() From fad904d68bab0e7f331982ee22d0468196c7c465 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Wed, 1 Jul 2026 16:18:44 +0530 Subject: [PATCH 066/195] fix(stock): backfill transferred qty for existing pick lists Pick Lists transferred before this feature have transferred_qty = 0 and their Stock Entry rows carry no pick_list_item link, so the new is_fully_transferred check would never fire and, with the old duplicate-entry guard removed, they could be transferred again. Set transferred_qty = picked_qty for non-Delivery submitted pick lists that already have a linked Stock Entry so they stay completed and locked. --- erpnext/patches.txt | 1 + .../backfill_pick_list_transferred_qty.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0d1c3f01025..9b13bbfefd1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -492,3 +492,4 @@ erpnext.patches.v16_0.rename_subscription_billing_period_fields erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb erpnext.patches.v16_0.set_default_close_opportunity_after_days execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600) +erpnext.patches.v16_0.backfill_pick_list_transferred_qty diff --git a/erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py b/erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py new file mode 100644 index 00000000000..6d3155c4c1a --- /dev/null +++ b/erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py @@ -0,0 +1,58 @@ +import frappe +from frappe.query_builder.functions import Sum +from frappe.utils import flt + + +def execute(): + StockEntry = frappe.qb.DocType("Stock Entry") + StockEntryDetail = frappe.qb.DocType("Stock Entry Detail") + + pick_lists = ( + frappe.qb.from_(StockEntry) + .select(StockEntry.pick_list) + .distinct() + .where((StockEntry.pick_list.isnotnull()) & (StockEntry.docstatus == 1)) + ).run(pluck=True) + + if not pick_lists: + return + + rows = ( + frappe.qb.from_(StockEntryDetail) + .join(StockEntry) + .on(StockEntryDetail.parent == StockEntry.name) + .select( + StockEntry.pick_list, + StockEntryDetail.item_code, + StockEntryDetail.s_warehouse, + Sum(StockEntryDetail.transfer_qty).as_("qty"), + ) + .where((StockEntry.pick_list.isin(pick_lists)) & (StockEntry.docstatus == 1)) + .groupby(StockEntry.pick_list, StockEntryDetail.item_code, StockEntryDetail.s_warehouse) + ).run(as_dict=True) + + transferred = {(r.pick_list, r.item_code, r.s_warehouse): flt(r.qty) for r in rows} + + items = frappe.get_all( + "Pick List Item", + filters={"parent": ("in", pick_lists), "picked_qty": (">", 0)}, + fields=["name", "parent", "item_code", "warehouse", "picked_qty"], + order_by="idx", + ) + + updates = {} + for row in items: + key = (row.parent, row.item_code, row.warehouse) + available = transferred.get(key, 0) + if available <= 0: + continue + qty = min(flt(row.picked_qty), available) + transferred[key] = available - qty + updates[row.name] = {"transferred_qty": qty} + + if not updates: + return + + frappe.db.auto_commit_on_many_writes = True + frappe.db.bulk_update("Pick List Item", updates) + frappe.db.auto_commit_on_many_writes = False From 9cf356f6f5eba1997713c760082e3e6e15edeb55 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 17:23:19 +0530 Subject: [PATCH 067/195] chore: remove redundant type case --- erpnext/stock/get_item_details.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index be563e9941a..879b3a51afb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1220,8 +1220,6 @@ def get_item_price(pctx: frappe._dict, item_code, ignore_party=False, force_batc optional fields transaction_date, customer, supplier :param item_code: str, Item Doctype field item_code """ - pctx: frappe._dict = frappe._dict(pctx) - ip = frappe.qb.DocType("Item Price") query = ( frappe.qb.from_(ip) From 981e90e4da111fffeb954d138eddbf122ea67f59 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Jul 2026 13:43:18 +0530 Subject: [PATCH 068/195] refactor: rename feature toggle in report master --- .../accounts/report/accounts_payable/accounts_payable.json | 4 ++-- .../report/accounts_receivable/accounts_receivable.json | 4 ++-- erpnext/accounts/report/general_ledger/general_ledger.json | 4 ++-- .../profit_and_loss_statement/profit_and_loss_statement.json | 4 ++-- erpnext/accounts/report/trial_balance/trial_balance.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 9c713fccf64..5caee4f5c1f 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 3, "is_standard": "Yes", - "modified": "2026-06-25 12:03:36.559152", + "modified": "2026-07-01 13:37:41.185347", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Payable", @@ -40,6 +40,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json index dcc3c2c6a49..ef9b6df88d4 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 5, "is_standard": "Yes", - "modified": "2026-06-25 12:03:28.812092", + "modified": "2026-07-01 13:37:44.167999", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Receivable", @@ -34,6 +34,6 @@ "role": "Accounts User" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 083f7b62ae8..a702e606edd 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-22 13:38:35.057216", + "modified": "2026-07-01 13:36:06.682661", "modified_by": "Administrator", "module": "Accounts", "name": "General Ledger", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json index 9aa088aefe0..5ddd3af7aa4 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 2, "is_standard": "Yes", - "modified": "2026-06-22 13:38:15.898375", + "modified": "2026-07-01 13:36:14.934965", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss Statement", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/trial_balance/trial_balance.json b/erpnext/accounts/report/trial_balance/trial_balance.json index 6793268a1e6..5a8bd5c006e 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.json +++ b/erpnext/accounts/report/trial_balance/trial_balance.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-22 13:38:42.740436", + "modified": "2026-07-01 17:32:21.801141", "modified_by": "Administrator", "module": "Accounts", "name": "Trial Balance", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } From 116b7bf67216c33cfca6cc30cda8bdddb6267265 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:18:17 +0530 Subject: [PATCH 069/195] fix: minor fix Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../test_stock_and_account_value_comparison.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py index 2e6ee2afe58..7eabe37cc91 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py @@ -67,7 +67,8 @@ class TestStockAndAccountValueComparison(ERPNextTestSuite): rows = self.run_report(account=account) - row = next(r for r in rows if r["voucher_no"] == receipt.name) + row = next((r for r in rows if r["voucher_no"] == receipt.name), None) + self.assertIsNotNone(row, "Tampered GL entry should cause the voucher to appear in the report") self.assertEqual(row["ledger_type"], "Stock Ledger Entry") self.assertEqual(row["stock_value"], 1000) # unchanged stock ledger value self.assertEqual(row["account_value"], 600) # tampered GL value From 039314c3066775a5a04d7fcfbb01454f45f68661 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:22:30 +0530 Subject: [PATCH 070/195] test: make Item Prices tests deterministic (fresh items, label-based columns) --- .../report/item_prices/test_item_prices.py | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index 6c521c9e025..147a3d4bc34 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -3,33 +3,36 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.item_prices.item_prices import execute from erpnext.tests.utils import ERPNextTestSuite -# Positional columns returned by the report (it returns string-format columns, -# not dicts, so rows are plain lists indexed by position). -ITEM_CODE = 0 -LAST_PURCHASE_RATE = 6 -VALUATION_RATE = 7 -SALES_PRICE_LIST = 8 -PURCHASE_PRICE_LIST = 9 - class TestItemPrices(ERPNextTestSuite): """Correctness tests for the Item Prices report.""" def run_report(self, **extra): filters = frappe._dict({"items": "Enabled Items only", **extra}) - return execute(filters)[1] + return execute(filters)[:2] - def row_for(self, data, item_code): + # The report returns string-format columns ("Label:fieldtype:width"); resolve positions + # by label so the tests self-correct if the column order changes. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + + def row_for(self, columns, data, item_code): + item_idx = self.labels(columns).index("Item") for row in data: - if row[ITEM_CODE] == item_code: + if row[item_idx] == item_code: return row self.fail(f"No report row found for item {item_code}") + def cell(self, columns, row, label): + return row[self.labels(columns).index(label)] + def test_item_selling_price_listed(self): """A Standard Selling Item Price shows up in the Sales Price List column.""" item = "_Test Item" @@ -42,12 +45,13 @@ class TestItemPrices(ERPNextTestSuite): } ).insert() - row = self.row_for(self.run_report(), item) - self.assertIn("250.0", row[SALES_PRICE_LIST]) - self.assertIn("Standard Selling", row[SALES_PRICE_LIST]) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertIn("250.0", self.cell(columns, row, "Sales Price List")) + self.assertIn("Standard Selling", self.cell(columns, row, "Sales Price List")) # A selling price must not leak into the buying column. - self.assertNotIn("250.0", row[PURCHASE_PRICE_LIST] or "") - self.assertNotIn("Standard Selling", row[PURCHASE_PRICE_LIST] or "") + self.assertNotIn("250.0", self.cell(columns, row, "Purchase Price List") or "") + self.assertNotIn("Standard Selling", self.cell(columns, row, "Purchase Price List") or "") def test_item_buying_price_listed(self): """A Standard Buying Item Price shows up in the Purchase Price List column.""" @@ -61,30 +65,34 @@ class TestItemPrices(ERPNextTestSuite): } ).insert() - row = self.row_for(self.run_report(), item) - self.assertIn("175.0", row[PURCHASE_PRICE_LIST]) - self.assertIn("Standard Buying", row[PURCHASE_PRICE_LIST]) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertIn("175.0", self.cell(columns, row, "Purchase Price List")) + self.assertIn("Standard Buying", self.cell(columns, row, "Purchase Price List")) # A buying price must not leak into the selling column. - self.assertNotIn("175.0", row[SALES_PRICE_LIST] or "") - self.assertNotIn("Standard Buying", row[SALES_PRICE_LIST] or "") + self.assertNotIn("175.0", self.cell(columns, row, "Sales Price List") or "") + self.assertNotIn("Standard Buying", self.cell(columns, row, "Sales Price List") or "") def test_last_purchase_rate_from_receipt(self): """The latest purchase rate (from a Purchase Receipt) shows in the Last Purchase Rate column.""" - item = "_Test Item" + # a fresh item has no other committed purchase records, so it is the only (and latest) row + item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1}).name make_purchase_receipt( item_code=item, qty=5, rate=500, company="_Test Company", posting_date="2026-06-01" ) - row = self.row_for(self.run_report(), item) - self.assertEqual(row[LAST_PURCHASE_RATE], 500) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertEqual(self.cell(columns, row, "Last Purchase Rate"), 500) def test_valuation_rate_from_stock(self): """The Bin valuation rate shows in the Valuation Rate column.""" - # _Test FG Item has no opening-stock baseline, so its valuation reflects only this receipt - item = "_Test FG Item" + # a fresh item has no other committed bins, so its average valuation is exactly this receipt's rate + item = make_item(properties={"is_stock_item": 1}).name make_stock_entry( item_code=item, to_warehouse="Stores - _TC", qty=10, rate=250, posting_date="2026-06-01" ) - row = self.row_for(self.run_report(), item) - self.assertEqual(row[VALUATION_RATE], 250) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertEqual(self.cell(columns, row, "Valuation Rate"), 250) From beb2974317f5355783c2eab4f1076994371a7df0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:25:54 +0530 Subject: [PATCH 071/195] test: flag an unlinked (orphan) serial and batch bundle --- .../test_incorrect_serial_and_batch_bundle.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py index 806a8d24ad1..86f32077bea 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py @@ -56,3 +56,28 @@ class TestIncorrectSerialAndBatchBundle(ERPNextTestSuite): flagged_names.intersection(bundles), msg="Healthy serial/batch bundles should not be flagged as incorrect.", ) + + def test_unlinked_bundle_is_flagged(self): + # an actual incorrect state: a submitted Serial and Batch Bundle left without any linking + # Stock Ledger Entry (e.g. the SLE was purged but the bundle survived) + batch_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "ISBB-ORPHAN-.#####", + } + ).name + + entry = make_stock_entry( + item_code=batch_item, qty=5, rate=100, to_warehouse="Stores - _TC", posting_date="2026-06-01" + ) + bundle = frappe.db.get_value("Serial and Batch Bundle", {"voucher_no": entry.name}, "name") + self.assertTrue(bundle) + + # orphan the bundle: drop the Stock Ledger Entry that referenced it + frappe.db.delete("Stock Ledger Entry", {"serial_and_batch_bundle": bundle}) + + flagged = {row.get("name"): row for row in self.run_report()} + self.assertIn(bundle, flagged) + self.assertEqual(flagged[bundle]["is_cancelled"], 0) From 527765001c528d868dad129d22e716db1220a355 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:27:56 +0530 Subject: [PATCH 072/195] test: flag a serial with mismatched in/out valuation --- .../test_incorrect_serial_no_valuation.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py index 9015c06cd1d..81f736928fe 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -3,6 +3,7 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute from erpnext.tests.utils import ERPNextTestSuite @@ -53,3 +54,28 @@ class TestIncorrectSerialNoValuation(ERPNextTestSuite): self.assertEqual(len(data), 1) self.assertEqual(data[-1].get("qty"), 0) self.assertEqual(data[-1].get("valuation_rate"), 0) + + def test_mismatched_in_out_valuation_is_flagged(self): + # fresh serial item so only this test's serial movements are considered + item = make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "ISV-BAD-.#####"} + ).name + + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=1, rate=100, posting_date="2026-06-01") + serial_no = frappe.get_all("Serial No", filters={"item_code": item}, pluck="name")[0] + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=1, posting_date="2026-06-02") + + # corrupt the outgoing valuation so the serial's in (100) and out no longer cancel: + # net qty is 0 but a residual value remains, which the report must flag + frappe.db.set_value( + "Serial and Batch Entry", + {"serial_no": serial_no, "qty": ["<", 0]}, + "incoming_rate", + 60, + update_modified=False, + ) + + data = self.run_report(item_code=item) + + flagged_serials = {row.get("serial_no") for row in data if isinstance(row, dict)} + self.assertIn(serial_no, flagged_serials) From e4d6c0854ba2388146d7d97092381d38f159d76a Mon Sep 17 00:00:00 2001 From: ervishnucs Date: Wed, 1 Jul 2026 19:28:38 +0530 Subject: [PATCH 073/195] fix: remove redundant conversion_rate --- erpnext/selling/doctype/customer/mapper.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/customer/mapper.py b/erpnext/selling/doctype/customer/mapper.py index 6d1ed14662d..4f230702b6b 100644 --- a/erpnext/selling/doctype/customer/mapper.py +++ b/erpnext/selling/doctype/customer/mapper.py @@ -6,8 +6,6 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from erpnext.setup.utils import get_exchange_rate - @frappe.whitelist() def make_quotation(source_name: str, target_doc: str | Document | None = None): @@ -31,11 +29,7 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): target_doc.selling_price_list = price_list if currency: target_doc.currency = currency - company_currency = frappe.get_cached_value("Company", target_doc.company, "default_currency") - if target_doc.company and target_doc.currency != company_currency: - target_doc.conversion_rate = get_exchange_rate( - target_doc.currency, company_currency, target_doc.transaction_date, "for_selling" - ) + target_doc.run_method("set_missing_values") target_doc.run_method("set_other_charges") target_doc.run_method("calculate_taxes_and_totals") From 875fc72842bca81810f2997e28c70d33522457e2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:29:16 +0530 Subject: [PATCH 074/195] test: flag an SLE whose balance is out of sync with the FIFO queue --- ...eue_vs_qty_after_transaction_comparison.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py index 8f1b32f1209..f930edad2df 100644 --- a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py @@ -28,6 +28,29 @@ class TestFifoQueueVsQtyAfterTransactionComparison(ERPNextTestSuite): item_codes = [row.get("item_code") for row in data if row] self.assertNotIn(item, item_codes) + def test_queue_out_of_sync_is_flagged(self): + item = "_Test Item 2" + warehouse = "Stores - _TC" + frappe.db.set_value("Item", item, "valuation_method", "FIFO") + + entry = make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01" + ) + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": entry.name, "item_code": item, "warehouse": warehouse}, + "name", + ) + + # corrupt the running balance so it no longer matches the FIFO queue (the queue holds 10, + # but the stored qty_after_transaction now claims 7) + frappe.db.set_value("Stock Ledger Entry", sle, "qty_after_transaction", 7, update_modified=False) + + data = self.run_report({"company": "_Test Company", "item_code": item, "warehouse": warehouse}) + + flagged = {row.get("name") for row in data if row} + self.assertIn(sle, flagged) + def test_requires_a_filter(self): with self.assertRaises(frappe.ValidationError): self.run_report({"company": "_Test Company"}) From bd874d09ef974888391ebb8c42e8cc2449c206af Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:33:00 +0530 Subject: [PATCH 075/195] test: drop unused variable and apply ruff formatting --- .../test_item_wise_sales_history.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py index 5eb4468b710..4d599832573 100644 --- a/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py @@ -47,7 +47,9 @@ class TestItemWiseSalesHistory(ERPNextTestSuite): def test_date_range_filters_on_transaction_date(self): so = make_sales_order(transaction_date="2026-06-01") - in_range = {row["sales_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1]} + in_range = { + row["sales_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1] + } self.assertIn(so.name, in_range) out_of_range = { @@ -67,10 +69,12 @@ class TestItemWiseSalesHistory(ERPNextTestSuite): item_codes = {row["item_code"] for row in self.run_report(item_code="_Test Item 2")[1]} self.assertEqual(item_codes, {"_Test Item 2"}) # the filtered-out line of the same order must not leak in - self.assertTrue(all(row["sales_order"] == so.name for row in self.run_report(item_code="_Test Item 2")[1])) + self.assertTrue( + all(row["sales_order"] == so.name for row in self.run_report(item_code="_Test Item 2")[1]) + ) def test_customer_filter(self): - so = make_sales_order(customer="_Test Customer 1", transaction_date="2026-06-01") + make_sales_order(customer="_Test Customer 1", transaction_date="2026-06-01") make_sales_order(customer="_Test Customer 2", transaction_date="2026-06-01") customers = {row["customer"] for row in self.run_report(customer="_Test Customer 1")[1]} From 86eff053030a3c6ca7a7ed993bc8557625df90b7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:34:03 +0530 Subject: [PATCH 076/195] test: drop unused variable and apply ruff formatting --- .../test_item_wise_purchase_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py index 568fa1e5446..7399d30d2b9 100644 --- a/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py +++ b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py @@ -81,7 +81,7 @@ class TestItemWisePurchaseHistory(ERPNextTestSuite): self.assertNotIn(po_other_group.name, names) def test_supplier_filter(self): - po = create_purchase_order(supplier="_Test Supplier", transaction_date="2026-06-01") + create_purchase_order(supplier="_Test Supplier", transaction_date="2026-06-01") create_purchase_order(supplier="_Test Supplier 1", transaction_date="2026-06-01") suppliers = {row["supplier"] for row in self.run_report(supplier="_Test Supplier")[1]} From b52a8f5a771ca08ad5cbd201bed46562f78766da Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:34:29 +0530 Subject: [PATCH 077/195] test: apply ruff formatting --- .../test_budget_variance_report.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py index 8d404b1c994..de1fb541cb6 100644 --- a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py @@ -33,9 +33,7 @@ class TestBudgetVarianceReport(ERPNextTestSuite): return execute(filters)[1] def report_row(self, data, dimension, account=ACCOUNT): - return next( - row for row in data if row["budget_against"] == dimension and row["account"] == account - ) + return next(row for row in data if row["budget_against"] == dimension and row["account"] == account) def field(self, label): return frappe.scrub(f"{label} {self.fy}") @@ -57,7 +55,9 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertTrue(columns) def test_budget_amount_shown_with_zero_actual(self): - make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) row = self.report_row(self.run_report(), COST_CENTER) self.assertEqual(row[self.field("Budget")], 120000) @@ -65,7 +65,9 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertEqual(row[self.field("Variance")], 120000) def test_actual_expense_updates_actual_and_variance(self): - make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) # book an actual expense well within the annual budget so the "Stop" action does not block it make_journal_entry(ACCOUNT, "_Test Bank - _TC", 50000, cost_center=COST_CENTER, submit=True) @@ -74,7 +76,9 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertEqual(row[self.field("Variance")], 70000) # 120000 - 50000 def test_budget_against_filter_limits_dimensions(self): - make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) make_budget( budget_against="Cost Center", cost_center=COST_CENTER_2, budget_amount=80000, submit_budget=1 ) @@ -84,7 +88,9 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertEqual(dimensions, {COST_CENTER}) def test_monthly_period_totals(self): - make_budget(budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1) + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) make_journal_entry(ACCOUNT, "_Test Bank - _TC", 50000, cost_center=COST_CENTER, submit=True) row = self.report_row(self.run_report(period="Monthly"), COST_CENTER) From ea5be1f7a55c63a9522ea760f84bb648c37e8400 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 20:12:42 +0530 Subject: [PATCH 078/195] fix: minor fix Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- erpnext/stock/report/item_prices/test_item_prices.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index 147a3d4bc34..49e36c0429a 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -29,6 +29,7 @@ class TestItemPrices(ERPNextTestSuite): if row[item_idx] == item_code: return row self.fail(f"No report row found for item {item_code}") + return None def cell(self, columns, row, label): return row[self.labels(columns).index(label)] From f47141a3b7fb3cc97a67d88eb79c0ff53531d07c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 20:15:32 +0530 Subject: [PATCH 079/195] test: flag an actual balance-qty variance in Stock Ledger Variance --- .../test_stock_ledger_variance.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py index 2459204ca8f..1787a0a75e5 100644 --- a/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py @@ -81,3 +81,26 @@ class TestStockLedgerVariance(ERPNextTestSuite): # self-consistent ledger, so no variance rows are expected. data = self.run_report(item_code=item) self.assertFalse([row for row in data if row.get("item_code") == item]) + + def test_incorrect_balance_qty_is_flagged(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = "_Test Item 2" + warehouse = "Stores - _TC" + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") + + entry = make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01" + ) + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": entry.name, "item_code": item, "warehouse": warehouse}, + "name", + ) + + # corrupt the stored running balance (expected 10 from the receipt, but now claims 7) + frappe.db.set_value("Stock Ledger Entry", sle, "qty_after_transaction", 7, update_modified=False) + + data = self.run_report(item_code=item, difference_in="Qty") + row = next(r for r in data if r.get("item_code") == item) + self.assertEqual(row["difference_in_qty"], -3) # 7 (stored) - 10 (expected) From 8aec16376e1bb2d14feee5fb2869a63219778661 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:02:02 +0530 Subject: [PATCH 080/195] test: add coverage for Sales Invoice Trends report --- .../test_sales_invoice_trends.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py diff --git a/erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py b/erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py new file mode 100644 index 00000000000..98979b1cbda --- /dev/null +++ b/erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py @@ -0,0 +1,118 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.sales_invoice_trends.sales_invoice_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +FISCAL_YEAR = "_Test Fiscal Year 2026" +POSTING_DATE = "2026-06-01" + + +class TestSalesInvoiceTrends(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": FISCAL_YEAR, + "based_on": "Item", + "period": "Yearly", + } + ) + filters.update(extra) + columns, data = execute(filters) + labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + return labels, data + + def _cell(self, data, key_label, key_value, col_label, labels): + """Return the value at column `col_label` for the row whose first-column + value equals `key_value`, or 0 if that row does not exist yet.""" + key_idx = labels.index(key_label) + col_idx = labels.index(col_label) + for row in data: + if row[key_idx] == key_value: + return row[col_idx] or 0 + return 0 + + def test_yearly_item_amount_and_total(self): + # Yearly period => a single " (Qty)"/"(Amt)" bucket, plus Total(Qty)/Total(Amt). + labels, before = self.run_report() + qty_col = f"{FISCAL_YEAR} (Qty)" + amt_col = f"{FISCAL_YEAR} (Amt)" + before_qty = self._cell(before, "Item", "_Test Item", qty_col, labels) + before_amt = self._cell(before, "Item", "_Test Item", amt_col, labels) + before_tot_qty = self._cell(before, "Item", "_Test Item", "Total(Qty)", labels) + before_tot_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + create_sales_invoice(item="_Test Item", qty=4, rate=200, posting_date=POSTING_DATE) + + labels, after = self.run_report() + self.assertEqual(self._cell(after, "Item", "_Test Item", qty_col, labels) - before_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", amt_col, labels) - before_amt, 800) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Qty)", labels) - before_tot_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot_amt, 800) + + def test_monthly_lands_in_june_bucket(self): + # Monthly period => one bucket per month; a 2026-06-01 invoice hits "Jun (Qty)"/"(Amt)". + labels, before = self.run_report(period="Monthly") + before_qty = self._cell(before, "Item", "_Test Item", "Jun (Qty)", labels) + before_amt = self._cell(before, "Item", "_Test Item", "Jun (Amt)", labels) + before_tot = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + create_sales_invoice(item="_Test Item", qty=3, rate=100, posting_date=POSTING_DATE) + + labels, after = self.run_report(period="Monthly") + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Qty)", labels) - before_qty, 3) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Amt)", labels) - before_amt, 300) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot, 300) + # Nothing should leak into an unrelated month. + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jan (Amt)", labels), 0) + + def test_quarterly_lands_in_apr_jun_bucket(self): + # Quarterly period over a Jan-Dec fiscal year => Apr-Jun is the 2nd quarter; June lands there. + labels, before = self.run_report(period="Quarterly") + before_qty = self._cell(before, "Item", "_Test Item", "Apr-Jun (Qty)", labels) + before_amt = self._cell(before, "Item", "_Test Item", "Apr-Jun (Amt)", labels) + + create_sales_invoice(item="_Test Item", qty=5, rate=50, posting_date=POSTING_DATE) + + labels, after = self.run_report(period="Quarterly") + self.assertEqual(self._cell(after, "Item", "_Test Item", "Apr-Jun (Qty)", labels) - before_qty, 5) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Apr-Jun (Amt)", labels) - before_amt, 250) + # Jan-Mar quarter must stay untouched. + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jan-Mar (Amt)", labels), 0) + + def test_based_on_customer_total(self): + # based_on=Customer => first column is "Customer"; the customer's Total(Amt) reflects the sale. + labels, before = self.run_report(based_on="Customer") + before_tot_qty = self._cell(before, "Customer", "_Test Customer", "Total(Qty)", labels) + before_tot_amt = self._cell(before, "Customer", "_Test Customer", "Total(Amt)", labels) + + create_sales_invoice( + customer="_Test Customer", item="_Test Item", qty=2, rate=300, posting_date=POSTING_DATE + ) + + labels, after = self.run_report(based_on="Customer") + self.assertEqual( + self._cell(after, "Customer", "_Test Customer", "Total(Qty)", labels) - before_tot_qty, 2 + ) + self.assertEqual( + self._cell(after, "Customer", "_Test Customer", "Total(Amt)", labels) - before_tot_amt, 600 + ) + + def test_group_by_item_under_customer(self): + # based_on=Customer + group_by=Item inserts an "Item" breakdown column before the period + # buckets; the per-item detail row carries the item key and the amount for that customer/item. + labels, before = self.run_report(based_on="Customer", group_by="Item") + # In group_by mode the detail rows key off the group_by column ("Item"), so snapshot by item. + before_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + create_sales_invoice( + customer="_Test Customer", item="_Test Item", qty=6, rate=100, posting_date=POSTING_DATE + ) + + labels, after = self.run_report(based_on="Customer", group_by="Item") + self.assertIn("Item", labels) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_amt, 600) From cb9ea22b6f5aef5ebcda033e131f33c8da7473dd Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:02:12 +0530 Subject: [PATCH 081/195] test: add coverage for Purchase Invoice Trends report --- .../test_purchase_invoice_trends.py | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py diff --git a/erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py b/erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py new file mode 100644 index 00000000000..f07555f3b13 --- /dev/null +++ b/erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py @@ -0,0 +1,171 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.purchase_invoice_trends.purchase_invoice_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +FISCAL_YEAR = "_Test Fiscal Year 2026" +COMPANY = "_Test Company" +SUPPLIER = "_Test Supplier" +ITEM = "_Test Item" +POSTING_DATE = "2026-06-01" + + +def make_dated_purchase_invoice(qty, rate): + # make_purchase_invoice ignores posting_date unless posting time is explicitly set, so build the + # invoice unsubmitted, pin the posting date, then submit to land it in the intended period bucket. + pi = make_purchase_invoice( + supplier=SUPPLIER, item_code=ITEM, qty=qty, rate=rate, posting_date=POSTING_DATE, do_not_submit=1 + ) + pi.set_posting_time = 1 + pi.posting_date = POSTING_DATE + pi.submit() + return pi + + +class TestPurchaseInvoiceTrends(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": COMPANY, + "fiscal_year": FISCAL_YEAR, + "period": "Yearly", + "based_on": "Item", + } + ) + filters.update(extra) + columns, data = execute(filters) + labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + return labels, data + + @staticmethod + def _cell(labels, row, label): + return row[labels.index(label)] + + def _find_row(self, data, key): + for row in data: + if row and row[0] == key: + return row + return None + + def test_yearly_item_qty_and_amount(self): + labels_before, data_before = self.run_report() + before = self._find_row(data_before, ITEM) + + qty, rate = 4, 250 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report() + self.assertIn("Item", labels) + self.assertIn("Item Name", labels) + self.assertIn("Currency", labels) + self.assertIn("Total(Qty)", labels) + self.assertIn("Total(Amt)", labels) + # Yearly period bucket uses the fiscal year name as the label prefix + self.assertIn(f"{FISCAL_YEAR} (Qty)", labels) + self.assertIn(f"{FISCAL_YEAR} (Amt)", labels) + + row = self._find_row(data, ITEM) + self.assertIsNotNone(row) + + before_qty = self._cell(labels_before, before, f"{FISCAL_YEAR} (Qty)") if before else 0 + before_amt = self._cell(labels_before, before, f"{FISCAL_YEAR} (Amt)") if before else 0 + before_tqty = self._cell(labels_before, before, "Total(Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, f"{FISCAL_YEAR} (Qty)") - before_qty, qty) + self.assertEqual(self._cell(labels, row, f"{FISCAL_YEAR} (Amt)") - before_amt, qty * rate) + self.assertEqual(self._cell(labels, row, "Total(Qty)") - before_tqty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) + + def test_monthly_bucket(self): + labels_before, data_before = self.run_report(period="Monthly") + before = self._find_row(data_before, ITEM) + + qty, rate = 3, 100 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(period="Monthly") + # posting_date 2026-06-01 -> June bucket + self.assertIn("Jun (Qty)", labels) + self.assertIn("Jun (Amt)", labels) + + row = self._find_row(data, ITEM) + before_qty = self._cell(labels_before, before, "Jun (Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Jun (Qty)") - before_qty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) + + def test_quarterly_bucket(self): + labels_before, data_before = self.run_report(period="Quarterly") + before = self._find_row(data_before, ITEM) + + qty, rate = 2, 150 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(period="Quarterly") + # 2026-06-01 falls in the Apr-Jun quarter + self.assertIn("Apr-Jun (Qty)", labels) + self.assertIn("Apr-Jun (Amt)", labels) + + row = self._find_row(data, ITEM) + before_qty = self._cell(labels_before, before, "Apr-Jun (Qty)") if before else 0 + before_amt = self._cell(labels_before, before, "Apr-Jun (Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Apr-Jun (Qty)") - before_qty, qty) + self.assertEqual(self._cell(labels, row, "Apr-Jun (Amt)") - before_amt, qty * rate) + + def test_based_on_supplier(self): + labels_before, data_before = self.run_report(based_on="Supplier") + before = self._find_row(data_before, SUPPLIER) + + qty, rate = 5, 200 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(based_on="Supplier") + self.assertIn("Supplier", labels) + self.assertIn("Supplier Name", labels) + self.assertIn("Supplier Group", labels) + + row = self._find_row(data, SUPPLIER) + self.assertIsNotNone(row) + + before_tqty = self._cell(labels_before, before, "Total(Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Total(Qty)") - before_tqty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) + + def test_group_by_item_under_supplier(self): + labels_before, data_before = self.run_report(based_on="Supplier", group_by="Item") + # group_by inserts an "Item" column; the item breakdown row carries the item key there + item_idx = labels_before.index("Item") + before = None + for r in data_before: + if r and r[0] != SUPPLIER and r[item_idx] == ITEM: + before = r + break + + qty, rate = 6, 300 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(based_on="Supplier", group_by="Item") + self.assertIn("Item", labels) + + item_idx = labels.index("Item") + row = None + for r in data: + if r and r[0] != SUPPLIER and r[0] != "'Total'" and r[item_idx] == ITEM: + row = r + break + self.assertIsNotNone(row) + + before_tqty = self._cell(labels_before, before, "Total(Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Total(Qty)") - before_tqty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) From 9ec2945e6edb469c1da742e0925827f61108a32c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:02:21 +0530 Subject: [PATCH 082/195] test: add coverage for Invalid Ledger Entries report --- .../test_invalid_ledger_entries.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py diff --git a/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py new file mode 100644 index 00000000000..3ce9f48fff3 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py @@ -0,0 +1,144 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import qb + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.invalid_ledger_entries.invalid_ledger_entries import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestInvalidLedgerEntries(ERPNextTestSuite): + """Tests for the Invalid Ledger Entries integrity report. + + The report flags vouchers that still have *active* ledger entries + (GL Entry with is_cancelled=0 or Payment Ledger Entry with delinked=0) + in the given period, but whose source voucher document is no longer + submitted (docstatus != 1). Such orphaned ledgers indicate corruption. + """ + + def setUp(self): + self.company = "_Test Company" + self.debit_account = "_Test Bank - _TC" + self.credit_account = "_Test Cash - _TC" + self.from_date = "2026-01-01" + self.to_date = "2026-12-31" + self.posting_date = "2026-06-01" + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": self.company, + "from_date": self.from_date, + "to_date": self.to_date, + } + ) + filters.update(extra) + return execute(filters)[1] + + def make_submitted_jv(self): + return make_journal_entry( + self.debit_account, + self.credit_account, + amount=500, + posting_date=self.posting_date, + company=self.company, + submit=True, + ) + + def test_healthy_voucher_not_flagged(self): + """A normal balanced, submitted Journal Entry must NOT be flagged.""" + jv = self.make_submitted_jv() + + # It genuinely posted active GL entries, so it is in scope of the scan. + self.assertTrue( + frappe.db.exists( + "GL Entry", + {"voucher_no": jv.name, "is_cancelled": 0, "company": self.company}, + ) + ) + + flagged = {row.get("voucher_no") for row in self.run_report()} + self.assertNotIn(jv.name, flagged) + + def test_orphaned_gl_entries_flagged(self): + """A voucher whose document was set non-submitted while its GL entries + remain active (is_cancelled=0) must be flagged as invalid.""" + jv = self.make_submitted_jv() + + # Corrupt the state: mark the source document as cancelled (docstatus=2) + # without cancelling/removing its GL Entries. This is the exact orphaned + # ledger condition the report detects. + frappe.db.set_value("Journal Entry", jv.name, "docstatus", 2, update_modified=False) + + data = self.run_report() + + matching = [ + row + for row in data + if row.get("voucher_no") == jv.name and row.get("voucher_type") == "Journal Entry" + ] + self.assertEqual(len(matching), 1, "Orphaned voucher should be flagged exactly once") + self.assertEqual(matching[0]["voucher_type"], "Journal Entry") + self.assertEqual(matching[0]["voucher_no"], jv.name) + + def test_voucher_no_filter_scopes_scan(self): + """The voucher_no filter must restrict the scan to that voucher only.""" + orphan = self.make_submitted_jv() + other = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + frappe.db.set_value("Journal Entry", other.name, "docstatus", 2, update_modified=False) + + flagged = {row.get("voucher_no") for row in self.run_report(voucher_no=orphan.name)} + self.assertIn(orphan.name, flagged) + self.assertNotIn(other.name, flagged) + + def test_account_filter_scopes_scan(self): + """The account filter (a MultiSelectList, so a list) must restrict the + scan to vouchers touching one of the given accounts.""" + orphan = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + + # Filtering on an account the voucher touches -> flagged. + flagged = {row.get("voucher_no") for row in self.run_report(account=[self.debit_account])} + self.assertIn(orphan.name, flagged) + + # Filtering on an unrelated account -> not in scope. + unrelated = "Creditors - _TC" + flagged = {row.get("voucher_no") for row in self.run_report(account=[unrelated])} + self.assertNotIn(orphan.name, flagged) + + def test_period_filter_excludes_out_of_range(self): + """Vouchers posted outside the from/to window must not be scanned.""" + orphan = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + + flagged = { + row.get("voucher_no") for row in self.run_report(from_date="2025-01-01", to_date="2025-12-31") + } + self.assertNotIn(orphan.name, flagged) + + def test_cancelled_gl_entries_not_flagged(self): + """If the ledger entries are properly cancelled (is_cancelled=1), the + voucher is out of scope even when its document is non-submitted.""" + jv = self.make_submitted_jv() + + gle = qb.DocType("GL Entry") + qb.update(gle).set(gle.is_cancelled, 1).where(gle.voucher_no == jv.name).run() + frappe.db.set_value("Journal Entry", jv.name, "docstatus", 2, update_modified=False) + + flagged = {row.get("voucher_no") for row in self.run_report()} + self.assertNotIn(jv.name, flagged) + + def test_missing_filters_raises(self): + """validate_filters must guard mandatory inputs.""" + self.assertRaises(frappe.ValidationError, execute, None) + + bad = frappe._dict({"from_date": self.from_date, "to_date": self.to_date}) + self.assertRaises(frappe.ValidationError, execute, bad) + + reversed_dates = frappe._dict( + {"company": self.company, "from_date": self.to_date, "to_date": self.from_date} + ) + self.assertRaises(frappe.ValidationError, execute, reversed_dates) From 38ebfd7bd67d2f9bb29cb96bfe233657221c0353 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:02:29 +0530 Subject: [PATCH 083/195] test: add coverage for Voucher-wise Balance report --- .../test_voucher_wise_balance.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py diff --git a/erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py new file mode 100644 index 00000000000..be04141e4b6 --- /dev/null +++ b/erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py @@ -0,0 +1,63 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.voucher_wise_balance.voucher_wise_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestVoucherWiseBalance(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + } + ) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, voucher_no): + for row in data: + if row.get("voucher_no") == voucher_no: + return row + return None + + def test_balanced_voucher_not_flagged(self): + jv = make_journal_entry( + "Sales - _TC", "_Test Bank - _TC", 1000, submit=True, posting_date="2026-06-01" + ) + + data = self.run_report() + self.assertIsNone( + self.find_row(data, jv.name), + msg="A balanced voucher (debit == credit) must not be flagged.", + ) + + def test_imbalanced_voucher_flagged(self): + jv = make_journal_entry( + "Sales - _TC", "_Test Bank - _TC", 1000, submit=True, posting_date="2026-06-01" + ) + + # Tamper one GL Entry: drop the debit side so debit != credit for this voucher. + gle_name = frappe.db.get_value( + "GL Entry", + {"voucher_no": jv.name, "is_cancelled": 0, "debit": [">", 0]}, + "name", + ) + self.assertIsNotNone(gle_name, msg="Expected a debit GL Entry for the journal entry.") + frappe.db.set_value("GL Entry", gle_name, {"debit": 400, "debit_in_account_currency": 400}) + + data = self.run_report() + row = self.find_row(data, jv.name) + self.assertIsNotNone(row, msg="An imbalanced voucher must be flagged by the report.") + + self.assertEqual(row.get("voucher_type"), "Journal Entry") + self.assertEqual(row.get("credit"), 1000) + self.assertEqual(row.get("debit"), 400) + self.assertNotEqual( + row.get("debit"), row.get("credit"), msg="Flagged rows must have debit != credit." + ) From 293f737e4a1af320c61b31a2ecf90874b282c198 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:02:36 +0530 Subject: [PATCH 084/195] test: add coverage for Calculated Discount Mismatch report --- .../test_calculated_discount_mismatch.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py diff --git a/erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py new file mode 100644 index 00000000000..dd4386d4afd --- /dev/null +++ b/erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py @@ -0,0 +1,82 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import json + +import frappe +from frappe.utils.formatters import format_value + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCalculatedDiscountMismatch(ERPNextTestSuite): + """Integrity detector: flag transactions whose stored ``discount_amount`` was tampered + after the fact (a Version records the change) while ``additional_discount_percentage`` + stayed the same, so the stored amount no longer matches the percentage-derived value. + """ + + def run_report(self, docname: str) -> dict | None: + """Run the (filter-less) report and return the row for ``docname``, if any.""" + _columns, data = execute(frappe._dict({})) + return next((row for row in data if row["docname"] == docname), None) + + def create_discounted_invoice(self) -> "frappe.Document": + """Draft Sales Invoice (rate 1000) with a 10% additional discount. + + The controller derives ``discount_amount`` = 10% of the grand total = 100.00, + so the stored amount is consistent with the percentage. + """ + invoice = create_sales_invoice(rate=1000, qty=1, do_not_submit=1) + invoice.additional_discount_percentage = 10 + invoice.save() + invoice.reload() + return invoice + + def test_consistent_discount_is_not_flagged(self): + """A submitted invoice whose discount_amount matches its percentage is not reported.""" + invoice = self.create_discounted_invoice() + invoice.submit() + invoice.reload() + + self.assertEqual(invoice.discount_amount, 100.0) + self.assertIsNone(self.run_report(invoice.name)) + + def test_tampered_discount_is_flagged(self): + """Directly overwriting discount_amount (leaving the percentage intact) is reported. + + This reproduces the real-world integrity breach: a Version records the + ``discount_amount`` change, its ``new`` value equals the current stored amount, and + ``additional_discount_percentage`` was not touched -- exactly the shape the report + queries for. + """ + invoice = self.create_discounted_invoice() + consistent_amount = invoice.discount_amount # 100.00, matches the 10% percentage + tampered_amount = 250.0 + + discount_field = frappe.get_meta("Sales Invoice").get_field("discount_amount") + # Format exactly as the report does so version.new == format_value(current amount). + suspected = format_value(consistent_amount, df=discount_field, currency=invoice.currency) + actual = format_value(tampered_amount, df=discount_field, currency=invoice.currency) + + # Tamper the stored amount directly, bypassing the controller that would recompute it. + frappe.db.set_value("Sales Invoice", invoice.name, "discount_amount", tampered_amount) + self.record_discount_change(invoice.name, suspected, actual) + + row = self.run_report(invoice.name) + + self.assertIsNotNone(row) + self.assertEqual(row["doctype"], "Sales Invoice") + self.assertEqual(row["actual_discount_percentage"], 10.0) + self.assertEqual(row["actual_discount_amount"], actual) + self.assertEqual(row["suspected_discount_amount"], suspected) + + def record_discount_change(self, docname: str, old: str, new: str) -> None: + """Insert the Version audit row a direct discount_amount edit would have produced.""" + version = frappe.new_doc("Version") + version.ref_doctype = "Sales Invoice" + version.docname = docname + version.data = json.dumps({"changed": [["discount_amount", old, new]]}, separators=(",", ":")) + version.flags.ignore_version = True + version.insert(ignore_permissions=True) From 237605889fa6d4208806d531fc31eaad86d499a4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:07:21 +0530 Subject: [PATCH 085/195] test: add coverage for Received Items To Be Billed report --- .../test_received_items_to_be_billed.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py diff --git a/erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py new file mode 100644 index 00000000000..e24dff96f8e --- /dev/null +++ b/erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py @@ -0,0 +1,101 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.received_items_to_be_billed.received_items_to_be_billed import execute +from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice as make_pi_from_pr +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.tests.utils import ERPNextTestSuite + + +class TestReceivedItemsToBeBilled(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "posting_date": "2026-06-30", + } + ) + filters.update(extra) + return execute(filters)[1] + + def get_row(self, data, purchase_receipt): + matches = [row for row in data if row.get("name") == purchase_receipt] + return matches[0] if matches else None + + def test_unbilled_receipt_appears_with_pending_amount(self): + pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + + row = self.get_row(self.run_report(), pr.name) + + self.assertIsNotNone(row, "Unbilled Purchase Receipt should appear in the report") + self.assertEqual(row.get("supplier"), "_Test Supplier") + self.assertEqual(row.get("item_code"), "_Test Item") + self.assertEqual(row.get("amount"), 1000.0) + self.assertEqual(row.get("billed_amount"), 0.0) + self.assertEqual(row.get("returned_amount"), 0.0) + self.assertEqual(row.get("pending_amount"), 1000.0) + + def test_billed_receipt_drops_out_of_report(self): + pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + + self.assertIsNotNone(self.get_row(self.run_report(), pr.name)) + + pi = make_pi_from_pr(pr.name) + pi.set_posting_time = 1 + pi.posting_date = "2026-06-02" + pi.submit() + + self.assertIsNone( + self.get_row(self.run_report(), pr.name), + "Fully billed Purchase Receipt should no longer appear in the report", + ) + + def test_reference_field_filter_limits_to_single_receipt(self): + first_pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + second_pr = make_purchase_receipt( + item_code="_Test Item", + qty=3, + rate=100, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + + data = self.run_report(purchase_receipt=first_pr.name) + + self.assertIsNotNone(self.get_row(data, first_pr.name)) + self.assertIsNone(self.get_row(data, second_pr.name)) + + def test_posting_date_cutoff_excludes_later_receipts(self): + pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-15", + ) + + self.assertIsNone( + self.get_row(self.run_report(posting_date="2026-06-01"), pr.name), + "Receipt dated after the cutoff should be excluded", + ) + self.assertIsNotNone(self.get_row(self.run_report(posting_date="2026-06-30"), pr.name)) From 196482348d487da0fe4d47219b6abaafe66715f5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:07:31 +0530 Subject: [PATCH 086/195] test: add coverage for Delivered Items To Be Billed report --- .../test_delivered_items_to_be_billed.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py new file mode 100644 index 00000000000..acdce19c105 --- /dev/null +++ b/erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py @@ -0,0 +1,89 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.delivered_items_to_be_billed.delivered_items_to_be_billed import execute +from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDeliveredItemsToBeBilled(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "posting_date": "2026-06-30", + } + ) + filters.update(extra) + return execute(filters)[1] + + def stock_up_item(self): + make_stock_entry( + item_code="_Test Item", + target="Stores - _TC", + qty=20, + basic_rate=100, + posting_date="2026-05-25", + ) + + def test_unbilled_delivery_note_appears(self): + self.stock_up_item() + dn = create_delivery_note( + item_code="_Test Item", + warehouse="Stores - _TC", + qty=5, + rate=300, + customer="_Test Customer", + posting_date="2026-06-01", + ) + + rows = self.run_report(delivery_note=dn.name) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row.name, dn.name) + self.assertEqual(row.customer, "_Test Customer") + self.assertEqual(row.item_code, "_Test Item") + self.assertEqual(row.amount, 1500) + self.assertEqual(row.billed_amount, 0) + self.assertEqual(row.returned_amount, 0) + self.assertEqual(row.pending_amount, 1500) + + def test_fully_billed_delivery_note_drops_out(self): + self.stock_up_item() + dn = create_delivery_note( + item_code="_Test Item", + warehouse="Stores - _TC", + qty=5, + rate=300, + customer="_Test Customer", + posting_date="2026-06-01", + ) + + self.assertEqual(len(self.run_report(delivery_note=dn.name)), 1) + + si = make_sales_invoice(dn.name) + si.posting_date = "2026-06-02" + si.set_posting_time = 1 + si.insert() + si.submit() + + self.assertEqual(self.run_report(delivery_note=dn.name), []) + + def test_date_filter_excludes_later_delivery_notes(self): + self.stock_up_item() + dn = create_delivery_note( + item_code="_Test Item", + warehouse="Stores - _TC", + qty=5, + rate=300, + customer="_Test Customer", + posting_date="2026-07-15", + ) + + rows = self.run_report(delivery_note=dn.name, posting_date="2026-06-30") + self.assertEqual(rows, []) From cccfdc72c9f49785b2feae35ee9fc8fe8541753d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:07:45 +0530 Subject: [PATCH 087/195] test: add coverage for Billed Items To Be Received report --- .../test_billed_items_to_be_received.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py diff --git a/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py new file mode 100644 index 00000000000..1112a9f7403 --- /dev/null +++ b/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py @@ -0,0 +1,89 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.billed_items_to_be_received.billed_items_to_be_received import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBilledItemsToBeReceived(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "posting_date": today(), + } + ) + filters.update(extra) + return execute(filters)[1] + + def get_rows_for(self, data, pi_name): + return [row for row in data if row.get("name") == pi_name] + + def test_billed_but_not_received_item_appears(self): + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=0, + ) + + rows = self.get_rows_for(self.run_report(), pi.name) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row.get("supplier"), "_Test Supplier") + self.assertEqual(row.get("company"), "_Test Company") + self.assertEqual(row.get("item_code"), "_Test Item") + self.assertEqual(row.get("qty"), 5) + self.assertEqual(row.get("received_qty"), 0) + self.assertEqual(row.get("rate"), 200) + self.assertEqual(row.get("amount"), 1000) + + def test_stock_updating_invoice_is_excluded(self): + """update_stock=1 means the item is already received; it must not appear.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=1, + ) + + rows = self.get_rows_for(self.run_report(), pi.name) + self.assertEqual(len(rows), 0) + + def test_fully_received_invoice_drops_off(self): + """When per_received reaches 100 the invoice is fully received and drops off.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=0, + ) + + # Present while nothing has been received. + self.assertEqual(len(self.get_rows_for(self.run_report(), pi.name)), 1) + + frappe.db.set_value("Purchase Invoice", pi.name, "per_received", 100) + + # Absent once fully received. + self.assertEqual(len(self.get_rows_for(self.run_report(), pi.name)), 0) + + def test_posting_date_upper_bound_filter(self): + """A PI posted after the filter's posting_date must be excluded.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=0, + ) + + rows = self.get_rows_for(self.run_report(posting_date="2000-01-01"), pi.name) + self.assertEqual(len(rows), 0) From ee8e6e806f4c73e17e7219ee0e7cd0d935a7ac12 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:07:56 +0530 Subject: [PATCH 088/195] test: add coverage for Payment Period Based On Invoice Date report --- ...st_payment_period_based_on_invoice_date.py | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py new file mode 100644 index 00000000000..a2d3953938e --- /dev/null +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py @@ -0,0 +1,127 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, getdate, nowdate + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.payment_period_based_on_invoice_date.payment_period_based_on_invoice_date import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): + """Depth tests for the Payment Period Based On Invoice Date report. + + The report lists Payment Ledger Entries against invoices and buckets the paid + amount by the invoice's age into ranges: range1 (0-30), range2 (30-60), + range3 (60-90), range4 (90 Above). The age is measured from the invoice + posting date up to today, so invoice posting dates are placed relative to + ``nowdate()`` to land deterministically in a given bucket. + """ + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "payment_type": "Incoming", + "party_type": "Customer", + "from_date": add_days(nowdate(), -180), + "to_date": nowdate(), + } + ) + filters.update(extra) + return execute(filters) + + def find_payment_row(self, data, payment_name): + # Row shape (positional): payment_document, payment_entry(voucher_no), + # party_type, party, posting_date, invoice(against_voucher_no), + # invoice_posting_date, due_date, amount, remarks, age, + # range1, range2, range3, range4, [delay_in_payment] + for row in data: + if row[1] == payment_name: + return row + return None + + def pay_invoice(self, invoice): + pe = get_payment_entry("Sales Invoice", invoice.name) + pe.posting_date = nowdate() + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.submit() + return pe + + def test_paid_amount_lands_in_0_30_bucket(self): + invoice = create_sales_invoice( + customer="_Test Customer", + rate=1000, + posting_date=add_days(nowdate(), -15), + ) + payment = self.pay_invoice(invoice) + + columns, data = self.run_report() + + row = self.find_payment_row(data, payment.name) + self.assertIsNotNone(row, "Payment row not found in report output") + + # Positional assertions on the row shape. + self.assertEqual(row[2], "Customer") + self.assertEqual(row[4], getdate(nowdate())) # payment posting date + self.assertEqual(row[5], invoice.name) # against invoice + self.assertEqual(row[6], getdate(add_days(nowdate(), -15))) # invoice posting date + self.assertEqual(row[8], 1000) # amount + + # Buckets: 0-30 filled, others empty. + self.assertEqual(row[11], 1000) # range1 (0-30) + self.assertEqual(row[12], 0) # range2 (30-60) + self.assertEqual(row[13], 0) # range3 (60-90) + self.assertEqual(row[14], 0) # range4 (90 Above) + + def test_paid_amount_lands_in_30_60_bucket(self): + invoice = create_sales_invoice( + customer="_Test Customer 1", + rate=1000, + posting_date=add_days(nowdate(), -45), + ) + payment = self.pay_invoice(invoice) + + columns, data = self.run_report() + + row = self.find_payment_row(data, payment.name) + self.assertIsNotNone(row, "Payment row not found in report output") + + self.assertEqual(row[8], 1000) # amount + # Buckets: 30-60 filled, others empty. + self.assertEqual(row[11], 0) # range1 (0-30) + self.assertEqual(row[12], 1000) # range2 (30-60) + self.assertEqual(row[13], 0) # range3 (60-90) + self.assertEqual(row[14], 0) # range4 (90 Above) + + def test_columns_expose_expected_age_buckets(self): + columns, _data = self.run_report() + labels_by_fieldname = {c["fieldname"]: c["label"] for c in columns} + self.assertEqual(labels_by_fieldname["range1"], "0-30") + self.assertEqual(labels_by_fieldname["range2"], "30-60") + self.assertEqual(labels_by_fieldname["range3"], "60-90") + self.assertEqual(labels_by_fieldname["range4"], "90 Above") + # Sales Invoice link for Incoming payments. + invoice_col = next(c for c in columns if c["fieldname"] == "invoice") + self.assertEqual(invoice_col["options"], "Sales Invoice") + + def test_invalid_payment_type_party_type_combo_throws(self): + # Incoming + Supplier is invalid. + self.assertRaises( + frappe.ValidationError, + self.run_report, + payment_type="Incoming", + party_type="Supplier", + ) + # Outgoing + Customer is invalid. + self.assertRaises( + frappe.ValidationError, + self.run_report, + payment_type="Outgoing", + party_type="Customer", + ) From 2a1461c754d3b9a8301d4345d02ed8654432bc3f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:08:05 +0530 Subject: [PATCH 089/195] test: add coverage for Bank Clearance Summary report --- .../test_bank_clearance_summary.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py diff --git a/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py new file mode 100644 index 00000000000..c7781269f86 --- /dev/null +++ b/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.bank_clearance_summary.bank_clearance_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + +BANK_ACCOUNT = "_Test Bank - _TC" + + +class TestBankClearanceSummary(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "account": BANK_ACCOUNT, + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + } + ) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, payment_entry): + for row in data: + if row[1] == payment_entry: + return row + return None + + def test_uncleared_then_cleared_journal_entry(self): + je = make_journal_entry(BANK_ACCOUNT, "Sales - _TC", 5000, submit=True, posting_date="2026-06-01") + + # Uncleared: the bank row appears with the debit amount and no clearance date + row = self.find_row(self.run_report(), je.name) + self.assertIsNotNone(row, "Journal Entry not listed in Bank Clearance Summary") + self.assertEqual(row[0], "Journal Entry") + self.assertEqual(frappe.utils.getdate(row[2]), frappe.utils.getdate("2026-06-01")) + self.assertEqual(row[4], None) # clearance_date empty -> uncleared + self.assertEqual(row[5], "Sales - _TC") # against account + self.assertEqual(row[6], 5000) # debit - credit on the bank account + + # Cleared: set the clearance date on the Journal Entry and re-run + frappe.db.set_value("Journal Entry", je.name, "clearance_date", "2026-06-05") + + row = self.find_row(self.run_report(), je.name) + self.assertIsNotNone(row) + self.assertEqual(frappe.utils.getdate(row[4]), frappe.utils.getdate("2026-06-05")) + self.assertEqual(row[6], 5000) + + def test_date_filter_excludes_out_of_range_entries(self): + je = make_journal_entry(BANK_ACCOUNT, "Sales - _TC", 3000, submit=True, posting_date="2026-06-10") + + # Within range: present + self.assertIsNotNone(self.find_row(self.run_report(), je.name)) + + # Narrow window after the posting date: excluded + data = self.run_report(from_date="2026-07-01", to_date="2026-12-31") + self.assertIsNone(self.find_row(data, je.name)) From 02460b46847a55628d287f5e1769ad6ae48bbddf Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:13:02 +0530 Subject: [PATCH 090/195] test: add coverage for Share Balance report --- .../share_balance/test_share_balance.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 erpnext/accounts/report/share_balance/test_share_balance.py diff --git a/erpnext/accounts/report/share_balance/test_share_balance.py b/erpnext/accounts/report/share_balance/test_share_balance.py new file mode 100644 index 00000000000..c118442d239 --- /dev/null +++ b/erpnext/accounts/report/share_balance/test_share_balance.py @@ -0,0 +1,153 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.share_balance.share_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" + + +class TestShareBalanceReport(ERPNextTestSuite): + def setUp(self): + self.share_type = create_share_type("_Test Share Balance Equity") + self.shareholder = create_shareholder("_Test Share Balance Holder", COMPANY) + + def test_date_filter_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({"shareholder": self.shareholder})) + + def test_no_shareholder_returns_empty_data(self): + # `shareholder` is optional; without it the report yields no rows. + columns, data = execute(frappe._dict({"date": "2026-06-01", "company": COMPANY})) + self.assertEqual(data, []) + self.assertEqual(len(columns), 5) + + def test_balance_after_issue(self): + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + + row = self.get_row(date="2026-06-05") + self.assertEqual(row[0], self.shareholder) + self.assertEqual(row[1], self.share_type) + self.assertEqual(row[2], 100) # no_of_shares + self.assertEqual(row[3], 10) # average rate + self.assertEqual(row[4], 1000) # amount = 100 * 10 + + def test_balance_increases_on_second_issue(self): + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=101, + to_no=200, + no_of_shares=100, + rate=20, + date="2026-06-10", + ) + + # The report groups by share type, summing shares and amount and + # recomputing the average rate: (1000 + 2000) / 200 = 15. + row = self.get_row(date="2026-06-15") + self.assertEqual(row[2], 200) + self.assertEqual(row[3], 15) + self.assertEqual(row[4], 3000) + + def test_balance_reduces_after_transfer_out(self): + other_holder = create_shareholder("_Test Share Balance Holder 2", COMPANY) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Transfer", + from_shareholder=self.shareholder, + to_shareholder=other_holder, + share_type=self.share_type, + from_no=1, + to_no=40, + no_of_shares=40, + rate=10, + date="2026-06-10", + ) + + row = self.get_row(date="2026-06-15") + self.assertEqual(row[2], 60) # 100 issued - 40 transferred out + self.assertEqual(row[4], 600) + + other_row = self.get_row(date="2026-06-15", shareholder=other_holder) + self.assertEqual(other_row[2], 40) + self.assertEqual(other_row[4], 400) + + def test_report_reflects_current_balance_regardless_of_date(self): + # NOTE: the report reads the Shareholder's current `share_balance` table + # and does NOT actually filter by the `date` value (it is required but + # unused in the computation). So an as-on date before the issue still + # shows the current holding rather than 0. This asserts the real + # behavior and flags the missing date-based filtering. + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + + row = self.get_row(date="2020-01-01") # long before the issue + self.assertEqual(row[2], 100) + + def get_row(self, date, shareholder=None): + filters = frappe._dict( + {"date": date, "company": COMPANY, "shareholder": shareholder or self.shareholder} + ) + data = execute(filters)[1] + holdings = [r for r in data if r[1] == self.share_type] + self.assertEqual(len(holdings), 1, f"Expected one row for share type, got: {data}") + return holdings[0] + + +def create_share_type(title): + if not frappe.db.exists("Share Type", title): + frappe.get_doc({"doctype": "Share Type", "title": title}).insert() + return title + + +def create_shareholder(title, company): + shareholder = frappe.get_doc({"doctype": "Shareholder", "title": title, "company": company}).insert() + return shareholder.name + + +def create_share_transfer(**kwargs): + kwargs.setdefault("company", COMPANY) + kwargs.setdefault("asset_account", "Cash - _TC") + kwargs.setdefault("equity_or_liability_account", "Creditors - _TC") + transfer = frappe.get_doc({"doctype": "Share Transfer", **kwargs}) + transfer.submit() + return transfer From 8e560f1d1c70d92ce656c6ce1db3e422a3f15664 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:13:12 +0530 Subject: [PATCH 091/195] test: add coverage for Share Ledger report --- .../report/share_ledger/test_share_ledger.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 erpnext/accounts/report/share_ledger/test_share_ledger.py diff --git a/erpnext/accounts/report/share_ledger/test_share_ledger.py b/erpnext/accounts/report/share_ledger/test_share_ledger.py new file mode 100644 index 00000000000..f7a96c06dae --- /dev/null +++ b/erpnext/accounts/report/share_ledger/test_share_ledger.py @@ -0,0 +1,98 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.share_ledger.share_ledger import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" + + +class TestShareLedger(ERPNextTestSuite): + def setUp(self): + self.shareholder = self.create_shareholder("_Test Share Ledger Holder") + # Issue 100 shares on 2026-06-01, then another 50 on 2026-06-10. + self.first = self.issue_shares(date="2026-06-01", from_no=1, to_no=100, rate=10) + self.second = self.issue_shares(date="2026-06-10", from_no=101, to_no=150, rate=12) + + def test_ledger_lists_all_transfers_upto_date(self): + data = self.run_report(shareholder=self.shareholder, date="2026-06-30") + + self.assertEqual(len(data), 2) + + first_row, second_row = data + self.assertEqual(first_row[0], self.shareholder) + self.assertEqual(first_row[1], frappe.utils.getdate("2026-06-01")) + self.assertEqual(first_row[2], "Issue") + self.assertEqual(first_row[3], "Equity") + self.assertEqual(first_row[4], 100) + self.assertEqual(first_row[5], 10) + self.assertEqual(first_row[6], 1000) + self.assertEqual(first_row[7], COMPANY) + self.assertEqual(first_row[8], self.first) + + self.assertEqual(second_row[1], frappe.utils.getdate("2026-06-10")) + self.assertEqual(second_row[4], 50) + self.assertEqual(second_row[5], 12) + self.assertEqual(second_row[6], 600) + self.assertEqual(second_row[8], self.second) + + def test_running_balance_of_shares(self): + data = self.run_report(shareholder=self.shareholder, date="2026-06-30") + + running = 0 + balances = [] + for row in data: + running += row[4] + balances.append(running) + + self.assertEqual(balances, [100, 150]) + + def test_as_on_date_between_transfers_shows_only_first(self): + data = self.run_report(shareholder=self.shareholder, date="2026-06-05") + + self.assertEqual(len(data), 1) + self.assertEqual(data[0][8], self.first) + self.assertEqual(data[0][4], 100) + + def test_missing_date_throws(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict(shareholder=self.shareholder)) + + def test_missing_shareholder_returns_no_rows(self): + data = self.run_report(date="2026-06-30") + self.assertEqual(data, []) + + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, **extra}) + return execute(filters)[1] + + def create_shareholder(self, title): + doc = frappe.get_doc( + { + "doctype": "Shareholder", + "title": title, + "company": COMPANY, + } + ).insert() + return doc.name + + def issue_shares(self, date, from_no, to_no, rate): + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Issue", + "date": date, + "to_shareholder": self.shareholder, + "share_type": "Equity", + "from_no": from_no, + "to_no": to_no, + "no_of_shares": to_no - from_no + 1, + "rate": rate, + "company": COMPANY, + "asset_account": "Cash - _TC", + "equity_or_liability_account": "Creditors - _TC", + } + ) + doc.submit() + return doc.name From 2c3285286c4976ed2f60bfbfb654ba57d31c2e84 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:13:21 +0530 Subject: [PATCH 092/195] test: add coverage for Consolidated Financial Statement report --- .../test_consolidated_financial_statement.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py new file mode 100644 index 00000000000..c928a82887e --- /dev/null +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -0,0 +1,116 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt, today + +from erpnext.accounts.report.consolidated_financial_statement.consolidated_financial_statement import ( + execute, +) +from erpnext.accounts.utils import get_fiscal_year +from erpnext.tests.utils import ERPNextTestSuite + +PARENT_COMPANY = "Parent Group Company India" +CHILD_COMPANY = "Child Company India" + + +class TestConsolidatedFinancialStatement(ERPNextTestSuite): + """Consolidation is exercised via the bootstrap group of companies + (`Parent Group Company India` with child `Child Company India`). Income and + expense posted in the child company must surface in the report that is run + for the parent (group) company.""" + + def setUp(self): + self.fiscal_year = get_fiscal_year(today(), company=PARENT_COMPANY)[0] + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": PARENT_COMPANY, + "filter_based_on": "Fiscal Year", + "from_fiscal_year": self.fiscal_year, + "to_fiscal_year": self.fiscal_year, + "periodicity": "Yearly", + "include_default_book_entries": 1, + } + ) + filters.update(extra) + return execute(filters)[1] + + def post_journal_entry(self, debit_account, credit_account, amount): + je = frappe.new_doc("Journal Entry") + je.posting_date = today() + je.company = CHILD_COMPANY + je.set( + "accounts", + [ + {"account": debit_account, "debit_in_account_currency": amount}, + {"account": credit_account, "credit_in_account_currency": amount}, + ], + ) + je.save() + je.submit() + return je + + def get_row(self, data, account_name_fragment): + for row in data: + if account_name_fragment in str(row.get("account_name") or ""): + return row + return None + + def test_profit_and_loss_reflects_child_company_income(self): + amount = 7000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) + + self.assertTrue(data, "Report returned no rows") + + # child's Sales account is mapped onto the parent chart (Sales - PGCI) + sales_row = self.get_row(data, "Sales") + self.assertIsNotNone(sales_row, "Sales row missing from consolidated P&L") + self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + + total_income_row = self.get_row(data, "Total Income (Credit)") + self.assertIsNotNone(total_income_row, "Total Income row missing") + self.assertGreaterEqual(flt(total_income_row.get("total")), amount) + + def test_profit_and_loss_reflects_child_company_expense(self): + amount = 3000 + self.post_journal_entry("Marketing Expenses - CCI", "Cash - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) + + expense_row = self.get_row(data, "Marketing Expenses") + self.assertIsNotNone(expense_row, "Marketing Expenses row missing from consolidated P&L") + self.assertEqual(flt(expense_row.get(CHILD_COMPANY)), amount) + + total_expense_row = self.get_row(data, "Total Expense (Debit)") + self.assertIsNotNone(total_expense_row, "Total Expense row missing") + self.assertGreaterEqual(flt(total_expense_row.get("total")), amount) + + def test_accumulated_in_group_company_rolls_up_to_parent(self): + """With `accumulated_in_group_company`, the child's amount is also + accumulated into the parent company column.""" + amount = 5000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=1) + + sales_row = self.get_row(data, "Sales") + self.assertIsNotNone(sales_row) + self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + # parent column picks up the child value when accumulated + self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), amount) + + def test_balance_sheet_executes_and_returns_rows(self): + # posting income leaves a balancing entry in the child's Cash (Asset) account + amount = 4000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Balance Sheet", accumulated_in_group_company=0) + + self.assertTrue(data, "Balance Sheet returned no rows") + cash_row = self.get_row(data, "Cash") + self.assertIsNotNone(cash_row, "Cash asset row missing from consolidated Balance Sheet") + self.assertGreaterEqual(flt(cash_row.get(CHILD_COMPANY)), amount) From f4088d48a11a35d3d6d62077afb9b3f05b070ec1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:16:20 +0530 Subject: [PATCH 093/195] fix: accept a scalar account filter in Invalid Ledger Entries report --- .../invalid_ledger_entries/invalid_ledger_entries.py | 3 ++- .../invalid_ledger_entries/test_invalid_ledger_entries.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py index 33fda705cf2..2ba0a7fb4ce 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -84,7 +84,8 @@ def build_query_filters(filters: dict | None = None) -> list: qb_filters = [] if filters: if filters.account: - qb_filters.append(qb.Field("account").isin(filters.account)) + accounts = filters.account if isinstance(filters.account, list | tuple) else [filters.account] + qb_filters.append(qb.Field("account").isin(accounts)) if filters.voucher_no: qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no)) diff --git a/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py index 3ce9f48fff3..9a193dc0779 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py @@ -109,6 +109,14 @@ class TestInvalidLedgerEntries(ERPNextTestSuite): flagged = {row.get("voucher_no") for row in self.run_report(account=[unrelated])} self.assertNotIn(orphan.name, flagged) + def test_account_filter_accepts_a_scalar(self): + """A scalar (non-list) account filter must not crash the query.""" + orphan = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + + flagged = {row.get("voucher_no") for row in self.run_report(account=self.debit_account)} + self.assertIn(orphan.name, flagged) + def test_period_filter_excludes_out_of_range(self): """Vouchers posted outside the from/to window must not be scanned.""" orphan = self.make_submitted_jv() From e9aac239131843015f6b89a9dcec193b07143b7e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:18:01 +0530 Subject: [PATCH 094/195] fix: filter Billed Items To Be Received by invoice name, not per_received --- .../billed_items_to_be_received.py | 2 +- .../test_billed_items_to_be_received.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index dc6192e7544..0ec4652146e 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -31,7 +31,7 @@ def get_report_filters(report_filters): ] if report_filters.get("purchase_invoice"): - filters.append(["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]]) + filters.append(["Purchase Invoice", "name", "=", report_filters.get("purchase_invoice")]) return filters diff --git a/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py index 1112a9f7403..aed83005a1a 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py @@ -87,3 +87,16 @@ class TestBilledItemsToBeReceived(ERPNextTestSuite): rows = self.get_rows_for(self.run_report(posting_date="2000-01-01"), pi.name) self.assertEqual(len(rows), 0) + + def test_purchase_invoice_filter_scopes_to_that_invoice(self): + """The optional purchase_invoice filter must narrow to that invoice only.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", item_code="_Test Item", qty=5, rate=200, update_stock=0 + ) + other = make_purchase_invoice( + supplier="_Test Supplier", item_code="_Test Item", qty=3, rate=200, update_stock=0 + ) + + names = {row.get("name") for row in self.run_report(purchase_invoice=pi.name)} + self.assertEqual(names, {pi.name}) + self.assertNotIn(other.name, names) From 249d519d02d400061470f2a6c70b784dfa39244e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:20:57 +0530 Subject: [PATCH 095/195] fix: compute Share Balance as-on the selected date --- .../report/share_balance/share_balance.py | 31 +++++++++++--- .../share_balance/test_share_balance.py | 40 +++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/share_balance/share_balance.py b/erpnext/accounts/report/share_balance/share_balance.py index 1d02a996b76..db475a86265 100644 --- a/erpnext/accounts/report/share_balance/share_balance.py +++ b/erpnext/accounts/report/share_balance/share_balance.py @@ -15,8 +15,6 @@ def execute(filters=None): columns = get_columns(filters) - filters.get("date") - data = [] if not filters.get("shareholder"): @@ -24,7 +22,7 @@ def execute(filters=None): else: share_type, no_of_shares, rate, amount = 1, 2, 3, 4 - all_shares = get_all_shares(filters.get("shareholder")) + all_shares = get_all_shares(filters.get("shareholder"), filters.get("date")) for share_entry in all_shares: row = False for datum in data: @@ -63,5 +61,28 @@ def get_columns(filters): return columns -def get_all_shares(shareholder): - return frappe.get_doc("Shareholder", shareholder).share_balance +def get_all_shares(shareholder, date): + """Share movements for the shareholder up to (and including) `date`, signed by direction: + shares received are positive, shares transferred/sold out are negative.""" + transfers = frappe.get_all( + "Share Transfer", + filters={"docstatus": 1, "date": ("<=", date)}, + fields=["share_type", "no_of_shares", "rate", "amount", "from_shareholder", "to_shareholder"], + order_by="date", + ) + + shares = [] + for transfer in transfers: + if transfer.to_shareholder == shareholder: + shares.append(transfer) + elif transfer.from_shareholder == shareholder: + shares.append( + frappe._dict( + share_type=transfer.share_type, + no_of_shares=-transfer.no_of_shares, + rate=transfer.rate, + amount=-transfer.amount, + ) + ) + + return shares diff --git a/erpnext/accounts/report/share_balance/test_share_balance.py b/erpnext/accounts/report/share_balance/test_share_balance.py index c118442d239..bb6da5a133d 100644 --- a/erpnext/accounts/report/share_balance/test_share_balance.py +++ b/erpnext/accounts/report/share_balance/test_share_balance.py @@ -103,12 +103,8 @@ class TestShareBalanceReport(ERPNextTestSuite): self.assertEqual(other_row[2], 40) self.assertEqual(other_row[4], 400) - def test_report_reflects_current_balance_regardless_of_date(self): - # NOTE: the report reads the Shareholder's current `share_balance` table - # and does NOT actually filter by the `date` value (it is required but - # unused in the computation). So an as-on date before the issue still - # shows the current holding rather than 0. This asserts the real - # behavior and flags the missing date-based filtering. + def test_as_on_date_before_issue_shows_no_holding(self): + # the report is as-on `date`: before any share transfer, the shareholder holds nothing create_share_transfer( transfer_type="Issue", to_shareholder=self.shareholder, @@ -120,8 +116,36 @@ class TestShareBalanceReport(ERPNextTestSuite): date="2026-06-01", ) - row = self.get_row(date="2020-01-01") # long before the issue - self.assertEqual(row[2], 100) + data = execute( + frappe._dict({"date": "2026-05-01", "company": COMPANY, "shareholder": self.shareholder}) + )[1] + self.assertEqual(data, []) + + def test_as_on_date_reflects_holding_up_to_that_date(self): + # two issues on different dates; an as-on date between them sees only the first + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=101, + to_no=200, + no_of_shares=100, + rate=20, + date="2026-06-10", + ) + + self.assertEqual(self.get_row(date="2026-06-05")[2], 100) # only the first issue + self.assertEqual(self.get_row(date="2026-06-15")[2], 200) # both issues def get_row(self, date, shareholder=None): filters = frappe._dict( From 028cc2cf498f5c98f6aa87d2f8f95ea239a447f2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:23:58 +0530 Subject: [PATCH 096/195] fix: age payments by the payment period (payment date - invoice date) --- .../payment_period_based_on_invoice_date.py | 6 ++- ...st_payment_period_based_on_invoice_date.py | 43 ++++++++----------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index da3fa0762aa..5bbe02e4a01 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -21,6 +21,8 @@ def execute(filters=None): entries = get_entries(filters) invoice_details = get_invoice_posting_date_map(filters) + report = ReceivablePayableReport(filters) + data = [] for d in entries: invoice = invoice_details.get(d.against_voucher_no) or frappe._dict() @@ -29,7 +31,9 @@ def execute(filters=None): d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount}) if d.against_voucher_no: - ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d) + # age the payment by how long after the invoice it was made (payment date - invoice date) + report.age_as_on = getdate(d.posting_date) + report.get_ageing_data(invoice.posting_date, d) row = [ d.voucher_type, diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py index a2d3953938e..2c88a2c1171 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py @@ -2,7 +2,7 @@ # See license.txt import frappe -from frappe.utils import add_days, getdate, nowdate +from frappe.utils import getdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -16,10 +16,9 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): """Depth tests for the Payment Period Based On Invoice Date report. The report lists Payment Ledger Entries against invoices and buckets the paid - amount by the invoice's age into ranges: range1 (0-30), range2 (30-60), - range3 (60-90), range4 (90 Above). The age is measured from the invoice - posting date up to today, so invoice posting dates are placed relative to - ``nowdate()`` to land deterministically in a given bucket. + amount by the payment period -- how long after the invoice the payment was made + (payment date - invoice date) -- into ranges: range1 (0-30), range2 (30-60), + range3 (60-90), range4 (90 Above). """ def run_report(self, **extra): @@ -28,8 +27,8 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): "company": "_Test Company", "payment_type": "Incoming", "party_type": "Customer", - "from_date": add_days(nowdate(), -180), - "to_date": nowdate(), + "from_date": "2026-01-01", + "to_date": "2026-12-31", } ) filters.update(extra) @@ -45,21 +44,18 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): return row return None - def pay_invoice(self, invoice): + def pay_invoice(self, invoice, payment_date): pe = get_payment_entry("Sales Invoice", invoice.name) - pe.posting_date = nowdate() + pe.posting_date = payment_date pe.reference_no = "1" - pe.reference_date = nowdate() + pe.reference_date = payment_date pe.submit() return pe def test_paid_amount_lands_in_0_30_bucket(self): - invoice = create_sales_invoice( - customer="_Test Customer", - rate=1000, - posting_date=add_days(nowdate(), -15), - ) - payment = self.pay_invoice(invoice) + # invoice 2026-06-01, paid 2026-06-20 -> 19 days after -> 0-30 bucket + invoice = create_sales_invoice(customer="_Test Customer", rate=1000, posting_date="2026-06-01") + payment = self.pay_invoice(invoice, "2026-06-20") columns, data = self.run_report() @@ -68,10 +64,11 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): # Positional assertions on the row shape. self.assertEqual(row[2], "Customer") - self.assertEqual(row[4], getdate(nowdate())) # payment posting date + self.assertEqual(row[4], getdate("2026-06-20")) # payment posting date self.assertEqual(row[5], invoice.name) # against invoice - self.assertEqual(row[6], getdate(add_days(nowdate(), -15))) # invoice posting date + self.assertEqual(row[6], getdate("2026-06-01")) # invoice posting date self.assertEqual(row[8], 1000) # amount + self.assertEqual(row[10], 19) # age = payment date - invoice date # Buckets: 0-30 filled, others empty. self.assertEqual(row[11], 1000) # range1 (0-30) @@ -80,12 +77,9 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): self.assertEqual(row[14], 0) # range4 (90 Above) def test_paid_amount_lands_in_30_60_bucket(self): - invoice = create_sales_invoice( - customer="_Test Customer 1", - rate=1000, - posting_date=add_days(nowdate(), -45), - ) - payment = self.pay_invoice(invoice) + # invoice 2026-06-01, paid 2026-07-16 -> 45 days after -> 30-60 bucket + invoice = create_sales_invoice(customer="_Test Customer 1", rate=1000, posting_date="2026-06-01") + payment = self.pay_invoice(invoice, "2026-07-16") columns, data = self.run_report() @@ -93,6 +87,7 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): self.assertIsNotNone(row, "Payment row not found in report output") self.assertEqual(row[8], 1000) # amount + self.assertEqual(row[10], 45) # age = payment date - invoice date # Buckets: 30-60 filled, others empty. self.assertEqual(row[11], 0) # range1 (0-30) self.assertEqual(row[12], 1000) # range2 (30-60) From 0e8b152c680fd9db0663f60057ecb5af59c30bef Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:26:04 +0530 Subject: [PATCH 097/195] fix: avoid double-counting the total in accumulated Consolidated Financial Statement --- .../consolidated_financial_statement.py | 7 ++++++- .../test_consolidated_financial_statement.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index fba7054e0a7..237f37e767f 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -582,7 +582,12 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com total += flt(row[company]) row["has_value"] = has_value - row["total"] = total + # when accumulating into the group company, that company's column already consolidates its + # descendants, so summing every company column would double-count; use the group total directly. + if filters.get("accumulated_in_group_company"): + row["total"] = flt(row.get(filters.company, 0.0), 3) + else: + row["total"] = total data.append(row) diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py index c928a82887e..202c495d378 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -102,6 +102,8 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) # parent column picks up the child value when accumulated self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), amount) + # the total must equal the consolidated (group) value, not the sum of parent + child columns + self.assertEqual(flt(sales_row.get("total")), amount) def test_balance_sheet_executes_and_returns_rows(self): # posting income leaves a balancing entry in the child's Cash (Asset) account From b2ad93be816d5d1a09ae38e73f92a307c84c458f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 14:05:09 +0530 Subject: [PATCH 098/195] perf: use recursive CTEs for BOM and Task tree traversal Reimplements get_ancestor_boms, BOM.traverse_tree, Task.check_recursion and the BOM Explorer report as single recursive-CTE queries (frappe.qb recursive=True), replacing query-per-node walks (N->1). Task cycle detection now catches cycles at any depth (was capped at 15 nodes). Requires the framework recursive-CTE support (frappe#40464). --- erpnext/manufacturing/doctype/bom/bom.py | 37 +++++++----- .../bom_update_log/bom_updation_utils.py | 36 ++++++----- .../report/bom_explorer/bom_explorer.py | 60 ++++++++++++------- erpnext/projects/doctype/task/task.py | 45 ++++++++------ 4 files changed, 109 insertions(+), 69 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 144cd302a4a..e9cbfd9c911 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -739,7 +739,7 @@ class BOM(WebsiteGenerator): ) ) - def check_recursion(self, bom_list=None): + def check_recursion(self): """Check whether recursion occurs in any bom""" bom_list = self.traverse_tree() child_items = frappe.get_all( @@ -861,21 +861,30 @@ class BOM(WebsiteGenerator): self.append("items", row) - def traverse_tree(self, bom_list=None): - count = 0 - if not bom_list: - bom_list = [] + def traverse_tree(self): + """Return this BOM and every descendant BOM. The whole sub-tree is fetched in one recursive + CTE (frappe.qb) instead of a query-per-node walk; the only caller (check_recursion) uses the + result purely as a membership set. Portable across postgres and mariadb 10.2+.""" + bom_item = frappe.qb.DocType("BOM Item") + tree = frappe.qb.Table("bom_tree") - if self.name not in bom_list: - bom_list.append(self.name) + seed = ( + frappe.qb.from_(bom_item) + .select(bom_item.bom_no.as_("bom")) + .where((bom_item.parent == self.name) & (bom_item.bom_no != "") & (bom_item.parenttype == "BOM")) + ) + recursion = ( + frappe.qb.from_(bom_item) + .join(tree) + .on(bom_item.parent == tree.bom) + .select(bom_item.bom_no) + .where((bom_item.bom_no != "") & (bom_item.parenttype == "BOM")) + ) + descendants = ( + frappe.qb.with_(seed + recursion, "bom_tree", recursive=True).from_(tree).select(tree.bom) + ).run(pluck=True) - while count < len(bom_list): - for child_bom in _get_bom_children(bom_list[count]): - if child_bom not in bom_list: - bom_list.append(child_bom) - count += 1 - bom_list.reverse() - return bom_list + return [self.name, *descendants] def company_currency(self): return erpnext.get_company_currency(self.company) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index 853de3ea945..ebc064396bf 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -67,29 +67,33 @@ def update_cost_in_level(doc: "BOMUpdateLog", bom_list: list[str], batch_name: i frappe.db.commit() # nosemgrep -def get_ancestor_boms(new_bom: str, bom_list: list | None = None) -> list: - "Recursively get all ancestors of BOM." - - bom_list = bom_list or [] +def get_ancestor_boms(new_bom: str) -> list: + """Return every ancestor BOM of `new_bom` (BOMs that consume it, transitively) in one recursive + CTE built with frappe.qb -- portable across postgres and mariadb 10.2+. `UNION` makes it + cycle-safe (it stops once no new BOM is reached); a BOM that is its own ancestor is rejected.""" bom_item = frappe.qb.DocType("BOM Item") + tree = frappe.qb.Table("ancestor_boms") - parents = ( + seed = ( frappe.qb.from_(bom_item) - .select(bom_item.parent) + .select(bom_item.parent.as_("bom")) .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) - .run(as_dict=True) ) + recursion = ( + frappe.qb.from_(bom_item) + .join(tree) + .on(bom_item.bom_no == tree.bom) + .select(bom_item.parent) + .where((bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) + ) + ancestors = ( + frappe.qb.with_(seed + recursion, "ancestor_boms", recursive=True).from_(tree).select(tree.bom) + ).run(pluck=True) - for d in parents: - if new_bom == d.parent: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) + if new_bom in ancestors: + frappe.throw(_("BOM recursion: {0} cannot be an ancestor of itself").format(new_bom)) - if d.parent not in tuple(bom_list): - bom_list.append(d.parent) - - get_ancestor_boms(d.parent, bom_list) - - return bom_list + return ancestors def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 680cb83b312..1f82ec847b3 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -2,6 +2,8 @@ # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ @@ -14,29 +16,47 @@ def execute(filters=None): def get_data(filters, data): - get_exploded_items(filters.bom, data) + children_map = fetch_exploded_bom_items(filters.bom) + build_exploded_rows(filters.bom, children_map, data) -def get_exploded_items(bom, data, indent=0, qty=1): - exploded_items = frappe.get_all( - "BOM Item", - filters={"parent": bom}, - fields=[ - "qty", - "bom_no", - "qty", - "item_code", - "item_name", - "description", - "uom", - "idx", - "is_phantom_item", - ], - order_by="idx ASC", +def fetch_exploded_bom_items(root_bom): + """Every BOM Item in the exploded tree of `root_bom`, grouped by its parent BOM, in one + recursive CTE -- replaces a query-per-node walk with a single query. UNION keeps it cycle-safe + and fetches each sub-BOM's items only once even when it is reused across the tree.""" + bom_item = frappe.qb.DocType("BOM Item") + tree = frappe.qb.Table("exploded_bom") + fields = [ + bom_item.parent, + bom_item.qty, + bom_item.bom_no, + bom_item.item_code, + bom_item.item_name, + bom_item.description, + bom_item.uom, + bom_item.idx, + bom_item.is_phantom_item, + ] + seed = frappe.qb.from_(bom_item).select(*fields).where(bom_item.parent == root_bom) + recursion = ( + frappe.qb.from_(bom_item) + .join(tree) + .on(bom_item.parent == tree.bom_no) + .select(*fields) + .where(tree.bom_no != "") ) + rows = ( + frappe.qb.with_(seed + recursion, "exploded_bom", recursive=True).from_(tree).select(tree.star) + ).run(as_dict=True) - for item in exploded_items: - item["indent"] = indent + children_map = defaultdict(list) + for row in rows: + children_map[row.parent].append(row) + return children_map + + +def build_exploded_rows(bom, children_map, data, indent=0, qty=1): + for item in sorted(children_map.get(bom, []), key=lambda row: row.idx): data.append( { "item_code": item.item_code, @@ -51,7 +71,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): } ) if item.bom_no: - get_exploded_items(item.bom_no, data, indent=indent + 1, qty=item.qty) + build_exploded_rows(item.bom_no, children_map, data, indent + 1, item.qty) def get_columns(): diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 9eda760a4e7..c431af5cf11 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -9,7 +9,7 @@ from frappe import _, throw from frappe.desk.form.assign_to import clear, close_all_assignments from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Max, Min, Sum -from frappe.utils import add_days, add_to_date, cstr, date_diff, flt, get_link_to_form, getdate, today +from frappe.utils import add_days, add_to_date, date_diff, flt, get_link_to_form, getdate, today from frappe.utils.data import format_date from frappe.utils.nestedset import NestedSet @@ -247,25 +247,32 @@ class Task(NestedSet): def check_recursion(self): if self.flags.ignore_recursion_check: return - check_list = [["task", "parent"], ["parent", "task"]] - for d in check_list: - task_list, count = [self.name], 0 - while len(task_list) > count: - tasks = frappe.get_all( - "Task Depends On", - filters={d[1]: cstr(task_list[count])}, - fields=[d[0]], - as_list=True, - ) - count = count + 1 - for b in tasks: - if b[0] == self.name: - frappe.throw(_("Circular Reference Error"), CircularReferenceError) - if b[0]: - task_list.append(b[0]) + # "Task Depends On" is a directed edge (parent depends on `task`); a cycle exists if this + # task is reachable from itself along either direction. One recursive CTE per direction + # fetches the whole reachable set in a single query -- UNION makes it cycle-safe at any + # depth, so unlike the old per-node BFS it needs no arbitrary depth cap. + for select_field, filter_field in (("task", "parent"), ("parent", "task")): + if self._reaches_self(select_field, filter_field): + frappe.throw(_("Circular Reference Error"), CircularReferenceError) - if count == 15: - break + def _reaches_self(self, select_field: str, filter_field: str) -> bool: + depends_on = frappe.qb.DocType("Task Depends On") + tree = frappe.qb.Table("dependency_tree") + seed = ( + frappe.qb.from_(depends_on) + .select(depends_on[select_field].as_("node")) + .where(depends_on[filter_field] == self.name) + ) + recursion = ( + frappe.qb.from_(depends_on) + .join(tree) + .on(depends_on[filter_field] == tree.node) + .select(depends_on[select_field]) + ) + reachable = ( + frappe.qb.with_(seed + recursion, "dependency_tree", recursive=True).from_(tree).select(tree.node) + ).run(pluck=True) + return self.name in reachable def reschedule_dependent_tasks(self): end_date = self.exp_end_date or self.act_end_date From bb184f90a756bd02fa13ff1d41831978eac209f0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 14:07:40 +0530 Subject: [PATCH 099/195] perf: serialize concurrent reposts with an advisory lock Wraps per-(item, warehouse) reposting in repost_future_sle with a session-level advisory lock in front of the existing for-update row locks -- an outer gate that turns lock-order deadlocks into an orderly wait. Postgres/MariaDB only; nullcontext elsewhere. Row locks still enforce correctness. Requires frappe advisory_lock. --- erpnext/stock/stock_ledger.py | 55 +++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f03e75c2b51..49cec270c86 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -5,6 +5,7 @@ import copy import gzip import json from collections import deque +from contextlib import nullcontext import frappe from frappe import _, bold, scrub @@ -261,6 +262,25 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): return sle +# Reposts wait this long for the per-(item, warehouse) gate before giving up. A repost of one item +# is bounded, and a QueryTimeoutError here is recoverable -- the repost job re-queues and retries. +REPOST_LOCK_TIMEOUT = 600 + + +def repost_gate(item_code, warehouse): + """Serialize concurrent reposts of the same (item, warehouse) with a session-level advisory lock + acquired BEFORE the inner `... for update` row locks. It is an outer gate: two reposts touching + the same item wait for each other instead of racing into a lock-order deadlock (which repost + otherwise only survives by retrying). The row locks still enforce correctness -- this only cuts + the deadlock/retry churn. Advisory locks exist on postgres + mariadb; elsewhere there is no gate.""" + # hasattr keeps this a graceful opt-in: if this ERPNext deploys before frappe.db.advisory_lock + # lands (frappe#40466), fall back to no gate rather than raising a non-recoverable AttributeError + # that would permanently mark the Repost Item Valuation as Failed. + if frappe.db.db_type in ("postgres", "mariadb") and hasattr(frappe.db, "advisory_lock"): + return frappe.db.advisory_lock(f"stock_repost:{item_code}:{warehouse}", timeout=REPOST_LOCK_TIMEOUT) + return nullcontext() + + def repost_future_sle( items_to_be_repost=None, voucher_type=None, @@ -289,22 +309,25 @@ def repost_future_sle( while index < len(items_to_be_repost): validate_item_warehouse(items_to_be_repost[index]) - obj = update_entries_after( - { - "item_code": items_to_be_repost[index].get("item_code"), - "warehouse": items_to_be_repost[index].get("warehouse"), - "posting_date": items_to_be_repost[index].get("posting_date"), - "posting_time": items_to_be_repost[index].get("posting_time"), - "creation": items_to_be_repost[index].get("creation"), - "current_idx": index, - "items_to_be_repost": items_to_be_repost, - "repost_doc": doc, - "repost_affected_transaction": repost_affected_transaction, - "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, - }, - allow_negative_stock=allow_negative_stock, - via_landed_cost_voucher=via_landed_cost_voucher, - ) + item_code = items_to_be_repost[index].get("item_code") + warehouse = items_to_be_repost[index].get("warehouse") + with repost_gate(item_code, warehouse): + obj = update_entries_after( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": items_to_be_repost[index].get("posting_date"), + "posting_time": items_to_be_repost[index].get("posting_time"), + "creation": items_to_be_repost[index].get("creation"), + "current_idx": index, + "items_to_be_repost": items_to_be_repost, + "repost_doc": doc, + "repost_affected_transaction": repost_affected_transaction, + "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) index += 1 From 7a9e901e5ebc85c884ccafa86a78078cbe95850c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:48:13 +0530 Subject: [PATCH 100/195] test: add coverage for Work Order Summary report --- .../test_work_order_summary.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py diff --git a/erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py new file mode 100644 index 00000000000..fa9271f5fd9 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.work_order_summary.work_order_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWorkOrderSummary(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": add_days(today(), -1), + "to_date": today(), + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_work_order_appears_with_expected_fields(self): + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + + rows = {row["name"]: row for row in self.run_report()} + self.assertIn(wo.name, rows) + + row = rows[wo.name] + self.assertEqual(row["production_item"], "_Test FG Item") + self.assertEqual(row["qty"], 10) + self.assertEqual(row["produced_qty"], 0) + self.assertEqual(row["status"], "Not Started") + + def test_status_filter_excludes_other_statuses(self): + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + self.assertEqual(wo.status, "Not Started") + + # A "Completed" filter must not return a "Not Started" work order. + names = {row["name"] for row in self.run_report(status="Completed")} + self.assertNotIn(wo.name, names) + + # The matching status still returns it. + names = {row["name"] for row in self.run_report(status="Not Started")} + self.assertIn(wo.name, names) + + def test_date_range_excludes_work_order_outside_window(self): + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + + # A window entirely in the past cannot contain a WO created today. + names = { + row["name"] + for row in self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5)) + } + self.assertNotIn(wo.name, names) + + # A window that includes today does contain it. + names = {row["name"] for row in self.run_report()} + self.assertIn(wo.name, names) From 4f3dcd9e3972e44e089c3e55e3a2ceb023cf5615 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:48:27 +0530 Subject: [PATCH 101/195] test: add coverage for Job Card Summary report --- .../job_card_summary/test_job_card_summary.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py diff --git a/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py new file mode 100644 index 00000000000..81af722ad50 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py @@ -0,0 +1,76 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + + +import frappe +from frappe.utils import add_days, today + +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.job_card_summary.job_card_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestJobCardSummary(ERPNextTestSuite): + def setUp(self): + # `_Test FG Item 2` has a default active BOM with operations, so submitting a + # Work Order for it auto-creates Job Cards (one per operation). + self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2) + self.job_cards = frappe.get_all( + "Job Card", + filters={"work_order": self.work_order.name}, + fields=["name", "operation", "workstation", "production_item", "status"], + ) + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": add_days(today(), -1), + "to_date": add_days(today(), 1), + } + ) + filters.update(extra) + return execute(filters)[1] + + def rows_for_work_order(self, rows): + return [row for row in rows if row.get("work_order") == self.work_order.name] + + def test_job_cards_are_listed(self): + self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards") + + rows = self.rows_for_work_order(self.run_report()) + self.assertEqual(len(rows), len(self.job_cards)) + + reported_names = {row.get("name") for row in rows} + self.assertEqual(reported_names, {jc.name for jc in self.job_cards}) + + # Fresh (unsubmitted) job cards are reported as Open, and each row carries the + # operation / workstation / production item pulled from the Job Card. + for jc in self.job_cards: + row = next(row for row in rows if row.get("name") == jc.name) + self.assertEqual(row.get("status"), "Open") + self.assertEqual(row.get("operation"), jc.operation) + self.assertEqual(row.get("workstation"), jc.workstation) + self.assertEqual(row.get("production_item"), jc.production_item) + + def test_operation_filter_scopes_rows(self): + operation = self.job_cards[0].operation + matching = {jc.name for jc in self.job_cards if jc.operation == operation} + + rows = self.rows_for_work_order(self.run_report(operation=operation)) + self.assertEqual({row.get("name") for row in rows}, matching) + + def test_status_filter(self): + open_rows = self.rows_for_work_order(self.run_report(status="Open")) + self.assertEqual({row.get("name") for row in open_rows}, {jc.name for jc in self.job_cards}) + + # None of the freshly created job cards are Completed yet. + completed_rows = self.rows_for_work_order(self.run_report(status="Completed")) + self.assertEqual(completed_rows, []) + + def test_date_filter_excludes_out_of_range(self): + # Job Card posting_date defaults to today; a past-only window should exclude them. + rows = self.rows_for_work_order( + self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5)) + ) + self.assertEqual(rows, []) From baae9bfb2250367f5642ca4a966aa6d1fae054eb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:48:35 +0530 Subject: [PATCH 102/195] test: add coverage for Production Analytics report --- .../test_production_analytics.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 erpnext/manufacturing/report/production_analytics/test_production_analytics.py diff --git a/erpnext/manufacturing/report/production_analytics/test_production_analytics.py b/erpnext/manufacturing/report/production_analytics/test_production_analytics.py new file mode 100644 index 00000000000..17a26ef06bd --- /dev/null +++ b/erpnext/manufacturing/report/production_analytics/test_production_analytics.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import get_first_day, get_last_day, today + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProductionAnalytics(ERPNextTestSuite): + def run_report(self, **extra): + from erpnext.manufacturing.report.production_analytics.production_analytics import execute + + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": get_first_day(today()), + "to_date": get_last_day(today()), + "range": "Monthly", + } + ) + filters.update(extra) + columns, data, _msg, _chart = execute(filters) + return columns, data + + def get_period_count(self, columns, data, status, period_label): + """Return the count for a status row under the period column resolved by label.""" + period_fieldname = next(col["fieldname"] for col in columns if col.get("label") == period_label) + row = next(row for row in data if row["status"] == status) + return row[period_fieldname] + + def test_submitted_work_order_increments_status_count(self): + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + + # The current month is the period a newly created Work Order falls into (bucketed by creation date). + cols_before, data_before = self.run_report() + period_label = cols_before[-1]["label"] + before = self.get_period_count(cols_before, data_before, "Not Started", period_label) + + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + self.assertEqual(wo.docstatus, 1) + # A freshly submitted Work Order with no material transfer has status "Not Started". + self.assertEqual(wo.status, "Not Started") + + cols_after, data_after = self.run_report() + after = self.get_period_count(cols_after, data_after, "Not Started", period_label) + + self.assertEqual(after, before + 1) + + def test_report_shape(self): + columns, data = self.run_report() + + # First column is the Status column, followed by one column per period. + self.assertEqual(columns[0]["fieldname"], "status") + self.assertGreaterEqual(len(columns), 2) + + # One row per known Work Order status. + statuses = {row["status"] for row in data} + for status in ("Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"): + self.assertIn(status, statuses) From eadaf376061c3e1e2c6f840da773d23e266f9747 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:48:43 +0530 Subject: [PATCH 103/195] test: add coverage for BOM Explorer report --- .../report/bom_explorer/test_bom_explorer.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py diff --git a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py new file mode 100644 index 00000000000..3a7a1351c5b --- /dev/null +++ b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py @@ -0,0 +1,70 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom +from erpnext.manufacturing.report.bom_explorer.bom_explorer import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBOMExplorer(ERPNextTestSuite): + def run_report(self, bom): + filters = frappe._dict({"bom": bom}) + return execute(filters)[1] + + def test_default_bom_lists_components_at_top_level(self): + bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1}) + self.assertIsNotNone(bom, "Default active BOM for _Test FG Item not found") + + data = self.run_report(bom) + rows_by_item = {row["item_code"]: row for row in data} + + self.assertIn("_Test Item", rows_by_item) + self.assertIn("_Test Item Home Desktop 100", rows_by_item) + + for item_code in ("_Test Item", "_Test Item Home Desktop 100"): + row = rows_by_item[item_code] + self.assertEqual(row["indent"], 0) + self.assertEqual(row["bom_level"], 0) + + def test_qty_matches_bom_item_qty(self): + bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1}) + data = self.run_report(bom) + rows_by_item = {row["item_code"]: row for row in data} + + for bom_item in frappe.get_all( + "BOM Item", filters={"parent": bom}, fields=["item_code", "qty", "uom"] + ): + row = rows_by_item[bom_item.item_code] + self.assertEqual(row["qty"], bom_item.qty) + self.assertEqual(row["uom"], bom_item.uom) + + def test_nested_bom_shows_deeper_level(self): + # Sub-assembly: "sub" is itself a BOM containing "leaf". + parent_bom = create_nested_bom( + {"parent": {"sub": {"leaf": {}}, "flat": {}}}, + prefix="_Test explorer ", + ) + + data = self.run_report(parent_bom.name) + rows_by_item = {row["item_code"]: row for row in data} + + sub_item = "_Test explorer sub" + leaf_item = "_Test explorer leaf" + flat_item = "_Test explorer flat" + + self.assertIn(sub_item, rows_by_item) + self.assertIn(flat_item, rows_by_item) + self.assertIn(leaf_item, rows_by_item) + + # Direct components of the parent sit at level 0. + self.assertEqual(rows_by_item[flat_item]["indent"], 0) + self.assertEqual(rows_by_item[sub_item]["indent"], 0) + + # The sub-assembly row carries its own BOM reference. + self.assertTrue(rows_by_item[sub_item]["bom"]) + + # The leaf belongs to the sub-assembly, so it is exploded one level deeper. + self.assertEqual(rows_by_item[leaf_item]["indent"], 1) + self.assertEqual(rows_by_item[leaf_item]["bom_level"], 1) From e0bf3713eaeeb1afd539a45fd3e119e8ba75ccac Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:48:53 +0530 Subject: [PATCH 104/195] test: add coverage for Quality Inspection Summary report --- .../test_quality_inspection_summary.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py diff --git a/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py new file mode 100644 index 00000000000..368e7f5c3c4 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py @@ -0,0 +1,73 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, nowdate + +from erpnext.manufacturing.report.quality_inspection_summary.quality_inspection_summary import execute +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.quality_inspection.test_quality_inspection import ( + create_quality_inspection, + make_minimal_job_card, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestQualityInspectionSummary(ERPNextTestSuite): + def setUp(self): + super().setUp() + create_item("_Test Item") + self.job_card = make_minimal_job_card(production_item="_Test Item") + self.qi = create_quality_inspection( + item_code="_Test Item", + reference_type="Job Card", + reference_name=self.job_card, + status="Accepted", + ) + + def run_report(self, **extra): + filters = frappe._dict(extra) + return execute(filters)[1] + + def _rows_for_qi(self, data): + return [row for row in data if row.get("name") == self.qi.name] + + def test_appears_in_date_range(self): + data = self.run_report(from_date=add_days(nowdate(), -1), to_date=add_days(nowdate(), 1)) + rows = self._rows_for_qi(data) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row["status"], "Accepted") + self.assertEqual(row["item_code"], "_Test Item") + self.assertEqual(row["reference_type"], "Job Card") + self.assertEqual(row["reference_name"], self.job_card) + + def test_excluded_outside_date_range(self): + data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5)) + self.assertEqual(self._rows_for_qi(data), []) + + def test_status_filter_includes_matching(self): + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + status=["Accepted"], + ) + self.assertEqual(len(self._rows_for_qi(data)), 1) + + def test_status_filter_excludes_non_matching(self): + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + status=["Rejected"], + ) + self.assertEqual(self._rows_for_qi(data), []) + + def test_item_code_filter_excludes_other_item(self): + other_item = frappe.generate_hash(length=10) + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + item_code=[other_item], + ) + self.assertEqual(self._rows_for_qi(data), []) From 1a8ef852f1ec8325ac44011d29dc7811d4675a7a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:49:01 +0530 Subject: [PATCH 105/195] test: add coverage for Downtime Analysis report --- .../test_downtime_analysis.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py diff --git a/erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py new file mode 100644 index 00000000000..8c1a8292eab --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py @@ -0,0 +1,92 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + + +import frappe +from frappe.utils import add_days, get_datetime, today + +from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.manufacturing.report.downtime_analysis.downtime_analysis import execute +from erpnext.setup.doctype.employee.test_employee import make_employee +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDowntimeAnalysis(ERPNextTestSuite): + def setUp(self): + self.workstation = make_workstation(workstation="_Test Downtime Workstation").name + self.other_workstation = make_workstation(workstation="_Test Downtime Workstation 2").name + self.operator = make_employee("test_downtime_operator@example.com", company="_Test Company") + + # from_time / to_time are two hours apart -> downtime of 120 minutes (2 hours). + self.from_time = get_datetime(f"{today()} 09:00:00") + self.to_time = get_datetime(f"{today()} 11:00:00") + self.entry = self.make_downtime_entry(self.workstation) + + def make_downtime_entry(self, workstation, **extra): + values = { + "doctype": "Downtime Entry", + "workstation": workstation, + "operator": self.operator, + "from_time": self.from_time, + "to_time": self.to_time, + "stop_reason": "Machine malfunction", + } + values.update(extra) + return frappe.get_doc(values).insert() + + def run_report(self, **extra): + filters = frappe._dict( + { + "from_date": add_days(today(), -1), + "to_date": add_days(today(), 1), + } + ) + filters.update(extra) + return execute(filters)[1] + + def row_for_entry(self, rows, name): + return next((row for row in rows if row.get("name") == name), None) + + def test_downtime_is_computed_in_hours(self): + # validate() stores downtime in minutes; the report converts it to hours. + self.assertEqual(self.entry.downtime, 120) + + row = self.row_for_entry(self.run_report(), self.entry.name) + self.assertIsNotNone(row, "Downtime Entry not present in report output") + self.assertEqual(row.get("workstation"), self.workstation) + self.assertEqual(row.get("operator"), self.operator) + self.assertEqual(row.get("stop_reason"), "Machine malfunction") + self.assertEqual(row.get("downtime"), 2.0) + + def test_workstation_filter_scopes_rows(self): + other = self.make_downtime_entry(self.other_workstation) + + rows = self.run_report(workstation=self.workstation) + names = {row.get("name") for row in rows} + self.assertIn(self.entry.name, names) + self.assertNotIn(other.name, names) + self.assertTrue(all(row.get("workstation") == self.workstation for row in rows)) + + def test_date_range_excludes_out_of_window_entries(self): + # The report filters from_time >= from_date and to_time <= to_date; a window + # ending before the entry's from_time must exclude it. + rows = self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5)) + self.assertIsNone(self.row_for_entry(rows, self.entry.name)) + + def test_chart_aggregates_downtime_per_workstation(self): + self.make_downtime_entry(self.workstation) + + chart = execute( + frappe._dict( + { + "from_date": add_days(today(), -1), + "to_date": add_days(today(), 1), + "workstation": self.workstation, + } + ) + )[3] + + self.assertIn(self.workstation, chart["data"]["labels"]) + index = chart["data"]["labels"].index(self.workstation) + # Two entries of 2 hours each for this workstation -> 4 hours aggregated. + self.assertEqual(chart["data"]["datasets"][0]["values"][index], 4.0) From 47ee1d126d32d746fa7b03009deaee8659823654 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:53:54 +0530 Subject: [PATCH 106/195] test: add coverage for Work Order Consumed Materials report --- .../test_work_order_consumed_materials.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py new file mode 100644 index 00000000000..c879de4dc02 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py @@ -0,0 +1,105 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, nowdate + +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.work_order_consumed_materials.work_order_consumed_materials import execute +from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWorkOrderConsumedMaterials(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": add_days(nowdate(), -1), + "to_date": add_days(nowdate(), 1), + } + ) + filters.update(extra) + return execute(filters)[1] + + def make_manufactured_work_order(self, qty=2): + """Create a submitted WO, stock its raw materials, transfer and fully manufacture it.""" + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=qty, company="_Test Company") + + for item in wo.required_items: + test_stock_entry.make_stock_entry( + item_code=item.item_code, + target=wo.wip_warehouse, + qty=item.required_qty, + basic_rate=100, + ) + + transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", qty)) + transfer.insert() + transfer.submit() + + manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", qty)) + manufacture.insert() + manufacture.submit() + + wo.reload() + return wo + + def get_wo_rows(self, data, work_order): + """The report blanks parent fields after the first raw-material row, so match by raw + material's parent work order instead of the (blanked) `name` column.""" + return [row for row in data if row.get("parent") == work_order] + + def test_consumed_materials_reported_after_manufacture(self): + wo = self.make_manufactured_work_order(qty=2) + + # fully producing the WO consumes exactly the required quantity of each raw material + self.assertEqual(wo.produced_qty, 2) + + data = self.run_report() + rows = self.get_wo_rows(data, wo.name) + + self.assertEqual(len(rows), len(wo.required_items)) + + reported = {row["raw_material_item_code"]: row for row in rows} + for item in wo.required_items: + row = reported[item.item_code] + self.assertEqual(row["required_qty"], item.required_qty) + self.assertEqual(row["transferred_qty"], item.required_qty) + self.assertEqual(row["consumed_qty"], item.required_qty) + # no over-consumption in a clean full manufacture + self.assertEqual(row["extra_consumed_qty"], 0.0) + self.assertEqual(row["returned_qty"], 0.0) + + # parent columns are populated on the first row only + first = rows[0] + self.assertEqual(first["status"], wo.status) + self.assertEqual(first["production_item"], "_Test FG Item") + self.assertEqual(first["qty"], 2) + self.assertEqual(first["produced_qty"], 2) + + def test_work_order_filter_scopes_output(self): + wo = self.make_manufactured_work_order(qty=1) + + data = self.run_report(name=wo.name) + + parents = {row.get("parent") for row in data} + self.assertEqual(parents, {wo.name}) + self.assertTrue(data) + + def test_draft_work_order_is_excluded(self): + # report only lists WOs in status In Process / Completed / Stopped + draft = make_wo_order_test_record( + production_item="_Test FG Item", qty=1, company="_Test Company", do_not_submit=True + ) + + data = self.run_report() + self.assertNotIn(draft.name, {row.get("parent") for row in data}) + + def test_date_range_filter_excludes_work_order(self): + wo = self.make_manufactured_work_order(qty=1) + + # a window that ends before the WO was created must not include it + data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5)) + self.assertNotIn(wo.name, {row.get("parent") for row in data}) From 376a5a2aeed418eb0fea45a126c849602538c876 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:54:02 +0530 Subject: [PATCH 107/195] test: add coverage for BOM Variance Report report --- .../test_bom_variance_report.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py diff --git a/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py new file mode 100644 index 00000000000..e70bad995b4 --- /dev/null +++ b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py @@ -0,0 +1,108 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.bom_variance_report.bom_variance_report import execute +from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBOMVarianceReport(ERPNextTestSuite): + def setUp(self): + self.production_item = "_Test FG Item" + self.warehouse = "_Test Warehouse - _TC" + self.bom_no = frappe.db.get_value( + "BOM", {"item": self.production_item, "is_active": 1, "is_default": 1} + ) + self.raw_materials = self.get_bom_raw_materials() + + self.overproduction_percentage = frappe.db.get_single_value( + "Manufacturing Settings", "overproduction_percentage_for_work_order" + ) + frappe.db.set_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order", 100) + + def tearDown(self): + frappe.db.set_single_value( + "Manufacturing Settings", + "overproduction_percentage_for_work_order", + self.overproduction_percentage, + ) + + def get_bom_raw_materials(self): + return { + row.item_code: row.qty + for row in frappe.get_all( + "BOM Item", filters={"parent": self.bom_no}, fields=["item_code", "qty"] + ) + } + + def create_over_produced_work_order(self, ordered_qty=2, produced_qty=3): + work_order = make_wo_order_test_record( + item=self.production_item, + qty=ordered_qty, + source_warehouse=self.warehouse, + skip_transfer=1, + ) + + for item_code in self.raw_materials: + test_stock_entry.make_stock_entry( + item_code=item_code, target=self.warehouse, qty=100, basic_rate=100 + ) + + stock_entry = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", produced_qty)) + stock_entry.submit() + + work_order.reload() + self.assertEqual(work_order.produced_qty, produced_qty) + return work_order + + def run_report(self, **extra): + filters = frappe._dict({"bom_no": self.bom_no, **extra}) + return execute(filters)[1] + + def test_over_produced_work_order_appears_with_planned_and_actual(self): + work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=3) + + data = self.run_report(work_order=work_order.name) + + summary_rows = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(len(summary_rows), 1) + + summary = summary_rows[0] + self.assertEqual(summary.get("production_item"), self.production_item) + self.assertEqual(summary.get("bom_no"), self.bom_no) + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 3) + + raw_material_rows = { + row.get("raw_material_code"): row for row in data if row.get("raw_material_code") + } + for item_code, per_unit_qty in self.raw_materials.items(): + self.assertIn(item_code, raw_material_rows) + # planned/required qty scales with the ordered qty on the work order + self.assertEqual(raw_material_rows[item_code].get("required_qty"), per_unit_qty * 2) + + def test_bom_no_filter_returns_over_produced_orders(self): + work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=3) + + data = self.run_report() + + matched = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(len(matched), 1) + self.assertEqual(matched[0].get("bom_no"), self.bom_no) + + def test_work_order_without_over_production_is_excluded(self): + work_order = make_wo_order_test_record( + item=self.production_item, + qty=2, + source_warehouse=self.warehouse, + skip_transfer=1, + ) + + data = self.run_report(work_order=work_order.name) + + matched = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(matched, []) From 222842b7d12784ac6876b40be1d883b5c9eeeace Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:54:09 +0530 Subject: [PATCH 108/195] test: add coverage for Process Loss Report report --- .../test_process_loss_report.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py diff --git a/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py new file mode 100644 index 00000000000..873b39446ff --- /dev/null +++ b/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py @@ -0,0 +1,118 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import nowdate + +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.process_loss_report.process_loss_report import execute +from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProcessLossReport(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": nowdate(), + "to_date": nowdate(), + } + ) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, work_order): + for row in data: + if row.get("name") == work_order: + return row + return None + + def make_manufactured_work_order(self, planned_qty, produced_qty): + """Create a submitted WO and manufacture `produced_qty` of `planned_qty`. + + The difference is booked as process loss on the Manufacture stock entry, + which propagates to the work order's `process_loss_qty`. + """ + wo_order = make_wo_order_test_record(production_item="_Test FG Item", qty=planned_qty) + + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=100, basic_rate=100 + ) + + transfer = frappe.get_doc( + make_stock_entry(wo_order.name, "Material Transfer for Manufacture", planned_qty) + ) + for d in transfer.get("items"): + d.s_warehouse = "Stores - _TC" + transfer.insert() + transfer.submit() + + manufacture = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", planned_qty)) + # Reduce the finished good qty below fg_completed_qty so the difference is + # recorded as process loss. + process_loss_qty = planned_qty - produced_qty + if process_loss_qty: + for d in manufacture.get("items"): + if d.is_finished_item: + d.qty = produced_qty + d.transfer_qty = produced_qty * (d.conversion_factor or 1) + manufacture.insert() + manufacture.submit() + + wo_order.reload() + return wo_order + + def test_work_order_with_process_loss_is_listed(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) + + self.assertEqual(wo_order.process_loss_qty, 1) + self.assertEqual(wo_order.produced_qty, 4) + + data = self.run_report(work_order=wo_order.name) + row = self.find_row(data, wo_order.name) + + self.assertIsNotNone(row, "Work order with process loss should appear in the report") + self.assertEqual(row.production_item, "_Test FG Item") + self.assertEqual(row.qty_to_manufacture, 5) + self.assertEqual(row.produced_qty, 4) + self.assertEqual(row.process_loss_qty, 1) + + # total_pl_value = process_loss_qty * (total_fg_value / qty_to_manufacture) + expected_pl_value = row.process_loss_qty * (row.total_fg_value / row.qty_to_manufacture) + self.assertAlmostEqual(row.total_pl_value, expected_pl_value) + self.assertGreater(row.total_pl_value, 0) + + def test_work_order_without_process_loss_is_not_listed(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=5) + + self.assertEqual(wo_order.process_loss_qty, 0) + self.assertEqual(wo_order.produced_qty, 5) + + data = self.run_report(work_order=wo_order.name) + self.assertIsNone( + self.find_row(data, wo_order.name), + "Work order that produced the full planned qty should not appear (no loss)", + ) + + def test_item_and_work_order_filters_are_ineffective(self): + """BUG: the `item` and `work_order` filters in process_loss_report.get_data + call `query.where(...)` without reassigning the result. frappe's query + builder is immutable, so `.where()` returns a new query and these extra + conditions are silently dropped. A non-matching item filter therefore fails + to exclude the row. This test documents the current (buggy) behaviour; if the + report is fixed to reassign the query, update the assertion below to + `assertIsNone`. + """ + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) + + # A non-matching item filter should exclude the row, but currently does not. + data = self.run_report(item="_Test FG Item 2") + self.assertIsNotNone( + self.find_row(data, wo_order.name), + "Filter bug regressed/fixed: `item` filter now takes effect - update this test", + ) From 75ba81c79a30c3005f03ff5edc2b66d670b94920 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:54:16 +0530 Subject: [PATCH 109/195] test: add coverage for Cost of Poor Quality Report report --- .../test_cost_of_poor_quality_report.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py new file mode 100644 index 00000000000..76813b3d2b7 --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils.data import add_to_date, now + +from erpnext.manufacturing.doctype.job_card.mapper import make_corrective_job_card +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.cost_of_poor_quality_report.cost_of_poor_quality_report import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCostOfPoorQualityReport(ERPNextTestSuite): + """A Job Card appears in this report only when it is submitted (docstatus == 1) and flagged + as a corrective job card (is_corrective_job_card == 1). Such a card is created against a + corrective Operation (is_corrective_operation == 1); without any corrective operation the + report returns no rows at all.""" + + def setUp(self): + self.load_test_records("BOM") + + def create_corrective_job_card(self, hour_rate=100): + """Produce a submitted corrective Job Card and return (corrective_jc, operation, workstation).""" + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2) + + job_card = frappe.get_last_doc("Job Card", {"work_order": work_order.name}) + job_card.append( + "time_logs", + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + ) + job_card.submit() + + corrective_operation = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_operation.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = hour_rate + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=2), + "to_time": add_to_date(now(), hours=2, minutes=30), + "completed_qty": 2, + }, + ) + corrective_job_card.submit() + + return corrective_job_card, corrective_operation.name, corrective_job_card.workstation + + def run_report(self, **filters): + return execute(frappe._dict(filters))[1] + + def test_corrective_job_card_is_listed_with_expected_fields(self): + corrective_jc, operation, workstation = self.create_corrective_job_card(hour_rate=100) + + rows = self.run_report(company="_Test Company") + row = next((r for r in rows if r["name"] == corrective_jc.name), None) + + self.assertIsNotNone(row, "Submitted corrective job card must appear in the report") + self.assertEqual(row["work_order"], corrective_jc.work_order) + self.assertEqual(row["operation"], operation) + self.assertEqual(row["workstation"], workstation) + self.assertEqual(row["item_code"], corrective_jc.production_item) + self.assertEqual(row["hour_rate"], 100) + self.assertEqual(row["total_time_in_mins"], corrective_jc.total_time_in_mins) + # operating_cost = hour_rate * total_time_in_mins / 60 + self.assertEqual(row["operating_cost"], 100 * corrective_jc.total_time_in_mins / 60.0) + + def test_non_corrective_job_card_is_excluded(self): + corrective_jc, _operation, _workstation = self.create_corrective_job_card() + + # The regular (non-corrective) job card the corrective one was raised against must not appear. + regular_jc = corrective_jc.for_job_card + rows = self.run_report(company="_Test Company") + self.assertNotIn(regular_jc, {r["name"] for r in rows}) + + def test_operation_filter_scopes_rows(self): + corrective_jc, operation, _workstation = self.create_corrective_job_card() + + matching = self.run_report(company="_Test Company", operation=operation) + self.assertIn(corrective_jc.name, {r["name"] for r in matching}) + + other_operation = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + filtered = self.run_report(company="_Test Company", operation=other_operation.name) + self.assertNotIn(corrective_jc.name, {r["name"] for r in filtered}) + + def test_workstation_filter_scopes_rows(self): + corrective_jc, _operation, workstation = self.create_corrective_job_card() + + matching = self.run_report(company="_Test Company", workstation=workstation) + self.assertIn(corrective_jc.name, {r["name"] for r in matching}) + + filtered = self.run_report(company="_Test Company", workstation="__non_existent_ws__") + self.assertNotIn(corrective_jc.name, {r["name"] for r in filtered}) + + def test_work_order_and_name_filters_scope_rows(self): + corrective_jc, _operation, _workstation = self.create_corrective_job_card() + + by_work_order = self.run_report(company="_Test Company", work_order=corrective_jc.work_order) + self.assertIn(corrective_jc.name, {r["name"] for r in by_work_order}) + + by_name = self.run_report(company="_Test Company", name=corrective_jc.name) + self.assertEqual({r["name"] for r in by_name}, {corrective_jc.name}) + + def test_date_filter_scopes_rows(self): + corrective_jc, _operation, _workstation = self.create_corrective_job_card() + + # Time logs sit ~2 hours from now; a window covering today includes the card. + within = self.run_report( + company="_Test Company", + work_order=corrective_jc.work_order, + from_date=add_to_date(now(), days=-1), + to_date=add_to_date(now(), days=1), + ) + self.assertIn(corrective_jc.name, {r["name"] for r in within}) + + # A future-only window excludes it, proving the Job Card Time Log join filters by time. + outside = self.run_report( + company="_Test Company", + work_order=corrective_jc.work_order, + from_date=add_to_date(now(), days=5), + to_date=add_to_date(now(), days=6), + ) + self.assertNotIn(corrective_jc.name, {r["name"] for r in outside}) From 304a247dc8379df4d99d4e41a37270c21f683b3d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:54:23 +0530 Subject: [PATCH 110/195] test: add coverage for BOM Operations Time report --- .../test_bom_operations_time.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py diff --git a/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py new file mode 100644 index 00000000000..44aadda19d3 --- /dev/null +++ b/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py @@ -0,0 +1,101 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +import frappe + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_operations_time.bom_operations_time import execute +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + +OPERATION = "_Test BOM Ops Time Operation" +WORKSTATION = "_Test BOM Ops Time Workstation" +TIME_IN_MINS = 45 + + +class TestBOMOperationsTime(ERPNextTestSuite): + def setUp(self): + ensure_workstation_and_operation() + self.rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name + self.fg_item = make_item(properties={"is_stock_item": 1}).name + self.bom = build_bom_with_operation(self.fg_item, self.rm_item) + + def run_report(self, **extra): + filters = frappe._dict({"bom_id": [self.bom.name]}) + filters.update(extra) + return execute(filters)[1] + + def test_operation_row_appears_with_expected_values(self): + rows = self.run_report() + + bom_rows = [row for row in rows if row.name == self.bom.name] + self.assertEqual(len(bom_rows), 1) + + row = bom_rows[0] + self.assertEqual(row.item, self.fg_item) + self.assertEqual(row.operation, OPERATION) + self.assertEqual(row.workstation, WORKSTATION) + self.assertEqual(row.time_in_mins, TIME_IN_MINS) + + def test_item_code_filter_scopes_to_bom(self): + rows = self.run_report(item_code=self.fg_item) + + self.assertTrue(rows) + self.assertTrue(all(row.item == self.fg_item for row in rows)) + self.assertIn(self.bom.name, {row.name for row in rows}) + + def test_workstation_filter(self): + matching = self.run_report(workstation=WORKSTATION) + self.assertIn(self.bom.name, {row.name for row in matching}) + + other_workstation = ensure_other_workstation() + non_matching = self.run_report(workstation=other_workstation) + self.assertNotIn(self.bom.name, {row.name for row in non_matching}) + + def test_draft_bom_excluded(self): + draft_bom = build_bom_with_operation( + make_item(properties={"is_stock_item": 1}).name, self.rm_item, do_not_submit=True + ) + + rows = execute(frappe._dict({"bom_id": [draft_bom.name]}))[1] + self.assertEqual(rows, []) + + +def ensure_workstation_and_operation(): + if not frappe.db.exists("Workstation", WORKSTATION): + frappe.get_doc({"doctype": "Workstation", "workstation_name": WORKSTATION}).insert( + ignore_permissions=True + ) + + if not frappe.db.exists("Operation", OPERATION): + frappe.get_doc({"doctype": "Operation", "name": OPERATION, "workstation": WORKSTATION}).insert( + ignore_permissions=True + ) + + +def ensure_other_workstation(): + name = "_Test BOM Ops Time Workstation 2" + if not frappe.db.exists("Workstation", name): + frappe.get_doc({"doctype": "Workstation", "workstation_name": name}).insert(ignore_permissions=True) + return name + + +def build_bom_with_operation(fg_item, rm_item, do_not_submit=False): + bom = make_bom( + item=fg_item, + raw_materials=[rm_item], + with_operations=1, + do_not_save=True, + ) + bom.append( + "operations", + { + "operation": OPERATION, + "workstation": WORKSTATION, + "time_in_mins": TIME_IN_MINS, + "hour_rate": 100, + }, + ) + bom.insert(ignore_permissions=True) + if not do_not_submit: + bom.submit() + return bom From c38363c16d783e1fc78e1931a65d865c8e645657 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:54:31 +0530 Subject: [PATCH 111/195] test: add coverage for Production Plan Summary report --- .../test_production_plan_summary.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py diff --git a/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py new file mode 100644 index 00000000000..44901b63da8 --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py @@ -0,0 +1,115 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import create_production_plan +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as make_se_from_wo +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.production_plan_summary.production_plan_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProductionPlanSummary(ERPNextTestSuite): + def run_report(self, production_plan): + filters = frappe._dict({"production_plan": production_plan}) + return execute(filters)[1] + + def make_plan(self, planned_qty=2): + return create_production_plan( + item_code="_Test FG Item", + planned_qty=planned_qty, + skip_getting_mr_items=1, + ) + + def make_submitted_work_order(self, plan, qty): + wo = make_wo_order_test_record( + item_code="_Test FG Item", + qty=qty, + company=plan.company, + wip_warehouse="Work In Progress - _TC", + fg_warehouse="Finished Goods - _TC", + skip_transfer=1, + use_multi_level_bom=1, + do_not_submit=True, + ) + wo.production_plan = plan.name + wo.production_plan_item = plan.po_items[0].name + wo.submit() + return wo + + def get_work_order_row(self, data, item_code): + for row in data: + if row.get("item_code") == item_code and row.get("document_type") == "Work Order": + return row + return None + + def get_summary_row(self, data, item_code): + for row in data: + if row.get("item_code") == item_code and not row.get("document_type"): + return row + return None + + def test_summary_without_work_order(self): + """A submitted plan with no work order still yields a summary row for the planned item.""" + plan = self.make_plan(planned_qty=2) + + data = self.run_report(plan.name) + summary = self.get_summary_row(data, "_Test FG Item") + + self.assertIsNotNone(summary) + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 0) + self.assertIsNone(self.get_work_order_row(data, "_Test FG Item")) + + def test_summary_with_pending_work_order(self): + """An unproduced work order shows full planned qty as pending.""" + plan = self.make_plan(planned_qty=2) + wo = self.make_submitted_work_order(plan, qty=2) + + data = self.run_report(plan.name) + wo_row = self.get_work_order_row(data, "_Test FG Item") + + self.assertIsNotNone(wo_row) + self.assertEqual(wo_row.get("document_name"), wo.name) + self.assertEqual(wo_row.get("qty"), 2) + self.assertEqual(wo_row.get("produced_qty"), 0) + self.assertEqual(wo_row.get("pending_qty"), 2) + + summary = self.get_summary_row(data, "_Test FG Item") + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 0) + + def test_summary_reflects_produced_qty(self): + """Producing part of the work order updates produced and pending quantities.""" + plan = self.make_plan(planned_qty=2) + wo = self.make_submitted_work_order(plan, qty=2) + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) + se.submit() + + data = self.run_report(plan.name) + wo_row = self.get_work_order_row(data, "_Test FG Item") + + self.assertEqual(wo_row.get("document_name"), wo.name) + self.assertEqual(wo_row.get("produced_qty"), 1) + self.assertEqual(wo_row.get("pending_qty"), 1) + + summary = self.get_summary_row(data, "_Test FG Item") + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 1) + self.assertEqual(summary.get("pending_qty"), 1) + + def test_summary_scoped_to_its_own_plan(self): + """Each plan's report only reports its own work order documents.""" + plan_a = self.make_plan(planned_qty=2) + wo_a = self.make_submitted_work_order(plan_a, qty=2) + + plan_b = self.make_plan(planned_qty=3) + wo_b = self.make_submitted_work_order(plan_b, qty=3) + + data_a = self.run_report(plan_a.name) + document_names = {row.get("document_name") for row in data_a if row.get("document_name")} + + self.assertIn(wo_a.name, document_names) + self.assertNotIn(wo_b.name, document_names) From 8b7780d49495bdcf0a97499a668079250496820c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 22:54:38 +0530 Subject: [PATCH 112/195] test: add coverage for Exponential Smoothing Forecasting report --- .../test_exponential_smoothing_forecasting.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py new file mode 100644 index 00000000000..3f8ffd74af9 --- /dev/null +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py @@ -0,0 +1,91 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.manufacturing.report.exponential_smoothing_forecasting.exponential_smoothing_forecasting import ( + execute, +) +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.tests.utils import ERPNextTestSuite + +TEST_ITEM = "_Test Item" +FROM_DATE = "2026-06-01" +TO_DATE = "2026-08-31" + + +class TestExponentialSmoothingForecasting(ERPNextTestSuite): + """Drive real submitted Sales Orders and assert the report buckets the ordered + quantities into the correct historical periods and produces a forecast.""" + + def test_monthly_qty_forecast_from_sales_orders(self): + # Historical demand: distinct calendar months strictly before FROM_DATE. + # Monthly period keys are derived from the period's last day (e.g. "mar_2026"). + history = {"mar_2026": 7, "apr_2026": 4, "may_2026": 9} + self.create_sales_orders( + { + "2026-03-15": history["mar_2026"], + "2026-04-15": history["apr_2026"], + "2026-05-15": history["may_2026"], + } + ) + + columns, row = self.run_report() + fields = {col["fieldname"] for col in columns} + + # For Monthly periodicity only future periods are exposed as columns, each as a + # forecast_ field. Historical demand lives in the row data (keyed by month) but is + # not surfaced as its own column. + self.assertIn("forecast_jun_2026", fields, "expected future forecast column") + self.assertNotIn("jun_2026", fields, "future period must not expose raw demand column") + self.assertNotIn("mar_2026", fields, "historical month is not a Monthly report column") + + # Historical buckets must exactly reflect the ordered quantities. + for key, qty in history.items(): + self.assertEqual(flt(row.get(key)), flt(qty), f"bucket {key} mismatch") + + # A forecast is produced for the first future period. The first non-zero + # historical period seeds the forecast at the average of non-zero months, + # so the future forecast must be positive. + expected_avg = sum(history.values()) / len(history) + self.assertGreater(flt(row.get("forecast_jun_2026")), 0.0) + self.assertLessEqual(flt(row.get("forecast_jun_2026")), max(history.values())) + self.assertAlmostEqual(flt(row.get("avg")), expected_avg, places=6) + + def test_ignores_documents_outside_range_and_other_docstatus(self): + self.create_sales_orders({"2026-05-10": 6}) + # A draft SO and a future-dated SO must not contribute to historical demand. + make_sales_order(item_code=TEST_ITEM, qty=100, transaction_date="2026-05-20", do_not_submit=True) + make_sales_order(item_code=TEST_ITEM, qty=100, transaction_date=FROM_DATE) + + _columns, row = self.run_report() + self.assertEqual(flt(row.get("may_2026")), 6.0) + + def create_sales_orders(self, date_to_qty): + for transaction_date, qty in date_to_qty.items(): + make_sales_order(item_code=TEST_ITEM, qty=qty, transaction_date=transaction_date) + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "based_on_document": "Sales Order", + "based_on_field": "Qty", + "no_of_years": 3, + "periodicity": "Monthly", + "from_date": FROM_DATE, + "to_date": TO_DATE, + "smoothing_constant": 0.5, + "item_code": TEST_ITEM, + } + ) + filters.update(extra) + + columns, data = execute(filters)[:2] + item_row = next( + (r for r in data if r.get("item_code") == TEST_ITEM), + None, + ) + self.assertIsNotNone(item_row, f"{TEST_ITEM} row missing from report output") + return columns, item_row From 4afbd4d3d9de4414744870c0c17c9ef504928d55 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 23:27:51 +0530 Subject: [PATCH 113/195] fix(item-attribute): clear attribute values when marking numeric Marking an attribute numeric hides the Item Attribute Values grid but leaves its rows in the doc, whose mandatory Attribute Value / Abbreviation block the save client-side before the server can clear them. Clear the table on the client too so the save goes through. Co-Authored-By: Claude Opus 4.8 --- .../stock/doctype/item_attribute/item_attribute.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.js b/erpnext/stock/doctype/item_attribute/item_attribute.js index 22c7978ac3c..d6f0f259174 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.js +++ b/erpnext/stock/doctype/item_attribute/item_attribute.js @@ -1,4 +1,13 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Item Attribute", {}); +frappe.ui.form.on("Item Attribute", { + numeric_values(frm) { + // Numeric attributes have no discrete values; drop the rows so their + // mandatory Attribute Value / Abbreviation don't block the save. + if (frm.doc.numeric_values) { + frm.clear_table("item_attribute_values"); + frm.refresh_field("item_attribute_values"); + } + }, +}); From 99152b83004d6719c97ccd8ed86e9cfed7540315 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 23:28:03 +0530 Subject: [PATCH 114/195] fix(item): rework multiple variant dialog for large numeric ranges The 'Create Multiple Variants' dialog rendered one checkbox per attribute value and read the numeric config from the variant attribute child row. This broke in several ways: - A template whose attribute was made numeric after being added kept numeric_values=0 on the child row, so the dialog treated it as non-numeric, queried the empty Item Attribute Value table, and showed no values. - Enumerating a large range (e.g. 1-100000) into checkboxes froze the browser. Rework the dialog: - Read numeric_values / from_range / to_range / increment from the Item Attribute master, and guard increment > 0. - Replace the checkbox-per-value list with one MultiSelectPills per attribute, with a search placeholder. - Stop enumerating numeric ranges: preview the first few values and validate typed input against the range on demand, so huge ranges stay instant. - Block variant creation with a modal error if any selected value or pending input is invalid (out of range, off-increment, or not a number), so garbage like '00A' can't reach creation. Co-Authored-By: Claude Opus 4.8 --- erpnext/stock/doctype/item/item.js | 267 ++++++++++++++++++----------- 1 file changed, 170 insertions(+), 97 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index ed6d4efe43d..75478b7b424 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -1074,62 +1074,135 @@ $.extend(erpnext.item, { function make_fields_from_attribute_values(attr_dict) { let fields = []; - let att_key = frm.doc.attributes.map((idx) => idx.attribute); - att_key.forEach((name, i) => { + let attributes = frm.doc.attributes.filter((row) => !row.disabled); + attributes.forEach((row, i) => { + let name = row.attribute; if (i % 3 === 0) { fields.push({ fieldtype: "Section Break" }); } - fields.push({ fieldtype: "Column Break", label: name }); + fields.push({ fieldtype: "Column Break" }); fields.push({ - fieldtype: "Data", - placeholder: "Search", - fieldname: `search_${frappe.scrub(name)}`, - onchange: function (e) { - let value = e.target.value; - let result = attr_dict[name].filter((attr_value) => - attr_value.toString().toLowerCase().includes(value.toLowerCase()) - ); - attr_dict[name].forEach((attr_value) => { - if (result.includes(attr_value)) { - me.multiple_variant_dialog.set_df_property(attr_value, "hidden", 0); - } else { - me.multiple_variant_dialog.set_df_property(attr_value, "hidden", 1); - } - }); - }, - }); - attr_dict[name].forEach((value) => { - fields.push({ - fieldtype: "Check", - label: value, - fieldname: value, - default: 0, - onchange: function () { - let selected_attributes = get_selected_attributes(); - let lengths = Object.keys(selected_attributes).map((key) => { - return selected_attributes[key].length; - }); - if (!lengths.length) { - me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants")); - me.multiple_variant_dialog.disable_primary_action(); - } else { - let no_of_combinations = lengths.reduce((a, b) => a * b, 1); - let msg; - if (no_of_combinations === 1) { - msg = __("Make {0} Variant", [no_of_combinations]); - } else { - msg = __("Make {0} Variants", [no_of_combinations]); - } - me.multiple_variant_dialog.get_primary_btn().html(msg); - me.multiple_variant_dialog.enable_primary_action(); - } - }, - }); + fieldtype: "MultiSelectPills", + label: name, + fieldname: frappe.scrub(name), + placeholder: __("Search values..."), + get_data: (txt) => get_attribute_suggestions(attr_dict[name], txt), + onchange: update_primary_action, }); }); return fields; } + function get_attribute_suggestions(spec, txt) { + if (!spec) return []; + return Array.isArray(spec) ? filter_list(spec, txt) : numeric_suggestions(spec, txt); + } + + // Cap matches so a long value list never hands everything to Awesomplete, + // which would freeze the browser. + function filter_list(values, txt) { + txt = (txt || "").toLowerCase(); + let matches = []; + for (let value of values) { + if (!txt || value.toLowerCase().includes(txt)) { + matches.push(value); + if (matches.length >= 50) break; + } + } + return matches; + } + + // Numeric ranges aren't enumerated. With no input, preview the first few + // values; once the user types, accept it only if it lies on the increment + // within [from, to]. Both paths are cheap even for huge ranges. + function numeric_suggestions(range, txt) { + let { from_range: from, to_range: to, increment } = range; + if (!(increment > 0) || from > to) return []; + + txt = (txt || "").trim(); + if (!txt) { + let preview = []; + for ( + let value = from; + value <= to && preview.length < 50; + value = flt(value + increment, 6) + ) { + preview.push(String(value)); + } + return preview; + } + + return is_valid_attribute_value(range, txt) ? [String(flt(txt, 6))] : []; + } + + function is_valid_attribute_value(spec, value) { + if (!spec || !value) return false; + if (Array.isArray(spec)) return spec.includes(value); + + let { from_range: from, to_range: to, increment } = spec; + if (!(increment > 0)) return false; + + // Reject anything that isn't cleanly a number ("abc", "5000xyz", ""); + // flt would coerce these to 0 and wrongly accept them. + let text = String(value).trim(); + let num = Number(text); + if (text === "" || !Number.isFinite(num)) return false; + + if (num < from || num > to) return false; + let steps = (num - from) / increment; + return Math.abs(Math.round(steps) - steps) <= 1e-6; + } + + // Block variant creation if ANY value is invalid. Checks both the committed + // pills and any text still sitting in the input box (typed but not selected), + // so garbage like "00A" can never slip through to creation. + function validate_selected_attributes() { + let invalid = []; + frm.doc.attributes.forEach((row) => { + if (row.disabled) return; + let field = me.multiple_variant_dialog.get_field(frappe.scrub(row.attribute)); + if (!field) return; + + let spec = attr_val_fields[row.attribute]; + let values = (field.get_value() || []).slice(); + let pending = (field.$input?.val() || "").trim(); + if (pending) values.push(pending); + + let bad = [...new Set(values.filter((v) => !is_valid_attribute_value(spec, v)))]; + if (bad.length) { + invalid.push(`${frappe.utils.escape_html(row.attribute)}: ${bad.join(", ")}`); + } + }); + + if (invalid.length) { + frappe.throw({ + title: __("Invalid Attribute Values"), + message: + __("Please remove the following invalid values before creating variants:") + + "

" + + invalid.join("
"), + indicator: "red", + }); + } + } + + function update_primary_action() { + let selected_attributes = get_selected_attributes(); + let counts = Object.keys(selected_attributes).map((key) => selected_attributes[key].length); + if (!counts.length) { + me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants")); + me.multiple_variant_dialog.disable_primary_action(); + } else { + let no_of_combinations = counts.reduce((a, b) => a * b, 1); + let msg = + no_of_combinations === 1 + ? __("Make {0} Variant", [no_of_combinations]) + : __("Make {0} Variants", [no_of_combinations]); + me.multiple_variant_dialog.get_primary_btn().html(msg); + me.multiple_variant_dialog.enable_primary_action(); + } + } + function make_and_show_dialog(fields) { me.multiple_variant_dialog = new frappe.ui.Dialog({ title: __("Select Attribute Values"), @@ -1155,6 +1228,8 @@ $.extend(erpnext.item, { }); me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => { + validate_selected_attributes(); + let selected_attributes = get_selected_attributes(); let use_template_image = me.multiple_variant_dialog.get_value("use_template_image"); @@ -1182,72 +1257,70 @@ $.extend(erpnext.item, { }); }); - $($(me.multiple_variant_dialog.$wrapper.find(".form-column")).find(".frappe-control")).css( - "margin-bottom", - "0px" - ); - me.multiple_variant_dialog.disable_primary_action(); me.multiple_variant_dialog.clear(); me.multiple_variant_dialog.show(); - me.multiple_variant_dialog.$wrapper - .find("div[data-fieldname^='search_']") - .find(".clearfix") - .hide(); } function get_selected_attributes() { let selected_attributes = {}; - me.multiple_variant_dialog.$wrapper.find(".form-column").each((i, col) => { - if (i === 0) return; - let attribute_name = $(col).find(".column-label").html().trim(); - selected_attributes[attribute_name] = []; - let checked_opts = $(col).find(".checkbox input"); - checked_opts.each((i, opt) => { - if ($(opt).is(":checked")) { - selected_attributes[attribute_name].push($(opt).attr("data-fieldname")); - } - }); - if (!selected_attributes[attribute_name].length) { - delete selected_attributes[attribute_name]; + frm.doc.attributes.forEach((row) => { + if (row.disabled) return; + let values = me.multiple_variant_dialog.get_value(frappe.scrub(row.attribute)); + if (values && values.length) { + selected_attributes[row.attribute] = values; } }); - return selected_attributes; } frm.doc.attributes.forEach(function (d) { if (!d.disabled) { let p = new Promise((resolve) => { - if (!d.numeric_values) { - frappe - .call({ - method: "frappe.client.get_list", - args: { - doctype: "Item Attribute Value", - filters: [["parent", "=", d.attribute]], - fields: ["attribute_value"], - limit_page_length: 0, - parent: "Item Attribute", - order_by: "idx", - }, - }) - .then((r) => { - if (r.message) { - attr_val_fields[d.attribute] = r.message.map(function (d) { - return d.attribute_value; + // Read the numeric configuration from the Item Attribute master + // instead of the variant attribute row, which may be stale or + // blank if the attribute was made numeric after it was added here. + frappe.db + .get_value("Item Attribute", d.attribute, [ + "numeric_values", + "from_range", + "to_range", + "increment", + ]) + .then((res) => { + let attr = res.message || {}; + + if (!attr.numeric_values) { + frappe + .call({ + method: "frappe.client.get_list", + args: { + doctype: "Item Attribute Value", + filters: [["parent", "=", d.attribute]], + fields: ["attribute_value"], + limit_page_length: 0, + parent: "Item Attribute", + order_by: "idx", + }, + }) + .then((r) => { + attr_val_fields[d.attribute] = (r.message || []).map( + (row) => row.attribute_value + ); + resolve(); }); - resolve(); - } - }); - } else { - let values = []; - for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { - values.push(i); - } - attr_val_fields[d.attribute] = values; - resolve(); - } + } else { + // Store the range instead of enumerating it; a large range + // (e.g. 1-100000) is slow to build and to search. Values are + // validated against the range on demand while typing. + attr_val_fields[d.attribute] = { + from_range: flt(attr.from_range), + to_range: flt(attr.to_range), + increment: flt(attr.increment), + }; + resolve(); + } + }); }); promises.push(p); From d4da9a3d7df040392e591a9bb075d971506ea5df Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 23:37:32 +0530 Subject: [PATCH 115/195] fix(item): error on uncommitted input and escape values in variant dialog Address review feedback: - A typed-but-not-selected value passed validation yet was dropped by get_selected_attributes (reads committed pills only). Treat any pending input as an error so it is never silently omitted from creation. - Escape pill / pending values before interpolating them into the HTML error message. Co-Authored-By: Claude Opus 4.8 --- erpnext/stock/doctype/item/item.js | 36 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 75478b7b424..5eb7f07f4bd 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -1153,34 +1153,40 @@ $.extend(erpnext.item, { return Math.abs(Math.round(steps) - steps) <= 1e-6; } - // Block variant creation if ANY value is invalid. Checks both the committed - // pills and any text still sitting in the input box (typed but not selected), - // so garbage like "00A" can never slip through to creation. + // Block variant creation if anything is wrong: an invalid committed pill, or + // text typed but not added as a pill (which get_selected_attributes would + // otherwise drop silently). The user must fix each before creation proceeds. function validate_selected_attributes() { - let invalid = []; + let errors = []; frm.doc.attributes.forEach((row) => { if (row.disabled) return; let field = me.multiple_variant_dialog.get_field(frappe.scrub(row.attribute)); if (!field) return; + let attribute = frappe.utils.escape_html(row.attribute); let spec = attr_val_fields[row.attribute]; - let values = (field.get_value() || []).slice(); - let pending = (field.$input?.val() || "").trim(); - if (pending) values.push(pending); - let bad = [...new Set(values.filter((v) => !is_valid_attribute_value(spec, v)))]; - if (bad.length) { - invalid.push(`${frappe.utils.escape_html(row.attribute)}: ${bad.join(", ")}`); + let invalid = [ + ...new Set((field.get_value() || []).filter((v) => !is_valid_attribute_value(spec, v))), + ]; + if (invalid.length) { + let values = invalid.map(frappe.utils.escape_html).join(", "); + errors.push(__("{0}: remove invalid value(s) {1}", [attribute, values])); + } + + let pending = (field.$input?.val() || "").trim(); + if (pending) { + let value = frappe.utils.escape_html(pending); + errors.push( + __("{0}: select the typed value {1} from the list or clear it", [attribute, value]) + ); } }); - if (invalid.length) { + if (errors.length) { frappe.throw({ title: __("Invalid Attribute Values"), - message: - __("Please remove the following invalid values before creating variants:") + - "

" + - invalid.join("
"), + message: errors.join("
"), indicator: "red", }); } From 5e1296a0b96da0450847782d30c775283a5db4b7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 1 Jul 2026 14:07:09 +0530 Subject: [PATCH 116/195] perf(postgres): partial/covering indexes + trigram item search Postgres-guarded on_doctype_update indexes: partial WHERE is_cancelled=0 + covering INCLUDE on GL Entry/SLE and Serial and Batch Bundle/Entry, and pg_trgm GIN on Item item_code/item_name (~128x faster LIKE search at scale). No-ops on MariaDB. Requires frappe framework support. --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 19 +++++++++++++++++ erpnext/stock/doctype/item/item.py | 13 +++++++++++- .../serial_and_batch_bundle.py | 21 +++++++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.py | 12 +++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index a838e1647d2..de1db2cab82 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -471,6 +471,25 @@ def on_doctype_update(): frappe.db.add_index("GL Entry", ["posting_date", "company"]) frappe.db.add_index("GL Entry", ["party_type", "party"]) + if frappe.db.db_type == "postgres": + # Postgres-only partial/covering indexes for the financial reports (General Ledger, Trial + # Balance, Balance Sheet, P&L), which always filter `is_cancelled = 0` and scope by company. + # `where`/`include` are no-ops on MariaDB and its optimizer ignores these anyway, so they are + # added only on postgres to avoid dead write overhead on this insert-hot table. + frappe.db.add_index( + "GL Entry", + ["company", "posting_date", "account"], + index_name="gle_active_detail", + where="is_cancelled = 0", + ) + frappe.db.add_index( + "GL Entry", + ["company", "account", "posting_date"], + index_name="gle_active_cover", + where="is_cancelled = 0", + include=["debit", "credit"], + ) + def rename_gle_sle_docs(): for doctype in ["GL Entry", "Stock Ledger Entry"]: diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index a26f58430bf..632b3874b47 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1372,7 +1372,8 @@ def get_purchase_voucher_details(doctype, item_code, document_name=None): query = query.select(parent_doc.transaction_date) query = query.orderby(parent_doc.transaction_date, parent_doc.name, order=Order.desc) - return query.run(as_dict=1) + # only the latest ([0]) row is ever used, so fetch just that instead of every purchase of the item + return query.limit(1).run(as_dict=1) def check_stock_uom_with_bin(item, stock_uom): @@ -1762,3 +1763,13 @@ def get_default_warehouse_for_opening_stock(item, company: str, warehouse: str | "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Stock Settings." ).format(frappe.bold(company)) ) + + +def on_doctype_update(): + if frappe.db.db_type == "postgres": + # The Item link-search (erpnext.controllers.queries.item_query) filters + # `item_code/item_name LIKE '%txt%'` -- a leading-wildcard LIKE no btree can serve. pg_trgm + # GIN indexes accelerate it. Item is read-heavy/write-light master data, so GIN maintenance + # cost is negligible. Postgres-only (`using` is a no-op on MariaDB, which has its own FULLTEXT). + frappe.db.add_index("Item", ["item_code"], using="gin_trgm") + frappe.db.add_index("Item", ["item_name"], using="gin_trgm") diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 747b43ca53f..09e6c87a58b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1814,6 +1814,27 @@ class SerialandBatchBundle(Document): self.set("entries", []) +def on_doctype_update(): + if frappe.db.db_type == "postgres": + # Bundle-direct lookups (get_ledgers_from_serial_batch_bundle, get_picked_*) always filter + # `is_cancelled = 0` and scope by voucher_no or item_code+warehouse -- none of which the parent + # bundle is otherwise indexed on (only voucher_type/voucher_detail_no are). Partial indexes keep + # only the active bundles. Postgres-only (`where` is a no-op on MariaDB, and MariaDB's optimizer + # ignores partial predicates anyway). + frappe.db.add_index( + "Serial and Batch Bundle", + ["voucher_no"], + index_name="sabb_active_voucher", + where="is_cancelled = 0", + ) + frappe.db.add_index( + "Serial and Batch Bundle", + ["item_code", "warehouse"], + index_name="sabb_active_item_wh", + where="is_cancelled = 0", + ) + + @frappe.whitelist() def download_blank_csv_template(content: str | list): csv_data = [] diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 8ec74a3df4d..f20f078f0f3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -364,3 +364,15 @@ class StockLedgerEntry(Document): def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["item_code", "warehouse", "posting_datetime", "creation"]) + + if frappe.db.db_type == "postgres": + # Postgres-only partial index for date-range stock reports (Stock Ledger / Stock Balance) + # that scan across all items: they filter `is_cancelled = 0` and sort by posting_datetime. + # The existing item_code-leading composite can't serve an all-items date scan. `where` is a + # no-op on MariaDB, so this is added only on postgres. + frappe.db.add_index( + "Stock Ledger Entry", + ["company", "posting_datetime", "creation"], + index_name="sle_active_posting", + where="is_cancelled = 0", + ) From 4e88157ed78562d5e0ac752d9ee70f6ec8613030 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 00:02:50 +0530 Subject: [PATCH 117/195] test: stock raw materials before manufacture to avoid negative stock in CI --- .../test_production_plan_summary.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py index 44901b63da8..fc47a3065d1 100644 --- a/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py @@ -7,6 +7,7 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import c from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as make_se_from_wo from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.report.production_plan_summary.production_plan_summary import execute +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.tests.utils import ERPNextTestSuite @@ -38,6 +39,17 @@ class TestProductionPlanSummary(ERPNextTestSuite): wo.submit() return wo + def stock_required_materials(self, wo): + # make sure every raw material is available in its source warehouse before manufacturing, + # otherwise a clean database raises NegativeStockError + for item in wo.required_items: + make_stock_entry( + item_code=item.item_code, + to_warehouse=item.source_warehouse or "_Test Warehouse - _TC", + qty=item.required_qty + 10, + rate=100, + ) + def get_work_order_row(self, data, item_code): for row in data: if row.get("item_code") == item_code and row.get("document_type") == "Work Order": @@ -84,6 +96,7 @@ class TestProductionPlanSummary(ERPNextTestSuite): """Producing part of the work order updates produced and pending quantities.""" plan = self.make_plan(planned_qty=2) wo = self.make_submitted_work_order(plan, qty=2) + self.stock_required_materials(wo) se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) se.submit() From b09889643fa04edbfc79012cb0a8c7e74afaef27 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 00:04:13 +0530 Subject: [PATCH 118/195] test: don't override tearDown; rely on ERPNextTestSuite rollback --- .../bom_variance_report/test_bom_variance_report.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py index e70bad995b4..718dba7d29c 100644 --- a/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py @@ -19,18 +19,10 @@ class TestBOMVarianceReport(ERPNextTestSuite): ) self.raw_materials = self.get_bom_raw_materials() - self.overproduction_percentage = frappe.db.get_single_value( - "Manufacturing Settings", "overproduction_percentage_for_work_order" - ) + # allow over-production so a Work Order can produce more than planned; ERPNextTestSuite + # rolls this back at tearDown, so no manual restore is needed frappe.db.set_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order", 100) - def tearDown(self): - frappe.db.set_single_value( - "Manufacturing Settings", - "overproduction_percentage_for_work_order", - self.overproduction_percentage, - ) - def get_bom_raw_materials(self): return { row.item_code: row.qty From 9e5b492db138364025814845e9c2e44329ac46b6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 00:20:15 +0530 Subject: [PATCH 119/195] fix: tighten repost gate timeout, key, and scope (review) - REPOST_LOCK_TIMEOUT 600 -> 300s: a contended waiter re-queues and frees its long-queue worker slot sooner instead of pinning it for up to 10 minutes (still well under the 1800s repost job timeout). - Collision-free lock key: pass a ("stock_repost", item, warehouse) tuple so a colon in item_code/warehouse can't map two distinct pairs onto one lock. - Document that the gate is repost-vs-repost only; the synchronous repost_current_voucher submit path is deliberately left ungated (gating a live submit behind a background repost would be a worse regression). --- erpnext/stock/stock_ledger.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 49cec270c86..db5bc5b5f66 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -262,22 +262,25 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): return sle -# Reposts wait this long for the per-(item, warehouse) gate before giving up. A repost of one item -# is bounded, and a QueryTimeoutError here is recoverable -- the repost job re-queues and retries. -REPOST_LOCK_TIMEOUT = 600 +# A repost waits this long for another repost's per-(item, warehouse) gate before giving up. Kept +# well under the 1800s repost job timeout so a wait can't burn the whole budget, and short enough +# that a contended worker re-queues (recoverable QueryTimeoutError) and frees the slot for other +# items instead of pinning it. +REPOST_LOCK_TIMEOUT = 300 def repost_gate(item_code, warehouse): - """Serialize concurrent reposts of the same (item, warehouse) with a session-level advisory lock - acquired BEFORE the inner `... for update` row locks. It is an outer gate: two reposts touching - the same item wait for each other instead of racing into a lock-order deadlock (which repost - otherwise only survives by retrying). The row locks still enforce correctness -- this only cuts - the deadlock/retry churn. Advisory locks exist on postgres + mariadb; elsewhere there is no gate.""" - # hasattr keeps this a graceful opt-in: if this ERPNext deploys before frappe.db.advisory_lock - # lands (frappe#40466), fall back to no gate rather than raising a non-recoverable AttributeError - # that would permanently mark the Repost Item Valuation as Failed. + """Serialize concurrent background reposts of the same (item, warehouse) with a session-level + advisory lock taken before the inner `... for update` row locks, so they take turns instead of + racing into a lock-order deadlock. Row locks still enforce correctness; this only cuts the + deadlock/retry churn. Scope is repost-vs-repost only -- the synchronous repost_current_voucher + submit path is deliberately not gated (blocking a submit behind a background repost would be a + worse regression) and keeps relying on the existing deadlock retry. No advisory locks, no gate.""" + # hasattr keeps this a graceful opt-in: on an ERPNext predating frappe.db.advisory_lock, fall + # back to no gate rather than raising and marking the Repost Item Valuation permanently Failed. if frappe.db.db_type in ("postgres", "mariadb") and hasattr(frappe.db, "advisory_lock"): - return frappe.db.advisory_lock(f"stock_repost:{item_code}:{warehouse}", timeout=REPOST_LOCK_TIMEOUT) + # Tuple key: a colon in item_code/warehouse can't collide two distinct pairs onto one lock. + return frappe.db.advisory_lock(("stock_repost", item_code, warehouse), timeout=REPOST_LOCK_TIMEOUT) return nullcontext() From 145a0b154ed4156376cb11953c417435cbf19af0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 00:34:36 +0530 Subject: [PATCH 120/195] fix: apply item and work_order filters in Process Loss Report --- .../process_loss_report.py | 8 ++--- .../test_process_loss_report.py | 30 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index 2ba9f4742fd..084bcbf6060 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -50,11 +50,11 @@ def get_data(filters: Filters) -> Data: .groupby(se.work_order) ) - if "item" in filters: - query.where(wo.production_item == filters.item) + if filters.get("item"): + query = query.where(wo.production_item == filters.item) - if "work_order" in filters: - query.where(wo.name == filters.work_order) + if filters.get("work_order"): + query = query.where(wo.name == filters.work_order) data = query.run(as_dict=True) diff --git a/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py index 873b39446ff..d7ac8dd67e3 100644 --- a/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py @@ -73,7 +73,7 @@ class TestProcessLossReport(ERPNextTestSuite): self.assertEqual(wo_order.process_loss_qty, 1) self.assertEqual(wo_order.produced_qty, 4) - data = self.run_report(work_order=wo_order.name) + data = self.run_report() row = self.find_row(data, wo_order.name) self.assertIsNotNone(row, "Work order with process loss should appear in the report") @@ -93,26 +93,22 @@ class TestProcessLossReport(ERPNextTestSuite): self.assertEqual(wo_order.process_loss_qty, 0) self.assertEqual(wo_order.produced_qty, 5) - data = self.run_report(work_order=wo_order.name) + data = self.run_report() self.assertIsNone( self.find_row(data, wo_order.name), "Work order that produced the full planned qty should not appear (no loss)", ) - def test_item_and_work_order_filters_are_ineffective(self): - """BUG: the `item` and `work_order` filters in process_loss_report.get_data - call `query.where(...)` without reassigning the result. frappe's query - builder is immutable, so `.where()` returns a new query and these extra - conditions are silently dropped. A non-matching item filter therefore fails - to exclude the row. This test documents the current (buggy) behaviour; if the - report is fixed to reassign the query, update the assertion below to - `assertIsNone`. - """ + def test_item_filter_scopes_rows(self): wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) - # A non-matching item filter should exclude the row, but currently does not. - data = self.run_report(item="_Test FG Item 2") - self.assertIsNotNone( - self.find_row(data, wo_order.name), - "Filter bug regressed/fixed: `item` filter now takes effect - update this test", - ) + # a matching production item includes the row, a non-matching one excludes it + self.assertIsNotNone(self.find_row(self.run_report(item="_Test FG Item"), wo_order.name)) + self.assertIsNone(self.find_row(self.run_report(item="_Test FG Item 2"), wo_order.name)) + + def test_work_order_filter_scopes_rows(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) + + # the matching work order is included, a different work order name is excluded + self.assertIsNotNone(self.find_row(self.run_report(work_order=wo_order.name), wo_order.name)) + self.assertIsNone(self.find_row(self.run_report(work_order=f"{wo_order.name}-XX"), wo_order.name)) From e5569f681adbd14f254e4e594d064628cc1e8ff0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 00:52:20 +0530 Subject: [PATCH 121/195] fix: classify repost recovery by exception type, not traceback string repost() decided whether a failed Repost Item Valuation was recoverable (re-queue as "In Progress") or permanently "Failed" by string-matching the traceback for "timeout" or MariaDB's "Deadlock found". On Postgres a deadlock surfaces as "deadlock detected" / "could not serialize access" and matches neither, so a retriable deadlock was marked Failed and never re-queued -- the scheduler only re-picks Queued/In Progress entries. Classify by isinstance(e, RecoverableErrors) instead, the same tuple already used to gate the error email. This covers deadlocks and lock/query timeouts on both engines (frappe raises QueryDeadlockError / QueryTimeoutError uniformly) and the advisory-lock repost gate's own QueryTimeoutError, which previously recovered only because its class name incidentally contains "timeout". --- .../repost_item_valuation.py | 11 ++++--- .../test_repost_item_valuation.py | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index b9bb3d931da..e113b8fe464 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -425,10 +425,10 @@ def repost(doc): if isinstance(message, dict): message = message.get("message") - status = "Failed" - # If failed because of timeout, set status to In Progress - if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback): - status = "In Progress" + # Recoverable errors (deadlock, lock/query timeout, job timeout) re-queue as In Progress. + # Classify by type: the old traceback string-match only knew MariaDB's "Deadlock found" and + # missed Postgres deadlocks ("deadlock detected"), failing them permanently. + status = "In Progress" if isinstance(e, RecoverableErrors) else "Failed" if traceback: message += "

" + "Traceback:
" + traceback @@ -447,7 +447,8 @@ def repost(doc): "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" ) - if outgoing_email_account and not isinstance(e, RecoverableErrors): + # status == "Failed" already implies e is not recoverable, so no need to re-check here. + if outgoing_email_account: notify_error_to_stock_managers(doc, message) doc.set_status("Failed") finally: diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 40df17a061d..d2c5eaa6096 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -220,6 +220,39 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin): sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), ) + def test_recoverable_error_requeues_instead_of_failing(self): + # A recoverable DB error (e.g. Postgres deadlock -> QueryDeadlockError) must re-queue the + # repost as "In Progress"; a non-recoverable error still fails. Regression: the old check + # string-matched MariaDB's "Deadlock found" and missed Postgres deadlocks ("deadlock detected"). + from unittest.mock import patch + + from frappe.exceptions import QueryDeadlockError + + from erpnext.stock.doctype.repost_item_valuation import repost_item_valuation as riv + + orig_max_writes = frappe.db.MAX_WRITES_PER_TRANSACTION + self.addCleanup(setattr, frappe.db, "MAX_WRITES_PER_TRANSACTION", orig_max_writes) + + def status_after(error): + doc = frappe.new_doc("Repost Item Valuation") + doc.name = "test-recoverable-riv" + doc.set_status = doc.log_error = doc.db_set = MagicMock() + captured = {} + with ( + patch.object(frappe, "in_test", False), + patch.object(frappe.db, "exists", return_value=True), + patch.object(frappe.db, "commit"), + patch.object(frappe.db, "rollback"), + patch.object(frappe.db, "set_value", side_effect=lambda *a, **k: captured.update(a[2])), + patch.object(riv, "repost_sl_entries", side_effect=error), + patch.object(frappe, "get_cached_value", return_value=None), + ): + riv.repost(doc) + return captured.get("status") + + self.assertEqual(status_after(QueryDeadlockError("deadlock detected")), "In Progress") + self.assertEqual(status_after(ValueError("boom")), "Failed") + def test_gl_repost_progress(self): from erpnext.accounts import utils From 48aef307f9fc2c625a79efa41198af0be62317d5 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 2 Jul 2026 02:19:38 +0530 Subject: [PATCH 122/195] fix: surface create payment entries as primary action on row selection --- .../accounts_payable/accounts_payable.js | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 2fa6ceeb08c..8541e094640 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -174,7 +174,17 @@ frappe.query_reports["Accounts Payable"] = { }, get_datatable_options(options) { - return Object.assign(options, { checkboxColumn: true }); + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: () => erpnext.accounts.toggle_create_pe_primary_action(frappe.query_report), + }, + }); + }, + + after_refresh: function (report) { + report.datatable?.rowmanager?.checkAll(false); + report.page.clear_primary_action(); }, onload: function (report) { @@ -186,20 +196,27 @@ frappe.query_reports["Accounts Payable"] = { if (frappe.boot.sysdefaults.default_ageing_range) { report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range); } - - if (frappe.model.can_create("Payment Entry")) { - report.page.add_inner_button( - __("Create Payment Entries"), - function () { - erpnext.accounts.create_payment_entries_from_payable_report(report); - }, - __("Actions") - ); - } }, }; frappe.provide("erpnext.accounts"); + +erpnext.accounts.toggle_create_pe_primary_action = function (report) { + if (!report || !report.datatable || !frappe.model.can_create("Payment Entry")) return; + + const has_purchase_invoice = report.datatable.rowmanager + .getCheckedRows() + .some((i) => report.datatable.datamanager.data[i]?.voucher_type === "Purchase Invoice"); + + if (has_purchase_invoice) { + report.page.set_primary_action(__("Create Payment Entries"), () => + erpnext.accounts.create_payment_entries_from_payable_report(report) + ); + } else { + report.page.clear_primary_action(); + } +}; + erpnext.accounts.create_payment_entries_from_payable_report = function (report) { const datatable = report.datatable; if (!datatable) return; From d84eb9a97b3e8fb6d9d7c63626c3a2e16b274d71 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 2 Jul 2026 04:25:22 +0530 Subject: [PATCH 123/195] fix: remove roles from budgeting workspace --- .../workspace/budgeting/budgeting.json | 78 +------------------ 1 file changed, 2 insertions(+), 76 deletions(-) diff --git a/erpnext/accounts/workspace/budgeting/budgeting.json b/erpnext/accounts/workspace/budgeting/budgeting.json index d330c74cd01..03e1d96b6e8 100644 --- a/erpnext/accounts/workspace/budgeting/budgeting.json +++ b/erpnext/accounts/workspace/budgeting/budgeting.json @@ -15,7 +15,7 @@ "label": "Budgeting", "link_type": "DocType", "links": [], - "modified": "2026-06-28 21:04:03.108867", + "modified": "2026-07-02 04:24:48.116724", "modified_by": "Administrator", "module": "Accounts", "name": "Budgeting", @@ -23,11 +23,7 @@ "owner": "Administrator", "public": 1, "quick_lists": [], - "roles": [ - { - "role": "Academics User" - } - ], + "roles": [], "sequence_id": 57.0, "shortcuts": [], "sidebar_items": [ @@ -45,20 +41,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "icon": "briefcase-business", - "indent": 0, - "keep_closed": 0, - "label": "Budget", - "link_to": "Budget", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, { "child": 0, "collapsible": 1, @@ -73,34 +55,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "icon": "badge-cent", - "indent": 0, - "keep_closed": 0, - "label": "Cost Center", - "link_to": "Cost Center", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "icon": "accounting", - "indent": 0, - "keep_closed": 0, - "label": "Accounting Dimension", - "link_to": "Accounting Dimension", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, { "child": 0, "collapsible": 1, @@ -129,34 +83,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "icon": "notepad-text", - "indent": 0, - "keep_closed": 0, - "label": "Cost Center Allocation", - "link_to": "Cost Center Allocation", - "link_type": "DocType", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "default_workspace": 0, - "icon": "sheet", - "indent": 0, - "keep_closed": 0, - "label": "Budget Variance", - "link_to": "Budget Variance Report", - "link_type": "Report", - "open_in_new_tab": 0, - "show_arrow": 0, - "type": "Link" - }, { "child": 0, "collapsible": 1, From ba1e8f0005b15f3967671bf6882906e8fca94d60 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:27:14 +0200 Subject: [PATCH 124/195] fix(Quotation): create Customer from Lead (#55923) --- erpnext/selling/doctype/quotation/mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/mapper.py b/erpnext/selling/doctype/quotation/mapper.py index 2182c969d4e..8ebdaf125d1 100644 --- a/erpnext/selling/doctype/quotation/mapper.py +++ b/erpnext/selling/doctype/quotation/mapper.py @@ -228,7 +228,7 @@ def _make_customer(source_name, ignore_permissions=False): def create_customer_from_lead(lead_name, ignore_permissions=False): - from erpnext.crm.doctype.lead.lead import _make_customer + from erpnext.crm.doctype.lead.mapper import _make_customer customer = _make_customer(lead_name, ignore_permissions=ignore_permissions) customer.flags.ignore_permissions = ignore_permissions From 9cea43b006124b9428b4fa0df807fd58959d5ab9 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 2 Jul 2026 07:42:34 +0530 Subject: [PATCH 125/195] fix(company): ignore user permissions for link fields having link to `Account` and `Cost Center` (#56748) --- erpnext/setup/doctype/company/company.json | 44 +++++++++++++++++++++- erpnext/setup/doctype/company/company.py | 1 + 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 1bf29c6801b..bc3b11e7854 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -353,33 +353,48 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_cost_center", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off Cost Center", + "no_copy": 1, "options": "Cost Center" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "write_off_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Write Off Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "exchange_gain_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Exchange Gain / Loss Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_exchange_gain_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Exchange Gain/Loss Account", + "no_copy": 1, "options": "Account" }, { @@ -526,15 +541,19 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Accumulated Depreciation Account", "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "depreciation_expense_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Depreciation Expense Account", "no_copy": 1, "options": "Account" @@ -549,29 +568,39 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "disposal_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Gain/Loss Account on Asset Disposal", "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "depreciation_cost_center", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Asset Depreciation Cost Center", "no_copy": 1, "options": "Cost Center" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "capital_work_in_progress_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Capital Work In Progress Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "asset_received_but_not_billed", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Asset Received But Not Billed", + "no_copy": 1, "options": "Account" }, { @@ -703,15 +732,21 @@ "options": "Warehouse" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Profit / Loss Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "default_discount_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Payment Discount Account", + "no_copy": 1, "options": "Account" }, { @@ -753,8 +788,10 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_received_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Advance Received Account", "mandatory_depends_on": "book_advance_payments_as_liability", + "no_copy": 1, "options": "Account" }, { @@ -763,8 +800,10 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_paid_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Advance Paid Account", "mandatory_depends_on": "book_advance_payments_as_liability", + "no_copy": 1, "options": "Account" }, { @@ -844,9 +883,12 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_for_opening", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off for Opening", + "no_copy": 1, "options": "Account" }, { @@ -1026,7 +1068,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2026-07-01 11:48:07.853494", + "modified": "2026-07-02 07:21:21.794533", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 2670de73902..8cfb46828ff 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -84,6 +84,7 @@ class Company(NestedSet): default_operating_cost_account: DF.Link | None default_payable_account: DF.Link | None default_provisional_account: DF.Link | None + default_purchase_price_variance_account: DF.Link | None default_receivable_account: DF.Link | None default_sales_contact: DF.Link | None default_scrap_warehouse: DF.Link | None From 0f812e068654effdda3605e870848fbfb3cd7494 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 07:54:52 +0530 Subject: [PATCH 126/195] test: strengthen BOM Operations Time filter isolation coverage --- .../test_bom_operations_time.py | 94 +++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py index 44aadda19d3..10a55e4829d 100644 --- a/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py @@ -9,77 +9,93 @@ from erpnext.tests.utils import ERPNextTestSuite OPERATION = "_Test BOM Ops Time Operation" WORKSTATION = "_Test BOM Ops Time Workstation" +OTHER_OPERATION = "_Test BOM Ops Time Operation 2" +OTHER_WORKSTATION = "_Test BOM Ops Time Workstation 2" TIME_IN_MINS = 45 class TestBOMOperationsTime(ERPNextTestSuite): def setUp(self): - ensure_workstation_and_operation() + ensure_workstation_and_operation(WORKSTATION, OPERATION) self.rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name self.fg_item = make_item(properties={"is_stock_item": 1}).name - self.bom = build_bom_with_operation(self.fg_item, self.rm_item) + self.bom = build_bom_with_operation(self.fg_item, self.rm_item, OPERATION, WORKSTATION) - def run_report(self, **extra): - filters = frappe._dict({"bom_id": [self.bom.name]}) - filters.update(extra) - return execute(filters)[1] + def run_report(self, **filters): + return execute(frappe._dict(filters))[1] + + def bom_names(self, rows): + return {row.name for row in rows} + + def build_other_bom(self): + """A submitted BOM for a different item, built on a different workstation.""" + ensure_workstation_and_operation(OTHER_WORKSTATION, OTHER_OPERATION) + other_fg = make_item(properties={"is_stock_item": 1}).name + return build_bom_with_operation(other_fg, self.rm_item, OTHER_OPERATION, OTHER_WORKSTATION) def test_operation_row_appears_with_expected_values(self): - rows = self.run_report() + rows = self.run_report(bom_id=[self.bom.name]) - bom_rows = [row for row in rows if row.name == self.bom.name] - self.assertEqual(len(bom_rows), 1) - - row = bom_rows[0] + self.assertEqual(len(rows), 1) + row = rows[0] + self.assertEqual(row.name, self.bom.name) self.assertEqual(row.item, self.fg_item) self.assertEqual(row.operation, OPERATION) self.assertEqual(row.workstation, WORKSTATION) self.assertEqual(row.time_in_mins, TIME_IN_MINS) - def test_item_code_filter_scopes_to_bom(self): - rows = self.run_report(item_code=self.fg_item) + def test_item_code_filter_includes_matching_and_excludes_other(self): + other_bom = self.build_other_bom() - self.assertTrue(rows) - self.assertTrue(all(row.item == self.fg_item for row in rows)) - self.assertIn(self.bom.name, {row.name for row in rows}) + # no bom_id here, so the item_code filter alone must scope the result + names = self.bom_names(self.run_report(item_code=self.fg_item)) + self.assertIn(self.bom.name, names) + self.assertNotIn(other_bom.name, names) - def test_workstation_filter(self): - matching = self.run_report(workstation=WORKSTATION) - self.assertIn(self.bom.name, {row.name for row in matching}) + # reverse direction: filtering the other item drops our BOM + other_names = self.bom_names(self.run_report(item_code=other_bom.item)) + self.assertIn(other_bom.name, other_names) + self.assertNotIn(self.bom.name, other_names) - other_workstation = ensure_other_workstation() - non_matching = self.run_report(workstation=other_workstation) - self.assertNotIn(self.bom.name, {row.name for row in non_matching}) + def test_workstation_filter_includes_matching_and_excludes_other(self): + other_bom = self.build_other_bom() + + # no bom_id here, so the workstation filter alone must scope the result + names = self.bom_names(self.run_report(workstation=WORKSTATION)) + self.assertIn(self.bom.name, names) + self.assertNotIn(other_bom.name, names) + + # reverse direction: filtering the other workstation drops our BOM + other_names = self.bom_names(self.run_report(workstation=OTHER_WORKSTATION)) + self.assertIn(other_bom.name, other_names) + self.assertNotIn(self.bom.name, other_names) def test_draft_bom_excluded(self): draft_bom = build_bom_with_operation( - make_item(properties={"is_stock_item": 1}).name, self.rm_item, do_not_submit=True + make_item(properties={"is_stock_item": 1}).name, + self.rm_item, + OPERATION, + WORKSTATION, + do_not_submit=True, ) - rows = execute(frappe._dict({"bom_id": [draft_bom.name]}))[1] + rows = self.run_report(bom_id=[draft_bom.name]) self.assertEqual(rows, []) -def ensure_workstation_and_operation(): - if not frappe.db.exists("Workstation", WORKSTATION): - frappe.get_doc({"doctype": "Workstation", "workstation_name": WORKSTATION}).insert( +def ensure_workstation_and_operation(workstation, operation): + if not frappe.db.exists("Workstation", workstation): + frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation}).insert( ignore_permissions=True ) - if not frappe.db.exists("Operation", OPERATION): - frappe.get_doc({"doctype": "Operation", "name": OPERATION, "workstation": WORKSTATION}).insert( + if not frappe.db.exists("Operation", operation): + frappe.get_doc({"doctype": "Operation", "name": operation, "workstation": workstation}).insert( ignore_permissions=True ) -def ensure_other_workstation(): - name = "_Test BOM Ops Time Workstation 2" - if not frappe.db.exists("Workstation", name): - frappe.get_doc({"doctype": "Workstation", "workstation_name": name}).insert(ignore_permissions=True) - return name - - -def build_bom_with_operation(fg_item, rm_item, do_not_submit=False): +def build_bom_with_operation(fg_item, rm_item, operation, workstation, do_not_submit=False): bom = make_bom( item=fg_item, raw_materials=[rm_item], @@ -89,8 +105,8 @@ def build_bom_with_operation(fg_item, rm_item, do_not_submit=False): bom.append( "operations", { - "operation": OPERATION, - "workstation": WORKSTATION, + "operation": operation, + "workstation": workstation, "time_in_mins": TIME_IN_MINS, "hour_rate": 100, }, From cab1b129c064bdbef8e01763f0f1201495d0d233 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:16:57 +0530 Subject: [PATCH 127/195] fix: validate reverse GL entries on current date under immutable ledger (#56709) * fix: validate reverse GL entries on current date under immutable ledger When Immutable Ledger is enabled, the reverse GL entry is posted on the current date, but the closed-period checks in make_reverse_gl_entries still validate against the original (backdated) posting date. This blocks cancelling a backdated voucher, such as a suspense Journal Entry for a migrated NPA loan, with a books-closed error even though the reverse entry lands in an open period. Validate both check_freezing_date and validate_against_pcv against the current date when Immutable Ledger is enabled. When it is disabled, behaviour is unchanged. Follow-up to #55268. * test: reset frozen till date after reverse entry test The freeze date set on the company was not reset, so it leaked into the next test which posts entries in that period. Reset it in a finally block. * fix: prefer explicit posting_date under immutable ledger Prefer the posting_date argument before frappe.form_dict and getdate, at both the validation and the GL entry site, so an explicit date passed by the caller is honoured and validation still matches the posted date. --- .../test_period_closing_voucher.py | 15 +++++++++------ erpnext/accounts/general_ledger.py | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index f19996dcf80..806a5934940 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -360,12 +360,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite): self.make_period_closing_voucher(posting_date="2021-03-31") - # Passed posting_date is after PCV end date, so cancellation should not fail. - make_reverse_gl_entries( - voucher_type="Journal Entry", - voucher_no=jv.name, - posting_date="2022-01-01", - ) + frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", "2021-12-31") + + try: + make_reverse_gl_entries( + voucher_type="Journal Entry", + voucher_no=jv.name, + ) + finally: + frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", None) totals_after_cancel = frappe.get_all( "GL Entry", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 1e54eb0370c..be48de11f8f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -640,13 +640,15 @@ def make_reverse_gl_entries( partial_cancel=partial_cancel, ) validate_accounting_period(gl_entries) - check_freezing_date(gl_entries[0]["posting_date"], gl_entries[0]["company"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) - # For reverse entries, use the posting_date parameter if provided and valid - # Otherwise fall back to original posting_date - validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + if immutable_ledger_enabled: + validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate() + else: + validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + + check_freezing_date(validation_date, gl_entries[0]["company"], adv_adj) validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"]) if partial_cancel: @@ -715,7 +717,7 @@ def make_reverse_gl_entries( if immutable_ledger_enabled: new_gle["is_cancelled"] = 0 - new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate() + new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate() elif posting_date: new_gle["posting_date"] = posting_date From 04468c3c33b37201810e742ee1ae8cdec2a24c74 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 2 Jul 2026 11:54:27 +0530 Subject: [PATCH 132/195] refactor: update title for process statement of accounts --- .../process_statement_of_accounts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 5aac9d902e1..c3603290f47 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -13,7 +13,7 @@ {% endif %} -

{{ _("GENERAL LEDGER") }}

+

{{ _("STATEMENT OF ACCOUNTS") }}

{% if filters.party[0] == filters.party_name[0] %}
{{ _("Customer: ") }} {{ filters.party_name[0] }}
From c47a95a4d259863baa911f6161b5128df95e42d4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 12:25:52 +0530 Subject: [PATCH 133/195] fix: don't treat batch slot at FIFO queue head as qty slot An incoming SLE without resolvable serial/batch details hit the negative-head branch in _compute_incoming_stock even when the head was a batch slot, because flt() on the batch number string returns 0.0. _add_to_negative_fifo_head then crashed with "TypeError: can only concatenate str (not 'float') to str". Guard the branch with is_qty_slot, mirroring the existing check in _add_transfer_slot_to_fifo_queue. Co-Authored-By: Claude Fable 5 --- .../stock/report/stock_ageing/stock_ageing.py | 2 +- .../report/stock_ageing/test_stock_ageing.py | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index a7b8a8a622a..c52d466b897 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -505,7 +505,7 @@ class FIFOSlots: self._add_serial_fifo_slots(row, fifo_queue, serial_nos) elif batch_nos and row.get("has_batch_no"): self._add_batch_fifo_slots(row, fifo_queue, batch_nos) - elif fifo_queue and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: + elif fifo_queue and is_qty_slot(fifo_queue[0]) and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: self._add_to_negative_fifo_head(row, fifo_queue) else: fifo_queue.append([flt(row.actual_qty), row.posting_date, flt(row.stock_value_difference)]) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 37b4b081cfe..02265ab65d0 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1434,6 +1434,47 @@ class TestStockAgeing(ERPNextTestSuite): self.assertEqual(item_result["total_qty"], -4.0) self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-11-10", -40.0]]) + def test_untagged_receipt_with_negative_batch_head(self): + """An incoming SLE without batch details must not treat a negative + batch slot at the queue head as a qty slot (TypeError: str += float).""" + sle = [ + frappe._dict( + name="Enclosure Item", + actual_qty=-10, + qty_after_transaction=-10, + stock_value_difference=-100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no="QI-06448", + ), + frappe._dict( + name="Enclosure Item", + actual_qty=45, + qty_after_transaction=35, + stock_value_difference=1051.65, + warehouse="WH 1", + posting_date="2021-12-05", + voucher_type="Purchase Receipt", + voucher_no="002", + has_serial_no=False, + serial_no=None, + batch_no=None, + serial_and_batch_bundle="SABB-00001294", + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + queue = slots["Enclosure Item"]["fifo_queue"] + + self.assertEqual(slots["Enclosure Item"]["total_qty"], 35.0) + self.assertEqual(queue[0][:3], ["QI-06448", None, -10.0]) + self.assertEqual(queue[1], [45.0, "2021-12-05", 1051.65]) + def test_batchwise_valuation_stock_reconciliation_with_bundle(self): from frappe.utils import add_days, getdate, nowdate From 8928b42d5d7952053aa191cfe9321015b8a102a1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 12:31:24 +0530 Subject: [PATCH 134/195] test: assert full negative batch slot in ageing regression test Co-Authored-By: Claude Fable 5 --- erpnext/stock/report/stock_ageing/test_stock_ageing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 02265ab65d0..7809451744d 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1472,7 +1472,7 @@ class TestStockAgeing(ERPNextTestSuite): queue = slots["Enclosure Item"]["fifo_queue"] self.assertEqual(slots["Enclosure Item"]["total_qty"], 35.0) - self.assertEqual(queue[0][:3], ["QI-06448", None, -10.0]) + self.assertEqual(queue[0], ["QI-06448", None, -10.0, "2021-12-01", -100.0]) self.assertEqual(queue[1], [45.0, "2021-12-05", 1051.65]) def test_batchwise_valuation_stock_reconciliation_with_bundle(self): From d10504af030dec5748b74a3fa17c0575ba520ece Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:34:38 +0530 Subject: [PATCH 135/195] fix: bucket late payments into 90 Above in Payment Period report --- .../payment_period_based_on_invoice_date.py | 7 +- ...st_payment_period_based_on_invoice_date.py | 68 ++++++++++++------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 5bbe02e4a01..47ada82755e 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -21,7 +21,12 @@ def execute(filters=None): entries = get_entries(filters) invoice_details = get_invoice_posting_date_map(filters) - report = ReceivablePayableReport(filters) + # Only four range columns are defined (range1-range4, the last being "90 Above"). + # Three thresholds yield exactly four buckets, so payments more than 90 days after + # the invoice land in range4 instead of an unread range5. + report_filters = frappe._dict(filters) + report_filters.range = "30, 60, 90" + report = ReceivablePayableReport(report_filters) data = [] for d in entries: diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py index 2c88a2c1171..f7c4f874c25 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py @@ -32,15 +32,15 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): } ) filters.update(extra) - return execute(filters) + columns, data = execute(filters) + fieldnames = [c["fieldname"] for c in columns] + # Map each positional row to a dict keyed by column fieldname so assertions + # stay correct even if a column is inserted or reordered. + return columns, [dict(zip(fieldnames, row, strict=False)) for row in data] def find_payment_row(self, data, payment_name): - # Row shape (positional): payment_document, payment_entry(voucher_no), - # party_type, party, posting_date, invoice(against_voucher_no), - # invoice_posting_date, due_date, amount, remarks, age, - # range1, range2, range3, range4, [delay_in_payment] for row in data: - if row[1] == payment_name: + if row["payment_entry"] == payment_name: return row return None @@ -57,42 +57,60 @@ class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): invoice = create_sales_invoice(customer="_Test Customer", rate=1000, posting_date="2026-06-01") payment = self.pay_invoice(invoice, "2026-06-20") - columns, data = self.run_report() + _columns, data = self.run_report() row = self.find_payment_row(data, payment.name) self.assertIsNotNone(row, "Payment row not found in report output") - # Positional assertions on the row shape. - self.assertEqual(row[2], "Customer") - self.assertEqual(row[4], getdate("2026-06-20")) # payment posting date - self.assertEqual(row[5], invoice.name) # against invoice - self.assertEqual(row[6], getdate("2026-06-01")) # invoice posting date - self.assertEqual(row[8], 1000) # amount - self.assertEqual(row[10], 19) # age = payment date - invoice date + self.assertEqual(row["party_type"], "Customer") + self.assertEqual(row["posting_date"], getdate("2026-06-20")) + self.assertEqual(row["invoice"], invoice.name) + self.assertEqual(row["invoice_posting_date"], getdate("2026-06-01")) + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["age"], 19) # age = payment date - invoice date # Buckets: 0-30 filled, others empty. - self.assertEqual(row[11], 1000) # range1 (0-30) - self.assertEqual(row[12], 0) # range2 (30-60) - self.assertEqual(row[13], 0) # range3 (60-90) - self.assertEqual(row[14], 0) # range4 (90 Above) + self.assertEqual(row["range1"], 1000) # 0-30 + self.assertEqual(row["range2"], 0) # 30-60 + self.assertEqual(row["range3"], 0) # 60-90 + self.assertEqual(row["range4"], 0) # 90 Above def test_paid_amount_lands_in_30_60_bucket(self): # invoice 2026-06-01, paid 2026-07-16 -> 45 days after -> 30-60 bucket invoice = create_sales_invoice(customer="_Test Customer 1", rate=1000, posting_date="2026-06-01") payment = self.pay_invoice(invoice, "2026-07-16") - columns, data = self.run_report() + _columns, data = self.run_report() row = self.find_payment_row(data, payment.name) self.assertIsNotNone(row, "Payment row not found in report output") - self.assertEqual(row[8], 1000) # amount - self.assertEqual(row[10], 45) # age = payment date - invoice date + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["age"], 45) # Buckets: 30-60 filled, others empty. - self.assertEqual(row[11], 0) # range1 (0-30) - self.assertEqual(row[12], 1000) # range2 (30-60) - self.assertEqual(row[13], 0) # range3 (60-90) - self.assertEqual(row[14], 0) # range4 (90 Above) + self.assertEqual(row["range1"], 0) + self.assertEqual(row["range2"], 1000) + self.assertEqual(row["range3"], 0) + self.assertEqual(row["range4"], 0) + + def test_payment_over_90_days_lands_in_90_above_bucket(self): + # invoice 2026-01-01, paid 2026-06-01 -> 151 days after -> "90 Above" bucket. + # Regression guard: with four range columns, a payment older than the last + # threshold must fall into range4 rather than an unread range5 (showing 0). + invoice = create_sales_invoice(customer="_Test Customer 2", rate=1000, posting_date="2026-01-01") + payment = self.pay_invoice(invoice, "2026-06-01") + + _columns, data = self.run_report() + + row = self.find_payment_row(data, payment.name) + self.assertIsNotNone(row, "Payment row not found in report output") + + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["age"], 151) + self.assertEqual(row["range1"], 0) + self.assertEqual(row["range2"], 0) + self.assertEqual(row["range3"], 0) + self.assertEqual(row["range4"], 1000) # 90 Above captures the full amount def test_columns_expose_expected_age_buckets(self): columns, _data = self.run_report() From ca7c6ca6da0b97f4d55ea6bfe0a050e8921187fe Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:36:38 +0530 Subject: [PATCH 136/195] fix: scope Share Balance report to the selected company --- .../report/share_balance/share_balance.py | 35 ++++++++++++++----- .../share_balance/test_share_balance.py | 24 +++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/share_balance/share_balance.py b/erpnext/accounts/report/share_balance/share_balance.py index db475a86265..b58c439a14c 100644 --- a/erpnext/accounts/report/share_balance/share_balance.py +++ b/erpnext/accounts/report/share_balance/share_balance.py @@ -22,7 +22,7 @@ def execute(filters=None): else: share_type, no_of_shares, rate, amount = 1, 2, 3, 4 - all_shares = get_all_shares(filters.get("shareholder"), filters.get("date")) + all_shares = get_all_shares(filters.get("shareholder"), filters.get("date"), filters.get("company")) for share_entry in all_shares: row = False for datum in data: @@ -61,16 +61,35 @@ def get_columns(filters): return columns -def get_all_shares(shareholder, date): +def get_all_shares(shareholder, date, company=None): """Share movements for the shareholder up to (and including) `date`, signed by direction: - shares received are positive, shares transferred/sold out are negative.""" - transfers = frappe.get_all( - "Share Transfer", - filters={"docstatus": 1, "date": ("<=", date)}, - fields=["share_type", "no_of_shares", "rate", "amount", "from_shareholder", "to_shareholder"], - order_by="date", + shares received are positive, shares transferred/sold out are negative. + + The shareholder and company predicates are pushed into the query so only the + relevant transfers are fetched instead of scanning the whole table.""" + share_transfer = frappe.qb.DocType("Share Transfer") + query = ( + frappe.qb.from_(share_transfer) + .select( + share_transfer.share_type, + share_transfer.no_of_shares, + share_transfer.rate, + share_transfer.amount, + share_transfer.from_shareholder, + share_transfer.to_shareholder, + ) + .where((share_transfer.docstatus == 1) & (share_transfer.date <= date)) + .where( + (share_transfer.to_shareholder == shareholder) | (share_transfer.from_shareholder == shareholder) + ) + .orderby(share_transfer.date) ) + if company: + query = query.where(share_transfer.company == company) + + transfers = query.run(as_dict=True) + shares = [] for transfer in transfers: if transfer.to_shareholder == shareholder: diff --git a/erpnext/accounts/report/share_balance/test_share_balance.py b/erpnext/accounts/report/share_balance/test_share_balance.py index bb6da5a133d..0b91d1525f3 100644 --- a/erpnext/accounts/report/share_balance/test_share_balance.py +++ b/erpnext/accounts/report/share_balance/test_share_balance.py @@ -42,6 +42,30 @@ class TestShareBalanceReport(ERPNextTestSuite): self.assertEqual(row[3], 10) # average rate self.assertEqual(row[4], 1000) # amount = 100 * 10 + def test_company_filter_scopes_transfers(self): + # the transfer is booked under `_Test Company` + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + + # matching company: the holding shows up + self.assertEqual(self.get_row(date="2026-06-05")[2], 100) + + # a different company must not surface this shareholder's transfer + other_company_data = execute( + frappe._dict( + {"date": "2026-06-05", "company": "_Test Company 1", "shareholder": self.shareholder} + ) + )[1] + self.assertEqual(other_company_data, []) + def test_balance_increases_on_second_issue(self): create_share_transfer( transfer_type="Issue", From 898a70d340efab8de3caaa34c712a76051db0ded Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:41:18 +0530 Subject: [PATCH 137/195] test: isolate Item-wise Consumption test with a unique item --- .../item_wise_consumption/test_item_wise_consumption.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py index 6ce53d3dd99..89d70d3ccdc 100644 --- a/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py +++ b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py @@ -4,6 +4,7 @@ import frappe from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.item_wise_consumption.item_wise_consumption import execute @@ -22,7 +23,9 @@ class TestItemWiseConsumption(ERPNextTestSuite): return execute(filters)[1] def test_consumed_vs_delivered_split(self): - item = "_Test Item" + # a uniquely-named item guarantees no residual stock/consumption from other + # tests leaks in -- the report aggregates an item across all warehouses. + item = make_item(properties={"is_stock_item": 1}).name # purchase receipt gives the supplier mapping and stocks the item make_purchase_receipt( item_code=item, @@ -41,5 +44,7 @@ class TestItemWiseConsumption(ERPNextTestSuite): self.assertEqual(row[5], 400) # consumed amount self.assertEqual(row[6], 3) # delivered qty self.assertEqual(row[7], 300) # delivered amount - self.assertEqual(row[8], 7) # total qty + self.assertEqual(row[8], 7) # total qty = consumed + delivered + self.assertEqual(row[9], 700) # total amount = consumed + delivered amount + self.assertEqual(row[9], row[5] + row[7]) # total aggregates the two amounts self.assertIn("_Test Supplier", row[10]) From 2e72c13aee9694093d9901c83be9b3ad05b8bff6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:43:07 +0530 Subject: [PATCH 138/195] test: isolate COGS By Item Group test with a dedicated item group --- .../test_cogs_by_item_group.py | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py index 36b9d4ad1da..a7c4b384fbe 100644 --- a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py @@ -4,15 +4,18 @@ import frappe from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.cogs_by_item_group.cogs_by_item_group import execute from erpnext.tests.utils import ERPNextTestSuite +COMPANY = "_Test Company with perpetual inventory" + class TestCogsByItemGroup(ERPNextTestSuite): def run_report(self, **extra) -> list: filters = frappe._dict( - company="_Test Company with perpetual inventory", + company=COMPANY, from_date="2026-01-01", to_date="2026-12-31", ) @@ -20,16 +23,19 @@ class TestCogsByItemGroup(ERPNextTestSuite): return execute(filters)[1] def test_cogs_for_item_group(self): - # Reuse the bootstrap item `_Test Item` (item group `_Test Item Group`). - # It has zero stock in `Stores - TCP1`, so this receipt starts from a clean balance. - item = "_Test Item" + # A dedicated item group with a single item keeps `agg_value` scoped to this + # test's COGS. The report sums COGS up the whole item-group tree keyed on the + # company's default expense account, so a shared group would accumulate COGS + # booked by any other test/fixture for the same company within the date range. + item_group = make_item_group("_Test COGS Item Group") + item = make_item(properties={"is_stock_item": 1, "item_group": item_group}).name make_stock_entry( item_code=item, to_warehouse="Stores - TCP1", qty=10, rate=100, - company="_Test Company with perpetual inventory", + company=COMPANY, posting_date="2026-06-01", ) @@ -40,7 +46,7 @@ class TestCogsByItemGroup(ERPNextTestSuite): qty=4, rate=150, warehouse="Stores - TCP1", - company="_Test Company with perpetual inventory", + company=COMPANY, update_stock=1, cost_center="Main - TCP1", parent_cost_center="Main - TCP1", @@ -51,7 +57,20 @@ class TestCogsByItemGroup(ERPNextTestSuite): ) data = self.run_report() - rows = [row for row in data if "_Test Item Group" in row.get("item_group")] - self.assertTrue(rows, "No row found for _Test Item Group") + rows = [row for row in data if item_group in row.get("item_group")] + self.assertTrue(rows, "No row found for the dedicated item group") # 4 units delivered at 100 valuation rate -> 400 COGS. self.assertEqual(rows[0].get("cogs_debit"), 400) + + +def make_item_group(name: str) -> str: + if not frappe.db.exists("Item Group", name): + frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": name, + "parent_item_group": "All Item Groups", + "is_group": 0, + } + ).insert() + return name From cb4f3588fa7fea0de9c03be20b9efe50f9fd4a38 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:45:01 +0530 Subject: [PATCH 139/195] test: isolate Profitability Analysis tests from shared cost centers --- .../test_profitability_analysis.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py index 19e0c57ceb2..e9c98f75821 100644 --- a/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py @@ -46,8 +46,9 @@ class TestProfitabilityAnalysis(ERPNextTestSuite): ) def test_income_expense_and_gross_profit(self): - # bootstrap leaf cost center; clean of committed GL so exact assertions hold - cc = "_Test Cost Center - _TC" + # a dedicated leaf cost center keeps these exact assertions free of GL that + # other tests may book against a shared cost center in the same fiscal year + cc = self.make_cc("_Test PA Income Expense") self.book_income(cc, 10000) self.book_expense(cc, 4000) @@ -74,7 +75,7 @@ class TestProfitabilityAnalysis(ERPNextTestSuite): self.assertEqual(parent_row["gross_profit_loss"], 7000) def test_date_range_excludes_out_of_period_entries(self): - cc = "_Test Cost Center 2 - _TC" + cc = self.make_cc("_Test PA Date Range") self.book_income(cc, 10000, posting_date="2025-06-01") # the 2025 income must not appear in a 2026 report (zero-value rows are dropped) @@ -97,7 +98,8 @@ class TestProfitabilityAnalysis(ERPNextTestSuite): data = self.run_report() # the report appends a blank separator row and a totals row at the end total_row = data[-1] - self.assertEqual(total_row["account"], "'Total'") + # the report wraps the (possibly translated) "Total" label in single quotes + self.assertEqual(total_row["account"], "'" + frappe._("Total") + "'") # total is built from direct (non-accumulated) values, so it stays internally consistent self.assertEqual(total_row["gross_profit_loss"], total_row["income"] - total_row["expense"]) # and it includes this test's bookings From 694328aab6c511683c83bfe2f03c4c55dfc0ec13 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:46:53 +0530 Subject: [PATCH 140/195] test: zero pre-committed actuals in Budget Variance report tests --- .../test_budget_variance_report.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py index de1fb541cb6..e1f2bc5ef0e 100644 --- a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py @@ -4,7 +4,7 @@ import frappe from frappe.utils import nowdate -from erpnext.accounts.doctype.budget.test_budget import make_budget +from erpnext.accounts.doctype.budget.test_budget import make_budget, set_total_expense_zero from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute from erpnext.accounts.utils import get_fiscal_year @@ -33,7 +33,12 @@ class TestBudgetVarianceReport(ERPNextTestSuite): return execute(filters)[1] def report_row(self, data, dimension, account=ACCOUNT): - return next(row for row in data if row["budget_against"] == dimension and row["account"] == account) + row = next( + (r for r in data if r["budget_against"] == dimension and r["account"] == account), + None, + ) + self.assertIsNotNone(row, f"No report row for {dimension} / {account}") + return row def field(self, label): return frappe.scrub(f"{label} {self.fy}") @@ -55,6 +60,8 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertTrue(columns) def test_budget_amount_shown_with_zero_actual(self): + # neutralise any committed actuals so the exact Actual/Variance assertions hold + set_total_expense_zero(nowdate(), "cost_center") make_budget( budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 ) @@ -65,6 +72,9 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertEqual(row[self.field("Variance")], 120000) def test_actual_expense_updates_actual_and_variance(self): + # zero out pre-committed actuals: keeps Actual exact and avoids the budget's + # "Stop" action rejecting the journal entry when prior actuals already exist + set_total_expense_zero(nowdate(), "cost_center") make_budget( budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 ) @@ -88,6 +98,8 @@ class TestBudgetVarianceReport(ERPNextTestSuite): self.assertEqual(dimensions, {COST_CENTER}) def test_monthly_period_totals(self): + # zero out pre-committed actuals so total_actual reflects only this test's entry + set_total_expense_zero(nowdate(), "cost_center") make_budget( budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 ) From 27f5235e67912f345265584fb7874b784eb6a368 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:48:29 +0530 Subject: [PATCH 141/195] test: cover inconsistent balance detection in Incorrect Balance Qty report --- ...incorrect_balance_qty_after_transaction.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py index db95dd96ff6..cee67261b7d 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py @@ -3,6 +3,7 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.incorrect_balance_qty_after_transaction.incorrect_balance_qty_after_transaction import ( execute, @@ -28,6 +29,30 @@ class TestIncorrectBalanceQtyAfterTransaction(ERPNextTestSuite): flagged = [row for row in data if row.get("item_code") == item] self.assertEqual(flagged, []) + def test_inconsistent_balance_qty_is_flagged(self): + # a unique item keeps this SLE the only ledger entry for the item/warehouse + item = make_item(properties={"is_stock_item": 1}).name + entry = make_stock_entry( + item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01" + ) + + # Corrupt the running balance so it no longer matches the cumulative actual_qty -- + # exactly the inconsistency this report exists to detect. set_value bypasses the + # ledger recompute that would otherwise keep the two in sync. + sle_name = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": entry.name, "item_code": item, "warehouse": WAREHOUSE}, + "name", + ) + frappe.db.set_value("Stock Ledger Entry", sle_name, "qty_after_transaction", 5) + + flagged = [row for row in self.run_report(item_code=item) if row.get("name") == sle_name] + self.assertEqual(len(flagged), 1, "The tampered stock ledger entry should be flagged") + row = flagged[0] + self.assertEqual(row["expected_balance_qty"], 10) # cumulative actual_qty + self.assertEqual(row["qty_after_transaction"], 5) # tampered balance + self.assertEqual(row["differnce"], 5) + def test_sequence_of_movements_not_flagged(self): item = "_Test Item 2" make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=20, rate=50, posting_date="2026-06-01") From b9e321c106a36ac4d00ef2ff543c2bbe206b0f1a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:57:16 +0530 Subject: [PATCH 142/195] test: cover negative-batch detection in Negative Batch Report --- .../test_negative_batch_report.py | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py index 16d50fa27c3..e0b0385f710 100644 --- a/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py +++ b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py @@ -3,28 +3,22 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.negative_batch_report.negative_batch_report import execute from erpnext.tests.utils import ERPNextTestSuite +WAREHOUSE = "Stores - _TC" +COMPANY = "_Test Company" + class TestNegativeBatchReport(ERPNextTestSuite): def run_report(self, item_code): - from erpnext.stock.report.negative_batch_report.negative_batch_report import execute + filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE, "item_code": item_code}) + return execute(filters)[1] - return execute( - frappe._dict( - { - "company": "_Test Company", - "warehouse": "Stores - _TC", - "item_code": item_code, - } - ) - )[1] - - def test_healthy_batch_not_negative(self): - from erpnext.stock.doctype.item.test_item import make_item - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - item = make_item( + def make_batch_item(self): + return make_item( properties={ "is_stock_item": 1, "has_batch_no": 1, @@ -33,22 +27,38 @@ class TestNegativeBatchReport(ERPNextTestSuite): } ).name + def receive_batch(self, item, qty, posting_date): + """Receive `qty` of `item`, creating its batch, and return the batch no.""" + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=qty, rate=100, posting_date=posting_date) + return frappe.get_all("Batch", filters={"item": item}, pluck="name")[0] + + def test_healthy_batch_not_negative(self): + item = self.make_batch_item() + batch = self.receive_batch(item, 10, "2026-06-01") + # issue from the same batch, staying within its balance make_stock_entry( - item_code=item, - to_warehouse="Stores - _TC", - qty=10, - rate=100, - posting_date="2026-06-01", - ) - make_stock_entry( - item_code=item, - from_warehouse="Stores - _TC", - qty=4, - posting_date="2026-06-02", + item_code=item, from_warehouse=WAREHOUSE, qty=4, batch_no=batch, posting_date="2026-06-02" ) + # received 10 then issued 4 -> running batch balance never goes negative + data = self.run_report(item) + self.assertFalse([row for row in data if row.get("batch_no") == batch]) + + def test_negative_batch_is_flagged(self): + # ERPNext blocks a negative batch balance at submission time (across several + # layers), so a genuinely negative batch only exists as corrupt historical + # data -- which is exactly what this report is meant to surface. Reproduce + # that state directly by forcing the batch's ledger quantity below zero. + item = self.make_batch_item() + batch = self.receive_batch(item, 10, "2026-06-10") + + sle = frappe.get_all("Stock Ledger Entry", filters={"item_code": item}, pluck="name")[0] + entry = frappe.get_all("Serial and Batch Entry", filters={"batch_no": batch}, pluck="name")[0] + frappe.db.set_value("Serial and Batch Entry", entry, "qty", -3) + frappe.db.set_value("Stock Ledger Entry", sle, {"actual_qty": -3, "qty_after_transaction": -3}) + data = self.run_report(item) - - # The batch was only received (10) before being issued (4), so its running - # balance never goes negative; the report must not list this item's batch. - self.assertFalse([row for row in data if row.get("item_code") == item]) + flagged = [row for row in data if row.get("batch_no") == batch] + self.assertEqual(len(flagged), 1, "A batch with a negative running balance must be flagged") + self.assertEqual(flagged[0]["qty_after_transaction"], -3) + self.assertEqual(flagged[0]["warehouse"], WAREHOUSE) From 99fbd61bd9d2e54aec7b7152467669683d75054e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 13:15:12 +0530 Subject: [PATCH 143/195] fix: restrict repost advisory-lock gate to Postgres MariaDB falls back to the existing deadlock-retry path; the advisory-lock serialization from #56697 now applies on Postgres only. Co-Authored-By: Claude Fable 5 --- erpnext/stock/stock_ledger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index db5bc5b5f66..229837d5eed 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -275,10 +275,11 @@ def repost_gate(item_code, warehouse): racing into a lock-order deadlock. Row locks still enforce correctness; this only cuts the deadlock/retry churn. Scope is repost-vs-repost only -- the synchronous repost_current_voucher submit path is deliberately not gated (blocking a submit behind a background repost would be a - worse regression) and keeps relying on the existing deadlock retry. No advisory locks, no gate.""" + worse regression) and keeps relying on the existing deadlock retry. Postgres only: MariaDB + keeps the plain deadlock-retry path.""" # hasattr keeps this a graceful opt-in: on an ERPNext predating frappe.db.advisory_lock, fall # back to no gate rather than raising and marking the Repost Item Valuation permanently Failed. - if frappe.db.db_type in ("postgres", "mariadb") and hasattr(frappe.db, "advisory_lock"): + if frappe.db.db_type == "postgres" and hasattr(frappe.db, "advisory_lock"): # Tuple key: a colon in item_code/warehouse can't collide two distinct pairs onto one lock. return frappe.db.advisory_lock(("stock_repost", item_code, warehouse), timeout=REPOST_LOCK_TIMEOUT) return nullcontext() From 0a05dd44264812dcfdeaac7aca2dbb193f64c989 Mon Sep 17 00:00:00 2001 From: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:17:14 +0530 Subject: [PATCH 144/195] fix: restore Save button on reverse journal entry (#56770) Reversing a submitted Journal Entry opened a draft with reversal_of set, which called frm.set_read_only(). That strips the write and submit perms from frm.perm, so the toolbar never rendered the Save (or later Submit) button and the reversal could not be saved. Lock the fields and the accounts grid as read_only instead, leaving perms intact so Save and Submit still work while nothing stays editable. Ticket: 72857 --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 9713b11bfe7..ffe6630c725 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -29,7 +29,7 @@ frappe.ui.form.on("Journal Entry", { refresh(frm) { if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) { - frm.set_read_only(); + erpnext.journal_entry.lock_reversal_entry(frm); } erpnext.toggle_naming_series(); @@ -232,6 +232,13 @@ Object.assign(erpnext.journal_entry, { } }, + lock_reversal_entry(frm) { + frm.fields + .filter((field) => field.has_input) + .forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1)); + frm.set_df_property("accounts", "read_only", 1); + }, + add_custom_buttons(frm) { if (frm.doc.docstatus > 0) { frm.add_custom_button( From 7248053c6abb971e8b8d7207c93897d22d722d6d Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin-114@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:34:25 +0530 Subject: [PATCH 145/195] feat(stock): add configurable Stock Delivered But Not Billed (SDBNB) support (#56070) * feat: add company setting to enable Stock Delivered But Not Billed accounting * test: add tests for Stock Delivered But Not Billed account config * fix(company): skip outstanding SDBNB validation when no previous config exists * test: add dedicated company fixture for SDBNB tests * test: use SDBNB company for Sales Invoice SDBNB test --------- Co-authored-by: Pugazhendhi Velu Co-authored-by: Pugazhendhi Velu <126157273+PugazhendhiVelu@users.noreply.github.com> --- .../in_standard_chart_of_accounts.json | 8 +- .../sales_invoice/services/gl_composer.py | 6 +- .../sales_invoice/test_sales_invoice.py | 16 ++-- erpnext/setup/doctype/company/company.js | 2 +- erpnext/setup/doctype/company/company.json | 11 +++ erpnext/setup/doctype/company/company.py | 59 +++++++++++++ erpnext/setup/doctype/company/test_company.py | 88 +++++++++++++++++++ .../setup/doctype/company/test_records.json | 14 ++- .../doctype/delivery_note/delivery_note.py | 9 +- .../delivery_note/test_delivery_note.py | 2 +- .../repost_item_valuation.py | 2 +- erpnext/stock/get_item_details.py | 2 + 12 files changed, 198 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json index 4a2584ae1b9..af0aca38c93 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json @@ -37,6 +37,10 @@ "account_type": "Stock", "account_category": "Stock Assets" }, + "Stock Delivered But Not Billed": { + "account_type": "Stock Delivered But Not Billed", + "account_category": "Stock Assets" + }, "account_type": "Stock", "account_category": "Stock Assets" }, @@ -223,10 +227,6 @@ "Stock Received But Not Billed": { "account_type": "Stock Received But Not Billed", "account_category": "Trade Payables" - }, - "Stock Delivered But Not Billed": { - "account_type": "Stock Delivered But Not Billed", - "account_category": "Trade Payables" } }, "Duties and Taxes": { diff --git a/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py b/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py index 2ec0dd335f1..ee9635af5a1 100644 --- a/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py +++ b/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py @@ -33,9 +33,11 @@ class SalesInvoiceGLComposer(BaseGLComposer): self.make_item_gl_entries(gl_entries) - disable_sdbnb_in_sr = frappe.get_cached_value("Company", doc.company, "disable_sdbnb_in_sr") + disable_sdbnb_in_sr, is_sdbnb_enabled = frappe.get_cached_value( + "Company", doc.company, ["disable_sdbnb_in_sr", "enable_stock_delivered_but_not_billed"] + ) - if not (doc.is_return and disable_sdbnb_in_sr): + if is_sdbnb_enabled and not (doc.is_return and disable_sdbnb_in_sr): self.stock_delivered_but_not_billed_gl_entries(gl_entries) self.make_precision_loss_gl_entry(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 93ddefefe85..770b24fe4ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1576,14 +1576,14 @@ class TestSalesInvoice(ERPNextTestSuite): frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1) def test_stock_delivered_but_not_billed_gl_on_invoice(self): - company = "_Test Company with perpetual inventory" + company = "_Test SDBNB Company" from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note make_purchase_receipt( company=company, item_code="_Test FG Item", - warehouse="Stores - TCP1", - cost_center="Main - TCP1", + warehouse="Stores - _TSDBNB", + cost_center="Main - _TSDBNB", qty=5, rate=100, ) @@ -1591,13 +1591,13 @@ class TestSalesInvoice(ERPNextTestSuite): dn = create_delivery_note( company=company, item_code="_Test FG Item", - warehouse="Stores - TCP1", - cost_center="Main - TCP1", + warehouse="Stores - _TSDBNB", + cost_center="Main - _TSDBNB", qty=2, rate=300, ) # A perpetual-inventory Delivery Note books the cost to the SDBNB account - self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - TCP1") + self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - _TSDBNB") si = make_sales_invoice(dn.name) si.insert() @@ -1609,9 +1609,9 @@ class TestSalesInvoice(ERPNextTestSuite): fields=["account", "debit", "credit"], ) sdbnb_credit = sum( - row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - TCP1" + row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - _TSDBNB" ) - cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - TCP1") + cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - _TSDBNB") # Billing reverses SDBNB and recognises the cost in COGS for an equal amount self.assertTrue(sdbnb_credit > 0) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 2173804a86f..84b79c95074 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -339,7 +339,7 @@ erpnext.company.setup_queries = function (frm) { ], [ "stock_delivered_but_not_billed", - { root_type: "Liability", account_type: "Stock Delivered But Not Billed" }, + { root_type: "Asset", account_type: "Stock Delivered But Not Billed" }, ], [ "service_received_but_not_billed", diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index bc3b11e7854..0036ea249ba 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -132,6 +132,7 @@ "default_purchase_price_variance_account", "default_manufacturing_variance_account", "stock_received_but_not_billed", + "enable_stock_delivered_but_not_billed", "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", "default_provisional_account", @@ -1048,18 +1049,28 @@ }, { "default": "0", + "depends_on": "enable_stock_delivered_but_not_billed", "fieldname": "disable_sdbnb_in_sr", "fieldtype": "Check", "label": "Disable Stock Delivered But Not Billed in Sales Return", "no_copy": 1 }, { + "depends_on": "enable_stock_delivered_but_not_billed", "fieldname": "stock_delivered_but_not_billed", "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Stock Delivered But Not Billed", + "mandatory_depends_on": "enable_stock_delivered_but_not_billed", "no_copy": 1, "options": "Account" + }, + { + "default": "0", + "description": "If enabled, the value of goods delivered before invoicing will be recorded in the Stock Delivered But Not Billed account.", + "fieldname": "enable_stock_delivered_but_not_billed", + "fieldtype": "Check", + "label": "Enable Stock Delivered But Not Billed" } ], "grid_page_length": 50, diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8cfb46828ff..5774d2cf09a 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -100,6 +100,7 @@ class Company(NestedSet): enable_item_wise_inventory_account: DF.Check enable_perpetual_inventory: DF.Check enable_provisional_accounting_for_non_stock_items: DF.Check + enable_stock_delivered_but_not_billed: DF.Check exception_budget_approver_role: DF.Link | None exchange_gain_loss_account: DF.Link | None existing_company: DF.Link | None @@ -186,6 +187,64 @@ class Company(NestedSet): self.validate_inventory_account_settings() self.cant_change_valuation_method() self.validate_pending_reposts(old_doc) + self.validate_sdbnb_configuration() + + def validate_outstanding_sdbnb_transactions(self, account): + GLEntry = frappe.qb.DocType("GL Entry") + DeliveryNote = frappe.qb.DocType("Delivery Note") + + delivery_notes = ( + frappe.qb.from_(GLEntry) + .join(DeliveryNote) + .on((GLEntry.voucher_type == "Delivery Note") & (GLEntry.voucher_no == DeliveryNote.name)) + .select(DeliveryNote.name) + .where( + (GLEntry.is_cancelled == 0) + & (GLEntry.company == self.name) + & (GLEntry.account == account) + & (DeliveryNote.per_billed < 100) + & (DeliveryNote.docstatus == 1) + & (DeliveryNote.status.isin(["To Bill", "Partially Billed"])) + ) + .distinct() + .run(pluck=True) + ) + + if delivery_notes: + dn_links = ", ".join(get_link_to_form("Delivery Note", dn) for dn in delivery_notes[:10]) + + frappe.throw( + _( + "Stock Delivered But Not Billed Account cannot be changed or disabled since account {0} contains outstanding Delivery Notes: {1}" + ).format( + bold(account), + dn_links, + ) + ) + + def validate_sdbnb_configuration(self): + if self.get("__islocal"): + return + + if self.enable_stock_delivered_but_not_billed and not self.stock_delivered_but_not_billed: + frappe.throw(_("Please select Stock Delivered But Not Billed Account")) + + doc_before_save = self.get_doc_before_save() + + if not (doc_before_save and doc_before_save.stock_delivered_but_not_billed): + return + + account_changed = ( + self.stock_delivered_but_not_billed != doc_before_save.stock_delivered_but_not_billed + ) + + feature_disabled = ( + doc_before_save.enable_stock_delivered_but_not_billed + and not self.enable_stock_delivered_but_not_billed + ) + + if account_changed or feature_disabled: + self.validate_outstanding_sdbnb_transactions(doc_before_save.stock_delivered_but_not_billed) def cant_change_valuation_method(self): doc_before_save = self.get_doc_before_save() diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index bdb87e4bfdc..64f4974ef1f 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -10,7 +10,11 @@ from frappe.utils import random_string from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( get_charts_for_country, ) +from erpnext.accounts.doctype.account.test_account import create_account from erpnext.setup.doctype.company.company import get_default_company_address +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.tests.utils import ERPNextTestSuite @@ -234,6 +238,44 @@ class TestCompany(ERPNextTestSuite): after = get_all_transactions_annual_history(company).get(key, 0) self.assertEqual(after - before, 2) + def test_sdbnb_validation_requires_account_when_enabled(self): + company = get_test_company() + + company.enable_stock_delivered_but_not_billed = 1 + company.stock_delivered_but_not_billed = None + + with self.assertRaises(frappe.ValidationError): + company.save() + + def test_disable_sdbnb_with_outstanding_delivery_note_fails(self): + company = get_test_company() + + item_code = create_stock_item_with_inventory() + create_outstanding_delivery_note(item_code) + + company.enable_stock_delivered_but_not_billed = 0 + + with self.assertRaises(frappe.ValidationError): + company.save() + + def test_cannot_change_sdbnb_account_with_outstanding_delivery_note(self): + company = get_test_company() + + item_code = create_stock_item_with_inventory() + create_outstanding_delivery_note(item_code) + + new_account = create_account( + account_name="Stock Delivered But Not Billed - New", + account_type="Stock Delivered But Not Billed", + parent_account="Stock Assets - _TSDBNB", + company=company.name, + ) + + company.stock_delivered_but_not_billed = new_account + + with self.assertRaises(frappe.ValidationError): + company.save() + def test_demo_data(self): from erpnext.setup.demo import clear_demo_data, setup_demo_data @@ -297,3 +339,49 @@ def create_test_lead_in_company(company): lead.company = company lead.save() return lead.name + + +def get_test_company(): + if frappe.db.exists("Company", "_Test SDBNB Company"): + return frappe.get_doc("Company", "_Test SDBNB Company") + + return frappe.get_doc( + { + "doctype": "Company", + "company_name": "_Test SDBNB Company", + "abbr": "_TSDBNB", + "country": "India", + "default_currency": "INR", + "enable_perpetual_inventory": 1, + "enable_stock_delivered_but_not_billed": 1, + } + ).insert() + + +def create_stock_item_with_inventory(): + item_code = make_item( + "SDBNB Test Item", + properties={"is_stock_item": 1}, + ).name + + make_stock_entry( + item_code=item_code, + target="Stores - _TSDBNB", + qty=10, + basic_rate=100, + company="_Test SDBNB Company", + ) + + return item_code + + +def create_outstanding_delivery_note(item_code): + return create_delivery_note( + item_code=item_code, + qty=5, + rate=150, + company="_Test SDBNB Company", + warehouse="Stores - _TSDBNB", + cost_center="Main - _TSDBNB", + expense_account="Stock Delivered But Not Billed - _TSDBNB", + ) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index d3faeec4672..794175c81ce 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -223,5 +223,17 @@ "doctype": "Company", "chart_of_accounts": "Standard", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TSDBNB", + "company_name": "_Test SDBNB Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "domain": "Manufacturing", + "chart_of_accounts": "Standard", + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 1, + "enable_stock_delivered_but_not_billed": 1 } -] +] \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7f524c82912..a3a1884cae2 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -426,6 +426,7 @@ class DeliveryNote(SellingController): "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", "default_expense_account", + "enable_stock_delivered_but_not_billed", ], as_dict=True, ) @@ -433,7 +434,7 @@ class DeliveryNote(SellingController): sdbnb_account = company_values.stock_delivered_but_not_billed disable_sdbnb_in_sr = company_values.disable_sdbnb_in_sr default_expense_account = company_values.default_expense_account - + is_enabled_sdbnb = company_values.enable_stock_delivered_but_not_billed for item in self.items: if item.get("against_sales_invoice"): if sdbnb_account and item.expense_account == sdbnb_account: @@ -447,14 +448,16 @@ class DeliveryNote(SellingController): # Only stock items if is_stock_item and not item.get("is_fixed_asset") and not item.get("is_subcontracted"): # Sales Return handling - if self.is_return and disable_sdbnb_in_sr: + if self.is_return and disable_sdbnb_in_sr and sdbnb_account and is_enabled_sdbnb: if default_expense_account and ( not item.expense_account or item.expense_account == sdbnb_account ): item.expense_account = default_expense_account - elif sdbnb_account: + elif sdbnb_account and is_enabled_sdbnb: item.expense_account = sdbnb_account + elif sdbnb_account and item.expense_account == sdbnb_account: + item.expense_account = default_expense_account if not item.expense_account and default_expense_account: item.expense_account = default_expense_account diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 971a2555b2c..c5db9cdcecb 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -50,7 +50,7 @@ class TestDeliveryNote(ERPNextTestSuite): self.load_test_records("Stock Entry") def get_perpetual_defaults(self): - company = frappe.get_doc("Company", "_Test Company with perpetual inventory") + company = frappe.get_doc("Company", "_Test SDBNB Company") self.perpetual_company = company.name self.perpetual_account = company.stock_delivered_but_not_billed self.perpetual_cost_center = company.cost_center diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index e113b8fe464..2d94a892aeb 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -511,7 +511,7 @@ def repost_gl_entries(doc): transactions = directly_dependent_transactions + list(repost_affected_transaction) # handle stock delivered but not billed ledger entries - if frappe.get_cached_value("Company", doc.company, "stock_delivered_but_not_billed"): + if frappe.get_cached_value("Company", doc.company, "enable_stock_delivered_but_not_billed"): _update_post_delivery_billed_vouchers(transactions) enable_separate_reposting_for_gl = frappe.db.get_single_value( diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 879b3a51afb..5a17d322288 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -455,6 +455,7 @@ def get_basic_details(ctx: frappe._dict, item, overwrite_warehouse=True) -> frap [ "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", + "enable_stock_delivered_but_not_billed", ], as_dict=True, ) @@ -464,6 +465,7 @@ def get_basic_details(ctx: frappe._dict, item, overwrite_warehouse=True) -> frap and ctx.is_stock_item and company_values and company_values.stock_delivered_but_not_billed + and company_values.enable_stock_delivered_but_not_billed and not ctx.get("is_fixed_asset") and not ctx.get("is_subcontracted") ): From c17517d22a17f409420a597e0a51dbf25e1a5ff1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 13:55:16 +0530 Subject: [PATCH 146/195] test: use a unique item group per run in COGS test --- .../report/cogs_by_item_group/test_cogs_by_item_group.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py index a7c4b384fbe..8e2004c7d46 100644 --- a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py @@ -27,7 +27,9 @@ class TestCogsByItemGroup(ERPNextTestSuite): # test's COGS. The report sums COGS up the whole item-group tree keyed on the # company's default expense account, so a shared group would accumulate COGS # booked by any other test/fixture for the same company within the date range. - item_group = make_item_group("_Test COGS Item Group") + # The group name is unique per run so items created by earlier runs (which + # reuse a fixed group name) can't inflate the total either. + item_group = make_item_group(f"_Test COGS Item Group {frappe.generate_hash(length=6)}") item = make_item(properties={"is_stock_item": 1, "item_group": item_group}).name make_stock_entry( From cc9d94efe884a54dfa81d674a1dff7655b35b658 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 13:57:07 +0530 Subject: [PATCH 147/195] test: use unique item and assert exact forecast in Exponential Smoothing test --- .../test_exponential_smoothing_forecasting.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py index 3f8ffd74af9..37d6c7da7ab 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py @@ -8,17 +8,24 @@ from erpnext.manufacturing.report.exponential_smoothing_forecasting.exponential_ execute, ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestSuite -TEST_ITEM = "_Test Item" FROM_DATE = "2026-06-01" TO_DATE = "2026-08-31" +SMOOTHING_CONSTANT = 0.5 class TestExponentialSmoothingForecasting(ERPNextTestSuite): """Drive real submitted Sales Orders and assert the report buckets the ordered quantities into the correct historical periods and produces a forecast.""" + def setUp(self): + # The forecast query has no lower date bound, so it would pick up any committed + # Sales Order for the item. A uniquely-named item keeps the buckets scoped to + # just this test's orders. + self.item = make_item(properties={"is_stock_item": 1}).name + def test_monthly_qty_forecast_from_sales_orders(self): # Historical demand: distinct calendar months strictly before FROM_DATE. # Monthly period keys are derived from the period's last day (e.g. "mar_2026"). @@ -45,26 +52,29 @@ class TestExponentialSmoothingForecasting(ERPNextTestSuite): for key, qty in history.items(): self.assertEqual(flt(row.get(key)), flt(qty), f"bucket {key} mismatch") - # A forecast is produced for the first future period. The first non-zero - # historical period seeds the forecast at the average of non-zero months, - # so the future forecast must be positive. + # The forecast seeds at the average of the non-zero historical months and then + # smooths through them in order: F = F + a*(actual - F). Asserting the exact + # analytical value pins the smoothing formula (Jun 2026 works out to ~7.2083). expected_avg = sum(history.values()) / len(history) - self.assertGreater(flt(row.get("forecast_jun_2026")), 0.0) - self.assertLessEqual(flt(row.get("forecast_jun_2026")), max(history.values())) self.assertAlmostEqual(flt(row.get("avg")), expected_avg, places=6) + forecast = expected_avg + for month in ("mar_2026", "apr_2026", "may_2026"): + forecast = forecast + SMOOTHING_CONSTANT * (history[month] - forecast) + self.assertAlmostEqual(flt(row.get("forecast_jun_2026")), forecast, places=6) + def test_ignores_documents_outside_range_and_other_docstatus(self): self.create_sales_orders({"2026-05-10": 6}) # A draft SO and a future-dated SO must not contribute to historical demand. - make_sales_order(item_code=TEST_ITEM, qty=100, transaction_date="2026-05-20", do_not_submit=True) - make_sales_order(item_code=TEST_ITEM, qty=100, transaction_date=FROM_DATE) + make_sales_order(item_code=self.item, qty=100, transaction_date="2026-05-20", do_not_submit=True) + make_sales_order(item_code=self.item, qty=100, transaction_date=FROM_DATE) _columns, row = self.run_report() self.assertEqual(flt(row.get("may_2026")), 6.0) def create_sales_orders(self, date_to_qty): for transaction_date, qty in date_to_qty.items(): - make_sales_order(item_code=TEST_ITEM, qty=qty, transaction_date=transaction_date) + make_sales_order(item_code=self.item, qty=qty, transaction_date=transaction_date) def run_report(self, **extra): filters = frappe._dict( @@ -76,16 +86,16 @@ class TestExponentialSmoothingForecasting(ERPNextTestSuite): "periodicity": "Monthly", "from_date": FROM_DATE, "to_date": TO_DATE, - "smoothing_constant": 0.5, - "item_code": TEST_ITEM, + "smoothing_constant": SMOOTHING_CONSTANT, + "item_code": self.item, } ) filters.update(extra) columns, data = execute(filters)[:2] item_row = next( - (r for r in data if r.get("item_code") == TEST_ITEM), + (r for r in data if r.get("item_code") == self.item), None, ) - self.assertIsNotNone(item_row, f"{TEST_ITEM} row missing from report output") + self.assertIsNotNone(item_row, f"{self.item} row missing from report output") return columns, item_row From 14091a8996547c8fe428ed8fdb960cf8934acc8f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 13:58:31 +0530 Subject: [PATCH 148/195] fix: report full planned qty as pending when a plan has no work order --- .../report/production_plan_summary/production_plan_summary.py | 4 +++- .../production_plan_summary/test_production_plan_summary.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index d7920af8141..82e150f807a 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -42,7 +42,9 @@ def get_production_plan_item_details(filters, data, order_details): order_qty = row.planned_qty total_produced_qty = 0.0 - pending_qty = 0.0 + # default to the full planned qty so a plan without any work order still + # reports everything as pending rather than a misleading zero + pending_qty = flt(order_qty) for work_order in work_orders: produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) pending_qty = flt(order_qty) - produced_qty diff --git a/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py index fc47a3065d1..674c653b12e 100644 --- a/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py @@ -72,6 +72,8 @@ class TestProductionPlanSummary(ERPNextTestSuite): self.assertIsNotNone(summary) self.assertEqual(summary.get("qty"), 2) self.assertEqual(summary.get("produced_qty"), 0) + # nothing produced yet, so the whole planned qty is pending + self.assertEqual(summary.get("pending_qty"), 2) self.assertIsNone(self.get_work_order_row(data, "_Test FG Item")) def test_summary_with_pending_work_order(self): From 2d3a1f5fab54119d641a1b80f478a43ae3c8c549 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:00:02 +0530 Subject: [PATCH 149/195] fix: expose hour rate column in Cost of Poor Quality report + robust float assert --- .../cost_of_poor_quality_report.py | 6 ++++++ .../test_cost_of_poor_quality_report.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index a86df319441..f95e7bcab5e 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -120,6 +120,12 @@ def get_columns(filters): "options": "Workstation", "width": "100", }, + { + "label": _("Hour Rate"), + "fieldtype": "Currency", + "fieldname": "hour_rate", + "width": "120", + }, { "label": _("Operating Cost"), "fieldtype": "Currency", diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py index 76813b3d2b7..191a57b01ed 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py @@ -67,8 +67,8 @@ class TestCostOfPoorQualityReport(ERPNextTestSuite): self.assertEqual(row["item_code"], corrective_jc.production_item) self.assertEqual(row["hour_rate"], 100) self.assertEqual(row["total_time_in_mins"], corrective_jc.total_time_in_mins) - # operating_cost = hour_rate * total_time_in_mins / 60 - self.assertEqual(row["operating_cost"], 100 * corrective_jc.total_time_in_mins / 60.0) + # operating_cost = hour_rate * total_time_in_mins / 60 (SQL float -> compare approximately) + self.assertAlmostEqual(row["operating_cost"], 100 * corrective_jc.total_time_in_mins / 60.0, places=6) def test_non_corrective_job_card_is_excluded(self): corrective_jc, _operation, _workstation = self.create_corrective_job_card() From 835a050cfbc0c15924956fbabf9a50a5b1527c4d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:01:22 +0530 Subject: [PATCH 150/195] test: cover produced-on-plan exclusion in BOM Variance report --- .../bom_variance_report/test_bom_variance_report.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py index 718dba7d29c..15e5e72d590 100644 --- a/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py @@ -86,7 +86,7 @@ class TestBOMVarianceReport(ERPNextTestSuite): self.assertEqual(len(matched), 1) self.assertEqual(matched[0].get("bom_no"), self.bom_no) - def test_work_order_without_over_production_is_excluded(self): + def test_unstarted_work_order_is_excluded(self): work_order = make_wo_order_test_record( item=self.production_item, qty=2, @@ -98,3 +98,13 @@ class TestBOMVarianceReport(ERPNextTestSuite): matched = [row for row in data if row.get("work_order") == work_order.name] self.assertEqual(matched, []) + + def test_work_order_produced_exactly_on_plan_is_excluded(self): + # the canonical no-variance case: produced qty equals the planned qty, so the + # report (which lists only over-produced orders) must not include it + work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=2) + + data = self.run_report(work_order=work_order.name) + + matched = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(matched, []) From f1e91b6be662a79396fae7a4157bb23cc5afc19e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:02:45 +0530 Subject: [PATCH 151/195] test: add positive anchor and robust row pairing in Work Order Consumed Materials --- .../test_work_order_consumed_materials.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py index c879de4dc02..825c80701fb 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py @@ -62,9 +62,12 @@ class TestWorkOrderConsumedMaterials(ERPNextTestSuite): self.assertEqual(len(rows), len(wo.required_items)) - reported = {row["raw_material_item_code"]: row for row in rows} - for item in wo.required_items: - row = reported[item.item_code] + # pair rows to required items by sorting rather than a dict keyed on item code, so + # a BOM with two lines for the same component wouldn't silently collapse to one row + rows_sorted = sorted(rows, key=lambda r: (r["raw_material_item_code"], r["required_qty"])) + items_sorted = sorted(wo.required_items, key=lambda i: (i.item_code, i.required_qty)) + for row, item in zip(rows_sorted, items_sorted, strict=True): + self.assertEqual(row["raw_material_item_code"], item.item_code) self.assertEqual(row["required_qty"], item.required_qty) self.assertEqual(row["transferred_qty"], item.required_qty) self.assertEqual(row["consumed_qty"], item.required_qty) @@ -100,6 +103,9 @@ class TestWorkOrderConsumedMaterials(ERPNextTestSuite): def test_date_range_filter_excludes_work_order(self): wo = self.make_manufactured_work_order(qty=1) + # positive anchor: the WO shows up within the default (current) window + self.assertIn(wo.name, {row.get("parent") for row in self.run_report()}) + # a window that ends before the WO was created must not include it data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5)) self.assertNotIn(wo.name, {row.get("parent") for row in data}) From 14f862f80ccfb8bca7ee549fc5db8b05a48c24c5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:03:45 +0530 Subject: [PATCH 152/195] test: add positive item_code filter case in Quality Inspection Summary --- .../test_quality_inspection_summary.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py index 368e7f5c3c4..08401329126 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py @@ -63,6 +63,14 @@ class TestQualityInspectionSummary(ERPNextTestSuite): ) self.assertEqual(self._rows_for_qi(data), []) + def test_item_code_filter_includes_matching(self): + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + item_code=["_Test Item"], + ) + self.assertEqual(len(self._rows_for_qi(data)), 1) + def test_item_code_filter_excludes_other_item(self): other_item = frappe.generate_hash(length=10) data = self.run_report( From b77f6168d9433665be060836849a94f65f5ccbbc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:05:15 +0530 Subject: [PATCH 153/195] test: load BOM fixtures and scope to top-level rows in BOM Explorer test --- .../report/bom_explorer/test_bom_explorer.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py index 3a7a1351c5b..54bfae2d6c2 100644 --- a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py @@ -9,16 +9,26 @@ from erpnext.tests.utils import ERPNextTestSuite class TestBOMExplorer(ERPNextTestSuite): + def setUp(self): + # the tests look up `_Test FG Item`'s BOM, which comes from the BOM fixtures; + # load them so the file also passes when run in isolation + self.load_test_records("BOM") + def run_report(self, bom): filters = frappe._dict({"bom": bom}) return execute(filters)[1] + def top_level_rows_by_item(self, data): + # key only the direct (indent 0) components, so an item that also appears in a + # deeper sub-assembly can't overwrite the top-level row we assert against + return {row["item_code"]: row for row in data if row["indent"] == 0} + def test_default_bom_lists_components_at_top_level(self): bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1}) self.assertIsNotNone(bom, "Default active BOM for _Test FG Item not found") data = self.run_report(bom) - rows_by_item = {row["item_code"]: row for row in data} + rows_by_item = self.top_level_rows_by_item(data) self.assertIn("_Test Item", rows_by_item) self.assertIn("_Test Item Home Desktop 100", rows_by_item) @@ -31,7 +41,7 @@ class TestBOMExplorer(ERPNextTestSuite): def test_qty_matches_bom_item_qty(self): bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1}) data = self.run_report(bom) - rows_by_item = {row["item_code"]: row for row in data} + rows_by_item = self.top_level_rows_by_item(data) for bom_item in frappe.get_all( "BOM Item", filters={"parent": bom}, fields=["item_code", "qty", "uom"] From ece8c9538deaf448032f35e3fab2b146877ad556 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:06:37 +0530 Subject: [PATCH 154/195] test: locale-safe status match and stable period window in Production Analytics --- .../test_production_analytics.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/report/production_analytics/test_production_analytics.py b/erpnext/manufacturing/report/production_analytics/test_production_analytics.py index 17a26ef06bd..c02e94f249f 100644 --- a/erpnext/manufacturing/report/production_analytics/test_production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/test_production_analytics.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe import _ from frappe.utils import get_first_day, get_last_day, today from erpnext.tests.utils import ERPNextTestSuite @@ -26,14 +27,19 @@ class TestProductionAnalytics(ERPNextTestSuite): def get_period_count(self, columns, data, status, period_label): """Return the count for a status row under the period column resolved by label.""" period_fieldname = next(col["fieldname"] for col in columns if col.get("label") == period_label) - row = next(row for row in data if row["status"] == status) + # the report stores the translated status label, so translate before matching + row = next(row for row in data if row["status"] == _(status)) return row[period_fieldname] def test_submitted_work_order_increments_status_count(self): from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + # pin the reporting window once so both runs use the same period even if the + # test happens to straddle a month boundary + from_date, to_date = get_first_day(today()), get_last_day(today()) + # The current month is the period a newly created Work Order falls into (bucketed by creation date). - cols_before, data_before = self.run_report() + cols_before, data_before = self.run_report(from_date=from_date, to_date=to_date) period_label = cols_before[-1]["label"] before = self.get_period_count(cols_before, data_before, "Not Started", period_label) @@ -42,7 +48,7 @@ class TestProductionAnalytics(ERPNextTestSuite): # A freshly submitted Work Order with no material transfer has status "Not Started". self.assertEqual(wo.status, "Not Started") - cols_after, data_after = self.run_report() + cols_after, data_after = self.run_report(from_date=from_date, to_date=to_date) after = self.get_period_count(cols_after, data_after, "Not Started", period_label) self.assertEqual(after, before + 1) @@ -57,4 +63,4 @@ class TestProductionAnalytics(ERPNextTestSuite): # One row per known Work Order status. statuses = {row["status"] for row in data} for status in ("Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"): - self.assertIn(status, statuses) + self.assertIn(_(status), statuses) From 7e7fd610cb9fa6240b952fe654884ea81757eca6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:08:19 +0530 Subject: [PATCH 155/195] test: guard job card list and derive status filter from stored status --- .../job_card_summary/test_job_card_summary.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py index 81af722ad50..6e59a0d559d 100644 --- a/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py @@ -54,6 +54,7 @@ class TestJobCardSummary(ERPNextTestSuite): self.assertEqual(row.get("production_item"), jc.production_item) def test_operation_filter_scopes_rows(self): + self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards") operation = self.job_cards[0].operation matching = {jc.name for jc in self.job_cards if jc.operation == operation} @@ -61,8 +62,18 @@ class TestJobCardSummary(ERPNextTestSuite): self.assertEqual({row.get("name") for row in rows}, matching) def test_status_filter(self): - open_rows = self.rows_for_work_order(self.run_report(status="Open")) - self.assertEqual({row.get("name") for row in open_rows}, {jc.name for jc in self.job_cards}) + self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards") + + # The status filter matches the Job Card's *stored* status, so derive the + # expected set from that rather than assuming fresh cards are literally "Open". + stored_status = self.job_cards[0].status + expected = {jc.name for jc in self.job_cards if jc.status == stored_status} + + rows = self.rows_for_work_order(self.run_report(status=stored_status)) + self.assertEqual({row.get("name") for row in rows}, expected) + # any non-completed card is displayed as "Open" regardless of its stored status + for row in rows: + self.assertEqual(row.get("status"), "Open") # None of the freshly created job cards are Completed yet. completed_rows = self.rows_for_work_order(self.run_report(status="Completed")) From 5adbc7babae4f0ad1554248d4d4f7479d21e82e0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:10:24 +0530 Subject: [PATCH 156/195] test: target leaf accounts and robust amount assertions in Consolidated Financial Statement --- .../test_consolidated_financial_statement.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py index 202c495d378..1fb6a68e3b6 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -52,11 +52,19 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): je.submit() return je - def get_row(self, data, account_name_fragment): + def get_row(self, data, account_name_fragment, last_match=False): + """Return the first (or last) row whose account_name contains the fragment. + + Pass ``last_match=True`` to get the leaf/most-specific match when the fragment + is also a prefix of a parent group account (parents precede children in tree order). + """ + found = None for row in data: if account_name_fragment in str(row.get("account_name") or ""): - return row - return None + if not last_match: + return row + found = row + return found def test_profit_and_loss_reflects_child_company_income(self): amount = 7000 @@ -67,9 +75,10 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): self.assertTrue(data, "Report returned no rows") # child's Sales account is mapped onto the parent chart (Sales - PGCI) - sales_row = self.get_row(data, "Sales") + sales_row = self.get_row(data, "Sales", last_match=True) self.assertIsNotNone(sales_row, "Sales row missing from consolidated P&L") - self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + # >= so a pre-existing Sales balance in the fiscal year doesn't make this brittle + self.assertGreaterEqual(flt(sales_row.get(CHILD_COMPANY)), amount) total_income_row = self.get_row(data, "Total Income (Credit)") self.assertIsNotNone(total_income_row, "Total Income row missing") @@ -81,9 +90,9 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) - expense_row = self.get_row(data, "Marketing Expenses") + expense_row = self.get_row(data, "Marketing Expenses", last_match=True) self.assertIsNotNone(expense_row, "Marketing Expenses row missing from consolidated P&L") - self.assertEqual(flt(expense_row.get(CHILD_COMPANY)), amount) + self.assertGreaterEqual(flt(expense_row.get(CHILD_COMPANY)), amount) total_expense_row = self.get_row(data, "Total Expense (Debit)") self.assertIsNotNone(total_expense_row, "Total Expense row missing") @@ -97,13 +106,15 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=1) - sales_row = self.get_row(data, "Sales") + sales_row = self.get_row(data, "Sales", last_match=True) self.assertIsNotNone(sales_row) - self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + child_value = flt(sales_row.get(CHILD_COMPANY)) + self.assertGreaterEqual(child_value, amount) # parent column picks up the child value when accumulated - self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), amount) - # the total must equal the consolidated (group) value, not the sum of parent + child columns - self.assertEqual(flt(sales_row.get("total")), amount) + self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), child_value) + # the total equals the consolidated (group) value, not the sum of parent + child + # columns -- this is the regression guard for the double-count fix + self.assertEqual(flt(sales_row.get("total")), child_value) def test_balance_sheet_executes_and_returns_rows(self): # posting income leaves a balancing entry in the child's Cash (Asset) account From a69590b6092128763951b02df1201da1f3d8ba76 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:12:24 +0530 Subject: [PATCH 157/195] test: named column indices and Transfer-label coverage in Share Ledger --- .../report/share_ledger/test_share_ledger.py | 111 +++++++++++++++--- 1 file changed, 92 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/report/share_ledger/test_share_ledger.py b/erpnext/accounts/report/share_ledger/test_share_ledger.py index f7a96c06dae..51309bd9f94 100644 --- a/erpnext/accounts/report/share_ledger/test_share_ledger.py +++ b/erpnext/accounts/report/share_ledger/test_share_ledger.py @@ -8,6 +8,18 @@ from erpnext.tests.utils import ERPNextTestSuite COMPANY = "_Test Company" +# The report returns legacy positional columns (no fieldnames); name the indices once +# here so a column reorder needs a single edit instead of silently shifting assertions. +COL_SHAREHOLDER = 0 +COL_DATE = 1 +COL_TRANSFER_TYPE = 2 +COL_SHARE_TYPE = 3 +COL_NO_OF_SHARES = 4 +COL_RATE = 5 +COL_AMOUNT = 6 +COL_COMPANY = 7 +COL_SHARE_TRANSFER = 8 + class TestShareLedger(ERPNextTestSuite): def setUp(self): @@ -22,29 +34,32 @@ class TestShareLedger(ERPNextTestSuite): self.assertEqual(len(data), 2) first_row, second_row = data - self.assertEqual(first_row[0], self.shareholder) - self.assertEqual(first_row[1], frappe.utils.getdate("2026-06-01")) - self.assertEqual(first_row[2], "Issue") - self.assertEqual(first_row[3], "Equity") - self.assertEqual(first_row[4], 100) - self.assertEqual(first_row[5], 10) - self.assertEqual(first_row[6], 1000) - self.assertEqual(first_row[7], COMPANY) - self.assertEqual(first_row[8], self.first) + self.assertEqual(first_row[COL_SHAREHOLDER], self.shareholder) + self.assertEqual(first_row[COL_DATE], frappe.utils.getdate("2026-06-01")) + self.assertEqual(first_row[COL_TRANSFER_TYPE], "Issue") + self.assertEqual(first_row[COL_SHARE_TYPE], "Equity") + self.assertEqual(first_row[COL_NO_OF_SHARES], 100) + self.assertEqual(first_row[COL_RATE], 10) + self.assertEqual(first_row[COL_AMOUNT], 1000) + self.assertEqual(first_row[COL_COMPANY], COMPANY) + self.assertEqual(first_row[COL_SHARE_TRANSFER], self.first) - self.assertEqual(second_row[1], frappe.utils.getdate("2026-06-10")) - self.assertEqual(second_row[4], 50) - self.assertEqual(second_row[5], 12) - self.assertEqual(second_row[6], 600) - self.assertEqual(second_row[8], self.second) + self.assertEqual(second_row[COL_DATE], frappe.utils.getdate("2026-06-10")) + self.assertEqual(second_row[COL_NO_OF_SHARES], 50) + self.assertEqual(second_row[COL_RATE], 12) + self.assertEqual(second_row[COL_AMOUNT], 600) + self.assertEqual(second_row[COL_SHARE_TRANSFER], self.second) def test_running_balance_of_shares(self): data = self.run_report(shareholder=self.shareholder, date="2026-06-30") + # The ledger records each transfer's raw no_of_shares (always positive); it does + # not sign by direction. With only incoming "Issue" rows here, summing them is a + # valid running total. (Directional balances are the Share Balance report's job.) running = 0 balances = [] for row in data: - running += row[4] + running += row[COL_NO_OF_SHARES] balances.append(running) self.assertEqual(balances, [100, 150]) @@ -53,8 +68,40 @@ class TestShareLedger(ERPNextTestSuite): data = self.run_report(shareholder=self.shareholder, date="2026-06-05") self.assertEqual(len(data), 1) - self.assertEqual(data[0][8], self.first) - self.assertEqual(data[0][4], 100) + self.assertEqual(data[0][COL_SHARE_TRANSFER], self.first) + self.assertEqual(data[0][COL_NO_OF_SHARES], 100) + + def test_transfer_type_label_when_shareholder_is_seller(self): + buyer = self.create_shareholder("_Test Share Ledger Buyer") + transfer = self.make_transfer( + from_shareholder=self.shareholder, + to_shareholder=buyer, + date="2026-06-15", + from_no=1, + to_no=40, + rate=10, + ) + + row = self.transfer_row(self.run_report(shareholder=self.shareholder, date="2026-06-30"), transfer) + # seller side: the label names the counterparty it went "to" + self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer to {buyer}") + + def test_transfer_type_label_when_shareholder_is_buyer(self): + seller = self.create_shareholder("_Test Share Ledger Seller") + # the seller must own shares before it can transfer them + self.issue_shares(date="2026-06-12", from_no=201, to_no=300, rate=10, shareholder=seller) + transfer = self.make_transfer( + from_shareholder=seller, + to_shareholder=self.shareholder, + date="2026-06-15", + from_no=201, + to_no=240, + rate=10, + ) + + row = self.transfer_row(self.run_report(shareholder=self.shareholder, date="2026-06-30"), transfer) + # buyer side: the label names the counterparty it came "from" + self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer from {seller}") def test_missing_date_throws(self): self.assertRaises(frappe.ValidationError, execute, frappe._dict(shareholder=self.shareholder)) @@ -67,6 +114,11 @@ class TestShareLedger(ERPNextTestSuite): filters = frappe._dict({"company": COMPANY, **extra}) return execute(filters)[1] + def transfer_row(self, data, transfer_name): + row = next((r for r in data if r[COL_SHARE_TRANSFER] == transfer_name), None) + self.assertIsNotNone(row, f"Share Transfer {transfer_name} missing from ledger") + return row + def create_shareholder(self, title): doc = frappe.get_doc( { @@ -77,13 +129,34 @@ class TestShareLedger(ERPNextTestSuite): ).insert() return doc.name - def issue_shares(self, date, from_no, to_no, rate): + def issue_shares(self, date, from_no, to_no, rate, shareholder=None): doc = frappe.get_doc( { "doctype": "Share Transfer", "transfer_type": "Issue", "date": date, - "to_shareholder": self.shareholder, + "to_shareholder": shareholder or self.shareholder, + "share_type": "Equity", + "from_no": from_no, + "to_no": to_no, + "no_of_shares": to_no - from_no + 1, + "rate": rate, + "company": COMPANY, + "asset_account": "Cash - _TC", + "equity_or_liability_account": "Creditors - _TC", + } + ) + doc.submit() + return doc.name + + def make_transfer(self, from_shareholder, to_shareholder, date, from_no, to_no, rate): + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": date, + "from_shareholder": from_shareholder, + "to_shareholder": to_shareholder, "share_type": "Equity", "from_no": from_no, "to_no": to_no, From 18d194715464db2af8f6b89574cb5d2aaa9e1267 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:13:29 +0530 Subject: [PATCH 158/195] test: assert to_date upper bound and use assertIsNone in Bank Clearance Summary --- .../test_bank_clearance_summary.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py index c7781269f86..b44c3f987e0 100644 --- a/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py @@ -37,7 +37,7 @@ class TestBankClearanceSummary(ERPNextTestSuite): self.assertIsNotNone(row, "Journal Entry not listed in Bank Clearance Summary") self.assertEqual(row[0], "Journal Entry") self.assertEqual(frappe.utils.getdate(row[2]), frappe.utils.getdate("2026-06-01")) - self.assertEqual(row[4], None) # clearance_date empty -> uncleared + self.assertIsNone(row[4]) # clearance_date empty -> uncleared self.assertEqual(row[5], "Sales - _TC") # against account self.assertEqual(row[6], 5000) # debit - credit on the bank account @@ -55,6 +55,10 @@ class TestBankClearanceSummary(ERPNextTestSuite): # Within range: present self.assertIsNotNone(self.find_row(self.run_report(), je.name)) - # Narrow window after the posting date: excluded - data = self.run_report(from_date="2026-07-01", to_date="2026-12-31") - self.assertIsNone(self.find_row(data, je.name)) + # Window entirely after the posting date (from_date lower bound): excluded + after = self.run_report(from_date="2026-07-01", to_date="2026-12-31") + self.assertIsNone(self.find_row(after, je.name)) + + # Window ending before the posting date (to_date upper bound): excluded + before = self.run_report(from_date="2026-01-01", to_date="2026-06-09") + self.assertIsNone(self.find_row(before, je.name)) From 0e8ae7548d12dbee6b50a02730d962f30b89765e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 2 Jul 2026 14:25:49 +0530 Subject: [PATCH 159/195] fix: block serialized to non-serialized item change when SABB exists (#56773) --- erpnext/stock/doctype/item/item.py | 20 ++++++++++++ erpnext/stock/doctype/item/test_item.py | 41 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 632b3874b47..3fff0cb1c28 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -239,6 +239,7 @@ class Item(Document): self.validate_item_defaults() self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() + self.validate_serialized_change_with_bundle() self.validate_standard_cost_change() self.validate_item_tax_net_rate_range() @@ -1130,6 +1131,25 @@ class Item(Document): frappe.throw(msg, title=_("Linked with submitted documents")) + def validate_serialized_change_with_bundle(self): + """Block turning a serialized item non-serialized while any Serial and Batch Bundle still exists + for it. Such bundles carry the item's serial numbers; the user must delete or cancel them first.""" + if self.is_new() or self.has_serial_no or not self._doc_before_save: + return + + # Only relevant when the item was serialized before and is now being unset. + if not self._doc_before_save.has_serial_no: + return + + # Draft (docstatus 0) or submitted (docstatus 1) bundles block the change; cancelled ones don't. + if frappe.db.count("Serial and Batch Bundle", {"item_code": self.name, "docstatus": ("<", 2)}): + frappe.throw( + _( + "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first." + ).format(frappe.bold(self.name)), + title=_("Serial and Batch Bundle Exists"), + ) + def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None: linked_doctypes = [ "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 06cc5c33f94..7569d1c538e 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -1120,6 +1120,47 @@ class TestItem(ERPNextTestSuite): sabb_qty = frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "total_qty") self.assertEqual(abs(sabb_qty), properties["opening_stock"]) + def test_cannot_unset_serialized_while_bundle_exists(self): + from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + make_serial_batch_bundle, + ) + + item = make_item( + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TSN-UNSET-.####"} + ).name + + serial_no = f"{item}-SN-01" + frappe.get_doc( + {"doctype": "Serial No", "serial_no": serial_no, "item_code": item, "company": "_Test Company"} + ).insert() + + # A draft (unsubmitted) Serial and Batch Bundle for the item must block the change. + bundle = make_serial_batch_bundle( + { + "item_code": item, + "warehouse": "_Test Warehouse - _TC", + "company": "_Test Company", + "qty": 1, + "rate": 100, + "voucher_type": "Stock Entry", + "serial_nos": [serial_no], + "type_of_transaction": "Inward", + "do_not_submit": True, + "ignore_sabb_validation": True, + } + ) + + doc = frappe.get_doc("Item", item) + doc.has_serial_no = 0 + self.assertRaises(frappe.ValidationError, doc.save) + + # Once the bundle is removed, the item can be made non-serialized. + frappe.delete_doc("Serial and Batch Bundle", bundle.name, force=True) + doc = frappe.get_doc("Item", item) + doc.has_serial_no = 0 + doc.save() + self.assertEqual(frappe.db.get_value("Item", item, "has_serial_no"), 0) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") From 0790d2e6df40e961719b48d1317abcab98d5ead8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:09:00 +0530 Subject: [PATCH 160/195] fix(manufacturing): include last-day records in Production Analytics `get_work_orders` bounded a BETWEEN on the datetime columns `creation` and `actual_end_date` with a bare date `to_date`, which MariaDB coerces to midnight. Work orders created after 00:00:00 on the period's last day were therefore dropped from the report (and made the new coverage test fail on month-end CI runs). Extend `to_date` to end of day. --- .../report/production_analytics/production_analytics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 9da87022e46..2fbd42210af 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub -from frappe.utils import getdate, today +from frappe.utils import get_datetime, getdate, today from erpnext.stock.report.stock_analytics.stock_analytics import ( get_period, @@ -31,7 +31,9 @@ def get_columns(period_columns): def get_work_orders(filters): from_date = filters.get("from_date") - to_date = filters.get("to_date") + # `creation` and `actual_end_date` are datetime columns, so a bare date upper + # bound would coerce to midnight and drop records created later on the last day. + to_date = get_datetime(filters.get("to_date")).replace(hour=23, minute=59, second=59) WorkOrder = frappe.qb.DocType("Work Order") From 08876ae07af5afce0912b348a104288f51e91795 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:32:13 +0530 Subject: [PATCH 161/195] test: Quotation Trends report coverage --- .../quotation_trends/test_quotation_trends.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 erpnext/selling/report/quotation_trends/test_quotation_trends.py diff --git a/erpnext/selling/report/quotation_trends/test_quotation_trends.py b/erpnext/selling/report/quotation_trends/test_quotation_trends.py new file mode 100644 index 00000000000..05b9c55e839 --- /dev/null +++ b/erpnext/selling/report/quotation_trends/test_quotation_trends.py @@ -0,0 +1,85 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.doctype.quotation.test_quotation import make_quotation +from erpnext.selling.report.quotation_trends.quotation_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +FISCAL_YEAR = "_Test Fiscal Year 2026" +TXN_DATE = "2026-06-01" + + +class TestQuotationTrends(ERPNextTestSuite): + """The trends report buckets submitted Quotation quantities/amounts by period + (Yearly/Monthly) for the chosen `based_on` dimension (Item, Customer, ...).""" + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": FISCAL_YEAR, + "based_on": "Item", + "period": "Yearly", + } + ) + filters.update(extra) + result = execute(filters) + columns, data = result[0], result[1] + labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + return labels, data + + def _cell(self, data, key_label, key_value, col_label, labels): + """Value at column `col_label` for the row whose `key_label` column equals + `key_value`, or 0 when that row doesn't exist yet.""" + key_idx = labels.index(key_label) + col_idx = labels.index(col_label) + for row in data: + if row[key_idx] == key_value: + return row[col_idx] or 0 + return 0 + + def test_yearly_item_amount_and_total(self): + # Yearly period => a single " (Qty)"/"(Amt)" bucket plus Total(Qty)/Total(Amt). + labels, before = self.run_report() + qty_col = f"{FISCAL_YEAR} (Qty)" + amt_col = f"{FISCAL_YEAR} (Amt)" + before_qty = self._cell(before, "Item", "_Test Item", qty_col, labels) + before_amt = self._cell(before, "Item", "_Test Item", amt_col, labels) + before_tot_qty = self._cell(before, "Item", "_Test Item", "Total(Qty)", labels) + before_tot_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + make_quotation(item="_Test Item", qty=4, rate=200, transaction_date=TXN_DATE) + + labels, after = self.run_report() + self.assertEqual(self._cell(after, "Item", "_Test Item", qty_col, labels) - before_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", amt_col, labels) - before_amt, 800) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Qty)", labels) - before_tot_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot_amt, 800) + + def test_monthly_lands_in_june_bucket(self): + # Monthly period => one bucket per month; a 2026-06-01 quotation hits "Jun (Qty)"/"(Amt)". + labels, before = self.run_report(period="Monthly") + before_jun_qty = self._cell(before, "Item", "_Test Item", "Jun (Qty)", labels) + before_may_qty = self._cell(before, "Item", "_Test Item", "May (Qty)", labels) + + make_quotation(item="_Test Item", qty=3, rate=100, transaction_date=TXN_DATE) + + labels, after = self.run_report(period="Monthly") + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Qty)", labels) - before_jun_qty, 3) + # nothing was quoted in May, so that bucket is unchanged + self.assertEqual(self._cell(after, "Item", "_Test Item", "May (Qty)", labels) - before_may_qty, 0) + + def test_based_on_customer_groups_amount_by_party(self): + # based_on Customer keys rows on the "Party" column (the customer id) + labels, before = self.run_report(based_on="Customer") + amt_col = f"{FISCAL_YEAR} (Amt)" + before_amt = self._cell(before, "Party", "_Test Customer", amt_col, labels) + + make_quotation( + party_name="_Test Customer", item="_Test Item", qty=2, rate=150, transaction_date=TXN_DATE + ) + + labels, after = self.run_report(based_on="Customer") + self.assertEqual(self._cell(after, "Party", "_Test Customer", amt_col, labels) - before_amt, 300) From 9865f636138d79cc9298b89cb72fd39031bc878d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:33:53 +0530 Subject: [PATCH 162/195] test: Customer-wise Item Price report coverage --- .../test_customer_wise_item_price.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py diff --git a/erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py new file mode 100644 index 00000000000..70181b19e79 --- /dev/null +++ b/erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py @@ -0,0 +1,69 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.report.customer_wise_item_price.customer_wise_item_price import execute +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + +PRICE_LIST = "Standard Selling" + + +class TestCustomerWiseItemPrice(ERPNextTestSuite): + """The report lists sales items with the selling rate from the customer's price + list and the available stock (summed across warehouses).""" + + def setUp(self): + self.item = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name + self.customer = self.create_customer() + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": self.item, + "price_list": PRICE_LIST, + "selling": 1, + "price_list_rate": 250, + } + ).insert() + make_stock_entry(item_code=self.item, to_warehouse="Stores - _TC", qty=10, rate=100) + + def create_customer(self): + name = "_Test CWIP Customer" + if not frappe.db.exists("Customer", name): + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": name, + "customer_group": "_Test Customer Group", + "territory": "_Test Territory", + "default_price_list": PRICE_LIST, + } + ).insert() + return name + + def run_report(self, **extra): + filters = frappe._dict({"customer": self.customer}) + filters.update(extra) + return execute(filters)[1] + + def test_customer_filter_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({})) + + def test_selling_rate_and_available_stock_for_item(self): + rows = self.run_report(item=self.item) + + row = next((r for r in rows if r["item_code"] == self.item), None) + self.assertIsNotNone(row, "Sales item missing from report") + self.assertEqual(row["item_name"], frappe.db.get_value("Item", self.item, "item_name")) + self.assertEqual(row["selling_rate"], 250) # from the customer's price list + self.assertEqual(row["available_stock"], 10) # stocked into Stores - _TC + self.assertEqual(row["price_list"], PRICE_LIST) + + def test_item_filter_scopes_to_single_item(self): + other = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name + + item_codes = {r["item_code"] for r in self.run_report(item=self.item)} + self.assertIn(self.item, item_codes) + self.assertNotIn(other, item_codes) From 55646667bebcbf9b9a96c3051dfa946ac13c559a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:36:47 +0530 Subject: [PATCH 163/195] test: Sales Person Commission Summary report coverage --- .../test_sales_person_commission_summary.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py diff --git a/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py new file mode 100644 index 00000000000..e807d33f506 --- /dev/null +++ b/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py @@ -0,0 +1,75 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.report.sales_person_commission_summary.sales_person_commission_summary import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestSalesPersonCommissionSummary(ERPNextTestSuite): + """The report joins a sales document (Sales Invoice/Order/Delivery Note) with its + Sales Team rows, listing each sales person's contribution and commission.""" + + def setUp(self): + # reuse the bootstrap sales persons (under the "Sales Team" group) + self.sales_person = "_Test Sales Person" + + def make_invoice_with_commission(self, percentage=100, commission_rate=5, incentives=50): + si = create_sales_invoice(rate=1000, qty=1, do_not_save=True, posting_date="2026-06-01") + si.append( + "sales_team", + { + "sales_person": self.sales_person, + "allocated_percentage": percentage, + "commission_rate": commission_rate, + "incentives": incentives, + }, + ) + si.insert() + si.submit() + return si + + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "doc_type": "Sales Invoice", "sales_person": self.sales_person} + ) + filters.update(extra) + return execute(filters)[1] + + def test_doc_type_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({"company": "_Test Company"})) + + def test_commission_row_matches_sales_team_entry(self): + si = self.make_invoice_with_commission(percentage=100, commission_rate=5, incentives=50) + team = si.sales_team[0] + + rows = self.run_report() + row = next((r for r in rows if r[0] == si.name), None) + self.assertIsNotNone(row, "Invoice with commission missing from report") + + # row: name, customer, territory, posting_date, base_net_amount, sales_person, + # allocated_percentage, commission_rate, allocated_amount, incentives + self.assertEqual(row[1], si.customer) + self.assertEqual(row[4], si.base_net_total) + self.assertEqual(row[5], self.sales_person) + self.assertEqual(row[6], team.allocated_percentage) + self.assertEqual(row[7], team.commission_rate) + self.assertEqual(row[8], team.allocated_amount) + self.assertEqual(row[9], team.incentives) + + def test_appends_total_row(self): + self.make_invoice_with_commission() + rows = self.run_report() + # the report appends a blank total row after the data rows + self.assertTrue(rows) + self.assertEqual(rows[-1], [""] * len(rows[0])) + + def test_sales_person_filter_scopes_rows(self): + si = self.make_invoice_with_commission() + + filtered = self.run_report(sales_person="_Test Sales Person 1") + self.assertNotIn(si.name, {r[0] for r in filtered if r[0]}) From 3092c920ff25b4cdffe0406270bad8bb4a2e16b1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:40:32 +0530 Subject: [PATCH 164/195] fix: pass only valid document filters in Sales Person-wise Transaction Summary --- .../sales_person_wise_transaction_summary.py | 65 ++++++++--------- ...t_sales_person_wise_transaction_summary.py | 69 +++++++++++++++++++ 2 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index f834f27df50..3616db44459 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -183,8 +183,22 @@ def get_entries(filters): .as_("contribution_amt") ) + # Only pass valid document-field filters to get_query; report-specific keys such as + # doc_type / sales_person / item_group are handled separately below. + doc_filters = {"docstatus": 1} + for field in ["company", "customer", "territory"]: + if filters.get(field): + doc_filters[field] = filters.get(field) + + if filters.get("from_date") and filters.get("to_date"): + doc_filters[date_field] = ["between", [filters.get("from_date"), filters.get("to_date")]] + elif filters.get("from_date"): + doc_filters[date_field] = [">=", filters.get("from_date")] + elif filters.get("to_date"): + doc_filters[date_field] = ["<=", filters.get("to_date")] + query = ( - frappe.get_query(dt, filters=filters, ignore_permissions=False) + frappe.get_query(dt, filters=doc_filters, ignore_permissions=False) .join(dt_item) .on(dt.name == dt_item.parent) .join(st) @@ -203,48 +217,27 @@ def get_entries(filters): contribution_amt_case, ) .where(st.parenttype == doc_type) - .where(dt.docstatus == 1) ) + if filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) + sp = frappe.qb.DocType("Sales Person") + query = query.where( + st.sales_person.isin(frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))) + ) + + items = get_items(filters) + if items: + query = query.where(dt_item.item_code.isin([d[0] for d in items])) + elif filters.get("item_group") or filters.get("brand"): + # item_group/brand filter matched nothing -> no rows + return [] + query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc) return query.run(as_dict=True) -def get_conditions(filters, date_field): - conditions = [""] - values = [] - - for field in ["company", "customer", "territory"]: - if filters.get(field): - conditions.append(f"dt.{field}=%s") - values.append(filters[field]) - - if filters.get("sales_person"): - lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) - conditions.append( - f"exists(select name from `tabSales Person` where lft >= {lft} and rgt <= {rgt} and name=st.sales_person)" - ) - - if filters.get("from_date"): - conditions.append(f"dt.{date_field}>=%s") - values.append(filters["from_date"]) - - if filters.get("to_date"): - conditions.append(f"dt.{date_field}<=%s") - values.append(filters["to_date"]) - - items = get_items(filters) - if items: - conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) - values += items - else: - # return empty result, if no items are fetched after filtering on 'item group' and 'brand' - conditions.append("dt_item.item_code = Null") - - return " and ".join(conditions), values - - def get_items(filters): item = qb.DocType("Item") diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py new file mode 100644 index 00000000000..2dbe8fee822 --- /dev/null +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py @@ -0,0 +1,69 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.report.sales_person_wise_transaction_summary.sales_person_wise_transaction_summary import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestSalesPersonWiseTransactionSummary(ERPNextTestSuite): + """Item-level summary joining a sales document with its Sales Team rows, showing + each sales person's contributed qty and amount per item line.""" + + def setUp(self): + self.sales_person = "_Test Sales Person" + + def make_invoice_with_commission(self, qty=5, rate=200, percentage=100): + si = create_sales_invoice( + item="_Test Item", qty=qty, rate=rate, do_not_save=True, posting_date="2026-06-01" + ) + si.append("sales_team", {"sales_person": self.sales_person, "allocated_percentage": percentage}) + si.insert() + si.submit() + return si + + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "doc_type": "Sales Invoice", "sales_person": self.sales_person} + ) + filters.update(extra) + return execute(filters)[1] + + def test_doc_type_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({"company": "_Test Company"})) + + def test_invalid_doc_type_throws(self): + self.assertRaises( + frappe.ValidationError, + execute, + frappe._dict({"company": "_Test Company", "doc_type": "Purchase Invoice"}), + ) + + def test_item_line_contribution(self): + si = self.make_invoice_with_commission(qty=5, rate=200, percentage=100) + item = si.items[0] + + rows = self.run_report() + row = next((r for r in rows if r[0] == si.name and r[5] == "_Test Item"), None) + self.assertIsNotNone(row, "Invoice item line missing from report") + + # row: name, customer, territory, warehouse, posting_date, item_code, item_group, + # brand, stock_qty, base_net_amount, sales_person, allocated_percentage, + # contributed_qty, contribution_amt, currency + self.assertEqual(row[1], si.customer) + self.assertEqual(row[8], item.stock_qty) + self.assertEqual(row[9], item.base_net_amount) + self.assertEqual(row[10], self.sales_person) + self.assertEqual(row[11], 100) + self.assertEqual(row[12], item.stock_qty * 100 / 100) # contributed qty + self.assertEqual(row[13], item.base_net_amount * 100 / 100) # contribution amount + + def test_appends_total_row(self): + self.make_invoice_with_commission() + rows = self.run_report() + self.assertTrue(rows) + self.assertEqual(rows[-1], [""] * len(rows[0])) From f95baa54dec0e4be164279b370017f348c57ef91 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:42:07 +0530 Subject: [PATCH 165/195] test: Territory-wise Sales report coverage --- .../test_territory_wise_sales.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py diff --git a/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py new file mode 100644 index 00000000000..f2b81bfb633 --- /dev/null +++ b/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py @@ -0,0 +1,59 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.doctype.quotation.test_quotation import make_quotation +from erpnext.selling.report.territory_wise_sales.territory_wise_sales import execute +from erpnext.tests.utils import ERPNextTestSuite + +TERRITORY = "_Test Territory" + + +class TestTerritoryWiseSales(ERPNextTestSuite): + """The report walks the Opportunity -> Quotation -> Sales Order -> Sales Invoice + funnel and totals each stage's amount per territory.""" + + def make_opportunity(self, amount=5000): + return frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Customer", + "party_name": "_Test Customer", + "territory": TERRITORY, + "company": "_Test Company", + "currency": "INR", + "opportunity_amount": amount, + "transaction_date": "2026-06-01", + } + ).insert() + + def make_quotation_for(self, opportunity, qty, rate): + qo = make_quotation(item="_Test Item", qty=qty, rate=rate, do_not_save=True) + qo.opportunity = opportunity.name + qo.insert() + qo.submit() + return qo + + def amount_for(self, territory, field): + for row in execute(frappe._dict({"company": "_Test Company"}))[1]: + if row["territory"] == territory: + return row[field] + return 0 + + def test_opportunity_amount_grouped_by_territory(self): + before = self.amount_for(TERRITORY, "opportunity_amount") + opp = self.make_opportunity(5000) + self.assertEqual(opp.territory, TERRITORY) + + after = self.amount_for(TERRITORY, "opportunity_amount") + self.assertEqual(after - before, 5000) + + def test_quotation_amount_flows_from_opportunity(self): + before = self.amount_for(TERRITORY, "quotation_amount") + + opp = self.make_opportunity() + quotation = self.make_quotation_for(opp, qty=2, rate=500) + + after = self.amount_for(TERRITORY, "quotation_amount") + self.assertEqual(after - before, quotation.base_grand_total) From 8d70385019fbede9fecfe55362039200b2b8c98a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:43:59 +0530 Subject: [PATCH 166/195] test: Territory Target Variance based on Item Group report coverage --- ...ory_target_variance_based_on_item_group.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py b/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py new file mode 100644 index 00000000000..dd7cca771e3 --- /dev/null +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py @@ -0,0 +1,71 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt, nowdate + +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.territory_target_variance_based_on_item_group.territory_target_variance_based_on_item_group import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestTerritoryTargetVarianceBasedOnItemGroup(ERPNextTestSuite): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def test_achieved_target_and_variance(self): + distribution = create_target_distribution(self.fiscal_year) + territory = create_territory_with_target( + "_Test Target Territory", self.fiscal_year, distribution.name, target_qty=50 + ) + + # a Sales Order in that territory contributes to the achieved quantity + so = make_sales_order(rate=1000, qty=20, do_not_submit=True) + so.territory = territory.name + so.submit() + + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Order", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + + rows = [frappe._dict(r) for r in result if r.get("territory") == territory.name] + self.assertTrue(rows, "Target territory missing from report") + achieved = sum(flt(r.total_achieved) for r in rows) + self.assertEqual(flt(rows[0].total_target, 2), 50) + self.assertEqual(flt(achieved, 2), 20) + self.assertEqual(flt(rows[0].total_variance, 2), -30) + + +def create_target_distribution(fiscal_year): + distribution = frappe.new_doc("Monthly Distribution") + distribution.distribution_id = "Target Report Distribution" + distribution.fiscal_year = fiscal_year + distribution.get_months() + return distribution.insert() + + +def create_territory_with_target(name, fiscal_year, distribution_id, target_qty=50): + doc = frappe.new_doc("Territory") + doc.territory_name = name + doc.parent_territory = "All Territories" + doc.is_group = 0 + doc.append( + "targets", + { + "fiscal_year": fiscal_year, + "target_qty": target_qty, + "target_amount": 30000, + "distribution_id": distribution_id, + }, + ) + return doc.insert() From 6e57bd325f2b516888f0df420f432e53a67c0c0f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:45:50 +0530 Subject: [PATCH 167/195] test: Purchase Analytics report coverage --- .../test_purchase_analytics.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 erpnext/buying/report/purchase_analytics/test_purchase_analytics.py diff --git a/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py b/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py new file mode 100644 index 00000000000..2c6369a5880 --- /dev/null +++ b/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py @@ -0,0 +1,84 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.buying.report.purchase_analytics.purchase_analytics import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" +SUPPLIER = "_Test Supplier" +SUPPLIER_GROUP = "_Test Supplier Group" +# A historical window that ordinary test fixtures don't post into. +FROM_DATE = "2019-04-01" +TO_DATE = "2019-06-30" + + +class TestPurchaseAnalytics(ERPNextTestSuite): + """purchase_analytics reuses the shared Analytics engine; these tests lock its + wiring (doc_type=Purchase Order) across the Supplier Group / Item Group trees.""" + + def setUp(self): + frappe.set_user("Administrator") + + def _filters(self, **overrides): + filters = { + "doc_type": "Purchase Order", + "value_quantity": "Value", + "range": "Monthly", + "company": COMPANY, + "from_date": FROM_DATE, + "to_date": TO_DATE, + } + filters.update(overrides) + return frappe._dict(filters) + + def _rows(self, filters): + return {row["entity"]: row for row in execute(filters)[1]} + + def make_po(self, qty=4, rate=250): + return create_purchase_order( + company=COMPANY, supplier=SUPPLIER, qty=qty, rate=rate, transaction_date="2019-04-10" + ) + + def test_supplier_group_tree_rolls_up_to_root(self): + filters = self._filters(tree_type="Supplier Group") + base = self._rows(filters) + base_group = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0)) + + po = self.make_po(qty=4, rate=250) + rows = self._rows(filters) + + # supplier is remapped to its group; the root sits at indent 0 + self.assertIn(SUPPLIER_GROUP, rows) + self.assertIn("All Supplier Groups", rows) + self.assertNotIn(SUPPLIER, rows) + self.assertEqual(rows["All Supplier Groups"]["indent"], 0) + + self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_group, flt(po.base_net_total), places=2) + self.assertGreaterEqual(flt(rows["All Supplier Groups"]["total"]), flt(po.base_net_total)) + + def test_item_group_tree_rolls_up_to_root(self): + item_group = frappe.db.get_value("Item", "_Test Item", "item_group") + filters = self._filters(tree_type="Item Group") + base = self._rows(filters) + base_group = flt(base.get(item_group, {}).get("total", 0.0)) + + po = self.make_po(qty=4, rate=250) + rows = self._rows(filters) + + self.assertIn(item_group, rows) + self.assertIn("All Item Groups", rows) + self.assertAlmostEqual(rows[item_group]["total"] - base_group, flt(po.base_net_total), places=2) + + def test_supplier_group_by_quantity(self): + filters = self._filters(tree_type="Supplier Group", value_quantity="Quantity") + base = self._rows(filters) + base_qty = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0)) + + po = self.make_po(qty=7, rate=100) + rows = self._rows(filters) + + self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_qty, flt(po.total_qty), places=2) From 56e7690e640a891aec58e79e7957cb6e7d65c5e2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:47:57 +0530 Subject: [PATCH 168/195] test: Subcontract Order Summary report coverage --- .../test_subcontract_order_summary.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py diff --git a/erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py new file mode 100644 index 00000000000..e6e0922eaf2 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.buying.report.subcontract_order_summary.subcontract_order_summary import execute +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_subcontracted_items, +) +from erpnext.tests.utils import ERPNextTestSuite + +FG_ITEM = "Subcontracted Item SA7" + + +class TestSubcontractOrderSummary(ERPNextTestSuite): + """The report lists Subcontracting Order finished items with their ordered and + received quantities within the transaction-date window.""" + + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "from_date": add_days(today(), -1), "to_date": add_days(today(), 1)} + ) + filters.update(extra) + return execute(filters)[1] + + def test_subcontracting_order_is_listed(self): + sco = get_subcontracting_order() + + rows = [r for r in self.run_report(name=sco.name) if r.get("item_code") == FG_ITEM] + self.assertTrue(rows, "Subcontracting Order finished item missing from report") + self.assertEqual(rows[0]["qty"], 10) + self.assertEqual(rows[0]["received_qty"], 0) # nothing received yet + + def test_out_of_range_date_excludes_order(self): + sco = get_subcontracting_order() + + data = self.run_report(name=sco.name, from_date="2019-01-01", to_date="2019-01-31") + self.assertEqual(data, []) From 2a8d26c0a7e3ae336e70b64458e99fd8524e332d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:49:52 +0530 Subject: [PATCH 169/195] test: Supplier Quotation Comparison report coverage --- .../test_supplier_quotation_comparison.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py diff --git a/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py new file mode 100644 index 00000000000..7eaed09cd14 --- /dev/null +++ b/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py @@ -0,0 +1,62 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" +ITEM = "_Test Item" + + +class TestSupplierQuotationComparison(ERPNextTestSuite): + """The report lists Supplier Quotation item lines so quotes for the same item can + be compared across suppliers.""" + + def make_quotation(self, supplier, qty, rate): + sq = frappe.get_doc( + { + "doctype": "Supplier Quotation", + "supplier": supplier, + "company": COMPANY, + "currency": "INR", + "transaction_date": "2026-06-01", + "items": [ + {"item_code": ITEM, "qty": qty, "rate": rate, "warehouse": "_Test Warehouse - _TC"} + ], + } + ) + sq.insert() + sq.submit() + return sq + + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, "from_date": "2026-01-01", "to_date": "2026-12-31"}) + filters.update(extra) + return execute(filters)[1] + + def test_no_filters_returns_empty(self): + self.assertEqual(execute(None)[1], []) + + def test_quotation_line_listed_with_price(self): + sq = self.make_quotation("_Test Supplier", qty=10, rate=100) + + rows = [r for r in self.run_report(item_code=ITEM) if r.get("quotation") == sq.name] + self.assertTrue(rows, "Supplier Quotation line missing from report") + row = rows[0] + self.assertEqual(row["supplier_name"], "_Test Supplier") + self.assertEqual(row["qty"], 10) + self.assertEqual(row["base_rate"], 100) + self.assertEqual(row["base_amount"], 1000) + self.assertEqual(row["price_per_unit"], 100) + + def test_compares_multiple_suppliers_for_item(self): + sq1 = self.make_quotation("_Test Supplier", qty=10, rate=100) + sq2 = self.make_quotation("_Test Supplier 1", qty=10, rate=120) + + quotes = {r["quotation"]: r for r in self.run_report(item_code=ITEM) if r.get("quotation")} + self.assertIn(sq1.name, quotes) + self.assertIn(sq2.name, quotes) + self.assertEqual(quotes[sq1.name]["base_rate"], 100) + self.assertEqual(quotes[sq2.name]["base_rate"], 120) From 5514c64b7cb9a5ccbab6b1226a171a4bf21e6a16 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:51:22 +0530 Subject: [PATCH 170/195] test: Lead Owner Efficiency report coverage --- .../test_lead_owner_efficiency.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py diff --git a/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py b/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py new file mode 100644 index 00000000000..f1063c2e8e7 --- /dev/null +++ b/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py @@ -0,0 +1,70 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.crm.report.lead_owner_efficiency.lead_owner_efficiency import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestLeadOwnerEfficiency(ERPNextTestSuite): + """Groups leads by their owner and counts the opportunity/quotation/order funnel + derived from those leads.""" + + def setUp(self): + # a unique owner keeps the per-owner counts isolated from other tests' leads + self.owner = self.make_user() + + def make_user(self): + email = f"lead_owner_{frappe.generate_hash(length=8)}@example.com" + frappe.get_doc( + {"doctype": "User", "email": email, "first_name": "Lead Owner", "send_welcome_email": 0} + ).insert() + return email + + def make_lead(self): + return frappe.get_doc( + { + "doctype": "Lead", + "lead_name": f"Lead {frappe.generate_hash(length=6)}", + "lead_owner": self.owner, + "company": "_Test Company", + } + ).insert() + + def run_report(self, **extra): + filters = frappe._dict({"from_date": add_days(today(), -1), "to_date": today()}) + filters.update(extra) + return execute(filters)[1] + + def owner_row(self, data): + return next((r for r in data if r["lead_owner"] == self.owner), None) + + def test_lead_count_grouped_by_owner(self): + self.make_lead() + self.make_lead() + + row = self.owner_row(self.run_report()) + self.assertIsNotNone(row, "Lead owner missing from report") + self.assertEqual(row["lead_count"], 2) + self.assertEqual(row["opp_count"], 0) + self.assertEqual(row["opp_lead"], 0.0) + + def test_opportunity_from_lead_is_counted(self): + lead = self.make_lead() + frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Lead", + "party_name": lead.name, + "company": "_Test Company", + "currency": "INR", + } + ).insert() + + row = self.owner_row(self.run_report()) + self.assertEqual(row["lead_count"], 1) + self.assertEqual(row["opp_count"], 1) + # one opportunity from one lead -> 100% opp/lead conversion + self.assertEqual(row["opp_lead"], 100.0) From c7fed2956953d500f5a4be9e12444f4ed6349f8c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:53:19 +0530 Subject: [PATCH 171/195] test: Project Summary report coverage --- .../project_summary/test_project_summary.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 erpnext/projects/report/project_summary/test_project_summary.py diff --git a/erpnext/projects/report/project_summary/test_project_summary.py b/erpnext/projects/report/project_summary/test_project_summary.py new file mode 100644 index 00000000000..9f0ef5c7a4a --- /dev/null +++ b/erpnext/projects/report/project_summary/test_project_summary.py @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.projects.report.project_summary.project_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProjectSummary(ERPNextTestSuite): + """Lists projects with their total / completed / overdue task counts.""" + + def make_project(self): + return frappe.get_doc( + { + "doctype": "Project", + "project_name": f"_Test PS {frappe.generate_hash(length=6)}", + "company": "_Test Company", + } + ).insert() + + def make_task(self, project, status="Open"): + task = frappe.get_doc( + { + "doctype": "Task", + "subject": f"Task {frappe.generate_hash(length=6)}", + "project": project.name, + } + ).insert() + if status != "Open": + # set the status directly; the report counts tasks by their stored status + frappe.db.set_value("Task", task.name, "status", status) + return task + + def run_report(self, project): + return execute(frappe._dict({"name": project.name})) + + def project_row(self, project): + _columns, data, *_rest = self.run_report(project) + return next((r for r in data if r["name"] == project.name), None) + + def test_task_counts(self): + project = self.make_project() + self.make_task(project, "Completed") + self.make_task(project, "Completed") + self.make_task(project, "Open") + self.make_task(project, "Overdue") + + row = self.project_row(project) + self.assertIsNotNone(row, "Project missing from report") + self.assertEqual(row["total_tasks"], 4) + self.assertEqual(row["completed_tasks"], 2) + self.assertEqual(row["overdue_tasks"], 1) + + def test_report_summary_totals(self): + project = self.make_project() + self.make_task(project, "Completed") + self.make_task(project, "Open") + + report_summary = self.run_report(project)[4] + summary = {s["label"]: s["value"] for s in report_summary} + self.assertEqual(summary["Total Tasks"], 2) + self.assertEqual(summary["Completed Tasks"], 1) + self.assertEqual(summary["Overdue Tasks"], 0) From f9ac05f4a16324ef003a849852b2cfd6232fe35c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 15:54:55 +0530 Subject: [PATCH 172/195] test: Timesheet Billing Summary report coverage --- .../test_timesheet_billing_summary.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py diff --git a/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py b/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py new file mode 100644 index 00000000000..5526d5db01c --- /dev/null +++ b/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py @@ -0,0 +1,62 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet +from erpnext.projects.report.timesheet_billing_summary.timesheet_billing_summary import execute +from erpnext.setup.doctype.employee.test_employee import make_employee +from erpnext.tests.utils import ERPNextTestSuite + + +class TestTimesheetBillingSummary(ERPNextTestSuite): + """Lists submitted Timesheet Detail rows with working/billing hours and amount, + optionally grouped by date/project/employee.""" + + def setUp(self): + self.employee = make_employee("timesheet_billing@example.com", company="_Test Company") + self.project = frappe.get_doc( + { + "doctype": "Project", + "project_name": f"_Test TBS {frappe.generate_hash(length=6)}", + "company": "_Test Company", + } + ).insert() + + def make_ts(self, is_billable=1): + return make_timesheet( + self.employee, simulate=True, is_billable=is_billable, project=self.project.name + ) + + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company", "employee": self.employee}) + filters.update(extra) + return execute(filters)[1] + + def test_billable_timesheet_row(self): + ts = self.make_ts(is_billable=1) + detail = ts.time_logs[0] + + rows = [r for r in self.run_report() if r.get("timesheet") == ts.name] + self.assertTrue(rows, "Timesheet missing from report") + row = rows[0] + self.assertEqual(row["hours"], 2) + self.assertEqual(row["billing_hours"], detail.billing_hours) + self.assertEqual(row["billing_amount"], detail.billing_amount) + self.assertEqual(row["project"], self.project.name) + + def test_group_by_project_sums_hours(self): + self.make_ts(is_billable=1) + + data = self.run_report(group_by="project") + group_rows = [r for r in data if r.get("is_group") and r.get("project") == self.project.name] + self.assertTrue(group_rows, "Grouped project row missing") + self.assertEqual(group_rows[0]["hours"], 2) + + def test_draft_excluded_unless_requested(self): + ts = make_timesheet(self.employee, is_billable=1, project=self.project.name, do_not_submit=True) + + # submitted-only by default: the draft timesheet is absent + self.assertNotIn(ts.name, {r.get("timesheet") for r in self.run_report()}) + # ... but included when draft timesheets are requested + self.assertIn(ts.name, {r.get("timesheet") for r in self.run_report(include_draft_timesheets=1)}) From 71f02d412a29f57abd4ac605ee374a6064373c0b Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 2 Jul 2026 16:56:34 +0530 Subject: [PATCH 173/195] fix: skip stock reservation for opted-out production plans --- .../purchase_receipt/services/reservation.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/services/reservation.py b/erpnext/stock/doctype/purchase_receipt/services/reservation.py index 8141ebb9ee5..6be9734d4f1 100644 --- a/erpnext/stock/doctype/purchase_receipt/services/reservation.py +++ b/erpnext/stock/doctype/purchase_receipt/services/reservation.py @@ -63,6 +63,13 @@ class PurchaseReceiptStockReservation: return production_plan_references = self.get_production_plan_references() + if not production_plan_references: + return + + reservable_plans = self.get_reservable_production_plans(production_plan_references) + if not reservable_plans: + return + production_plan_items = [] doc.reload() @@ -70,6 +77,9 @@ class PurchaseReceiptStockReservation: for row in doc.items: if row.material_request_item and row.material_request_item in production_plan_references: _ref = production_plan_references[row.material_request_item] + if _ref.production_plan not in reservable_plans: + continue + docnames.append(_ref.production_plan) row.update( { @@ -95,6 +105,25 @@ class PurchaseReceiptStockReservation: docnames, from_doctype="Production Plan", to_doctype="Work Order" ) + def get_reservable_production_plans(self, production_plan_references: frappe._dict) -> set: + """Production Plans that opted into stock reservation (``reserve_stock``). + + A Production Plan only gets this flag set if "Auto Reserve Stock" was enabled in + Stock Settings when it was created, or the user ticked "Reserve Stock" manually. + Without this check, a Purchase Receipt would auto-reserve stock for every + Production Plan whenever "Enable Stock Reservation" is on, ignoring both of those. + """ + plan_names = {ref.production_plan for ref in production_plan_references.values()} + return { + p.name + for p in frappe.get_all( + "Production Plan", + filters={"name": ["in", list(plan_names)]}, + fields=["name", "reserve_stock"], + ) + if p.reserve_stock + } + def get_production_plan_references(self) -> frappe._dict: production_plan_references = frappe._dict() material_request_items = [] From 820f5498e74ca206d54c83dc57acdec677cd9629 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 2 Jul 2026 16:56:44 +0530 Subject: [PATCH 174/195] test: cover reserve stock gating on purchase receipt submit --- .../production_plan/test_production_plan.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index e720bd96319..ac2b38ea216 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2322,6 +2322,145 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(len(reserved_entries), 0) frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_no_stock_reservation_via_purchase_receipt_when_reserve_stock_disabled(self): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.mapper import make_purchase_order + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0) + + bom_tree = {"FG For SR No Auto Reserve": {"RM For SR No Auto Reserve": {}}} + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + # reserve_stock is deliberately left unset (defaults to 0): this is what happens when + # "Auto Reserve Stock" is off and nobody ticks "Reserve Stock" on the Production Plan by hand. + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=5, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + for_warehouse=warehouse, + ) + plan.get_sub_assembly_items() + plan.set("mr_items", []) + for d in get_items_for_material_requests(plan.as_dict()): + plan.append("mr_items", d) + plan.save() + + self.assertEqual(plan.reserve_stock, 0) + plan.submit() + + plan.submit_material_request = 1 + plan.make_material_request() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + self.assertGreater(len(material_requests), 0) + + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertEqual(len(reserved_entries), 0) + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_ignores_production_plans_with_reserve_stock_off_on_shared_purchase_order(self): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0) + + warehouse = "_Test Warehouse - _TC" + + bom_reserve = create_nested_bom({"FG SR Mixed Reserve": {"RM SR Mixed Reserve": {}}}, prefix="") + bom_skip = create_nested_bom({"FG SR Mixed Skip": {"RM SR Mixed Skip": {}}}, prefix="") + + def make_submitted_plan(item_code, reserve_stock): + plan = create_production_plan( + item_code=item_code, + planned_qty=5, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=reserve_stock, + ) + plan.get_sub_assembly_items() + plan.set("mr_items", []) + for d in get_items_for_material_requests(plan.as_dict()): + plan.append("mr_items", d) + plan.save() + plan.submit() + plan.submit_material_request = 1 + plan.make_material_request() + return plan + + plan_reserve = make_submitted_plan(bom_reserve.item, reserve_stock=1) + plan_skip = make_submitted_plan(bom_skip.item, reserve_stock=0) + + self.assertEqual(plan_reserve.reserve_stock, 1) + self.assertEqual(plan_skip.reserve_stock, 0) + + mr_reserve = frappe.get_all( + "Material Request", filters={"production_plan": plan_reserve.name}, pluck="name" + )[0] + mr_skip = frappe.get_all( + "Material Request", filters={"production_plan": plan_skip.name}, pluck="name" + )[0] + + # One Purchase Order pulling rows from both Material Requests, so the Purchase Receipt made + # from it has both a reservable and a non-reservable Production Plan reference in `doc.items`. + po = frappe.new_doc("Purchase Order") + po.supplier = "_Test Supplier" + po.company = plan_reserve.company + po.schedule_date = nowdate() + + for mr_name in (mr_reserve, mr_skip): + mr = frappe.get_doc("Material Request", mr_name) + for item in mr.items: + po.append( + "items", + { + "item_code": item.item_code, + "qty": item.qty, + "rate": 100, + "schedule_date": nowdate(), + "warehouse": warehouse, + "material_request": mr.name, + "material_request_item": item.name, + }, + ) + + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + reserved_for_plan_reserve = StockReservation(plan_reserve).get_reserved_entries( + "Production Plan", plan_reserve.name + ) + reserved_for_plan_skip = StockReservation(plan_skip).get_reserved_entries( + "Production Plan", plan_skip.name + ) + + self.assertGreater(len(reserved_for_plan_reserve), 0) + self.assertEqual(len(reserved_for_plan_skip), 0) + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_stock_reservation_restored_on_work_order_cancel(self): # Spec #5 (cancellation path): when a Work Order created from a Production Plan is cancelled, # the reservation that was transferred PP -> WO must flow back to the still-open Production From 3f9b8fe37ee8a9c4ba0d67c77b0e6446727bae11 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:03:32 +0530 Subject: [PATCH 175/195] test: reconcile negative-stock warehouses in reset_item_valuation_rate --- erpnext/manufacturing/doctype/bom/test_bom.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 7797d819e39..2af6dbecb1a 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -881,8 +881,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non warehouse_list = [warehouse_list] if not warehouse_list: + # Reconcile every warehouse the item has a non-zero balance in -- including + # negative balances left by other tests. `get_valuation_rate` averages + # Sum(stock_value)/Sum(actual_qty) across all bins, so a leftover negative + # balance in one warehouse can cancel the reset qty elsewhere and make the + # average collapse to 0, which is a source of flaky BOM-cost failures. warehouse_list = frappe.get_all( - "Bin", filters={"item_code": item_code, "actual_qty": [">", 0]}, pluck="warehouse" + "Bin", filters={"item_code": item_code, "actual_qty": ["!=", 0]}, pluck="warehouse" ) if not warehouse_list: From e0b0926dfff0f9105f5f4c57f116d1d4f28efd57 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:10:19 +0530 Subject: [PATCH 177/195] fix: only resolve items when item_group/brand filter is set --- .../sales_person_wise_transaction_summary.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index 3616db44459..180740dcb6e 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -226,12 +226,14 @@ def get_entries(filters): st.sales_person.isin(frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))) ) - items = get_items(filters) - if items: + # only resolve items when an item_group/brand filter is set; otherwise get_items + # would return every item in the system and add a huge IN() clause on each run + if filters.get("item_group") or filters.get("brand"): + items = get_items(filters) + if not items: + # the item_group/brand filter matched nothing -> no rows + return [] query = query.where(dt_item.item_code.isin([d[0] for d in items])) - elif filters.get("item_group") or filters.get("brand"): - # item_group/brand filter matched nothing -> no rows - return [] query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc) From 5298438905df787aab07be7ce598a6e00a47ed6f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:11:37 +0530 Subject: [PATCH 178/195] test: also assert Jun amount bucket in Quotation Trends monthly test --- .../selling/report/quotation_trends/test_quotation_trends.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/selling/report/quotation_trends/test_quotation_trends.py b/erpnext/selling/report/quotation_trends/test_quotation_trends.py index 05b9c55e839..4ff03a5b53c 100644 --- a/erpnext/selling/report/quotation_trends/test_quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/test_quotation_trends.py @@ -62,12 +62,15 @@ class TestQuotationTrends(ERPNextTestSuite): # Monthly period => one bucket per month; a 2026-06-01 quotation hits "Jun (Qty)"/"(Amt)". labels, before = self.run_report(period="Monthly") before_jun_qty = self._cell(before, "Item", "_Test Item", "Jun (Qty)", labels) + before_jun_amt = self._cell(before, "Item", "_Test Item", "Jun (Amt)", labels) before_may_qty = self._cell(before, "Item", "_Test Item", "May (Qty)", labels) make_quotation(item="_Test Item", qty=3, rate=100, transaction_date=TXN_DATE) labels, after = self.run_report(period="Monthly") self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Qty)", labels) - before_jun_qty, 3) + # the amount path is a separate SUM(base_net_amount) case, so assert it too + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Amt)", labels) - before_jun_amt, 300) # nothing was quoted in May, so that bucket is unchanged self.assertEqual(self._cell(after, "Item", "_Test Item", "May (Qty)", labels) - before_may_qty, 0) From 2bab709ac442566c313d70c189d09fa9fb980074 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:12:58 +0530 Subject: [PATCH 179/195] test: scope date range, reload invoice, strengthen total-row check --- .../test_sales_person_commission_summary.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py index e807d33f506..b1385ca4f09 100644 --- a/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py @@ -31,11 +31,20 @@ class TestSalesPersonCommissionSummary(ERPNextTestSuite): ) si.insert() si.submit() + si.reload() # reflect any values recomputed on submit return si def run_report(self, **extra): filters = frappe._dict( - {"company": "_Test Company", "doc_type": "Sales Invoice", "sales_person": self.sales_person} + { + "company": "_Test Company", + "doc_type": "Sales Invoice", + "sales_person": self.sales_person, + # scope to this test's posting date so the query isn't unbounded over + # every invoice for the shared sales person + "from_date": "2026-06-01", + "to_date": "2026-06-01", + } ) filters.update(extra) return execute(filters)[1] @@ -64,8 +73,9 @@ class TestSalesPersonCommissionSummary(ERPNextTestSuite): def test_appends_total_row(self): self.make_invoice_with_commission() rows = self.run_report() - # the report appends a blank total row after the data rows - self.assertTrue(rows) + # the report appends a blank total row after one or more real data rows + self.assertGreaterEqual(len(rows), 2) + self.assertTrue(any(r[0] for r in rows[:-1]), "expected real data rows before the total row") self.assertEqual(rows[-1], [""] * len(rows[0])) def test_sales_person_filter_scopes_rows(self): From 087fb29d51f8e8a3bdced4a3b3287b28fc866720 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:13:42 +0530 Subject: [PATCH 180/195] test: narrow Territory-wise Sales docstring to covered stages --- .../report/territory_wise_sales/test_territory_wise_sales.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py index f2b81bfb633..8a069810b8d 100644 --- a/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py @@ -12,7 +12,10 @@ TERRITORY = "_Test Territory" class TestTerritoryWiseSales(ERPNextTestSuite): """The report walks the Opportunity -> Quotation -> Sales Order -> Sales Invoice - funnel and totals each stage's amount per territory.""" + funnel and totals each stage's amount per territory. + + These tests cover the Opportunity and Quotation stages; the Sales Order and + Sales Invoice (order_amount / billing_amount) stages are not yet exercised.""" def make_opportunity(self, amount=5000): return frappe.get_doc( From 08884056402bbbc7972d22a450bb1ca819622439 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:15:05 +0530 Subject: [PATCH 181/195] test: reuse shared distribution helper and assert a single territory row --- ...ory_target_variance_based_on_item_group.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py b/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py index dd7cca771e3..8c98a98bd7c 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py @@ -6,6 +6,9 @@ from frappe.utils import flt, nowdate from erpnext.accounts.utils import get_fiscal_year from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import ( + create_target_distribution, +) from erpnext.selling.report.territory_target_variance_based_on_item_group.territory_target_variance_based_on_item_group import ( execute, ) @@ -38,20 +41,14 @@ class TestTerritoryTargetVarianceBasedOnItemGroup(ERPNextTestSuite): ) )[1] + # no item_group is set on the target, so the report emits exactly one row per + # territory -- assert all three figures against that single row rows = [frappe._dict(r) for r in result if r.get("territory") == territory.name] - self.assertTrue(rows, "Target territory missing from report") - achieved = sum(flt(r.total_achieved) for r in rows) - self.assertEqual(flt(rows[0].total_target, 2), 50) - self.assertEqual(flt(achieved, 2), 20) - self.assertEqual(flt(rows[0].total_variance, 2), -30) - - -def create_target_distribution(fiscal_year): - distribution = frappe.new_doc("Monthly Distribution") - distribution.distribution_id = "Target Report Distribution" - distribution.fiscal_year = fiscal_year - distribution.get_months() - return distribution.insert() + self.assertEqual(len(rows), 1, "expected exactly one row for the target territory") + row = rows[0] + self.assertEqual(flt(row.total_target, 2), 50) + self.assertEqual(flt(row.total_achieved, 2), 20) + self.assertEqual(flt(row.total_variance, 2), -30) def create_territory_with_target(name, fiscal_year, distribution_id, target_qty=50): From 50b6f50b88d2ee9855999bab4ab0262591282991 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 17:16:20 +0530 Subject: [PATCH 182/195] test: assert root rollup and no item leak in Purchase Analytics --- .../report/purchase_analytics/test_purchase_analytics.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py b/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py index 2c6369a5880..35cd9ebac58 100644 --- a/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py +++ b/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py @@ -71,14 +71,23 @@ class TestPurchaseAnalytics(ERPNextTestSuite): self.assertIn(item_group, rows) self.assertIn("All Item Groups", rows) + # the raw item code must not leak as its own entity; the root sits at indent 0 + self.assertNotIn("_Test Item", rows) + self.assertEqual(rows["All Item Groups"]["indent"], 0) self.assertAlmostEqual(rows[item_group]["total"] - base_group, flt(po.base_net_total), places=2) + self.assertGreaterEqual(flt(rows["All Item Groups"]["total"]), flt(po.base_net_total)) def test_supplier_group_by_quantity(self): filters = self._filters(tree_type="Supplier Group", value_quantity="Quantity") base = self._rows(filters) base_qty = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0)) + base_root_qty = flt(base.get("All Supplier Groups", {}).get("total", 0.0)) po = self.make_po(qty=7, rate=100) rows = self._rows(filters) self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_qty, flt(po.total_qty), places=2) + # the quantity must roll up to the root too, not just the leaf group + self.assertAlmostEqual( + rows["All Supplier Groups"]["total"] - base_root_qty, flt(po.total_qty), places=2 + ) From 6591ae195d8790f22d5d202df7ed3ae9e8d65603 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 2 Jul 2026 17:38:14 +0530 Subject: [PATCH 183/195] fix(manufacturing): update work order status on partial pick-list transfer A stock entry created from a pick list has fg_completed_qty=0, so material_transferred_for_manufacturing is derived from the min-fraction of item-level transfers. When a pick list moves only some required items, the un-picked item stays at 0, which zeroes the aggregate and leaves the work order status at "not started" even though material is already in wip. Promote the status to "in process" when any raw material has been transferred via a pick list. material_transferred_for_manufacturing stays min-fraction based (0 correctly means no full finished good can be started yet). --- .../doctype/work_order/services/status.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py index ce67978afd7..d22ee8bd937 100644 --- a/erpnext/manufacturing/doctype/work_order/services/status.py +++ b/erpnext/manufacturing/doctype/work_order/services/status.py @@ -132,7 +132,9 @@ class StatusService: status = ( "In Process" - if flt(self.doc.material_transferred_for_manufacturing) > 0 or self.doc.skip_transfer + if flt(self.doc.material_transferred_for_manufacturing) > 0 + or self.doc.skip_transfer + or self._has_transferred_material() else "Not Started" ) precision = frappe.get_precision("Work Order", "produced_qty") @@ -141,6 +143,26 @@ class StatusService: status = "Completed" return status + def _has_transferred_material(self): + """True if any raw material was transferred against this work order via a pick list + (these leave material_transferred_for_manufacturing at 0 via the min-fraction rule).""" + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") + qty = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on(ste_child.parent == ste.name) + .select(Sum(ste_child.transfer_qty)) + .where( + (ste.work_order == self.doc.name) + & (ste.docstatus == 1) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 0) + & (ste.pick_list.isnotnull()) + ) + ).run()[0][0] + return flt(qty) > 0 + def _is_partial_skip_transfer(self): return bool( self.doc.skip_transfer From f85f6be3cf1afa64e47b80ba4caad0c5d6e2175c Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 2 Jul 2026 17:39:23 +0530 Subject: [PATCH 184/195] test(manufacturing): add test to validate the work order status on partial pick-list transfer Cover the pick-list flow where a stock entry moves only one of the work order's required items: material_transferred_for_manufacturing stays 0 (min fraction) while the status must move to "in process". --- .../doctype/work_order/test_work_order.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0a4bbcddd2a..a101bb04b4d 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1528,6 +1528,38 @@ class TestWorkOrder(ERPNextTestSuite): work_order.reload() self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0) + def test_status_in_process_when_only_one_required_item_transferred(self): + """Stock Entry created from a Pick List that picked only one of the required items: + min-fraction keeps material_transferred_for_manufacturing at 0, but the work order must + still move to In Process because material is already in WIP.""" + from erpnext.manufacturing.doctype.work_order.mapper import create_pick_list + from erpnext.stock.doctype.pick_list.mapper import create_stock_entry + + work_order = make_wo_order_test_record( + planned_start_date=now(), qty=2, source_warehouse="Stores - _TC" + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) + + pick_list = create_pick_list(work_order.name, for_qty=work_order.qty) + # pick only _Test Item; the other required item is left out of this pick list + pick_list.pick_manually = 1 + pick_list.locations = [loc for loc in pick_list.locations if loc.item_code == "_Test Item"] + pick_list.save() + pick_list.submit() + + stock_entry = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(stock_entry.fg_completed_qty, 0.0) + stock_entry.submit() + + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 0.0) + self.assertEqual(work_order.status, "In Process") + def test_backflushed_batch_raw_materials_based_on_transferred(self): frappe.db.set_single_value( "Manufacturing Settings", From 5b738b7b0d21289569c92f0f3a85c7a57f3a2981 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:33:26 +0200 Subject: [PATCH 185/195] fix: don't attempt to create SABB for non-serialized / non-batch items (#56627) * fix: don't attempt to create SABB for non-serialized / non-batch items * fix(stock): skip serial batch lookup for rows without item code --- erpnext/stock/services/serial_batch_bundle_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/services/serial_batch_bundle_service.py b/erpnext/stock/services/serial_batch_bundle_service.py index 29d732c1e32..2e752371ed5 100644 --- a/erpnext/stock/services/serial_batch_bundle_service.py +++ b/erpnext/stock/services/serial_batch_bundle_service.py @@ -176,6 +176,10 @@ class SerialBatchBundleService: parent_details = self.get_parent_details_for_packed_items() for row in self.doc.get(table_name): + item_code = row.get("rm_item_code") or row.get("item_code") + if not item_code or not self.is_serial_batch_item(item_code): + continue + if ( not via_landed_cost_voucher and row.serial_and_batch_bundle From caa4358057c9e22c32a81045b9ba5819442b5cc1 Mon Sep 17 00:00:00 2001 From: Shllokkk <140623894+Shllokkk@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:33:16 +0530 Subject: [PATCH 186/195] fix: guard against missing DocType in onboarding steps patch (#56804) --- .../patches/v16_0/complete_onboarding_steps_for_older_sites.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py b/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py index 7230334266e..3f6e30bcbc5 100644 --- a/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py +++ b/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py @@ -34,6 +34,7 @@ def complete_onboarding_steps_if_record_exists(steps): if ( step.action == "Create Entry" and step.reference_document + and frappe.db.exists("DocType", step.reference_document) and frappe.get_all(step.reference_document, limit=1) ): frappe.db.set_value("Onboarding Step", step.name, "is_complete", 1, update_modified=False) From ceadc4f2696b6cad1991d8e425845469dbc8147c Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 2 Jul 2026 20:38:31 +0530 Subject: [PATCH 187/195] fix: sync translations from crowdin (#56673) --- erpnext/locale/bs.po | 545 ++++++++++++++++++++++--------------------- erpnext/locale/fa.po | 38 +-- erpnext/locale/hr.po | 545 ++++++++++++++++++++++--------------------- erpnext/locale/sv.po | 20 +- 4 files changed, 577 insertions(+), 571 deletions(-) diff --git a/erpnext/locale/bs.po b/erpnext/locale/bs.po index cf5e37e713c..681a4eddbc6 100644 --- a/erpnext/locale/bs.po +++ b/erpnext/locale/bs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-28 20:03\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -269,7 +269,7 @@ msgstr "'Dozvoli višestruke Prodajne Naloge naspram Nabavnog Naloga Klijenta'" #: erpnext/controllers/trends.py:62 msgid "'Based On' and 'Group By' can not be the same" -msgstr "" +msgstr "'Na Osnovu' i 'Grupiraj Po' ne mogu biti isti" #: erpnext/selling/report/inactive_customers/inactive_customers.py:23 msgid "'Days Since Last Order' must be greater than or equal to zero" @@ -295,15 +295,15 @@ msgstr "'Od datuma' mora biti nakon 'Do datuma'" #: erpnext/stock/doctype/item/item.py:466 msgid "'Has Serial No' cannot be 'Yes' for non-stock item" -msgstr "" +msgstr "'Ima Serijski Broj' ne može biti 'Da' za artikal koji nije na zalihama" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:145 msgid "'Inspection Required before Delivery' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Dostave' je onemogućena za artikal {0}, nema potrebe za kreiranjem Kontrole Kvaliteta" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:136 msgid "'Inspection Required before Purchase' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Nabave' je onemogućena za artikal {0}, nema potrebe za kreiranjem Kontrole Kvaliteta" #: erpnext/stock/report/stock_ledger/stock_ledger.py:684 #: erpnext/stock/report/stock_ledger/stock_ledger.py:725 @@ -323,7 +323,7 @@ msgstr "'Do Paketa Broj' ne može biti manje od 'Od Paketa Broj.'" #: erpnext/controllers/sales_and_purchase_return.py:80 msgid "'Update Stock' cannot be checked because items are not delivered via {0}" -msgstr "" +msgstr "'Ažuriraj Zalihe' ne se može provjeriti jer artikli nisu dostavljeni putem {0}" #: erpnext/accounts/doctype/sales_invoice/services/fixed_assets.py:46 msgid "'Update Stock' cannot be checked for fixed asset sale" @@ -827,7 +827,7 @@ msgstr "

Ne može se fakturisati više od predviđenog iznosa za sljedeće art #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:159 msgid "

Following {0}s do not belong to Company {1}:

" -msgstr "" +msgstr "

Slijedeći {0} ne pripadaju {1}:

" #. Content of the 'html_llwp' (HTML) field in DocType 'Request for Quotation' #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -1042,7 +1042,7 @@ msgstr "A - C" #: erpnext/selling/doctype/customer/customer.py:358 msgid "A Customer Group exists with the same name. Please change the Customer name or rename the Customer Group" -msgstr "" +msgstr "Grupa Klijenta postoji sa istim imenom, preimenujte klijenta ili preimenujte Grupu Klijenta" #: erpnext/manufacturing/doctype/workstation/workstation.js:73 msgid "A Holiday List can be added to exclude counting these days for the Workstation." @@ -1054,7 +1054,7 @@ msgstr "Potencijalni Klijent zahtijeva ili ime osobe ili ime poduzeća" #: erpnext/stock/doctype/packing_slip/packing_slip.py:83 msgid "A Packing Slip can only be created for a Draft Delivery Note." -msgstr "" +msgstr "Nalog Pakovanja se može kreirati samo za nacrt Dostavnice." #: erpnext/accounts/services/gl_validator.py:123 msgid "A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more." @@ -1312,7 +1312,7 @@ msgstr "Pristupni ključ je potreban za davaoca usluga: {0}" #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:423 msgid "Access to Request for Quotation from the portal is disabled. To allow access, enable it in Portal Settings." -msgstr "" +msgstr "Pristup Zahtjevu za Ponudu sa portala je onemogućen. Da biste omogućili pristup, omogući ga u Postavkama Portala." #. Description of the 'Common Code' (Data) field in DocType 'UOM' #: erpnext/setup/doctype/uom/uom.json @@ -2062,7 +2062,7 @@ msgstr "Knjigovodstveni Period" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:49 msgid "Accounting Period cannot be created for a future date. End Date {0} is after today." -msgstr "" +msgstr "Knjigovodstveni Period se ne može kreirati za budući datum. Datum završetka {0} je sutra." #: erpnext/accounts/doctype/accounting_period/accounting_period.py:77 msgid "Accounting Period overlaps with {0}" @@ -2921,7 +2921,7 @@ msgstr "Dodata uloga dobavljača korisniku {0}." #: erpnext/controllers/website_list_for_contact.py:311 msgid "Added {1} role to user {0}." -msgstr "" +msgstr "Dodana je uloga {1} korisniku {0}." #: erpnext/crm/doctype/lead/lead.js:81 msgid "Adding Lead to Prospect..." @@ -3183,7 +3183,7 @@ msgstr "Dodatna Prenesena Količina" #: erpnext/manufacturing/doctype/work_order/work_order.py:591 msgid "Additional Transferred Qty {0} cannot be greater than {1}. To fix this, increase the percentage value of the field 'Transfer Extra Raw Materials to WIP' in Manufacturing Settings." -msgstr "" +msgstr "Dodatna Prenesena Količina {0} ne može biti veća od {1}. Da biste ovo ispravili, povećajte procentualnu vrijednost 'Prenesi Dodatne Sirovine u Nedovršenu Proizvodnju' u Postavkama Proizvodnje." #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:630 msgid "Additional {0} {1} of item {2} required as per BOM to complete this transaction" @@ -3779,7 +3779,7 @@ msgstr "Algoritam" #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/selling/doctype/customer/customer.json msgid "Alias" -msgstr "" +msgstr "Nadimak" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185 @@ -3989,7 +3989,7 @@ msgstr "Svi komentari i e-pošta kopirat će se iz jednog dokumenta u drugi novo #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:204 msgid "All the items have already been returned." -msgstr "" +msgstr "Svi artikli su već vraćeni." #: erpnext/manufacturing/doctype/work_order/work_order.js:1272 msgid "All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table." @@ -3997,7 +3997,7 @@ msgstr "Svi obavezni Artikli (sirovine) bit će preuzeti iz Sastavnice i popunje #: erpnext/stock/doctype/delivery_note/mapper.py:82 msgid "All these items have already been invoiced/returned" -msgstr "" +msgstr "Svi ovi artikli su već fakturisani/vraćeni" #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:100 #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:101 @@ -4144,7 +4144,7 @@ msgstr "Dozvoli Alternativni Artikal" #: erpnext/stock/doctype/item_alternative/item_alternative.py:68 msgid "Allow Alternative Item must be checked on Item {0}" -msgstr "" +msgstr "Dozvoli Alternativni Artikal mora biti odabrano za Artikal {0}" #. Label of the material_consumption (Check) field in DocType 'Manufacturing #. Settings' @@ -4536,7 +4536,7 @@ msgstr "Dozvoljena Transakcija sa" #. Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Allowed Users" -msgstr "" +msgstr "Dozvoljeni Korisnici" #: erpnext/accounts/doctype/party_link/party_link.py:27 msgid "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only." @@ -5459,7 +5459,7 @@ msgstr "Termin s" #: erpnext/www/book_appointment/index.js:237 msgid "Appointment created successfully" -msgstr "" +msgstr "Termin je uspješno zakazan" #: erpnext/crm/doctype/appointment/appointment.py:101 msgid "Appointment was created. But no lead was found. Please check the email to confirm" @@ -5505,11 +5505,11 @@ msgstr "Jeste li sigurni da želite izbrisati sve demo podatke?" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:51 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:100 msgid "Are you sure you want to create Reposting Entries?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:66 msgid "Are you sure you want to create a Reposting Entry?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:480 msgid "Are you sure you want to delete this Item?" @@ -5603,7 +5603,7 @@ msgstr "Pošto ima dovoljno sirovina, Materijalni Nalog nije potreban za Skladi #: erpnext/stock/doctype/stock_settings/stock_settings.py:250 msgid "As there is reserved stock, you cannot disable {0}." -msgstr "" +msgstr "Pošto postoje rezervisane zalihe, ne možete onemogućiti {0}." #: erpnext/stock/doctype/stock_settings/stock_settings.py:224 #: erpnext/stock/doctype/stock_settings/stock_settings.py:236 @@ -6194,7 +6194,7 @@ msgstr "Dodijeli Imenu" #: erpnext/buying/doctype/purchase_order/purchase_order.js:593 #: erpnext/public/js/controllers/buying.js:555 msgid "Assigning {0} to {1} (row {2})" -msgstr "" +msgstr "Dodjeljuje se {0} {1} (red {2})" #. Label of the filters_section (Section Break) field in DocType 'Service Level #. Agreement' @@ -6220,7 +6220,7 @@ msgstr "U Redu {0}: U Serijskom i Šaržnom Paketu {1} mora imati status dokumen #: erpnext/accounts/services/internal_transfer.py:98 msgid "At Row {0}: The field {1} is mandatory for internal transfer" -msgstr "" +msgstr "U Redu {0}: Polje {1} je obavezno za interni prenos" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:85 msgid "At least one account with exchange gain or loss is required" @@ -6253,7 +6253,7 @@ msgstr "Najmanje jedno od Prodaje ili Nabave mora biti odabrano" #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:226 msgid "At least one raw material for Finished Good Item {0} should be customer provided." -msgstr "" +msgstr "Klijent treba da obezbijedi barem jednu sirovinu za gotov proizvod {0}." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:60 msgid "At least one raw material item must be present in the stock entry for the type {0}" @@ -6293,7 +6293,7 @@ msgstr "Red {0}: Serijski Broj je obavezan za Artikal {1}" #: erpnext/stock/services/serial_batch_bundle_service.py:498 msgid "At row {0}: Serial and Batch Bundle {1} has already been created. Please remove the values from the serial no or batch no fields." -msgstr "" +msgstr "U Redu {0}: Serijski i Šaržni Paket {1} je već stvoren. Uklonite vrijednosti iz polja za serijski ili šaržni broj." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:123 msgid "At row {0}: set Parent Row No for item {1}" @@ -6930,7 +6930,7 @@ msgstr "Sastavnica 1" #: erpnext/manufacturing/doctype/bom/mapper.py:82 msgid "BOM 1 {0} and BOM 2 {1} should not be the same" -msgstr "" +msgstr "Sastavnica 1 {0} i Sastavnica 2 {1} ne bi trebale biti iste" #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:38 msgid "BOM 2" @@ -7188,7 +7188,7 @@ msgstr "Rekurzija Sastavnice: {1} ne može biti nadređena ili podređena {0}" #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js:81 msgid "BOM update is queued and may take a few minutes. Check {0} for progress." -msgstr "" +msgstr "Ažuriranje Sastavnice je u redu čekanja i može potrajati nekoliko minuta. Provjeri {0} za napredak." #: erpnext/manufacturing/doctype/bom/bom.py:1404 msgid "BOM {0} does not belong to Item {1}" @@ -7351,7 +7351,7 @@ msgstr "Sažetak Bilansa Stanja" #: erpnext/accounts/report/balance_sheet/balance_sheet.py:284 msgid "Balance Sheet requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilansa Stanja zahtijeva da se {0} sinhronizira s DuckDB-om" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:13 msgid "Balance Stock Qty" @@ -7514,7 +7514,7 @@ msgstr "Tip Bankovnog Računa" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:443 msgid "Bank Account {0} in Bank Transaction {1} is not matching with Bank Account {2}" -msgstr "" +msgstr "Bankovni Račun {0} u Bankovnoj Transakciji {1} nije usklađen s Bankovnim Računom {2}" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:15 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:20 @@ -8096,7 +8096,7 @@ msgstr "Broj Šarže je obavezan" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3495 msgid "Batch No {0} does not exist" -msgstr "" +msgstr "Broj Šarže {0} ne postoji" #: erpnext/stock/utils.py:625 msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." @@ -8108,7 +8108,7 @@ msgstr "Broj Šarže {0} nije prisutan u originalnom {1} {2}, stoga ga ne možet #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:658 msgid "Batch No {0} of Item {1} has negative stock of quantity {2} in the warehouse {3}" -msgstr "" +msgstr "Broj Šarže {0} Artikla {1} ima negativnu količinu zaliha {2} u skladištu {3}" #. Label of the batch_no (Int) field in DocType 'BOM Update Batch' #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -8177,7 +8177,7 @@ msgstr "Šarža i Serijski Broj" #: erpnext/manufacturing/doctype/work_order/work_order.py:742 msgid "Batch not created for item {0} since it does not have a batch series." -msgstr "" +msgstr "Šarža nije kreirana za artikal {0} jer nema Broj Šarže." #. Description of the 'Automatically Create New Batch' (Check) field in DocType #. 'Item' @@ -8765,7 +8765,7 @@ msgstr "Proknjižena Osnovna Imovina" #: erpnext/accounts/services/gl_validator.py:143 msgid "Books have been closed until the period ending on {0}" -msgstr "" +msgstr "Knjigovodstvo je zatvoreno do perioda koji se završava {0}" #. Option for the 'Type of Transaction' (Select) field in DocType 'Inventory #. Dimension' @@ -8993,7 +8993,7 @@ msgstr "Proračun se ne može dodijeliti naspram Grupnog Računu {0}" #: erpnext/accounts/doctype/budget/budget.py:165 msgid "Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense" -msgstr "" +msgstr "Proračun se ne može dodijeliti za {0}, jer njegova kontna Klasa nije Prihod ili Rashod" #: erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py:9 msgid "Budgets" @@ -9347,7 +9347,7 @@ msgstr "Izračunata Razlika Popusta" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:95 msgid "Calculating arrival times" -msgstr "" +msgstr "Izračunavanje vremena dolaska" #. Label of the section_break_11 (Section Break) field in DocType 'Supplier #. Scorecard Period' @@ -9565,7 +9565,7 @@ msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije na #: erpnext/stock/doctype/stock_settings/stock_settings.py:191 msgid "Can't change the valuation method, as there are transactions against some items which do not have their own valuation method" -msgstr "" +msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije naspram nekih artikala koji nemaju metod vrijednovanja" #: erpnext/support/doctype/warranty_claim/warranty_claim.py:79 msgid "Cancel Material Visit {0} before cancelling this Warranty Claim" @@ -9644,7 +9644,7 @@ msgstr "Ne može biti artikal fiksne imovine jer je kreiran Registar Zaliha." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:92 #: erpnext/stock/doctype/delivery_trip/delivery_trip.py:219 msgid "Cannot calculate arrival time as the driver address is missing." -msgstr "" +msgstr "Ne može se izračunati vrijeme dolaska jer nedostaje adresa vozača." #: erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py:117 msgid "Cannot cancel Asset Depreciation Schedule {0} as it has a draft journal entry {1}." @@ -9656,7 +9656,7 @@ msgstr "Ne može se otkazati Unos Zatvaranja Kase" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:140 msgid "Cannot cancel Stock Reservation Entry {0}, as it has been used in the work order {1}. Please cancel the work order first or unreserve the stock" -msgstr "" +msgstr "Ne može se otkazati Unos Rezervacije Zaliha {0}, jer je korišten u radnom nalogu {1}. Molimo prvo otkazati radni nalog ili otkloniti rezervaciju zaliha" #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:275 msgid "Cannot cancel as processing of cancelled documents is pending." @@ -9708,7 +9708,7 @@ msgstr "Nije moguće promijeniti standard valutu poduzeća, jer postoje postoje #: erpnext/projects/doctype/task/task.py:146 msgid "Cannot complete task {0} as its dependent task {1} is not completed / cancelled." -msgstr "" +msgstr "Ne može završiti zadatak {0} jer njegov zavisni zadatak {1} nije dovršen/poništen." #: erpnext/accounts/doctype/cost_center/cost_center.py:61 msgid "Cannot convert Cost Center to ledger as it has child nodes" @@ -9745,7 +9745,7 @@ msgstr "Nije moguće kreirati knjigovodstvene unose naspram onemogućenih račun #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:146 msgid "Cannot create more Subcontracting Orders against the Purchase Order {0}." -msgstr "" +msgstr "Ne može se stvoriti više Podugovornih Naloga na osnovu Naloga Nabave {0}." #: erpnext/controllers/sales_and_purchase_return.py:444 msgid "Cannot create return for consolidated invoice {0}." @@ -9840,7 +9840,7 @@ msgstr "Nije moguće spojiti {0} '{1}' u '{2}' jer oba imaju postojeće knjigovo #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:125 msgid "Cannot optimize route as the driver address is missing." -msgstr "" +msgstr "Ne može se optimizirati ruta jer nedostaje adresa vozača." #: erpnext/manufacturing/doctype/work_order/services/status.py:41 msgid "Cannot produce more Item {0} than Sales Order quantity {1} {2}" @@ -9870,7 +9870,7 @@ msgstr "Ne može se upućivati na broj reda veći ili jednak trenutnom broju red #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:626 msgid "Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

The Allowed Qty is calculated as follows:
  • Actual Qty [Available Qty at Warehouse] = {5}
  • Reserved Stock [Ignore current SRE] = {6}
  • Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}
  • Voucher Qty [Voucher Item Qty] = {8}
  • Delivered Qty [Qty delivered against the Voucher Item] = {9}
  • Total Reserved Qty [Qty reserved against the Voucher Item] = {10}
  • Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}
" -msgstr "" +msgstr "Ne može se rezervisati više od Dozvoljene Količine {0} {1} za artikal {2} prema {3} {4}.

Dozvoljena Količina se izračunava na sljedeći način:
  • Stvarna Količina [Dostupna Količina u Skladištu] = {5}
  • Rezervirana Zaliha [Ignoriši trenutni SRE] = {6}
  • Dostupna Količina za Rezervaciju [Stvarna Količina - Rezervirane Zalihe] = {7}
  • Količina Verifikata [Količina Artikal Verifikata] = {8}
  • Dostavljena Količina [Količina Dostavljena prema Artiklu Verifikata] = {9}
  • Ukupna Rezervirana Količina [Količina Rezervirana po Artiklu Verifikata] = {10}
  • Dozvoljena Količina [Minimum od (Količina Dostupna za Rezervaciju, (Količina Verifikata - Dostavljena Količina - Ukupna Rezervisana Količina))] = {11}
" #: erpnext/accounts/doctype/bank/bank.js:63 msgid "Cannot retrieve link token for update. Check Error Log for more information" @@ -9895,7 +9895,7 @@ msgstr "Nije moguće odabrati tip naknade kao 'Iznos na Prethodnom Redu' ili 'Uk #: erpnext/stock/doctype/item_alternative/item_alternative.py:36 msgid "Cannot set alternative item for the item {0}" -msgstr "" +msgstr "Ne može se postaviti alternativni artikal za artikal. {0}" #: erpnext/selling/doctype/quotation/quotation.py:293 msgid "Cannot set as Lost as Sales Order is made." @@ -10325,7 +10325,7 @@ msgstr "Ručno promijenite ovaj datum da postavite sljedeći datum početka sinh #: erpnext/selling/doctype/customer/customer.py:161 msgid "Changed customer name to '{0}' as '{1}' already exists." -msgstr "" +msgstr "Ime klijenta je promijenjeno u '{0}' jer '{1}' već postoji." #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:160 msgid "Changes in {0}" @@ -10615,7 +10615,7 @@ msgstr "Podređena tabela nije dozvoljena" #: erpnext/projects/doctype/task/task.py:319 msgid "Child Task exists for this Task. You cannot delete this Task." -msgstr "" +msgstr "Podređeni Zadatak postoji za ovaj Zadatak. Ne možete izbrisati ovaj Zadatak." #: erpnext/stock/doctype/warehouse/warehouse_tree.js:21 msgid "Child nodes can be only created under 'Group' type nodes" @@ -11795,11 +11795,11 @@ msgstr "Naziv polja za link poduzeća koji se koristi za filtriranje (opciono - #: erpnext/setup/doctype/company/company.js:239 msgid "Company name does not match" -msgstr "" +msgstr "Naziv poduzeća ne odgovara" #: erpnext/assets/doctype/asset/asset.py:330 msgid "Company of asset {0} and purchase document {1} does not match." -msgstr "" +msgstr "Poduzeće imovine {0} i nabavni dokument {1} ne odgovara." #: erpnext/setup/doctype/employee/employee.py:164 msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" @@ -11839,11 +11839,11 @@ msgstr "Poduzeće {0} ne postoji" #: erpnext/setup/setup_wizard/operations/taxes_setup.py:14 msgid "Company {0} does not exist yet. Taxes setup aborted." -msgstr "" +msgstr "Poduzeće {0} još ne postoji. Postavljanje Pdv-a je prekinuto." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:579 msgid "Company {0} does not match with POS Profile Company {1}" -msgstr "" +msgstr "Poduzeće {0} nije usklađeno s Kasa Profilom {1}" #: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:105 msgid "Company {0} is added more than once" @@ -12319,7 +12319,7 @@ msgstr "Potrošena Količina" #: erpnext/manufacturing/doctype/work_order/services/reservation.py:186 msgid "Consumed Qty {0} cannot be greater than Reserved Qty {1} for item {2}" -msgstr "" +msgstr "Potrošena Količina {0} ne može biti veća od Rezervisane Količine {1} za artikal {2}" #. Label of the consumed_quantity (Data) field in DocType 'Asset Repair #. Consumed Item' @@ -13052,11 +13052,11 @@ msgstr "Centar Troškova {0} ne može se koristiti za dodjelu jer se koristi kao #: erpnext/assets/doctype/asset/asset.py:358 msgid "Cost Center {0} does not belong to Company {1}" -msgstr "" +msgstr "Centar Troškova {0} ne pripada {1}" #: erpnext/assets/doctype/asset/asset.py:365 msgid "Cost Center {0} is a group cost center and group cost centers cannot be used in transactions" -msgstr "" +msgstr "Centar Troškova {0} je grupni centar troškova a grupni centri troškova ne mogu se koristiti u transakcijama" #: erpnext/accounts/report/financial_statements.py:685 msgid "Cost Center: {0} does not exist" @@ -13181,7 +13181,7 @@ msgstr "Obračun Troškova i Fakturisanje" #: erpnext/projects/doctype/project/project.js:140 msgid "Costing and Billing fields have been updated" -msgstr "" +msgstr "Polja Troškova i Fakturisanje su ažurirana" #: erpnext/setup/demo.py:78 msgid "Could Not Delete Demo Data" @@ -13210,7 +13210,7 @@ msgstr "Nije moguće pronaći odgovarajuću promjenu koja bi odgovarala razlici: #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:46 #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:50 msgid "Could not find path for {0}" -msgstr "" +msgstr "Nije moguće pronaći putanju za {0}" #: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:101 msgid "Could not re-extract the table." @@ -14335,7 +14335,7 @@ msgstr "Trenutna Sastavnica" #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py:80 msgid "Current BOM and New BOM cannot be the same" -msgstr "" +msgstr "Trenutna i Nova Sastavnica ne mogu biti iste" #. Label of the current_exchange_rate (Float) field in DocType 'Exchange Rate #. Revaluation Account' @@ -17059,11 +17059,11 @@ msgstr "Račun Razlike u Postavkama Artikla" #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:155 msgid "Difference Account must be an Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" -msgstr "" +msgstr "Razlika u računu mora biti tip računa Imovine/Obaveza (Privremeno Početno), budući da je ovaj unos zaliha početni unos" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:985 msgid "Difference Account must be an Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" -msgstr "" +msgstr "Račun razlike mora biti račun tipa Imovina/Obaveze, budući da je ovo usklađivanje Zaliha Početni Unos" #. Label of the difference_amount (Currency) field in DocType 'Payment #. Reconciliation Allocation' @@ -17309,7 +17309,7 @@ msgstr "Onemogućeni artikli se ne mogu odabrati ni u jednoj transakciji." #: erpnext/accounts/services/internal_transfer.py:120 msgid "Disabled pricing rules since this {0} is an internal transfer" -msgstr "" +msgstr "Pravila određivanja cijena su onemogućena jer je ovo {0} interni prijenos" #. Description of the 'Disabled' (Check) field in DocType 'Supplier' #: erpnext/buying/doctype/supplier/supplier.json @@ -17318,7 +17318,7 @@ msgstr "Onemogućeni dobavljači su skriveni od odabira u novim transakcijama, a #: erpnext/accounts/services/internal_transfer.py:136 msgid "Disabled tax included prices since this {0} is an internal transfer" -msgstr "" +msgstr "Cijene bez PDV-a budući da je ovo {0} interni prijenos" #: erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py:82 msgid "Disabled template must not be default template" @@ -17569,7 +17569,7 @@ msgstr "Popust mora biti manji od 100%" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3090 msgid "Discount of {0} applied as per Payment Term" -msgstr "" +msgstr "Popust od {0} primjenjen prema Uslovima Plaćanja" #. Label of the section_break_18 (Section Break) field in DocType 'Pricing #. Rule' @@ -17934,7 +17934,7 @@ msgstr "Želiš li podnijeti unos zaliha?" #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py:50 #: erpnext/selling/report/sales_partner_commission_summary/test_sales_partner_commission_summary.py:22 msgid "DocType can be one of {0}" -msgstr "" +msgstr "DocType može biti jedan od {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:182 #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:458 @@ -18659,7 +18659,7 @@ msgstr "Verifikacija e-pošte nije uspjela." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:20 msgid "Emails queued" -msgstr "" +msgstr "E-pošta u redu čekanja" #. Label of the emergency_contact_details (Section Break) field in DocType #. 'Employee' @@ -18938,7 +18938,7 @@ msgstr "Omogući Evropski Pristup" #. 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Enable Frappe CRM Data Synchronization" -msgstr "" +msgstr "Omogući sinhronizaciju podataka Prodajne Podrške" #. Label of the enable_fuzzy_matching (Check) field in DocType 'Accounts #. Settings' @@ -19397,7 +19397,7 @@ msgstr "Unesi {0} iznos." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:170 msgid "Enter {0} name." -msgstr "" +msgstr "Unesi {0} ime." #: erpnext/setup/setup_wizard/data/industry_type.txt:22 msgid "Entertainment & Leisure" @@ -19496,15 +19496,15 @@ msgstr "Greška prilikom ponovnog knjiženja vrijednosti artikla" #: erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py:175 msgid "Error: This asset already has {0} depreciation periods booked. The `depreciation start` date must be at least {1} periods after the `available for use` date. Please correct the dates accordingly." -msgstr "" +msgstr "Greška: Ova imovina već ima uknjiženih {0} perioda amortizacije. Datum `početka amortizacije` mora biti najmanje {1} perioda nakon datuma `dostupno za upotrebu`. Molimo vas da ispravite datume u skladu s tim." #: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py:93 msgid "Error: {0}" -msgstr "" +msgstr "Greška: {0}" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:980 msgid "Error: {0} is a mandatory field" -msgstr "" +msgstr "Greška: {0} je obavezno polje" #. Label of the errors_notification_section (Section Break) field in DocType #. 'Stock Reposting Settings' @@ -20197,7 +20197,7 @@ msgstr "Neuspješni Unosi" #: erpnext/utilities/doctype/video_settings/video_settings.py:35 msgid "Failed to authenticate the API key. Please check the error logs." -msgstr "" +msgstr "Autentifikacija API ključa nije uspjela. Molimo provjerite zapise o greškama." #: erpnext/setup/setup_wizard/setup_wizard.py:37 #: erpnext/setup/setup_wizard/setup_wizard.py:38 @@ -21176,11 +21176,11 @@ msgstr "Za Radni Nalog" #: erpnext/controllers/status_updater.py:292 msgid "For an item {0}, quantity must be a negative number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti negativan broj" #: erpnext/controllers/status_updater.py:289 msgid "For an item {0}, quantity must be a positive number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti pozitivan broj" #. Description of the 'Income Account' (Link) field in DocType 'Dunning' #: erpnext/accounts/doctype/dunning/dunning.json @@ -21214,11 +21214,11 @@ msgstr "Za individualnog Dobavljača" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:379 msgid "For item {0}, only {1} assets have been created or linked to {2}. Please create or link {3} more assets with the respective document." -msgstr "" +msgstr "Za artikal {0}, samo {1} imovina je stvorena ili povezana s {2}. Stvori ili poveži još {3} imovine s odgovarajućim dokumentom." #: erpnext/controllers/status_updater.py:302 msgid "For item {0}, rate must be a positive number. To allow negative rates, enable {1} in {2}" -msgstr "" +msgstr "Za artikal {0}, cijena mora biti pozitivan broj. Da biste omogućili negativne cijene, omogućite {1} u {2}" #. Description of the 'Do not fetch incoming rate from Serial No' (Check) field #. in DocType 'Stock Reposting Settings' @@ -21232,7 +21232,7 @@ msgstr "Za operaciju {0} u redu {1}, molimo dodajte sirovine ili postavite Sasta #: erpnext/manufacturing/doctype/work_order/mapper.py:379 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity ({2})" -msgstr "" +msgstr "Za Operaciju {0}: Količina ({1}) ne može biti veća od količine na čekanju ({2})" #: erpnext/projects/doctype/project/project.js:208 msgid "For project - {0}, update your status" @@ -21278,7 +21278,7 @@ msgstr "Za praktičnost Klienta, ovi kodovi se mogu koristiti u formatima za isp #: erpnext/stock/serial_batch_bundle.py:1234 msgid "For the item {0}, the Available qty {1} is less than the Required Qty {2} in the warehouse {3}. Please add sufficient qty in the warehouse." -msgstr "" +msgstr "Za artikal {0}, Dostupna količina {1} je manja od Potrebne količine {2} u skladištu {3}. Dodaj dovoljnu količinu u skladište." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:893 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." @@ -21381,11 +21381,11 @@ msgstr "Podrška Prodaje" #. Name of a DocType #: erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json msgid "Frappe CRM Allowed User" -msgstr "" +msgstr "Dozvoljeni korisnik Prodajne Podrške" #: erpnext/crm/frappe_crm_api.py:168 msgid "Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Sinhronizacija podataka Prodajne Podrške nije omogućena na Sistemu. Kontaktiraj Odgovornog Sistema." #: erpnext/setup/install.py:232 msgid "Frappe School" @@ -22057,7 +22057,7 @@ msgstr "Dužina napomena Knjigovodstvenog Registra" #: erpnext/accounts/report/general_ledger/general_ledger.py:829 msgid "General Ledger requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Knjigovodstveni Registar zahtijeva da se {0} sinhronizira sa DuckDB-om" #. Label of the gs (Section Break) field in DocType 'Item Group' #: erpnext/setup/doctype/item_group/item_group.json @@ -24098,7 +24098,7 @@ msgstr "Uvezi Fakture" #. Import' #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json msgid "Import MT940 Format" -msgstr "" +msgstr "Uvezi MT940 Format" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:144 msgid "Import Successful" @@ -25495,7 +25495,7 @@ msgstr "Nevažeće Skladište" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:460 msgid "Invalid amount in accounting entries of {0} {1} for Account {2}: {3}" -msgstr "" +msgstr "Nevažeći iznos u knjigovodstvenim unosima {0} {1} za račun {2}: {3}" #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:312 msgid "Invalid condition expression" @@ -27846,7 +27846,7 @@ msgstr "Artikal {0} se nemože naručiti više od {1} u odnosu na Ugovorni Nalog #: erpnext/stock/services/internal_transfer.py:104 msgid "Item {0} cannot be received in more than {1} qty against the {2} {3}" -msgstr "" +msgstr "Artikal {0} ne može biti primljen u količini većoj od {1} u odnosu na {2} {3}" #: erpnext/assets/doctype/asset/asset.py:343 #: erpnext/stock/doctype/item/item.py:693 @@ -27892,7 +27892,7 @@ msgstr "Artikal {0} zanemaren jer nije artikal na zalihama" #: erpnext/stock/get_item_details.py:359 msgid "Item {0} is a template, please select one of its variants" -msgstr "" +msgstr "Artikal {0} je šablon, molimo odaberite jednu od njenih varijanti" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:614 msgid "Item {0} is already reserved/delivered against Sales Order {1}." @@ -28010,7 +28010,7 @@ msgstr "Artikal: {0} ne postoji u sistemu" #: erpnext/manufacturing/doctype/bom/bom.py:970 msgid "Item: {0} with Stock UOM: {1} cannot have fractional process loss qty as UOM {2} is a whole number." -msgstr "" +msgstr "Artikal: {0} sa Jedinicom Zalihe: {1} ne može imati količinu frakcijskog gubitka procesa jer je jedinica mjere {2} cijeli broj." #. Label of a Card Break in the Buying Workspace #. Label of a Workspace Sidebar Item @@ -28217,7 +28217,7 @@ msgstr "Radne Kartice {0} je završen" #: erpnext/manufacturing/doctype/job_card/job_card.py:1416 msgid "Job Card {0}: As per the sequence of the operations in the work order {1}, complete the operation {2} before the operation {3}." -msgstr "" +msgstr "Radna Kartica {0}: Prema redoslijedu operacija u radnom nalogu {1}, dovršite operaciju {2} prije operacije {3}." #. Label of the dashboard_tab (Tab Break) field in DocType 'Workstation' #: erpnext/manufacturing/doctype/workstation/workstation.json @@ -28292,11 +28292,11 @@ msgstr "Radna Kartica {0} kreirana" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:106 msgid "Job paused" -msgstr "" +msgstr "Posao pauziran" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:64 msgid "Job started" -msgstr "" +msgstr "Posao započet" #: erpnext/utilities/bulk_transaction.py:72 msgid "Job: {0} has been triggered for processing failed transactions" @@ -28645,7 +28645,7 @@ msgstr "Prošla Fiskalna Godina" #: erpnext/accounts/doctype/account/account.py:673 msgid "Last GL Entry update was done {0}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying." -msgstr "" +msgstr "Posljednje ažuriranje Knjigovodstvenog Registra je obavljeno {0}. Ova operacija nije dozvoljena dok se sistem aktivno koristi. Pričekaj 5 minuta prije ponovnog pokušaja." #. Label of the last_integration_date (Date) field in DocType 'Bank Account' #: erpnext/accounts/doctype/bank_account/bank_account.json @@ -29163,7 +29163,7 @@ msgstr "Povezivanje s klijentom nije uspjelo. Molimo pokušajte ponovo." #: erpnext/selling/doctype/customer/customer.js:282 msgid "Linking to Supplier failed. Please try again." -msgstr "" +msgstr "Povezivanje s Dobavljačem nije uspjelo. Pokušaj ponovo." #: erpnext/accounts/report/financial_ratios/financial_ratios.js:55 #: erpnext/accounts/report/financial_ratios/financial_ratios.py:152 @@ -30731,7 +30731,7 @@ msgstr "Materijali su već primljeni naspram {0} {1}" #: erpnext/manufacturing/doctype/job_card/job_card.py:189 #: erpnext/manufacturing/doctype/job_card/job_card.py:903 msgid "Materials need to be transferred to the work in progress warehouse for the job card {0}" -msgstr "" +msgstr "Materijali se moraju prenijeti u skladište nedovršene proizvodnje za radnu karticu {0}" #. Label of the max_amount (Currency) field in DocType 'Bank Transaction Rule' #. Label of the max_amount (Currency) field in DocType 'Promotional Scheme @@ -31618,7 +31618,7 @@ msgstr "Više Računa (Šablon Naloga Knjiženja)" #: erpnext/selling/doctype/customer/customer.py:443 msgid "Multiple Loyalty Programs found for Customer {0}. Please select manually." -msgstr "" +msgstr "Višestruki Programi Lojalnosti su pronađeni za Klijenta {0}. Odaberi ručno." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:253 msgid "Multiple POS Opening Entry" @@ -31626,7 +31626,7 @@ msgstr "Višestruki Unos Otvaranja Kase" #: erpnext/accounts/doctype/pricing_rule/utils.py:345 msgid "Multiple Price Rules exist with same criteria, please resolve conflict by assigning priority. Price Rules: {0}" -msgstr "" +msgstr "Postoji više pravila za cijene s istim kriterijima, riješi sukob dodjeljivanjem prioriteta. Pravila Cijena: {0}" #. Option for the 'Loyalty Program Type' (Select) field in DocType 'Loyalty #. Program' @@ -32289,7 +32289,7 @@ msgstr "Novi Radni Prostor" #: erpnext/selling/doctype/customer/customer.py:408 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be at least {0}" -msgstr "" +msgstr "Novo kreditno ograničenje je niže od trenutnog iznosa klijenta. Kreditno ograničenje mora biti najmanje {0}" #. Description of the 'Bill Even If Previous Invoice Unpaid' (Check) field in #. DocType 'Subscription' @@ -32299,7 +32299,7 @@ msgstr "Nove fakture će se generirati prema rasporedu čak i ako su trenutne fa #: erpnext/support/doctype/issue/issue.js:126 msgid "New issue created: {0}" -msgstr "" +msgstr "Novi zahtjev stvoren: {0}" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 msgid "New release date should be in the future" @@ -32383,7 +32383,7 @@ msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:146 msgid "No Delivery Note selected for Customer {0}" -msgstr "" +msgstr "Nije odabrana Dostavnica za Klijenta {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:767 msgid "No DocTypes in To Delete list. Please generate or import the list before submitting." @@ -32548,7 +32548,7 @@ msgstr "Nisu pronađeni kontakti s e-poštom." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:164 msgid "No customers found with selected options." -msgstr "" +msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/selling/page/sales_funnel/sales_funnel.js:137 msgid "No data for this period" @@ -32769,7 +32769,7 @@ msgstr "Nije pronađen nijedan zapis" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:22 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:39 msgid "No records for these settings." -msgstr "" +msgstr "Nema zapisa za ove postavke." #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:773 msgid "No records found in Allocation table" @@ -33268,7 +33268,7 @@ msgstr "Numeričke Vrijednosti" #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py:88 msgid "Numero has not been set in the XML file" -msgstr "" +msgstr "Broj nije postavljen u XML datoteci" #. Option for the 'Blood Group' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -33444,11 +33444,11 @@ msgstr "Nakon postavljanja, ova faktura će biti na čekanju do postavljenog dat #: erpnext/manufacturing/doctype/work_order/work_order.js:763 msgid "Once the Work Order is Closed, it cannot be resumed." -msgstr "" +msgstr "Nakon što je Radni Nalog Zatvoren. Ne može se ponovo otvoriti." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:39 msgid "One customer can be part of only a single Loyalty Program." -msgstr "" +msgstr "Jedan Klijent može biti dio samo jednog Programa Lojalnosti." #. Option for the 'Status' (Select) field in DocType 'Subcontracting Inward #. Order' @@ -34045,7 +34045,7 @@ msgstr "Operacija {0} ne pripada radnom nalogu {1}" #: erpnext/manufacturing/doctype/workstation/workstation.py:453 msgid "Operation {0} is longer than any available working hours in workstation {1}, break down the operation into multiple operations" -msgstr "" +msgstr "Operacija {0} traje duže od bilo kojeg raspoloživog radnog vremena na radnoj stanici {1}, podijeli operaciju na više operacija" #. Label of the operations (Table) field in DocType 'BOM' #. Label of the operations_section_section (Section Break) field in DocType @@ -34232,7 +34232,7 @@ msgstr "Optimiziraj Rutu" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:128 msgid "Optimizing route" -msgstr "" +msgstr "Optimizacija rute" #: erpnext/manufacturing/doctype/work_order/work_order.js:1029 msgid "Optional. Select a specific manufacture entry to reverse." @@ -34693,7 +34693,7 @@ msgstr "Preko Odbitka" #: erpnext/accounts/services/billing_validation.py:56 msgid "Overbilling of {0} ignored because you have {1} role." -msgstr "" +msgstr "Prekomjerno Fakturisanje {0} zanemareno jer imate {1} ulogu." #: erpnext/controllers/status_updater.py:519 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." @@ -34815,7 +34815,7 @@ msgstr "Verifikat Zatvaranje Perioda" #. Label of the pcv_job_timeout (Int) field in DocType 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "PCV Job Timeout (seconds)" -msgstr "" +msgstr "Vremensko Ograničenje Zadatka Završnog Verifikata Perioda (sekunde)" #: erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js:35 msgid "PCV Paused" @@ -34963,7 +34963,7 @@ msgstr "Kasa Faktura nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:130 msgid "POS Invoice isn't created by user {0}" -msgstr "" +msgstr "Korisnik {0} nije stvorio Kasa Fakturu" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:208 msgid "POS Invoice should have the field {0} checked." @@ -35087,7 +35087,7 @@ msgstr "Korisnik Kasa Profila" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:124 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:189 msgid "POS Profile doesn't match {0}" -msgstr "" +msgstr "Kasa Profil ne poklapa se s {0}" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:210 msgid "POS Profile is mandatory to mark this invoice as POS Transaction." @@ -35099,19 +35099,19 @@ msgstr "Kasa profil {0} ne može biti onemogućen jer su Kasa sesije u toku." #: erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py:62 msgid "POS Profile {0} contains Mode of Payment {1}. Please remove them to disable this mode." -msgstr "" +msgstr "Kasa Profil {0} sadrži ovaj način plaćanja {1}. Uklonite ga da onemogućite ovaj način." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:58 msgid "POS Profile {0} does not belong to company {1}" -msgstr "" +msgstr "Kasa Profil {0} ne pripada {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:47 msgid "POS Profile {0} does not exist." -msgstr "" +msgstr "Kasa Profil {0} ne postoji." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:54 msgid "POS Profile {0} is disabled." -msgstr "" +msgstr "Kasa Profil {0} je onemogućen." #. Name of a report #: erpnext/accounts/report/pos_register/pos_register.json @@ -36059,7 +36059,7 @@ msgstr "Stranka je Obavezna" #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:69 msgid "Party is required to create a payment entry." -msgstr "" +msgstr "Stranka je obavezna za stvaranje unosa plaćanja." #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:66 msgid "Party type is required to create a payment entry." @@ -36766,7 +36766,7 @@ msgstr "Tip Plaćanja" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:624 msgid "Payment Type must be one of Receive, Pay, or Internal Transfer" -msgstr "" +msgstr "Tip Plaćanja mora biti Uplata, Isplata ili Interni Prijenos" #. Label of the payment_url (Data) field in DocType 'Payment Request' #: erpnext/accounts/doctype/payment_request/payment_request.json @@ -37723,7 +37723,7 @@ msgstr "Dodaj račun za pravilo bankovnog unosa." #: erpnext/public/js/utils/serial_no_batch_selector.js:663 msgid "Please add at least one Serial No / Batch No" -msgstr "" +msgstr "Dodaj barem jedan Serijski / Šaržni Broj" #: erpnext/stock/doctype/item/item.js:914 msgid "Please add at least one row in Item Defaults with a Company before setting opening stock." @@ -37731,7 +37731,7 @@ msgstr "Dodaj barem jedan red u Postavke Artikala sa poduzećem prije postavljan #: erpnext/crm/doctype/crm_settings/crm_settings.py:51 msgid "Please add at least one user on Allowed Users to allow Data Synchronization from Frappe CRM site." -msgstr "" +msgstr "Dodaj barem jednog korisnika na listu Dozvoljeni Korisnici kako biste omogućili sinhronizaciju podataka sa Prodajnom Podrškom." #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py:84 msgid "Please add the Bank Account column" @@ -37823,7 +37823,7 @@ msgstr "Konfiguriraj račune za pravilo bankovnog unosa." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:345 msgid "Please contact any of the following users for this transaction." -msgstr "" +msgstr "Kontaktiraj bilo kojeg od sljedećih korisnika za ovu transakciju." #: erpnext/selling/doctype/customer/customer.py:534 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" @@ -37895,7 +37895,7 @@ msgstr "Omogući {0} u {1}." #: erpnext/controllers/selling_controller.py:872 msgid "Please enable {0} in {1} to allow same item in multiple rows" -msgstr "" +msgstr "Omogući {0} u {1} kako biste dozvolili isti artikal u više redova" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:378 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." @@ -37907,11 +37907,11 @@ msgstr "Potvrdi da je {0} račun {1} Troškovni račun. Možete promijeniti vrst #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:758 msgid "Please ensure {0} account is a Balance Sheet account." -msgstr "" +msgstr "Provjeri da li je račun {0} račun Bilansa Stanja." #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:768 msgid "Please ensure {0} account {1} is a Receivable account." -msgstr "" +msgstr "Provjeri da li je {0} račun {1} račun Potraživanja." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:140 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" @@ -38118,7 +38118,7 @@ msgstr "Molimo vas da generirate listu za brisanje prije podnošenja" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:67 msgid "Please import accounts against parent company or enable {0} in company master." -msgstr "" +msgstr "Uvezi račune naspram matičnog poduzeća ili omogući {0} u Postavkama Poduzeća." #: erpnext/setup/doctype/employee/employee.py:291 msgid "Please make sure the employees above report to another Active employee." @@ -38216,7 +38216,7 @@ msgstr "Odaberi Poduzeće" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:139 #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:76 msgid "Please select Company and Posting Date to get entries" -msgstr "" +msgstr "Odaberi Poduzeće i Datum Knjiženja da biste preuzeli unose" #: erpnext/accounts/doctype/journal_entry/journal_entry.js:435 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:28 @@ -38249,7 +38249,7 @@ msgstr "Odaberi Kod Artikla" #: erpnext/selling/doctype/sales_order/sales_order.js:1756 msgid "Please select Items from the Table" -msgstr "" +msgstr "Odaberi Artikle iz Tabele" #: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py:55 msgid "Please select Maintenance Status as Completed or remove Completion Date" @@ -38398,7 +38398,7 @@ msgstr "Odaberi red za kreiranje Unosa Ponovnog Knjiženje" #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:146 msgid "Please select a supplier" -msgstr "" +msgstr "Odaberi Dobavljača" #: erpnext/accounts/report/purchase_register/purchase_register.py:37 msgid "Please select a supplier for fetching payments." @@ -38410,7 +38410,7 @@ msgstr "Odaberi važeći Nabavni Nalog koji je konfigurisan za Podizvođača." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:200 msgid "Please select a valid document type." -msgstr "" +msgstr "Odaberi važeći tip dokumenta." #: erpnext/selling/doctype/quotation/quotation.js:245 msgid "Please select a value for {0} quotation_to {1}" @@ -38430,7 +38430,7 @@ msgstr "Odaberi barem jedan filter: Šifra Artikla, Šarža ili Serijski Broj." #: erpnext/selling/doctype/sales_order/sales_order.js:1368 msgid "Please select at least one item to continue" -msgstr "" +msgstr "Odaberi jedan artikal za nastavak" #: erpnext/buying/doctype/purchase_order/services/drop_ship.py:17 msgid "Please select at least one item to update delivered quantity." @@ -38438,7 +38438,7 @@ msgstr "Molimo odaberite barem jedan artikal za ažuriranje isporučene količin #: erpnext/manufacturing/doctype/work_order/work_order.js:392 msgid "Please select at least one operation to create Job Card" -msgstr "" +msgstr "Odaberi barem jednu operaciju za stvaranje Kartice Posla" #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js:33 msgid "Please select at least one row to fix" @@ -38504,7 +38504,7 @@ msgstr "Odaberi Poduzeće" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:65 msgid "Please select the Multiple Tier Program type for more than one collection rule." -msgstr "" +msgstr "Odaberi Tip Višeslojnog Programa za više od jednog pravila prikupljanja." #: erpnext/stock/doctype/item/item.js:433 msgid "Please select the Warehouse first" @@ -38566,7 +38566,7 @@ msgstr "Postavi Račun u Skladištu {0} ili Standard Račun Zaliha u {1}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:333 msgid "Please set Accounting Dimension {0} in {1}" -msgstr "" +msgstr "Postavi Knjigovodstvenu Dimenziju {0} u {1}" #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:23 #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:34 @@ -38608,7 +38608,7 @@ msgstr "Postavi Račun Osnovne Imovine u Kategoriju Imovine {0}" #: erpnext/accounts/doctype/purchase_invoice/services/expense_account.py:151 msgid "Please set Fixed Asset Account in {0} against {1}." -msgstr "" +msgstr "Postavi Račun Osnovnih Sredstava u {0} na {1}." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:296 msgid "Please set Parent Row No for item {0}" @@ -38645,7 +38645,7 @@ msgstr "Postavi Poduzeće" #: erpnext/assets/doctype/asset/asset.py:374 msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {0}" -msgstr "" +msgstr "Postavi Centar Troškova za Imovinu ili postavite Centar Troškova Amortizacije za {0}" #: erpnext/stock/doctype/item/item.py:339 #: erpnext/stock/doctype/item/item.py:1623 @@ -38699,11 +38699,11 @@ msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Način Plaćanja {0}" #: erpnext/accounts/doctype/pos_profile/pos_profile.py:207 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:367 msgid "Please set default Cash or Bank account in Mode of Payments {0}" -msgstr "" +msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Načine Plaćanja {0}" #: erpnext/accounts/utils.py:2568 msgid "Please set default Exchange Gain/Loss Account in Company {0}" -msgstr "" +msgstr "Postavi Standard Račun Rezultata od Kursnih Razlika u {0}" #: erpnext/assets/doctype/asset_repair/services/gl_composer.py:92 msgid "Please set default Expense Account in Company {0}" @@ -38842,11 +38842,11 @@ msgstr "Navedi od/Do Raspona" #: erpnext/public/js/controllers/transaction.js:2634 msgid "Please specify {0}. It is needed to fetch Item Details." -msgstr "" +msgstr "Navedi {0}. Potrebno je za preuzimanje Detalja Artikla." #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:142 msgid "Please submit Purchase Order {0} before proceeding." -msgstr "" +msgstr "Podnesite Nalog Nabave {0} prije nego što nastavite." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:276 msgid "Please try again in an hour." @@ -39080,7 +39080,7 @@ msgstr "Datum Knjiženja" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:263 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:146 msgid "Posting Date cannot be a future date" -msgstr "" +msgstr "Datum knjiženja ne može biti budući datum" #. Label of the exchange_gain_loss_posting_date (Select) field in DocType #. 'Accounts Settings' @@ -39283,7 +39283,7 @@ msgstr "Uplaćeni Troškovi" #: erpnext/accounts/report/general_ledger/general_ledger.py:682 msgid "Presentation Currency cannot be {0}, when {1} is enabled." -msgstr "" +msgstr "Valuta prikaza ne može biti {0}, kada je omogućen {1}." #: erpnext/setup/setup_wizard/data/designation.txt:24 msgid "President" @@ -39957,7 +39957,7 @@ msgstr "Prioriteti" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:61 msgid "Priority cannot be less than 1." -msgstr "" +msgstr "Prioritet ne može biti manji od 1." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:764 msgid "Priority has been changed to {0}." @@ -40523,7 +40523,7 @@ msgstr "Bilans Uspjeha" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:215 msgid "Profit and Loss Statement requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilansa Uspjeha zahtijeva da se {0} sinhronizira s DuckDB-om" #. Label of the heading_cppb (Heading) field in DocType 'Bisect Accounting #. Statements' @@ -41282,7 +41282,7 @@ msgstr "Nabavni Nalog Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:466 msgid "Purchase Order Required for item {0}" -msgstr "" +msgstr "Nabavni Nalog je obavezan za artikal {0}" #. Name of a report #. Label of a chart in the Buying Workspace @@ -41342,7 +41342,7 @@ msgstr "Nabavni Nalozi za Prijem" #: erpnext/controllers/accounts_controller.py:1236 msgid "Purchase Orders {0} are unlinked" -msgstr "" +msgstr "Nabavni Nalozi {0} nisu povezani" #: erpnext/stock/report/item_prices/item_prices.py:59 msgid "Purchase Price List" @@ -41432,7 +41432,7 @@ msgstr "Nabavni Račun je Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:488 msgid "Purchase Receipt Required for item {0}" -msgstr "" +msgstr "Nabavni Račun je obavezan za artikal {0}" #. Label of a Link in the Buying Workspace #. Name of a report @@ -41452,7 +41452,7 @@ msgstr "Statistika Nabavnog Računa " #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:356 msgid "Purchase Receipt does not have any Item for which Retain Sample is enabled." -msgstr "" +msgstr "Nabavni Račun nema nijedan artikal za koju je omogućeno Zadržavanje Uzorka." #: erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py:135 msgid "Purchase Receipt {0} created." @@ -42483,7 +42483,7 @@ msgstr "Količina za Skeniranje" #: erpnext/stock/doctype/stock_entry/stock_entry.py:910 msgid "Quantity {0} should not be greater than allowed quantity {1}" -msgstr "" +msgstr "Količina {0} ne smije biti veća od dozvoljene količine {1}" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -42935,7 +42935,7 @@ msgstr "PDV Stopa" #: erpnext/accounts/services/child_item_update.py:515 msgid "Rate of '{0}' items cannot be changed" -msgstr "" +msgstr "Cijena '{0}' artikala ne može se mijenjati" #. Label of the rate_of_depreciation (Percent) field in DocType 'Asset #. Depreciation Schedule' @@ -43674,7 +43674,7 @@ msgstr "Zabilježite prijenos između dva bankovna računa" #: erpnext/stock/doctype/item_alternative/item_alternative.py:84 msgid "Record already exists for the item {0}" -msgstr "" +msgstr "Zapis za artikal {0} već postoji" #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:513 #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:519 @@ -44099,7 +44099,7 @@ msgstr "Odbijeno Skladište" #: erpnext/public/js/utils/serial_no_batch_selector.js:671 msgid "Rejected Warehouse and Accepted Warehouse cannot be the same." -msgstr "" +msgstr "Odbijeno i Prihvaćeno Skladište ne mogu biti isto." #: erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py:23 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py:14 @@ -44528,11 +44528,11 @@ msgstr "Datoteke Podataka Ponovnog Knjiženja" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:47 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:96 msgid "Reposting Entries will change the value of accounts Stock In Hand, and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje će promijeniti vrijednost računa Zalihe na raspolaganju i Troškovi zaliha u izvještaju o probnom bilansu, a također će promijeniti i vrijednost stanja u izvještaju o stanju zaliha." #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:62 msgid "Reposting Entry will change the value of accounts Stock In Hand and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje će promijeniti vrijednost računa Zalihe na raspolaganju i Troškovi zaliha u izvještaju o probnom bilansu, a također će promijeniti i vrijednost stanja u izvještaju o stanju zaliha." #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' @@ -44919,7 +44919,7 @@ msgstr "Rezervno Skladište" #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:178 msgid "Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {0}." -msgstr "" +msgstr "Rezervno Skladište mora biti različito od Dobavljačevog Skladišta za Isporučeni Artikal {0}." #: erpnext/manufacturing/doctype/production_plan/production_plan.js:287 msgid "Reserve for Raw Materials" @@ -45522,7 +45522,7 @@ msgstr "Povrati" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:359 msgid "Revaluation Journal: {0}" -msgstr "" +msgstr "Žurnal Revalorizacije: {0}" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:151 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:113 @@ -46038,7 +46038,7 @@ msgstr "Red #{0}: Broj Šarže {1} je već odabran." #: erpnext/controllers/subcontracting_inward_controller.py:443 msgid "Row #{0}: Batch No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)." -msgstr "" +msgstr "Red #{0}: Broj(evi) Šarže {1} nisu dio povezanog Internog Podugovaračkog Naloga. Odaberi važeći Broj(eve) Šarže." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:880 msgid "Row #{0}: Cannot allocate more than {1} against payment term {2}" @@ -46130,7 +46130,7 @@ msgstr "Red #{0}: Kumulativni prag ne može biti manji od praga pojedinačne tra #: erpnext/assets/doctype/asset_category/asset_category.py:66 msgid "Row #{0}: Currency of {1} - {2} does not match company currency." -msgstr "" +msgstr "Red #{0}: Valuta od {1} - {2} ne odgovara valuti poduzeća." #: erpnext/controllers/subcontracting_inward_controller.py:91 msgid "Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times." @@ -46184,7 +46184,7 @@ msgstr "Red #{0}: Duplikat unosa u Referencama {1} {2}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:113 msgid "Row #{0}: Either Party ID or Party Name is required" -msgstr "" +msgstr "Red #{0}: Obavezan je ili ID Stranke ili Naziv Stranke" #: erpnext/selling/doctype/sales_order/sales_order.py:270 msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" @@ -46200,7 +46200,7 @@ msgstr "Red #{0}: Račun troškova {1} nije važeći za Nabavnu Fakturu {2}. Doz #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{0}: Finance Book should not be empty since you're using multiple." -msgstr "" +msgstr "Red #{0}: Finansijski Registar ne smije biti prazan jer ih koristite više." #: erpnext/selling/doctype/sales_order/services/subcontracting.py:40 msgid "Row #{0}: Finished Good Item Qty can not be zero" @@ -46208,7 +46208,7 @@ msgstr "Red #{0}: Količina gotovog proizvoda artikla ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:39 msgid "Row #{0}: Finished Good Item Qty cannot be zero" -msgstr "" +msgstr "Red #{0}: Količina gotovog proizvoda ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:21 #: erpnext/selling/doctype/sales_order/services/subcontracting.py:20 @@ -46259,7 +46259,7 @@ msgstr "Red #{0}: Polja Od i Do su obavezna" #: erpnext/stock/doctype/pick_list/pick_list.py:650 msgid "Row #{0}: Item Code is Mandatory" -msgstr "" +msgstr "Red #{0}: Šifra Artikla je obavezna" #: erpnext/public/js/utils/barcode_scanner.js:427 msgid "Row #{0}: Item added" @@ -46316,15 +46316,15 @@ msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može s #: erpnext/controllers/subcontracting_inward_controller.py:80 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted, add another row instead." -msgstr "" +msgstr "Red #{0}: Artikal {1} ne odgovara. Promjena šifre artikla nije dozvoljena, umjesto toga dodaj još jedan red." #: erpnext/controllers/subcontracting_inward_controller.py:129 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted." -msgstr "" +msgstr "Red #{0}: Artikal {1} ne odgovara. Promjena šifre artikla nije dozvoljena." #: erpnext/stock/doctype/stock_entry/services/subcontracting.py:94 msgid "Row #{0}: Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" -msgstr "" +msgstr "Red #{0}: Artikal {1} nije pronađen u tabeli 'Dostavljene Sirovine' u {2} {3}" #: erpnext/stock/doctype/stock_entry/services/disassemble.py:115 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." @@ -46365,19 +46365,19 @@ msgstr "Red #{0}: Prekomjerna potrošnja Klijent Dostavljenog Artikla {1} u odno #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:92 msgid "Row #{0}: POS Invoice {1} has been {2}" -msgstr "" +msgstr "Red #{0}: Kasa Faktura {1} je {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:73 msgid "Row #{0}: POS Invoice {1} is not against customer {2}" -msgstr "" +msgstr "Red #{0}: Kasa Faktura {1} nije vezana za klijenta {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:88 msgid "Row #{0}: POS Invoice {1} is not submitted yet" -msgstr "" +msgstr "Red #{0}: Kasa Faktura {1} još nije podnešena" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:123 msgid "Row #{0}: Party ID is required" -msgstr "" +msgstr "Red #{0}: ID Stranke je obavezan" #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:80 msgid "Row #{0}: Please select Item Code in Assembly Items" @@ -46385,11 +46385,11 @@ msgstr "Red #{0}: Odaberi Kod Artikla u Artiklima Montaže" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:340 msgid "Row #{0}: Please select a valid Quality Inspection with Item Code {1}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete sa Šifrom Artikla {1}." #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:333 msgid "Row #{0}: Please select a valid Quality Inspection with Reference Type {1} and Reference Name {2}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete s Tipom Reference {1} i Nazivom Reference {2}." #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:82 msgid "Row #{0}: Please select the BOM No in Assembly Items" @@ -46413,7 +46413,7 @@ msgstr "Red #{0}: Ažuriraj račun odloženih prihoda/troškova u redu artikla i #: erpnext/assets/doctype/asset/asset.py:413 msgid "Row #{0}: Please use a different Finance Book." -msgstr "" +msgstr "Red #{0}: Koristi drugi Finansijski Registar." #: erpnext/manufacturing/doctype/bom/bom.py:378 #, python-format @@ -46435,7 +46435,7 @@ msgstr "Red #{0}: Količina mora biti pozitivan broj" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:429 msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Item {2} against Batch {3} in Warehouse {4}." -msgstr "" +msgstr "Red #{0}: Količina bi trebala biti manja ili jednaka Dostupnoj Količini za Rezervaciju (Stvarna količina - Rezervisana količina) {1} za artikal {2} naspram Šarže {3} u Skladištu {4}." #: erpnext/stock/services/quality_inspection_service.py:77 msgid "Row #{0}: Quality Inspection is required for Item {1}" @@ -46512,7 +46512,10 @@ msgid "Row #{0}: Selling rate for item {1} is lower than its {2}.\n" "\t\t\t\t\tSelling {3} should be at least {4}.

Alternatively,\n" "\t\t\t\t\tyou can disable '{5}' in {6} to bypass\n" "\t\t\t\t\tthis validation." -msgstr "" +msgstr "Red #{0}: Prodajna cijena za artikal {1} je niža od njegove {2}.\n" +"\t\t\t\t\tProdaja {3} treba biti najmanje {4}.

Alternativno,\n" +"\t\t\t\t\tmožete onemogućiti '{5}' u {6} da biste zaobišli\n" +"\t\t\t\t\tovu validaciju." #: erpnext/manufacturing/doctype/work_order/work_order.py:348 msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." @@ -46520,7 +46523,7 @@ msgstr "Red #{0}: ID Sekvence mora biti {1} ili {2} za Operaciju {3}." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:528 msgid "Row #{0}: Serial No {1} cannot be returned since it was not transacted in original invoice {2}" -msgstr "" +msgstr "Red #{0}: Serijski Broj {1} ne može se vratiti jer nije naveden u originalnoj fakturi {2}" #: erpnext/stock/services/serial_batch_bundle_service.py:123 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" @@ -46637,11 +46640,11 @@ msgstr "Red #{0}: Šarža {1} je već istekla." #: erpnext/stock/doctype/stock_entry/stock_entry.py:408 msgid "Row #{0}: The job card item reference is missing. Kindly create the stock entry from the job card. If you have added the row manually then you won't be able to add job card item reference." -msgstr "" +msgstr "Red #{0}: Nedostaje referenca artikla na radnoj kartici. Stvori unos zaliha iz radne kartice. Ako ste red dodali ručno, nećete moći dodati referencu artikla na radnu karticu." #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:103 msgid "Row #{0}: The original Invoice {1} of return invoice {2} is not consolidated." -msgstr "" +msgstr "Red #{0}: Originalna Faktura {1} povratne fakture {2} nije konsolidovana." #: erpnext/stock/doctype/item/item.py:599 msgid "Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}" @@ -46649,7 +46652,7 @@ msgstr "Red #{0}: Skladište {1} nije podređeno skladište grupnog skladišta { #: erpnext/manufacturing/doctype/workstation/workstation.py:190 msgid "Row #{0}: Timings conflict with row {1}" -msgstr "" +msgstr "Red #{0}: Vremenski sukob je u odnosu na red {1}" #: erpnext/assets/doctype/asset/asset.py:656 msgid "Row #{0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" @@ -46673,7 +46676,7 @@ msgstr "Red #{0}: Radni Nalog postoji za punu ili djelomičnu količinu artiikla #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:500 msgid "Row #{0}: You cannot add positive quantities in a return invoice. Please remove item {1} to complete the return." -msgstr "" +msgstr "Red #{0}: Ne možete dodati pozitivne količine u povratnu fakturu. Molimo vas da uklonite artikal {1} da biste dovršili povrat." #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:110 msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." @@ -46685,7 +46688,7 @@ msgstr "Red #{0}: Odaberi Imovinu za Artikal {1}." #: erpnext/stock/doctype/pick_list/pick_list.py:235 msgid "Row #{0}: item {1} has been picked already." -msgstr "" +msgstr "Red #{0}: artikal {1} je već odabran." #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:142 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:207 @@ -46694,7 +46697,7 @@ msgstr "Red #{0}: {1}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:142 msgid "Row #{0}: {1} account is not of type {2}" -msgstr "" +msgstr "Red #{0}: {1} račun nije tipa {2}" #: erpnext/public/js/controllers/buying.js:261 msgid "Row #{0}: {1} can not be negative for item {2}" @@ -46714,11 +46717,11 @@ msgstr "Red #{0}: {1} od {2} bi trebao biti {3}. Ažuriraj {1} ili odaberi drugi #: erpnext/stock/doctype/item/item.py:1511 msgid "Row #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}." -msgstr "" +msgstr "Red #{0}: {1} {2} ne pripada {3}. Odaberi važeći {4}." #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{0}: {1} {2} does not exist." -msgstr "" +msgstr "Red #{0}: {1} {2} ne postoji." #: erpnext/accounts/services/child_item_update.py:251 msgid "Row #{0}:Quantity for Item {1} cannot be zero." @@ -46895,7 +46898,7 @@ msgstr "Red {0}: Od vremena i do vremena je obavezano." #: erpnext/manufacturing/doctype/job_card/job_card.py:355 msgid "Row {0}: From Time and To Time of {1} are overlapping with {2}" -msgstr "" +msgstr "Red {0}: Vrijeme od i Vrijeme do {1} se preklapaju sa {2}" #: erpnext/projects/doctype/timesheet/timesheet.py:225 msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" @@ -46919,7 +46922,7 @@ msgstr "Red {0}: Nevažeća referenca {1}" #: erpnext/controllers/taxes_and_totals.py:134 msgid "Row {0}: Item Tax template for {1} updated as per validity and rate applied" -msgstr "" +msgstr "Red {0}: Predložak Pdv-a za Artikal {1} ažuriran je u skladu s važećim rokom i primijenjenom stopom" #: erpnext/controllers/selling_controller.py:659 msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" @@ -46983,7 +46986,7 @@ msgstr "Red {0}: Odaberi Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:214 msgid "Row {0}: Please select a valid BOM for Item {1}." -msgstr "" +msgstr "Red {0}: Odaberi važeću Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:208 msgid "Row {0}: Please select an active BOM for Item {1}." @@ -47055,7 +47058,7 @@ msgstr "Red {0}: Cijeli iznos troška za račun {1} u {2} je već dodijeljen." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:103 msgid "Row {0}: The item {1}, quantity must be a positive number" -msgstr "" +msgstr "Red {0}: Artikal {1}, količina mora biti pozitivan broj" #: erpnext/accounts/services/taxes.py:269 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" @@ -47116,7 +47119,7 @@ msgstr "Red {0}: {1} {2} je povezan sa {3}. Odaberi dokument koji pripada {4}." #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:132 msgid "Row {0}: {1} {2} must be submitted" -msgstr "" +msgstr "Red {0}: {1} {2} mora biti podnešen" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:111 msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" @@ -47162,7 +47165,7 @@ msgstr "Redovi: {0} imaju 'Unos Plaćanja' kao Tip Reference. Ovo ne treba posta #: erpnext/controllers/accounts_controller.py:276 msgid "Rows: {0} in {1} section are invalid. Reference Name should point to a valid Payment Entry or Journal Entry." -msgstr "" +msgstr "Redovi: {0} u {1} sekciji su nevažeći. Naziv reference treba da ukazuje na važeći Unos Plaćanja ili Nalog Knjiženja." #. Label of the rule_applied (Check) field in DocType 'Pricing Rule Detail' #: erpnext/accounts/doctype/pricing_rule_detail/pricing_rule_detail.json @@ -47589,7 +47592,7 @@ msgstr "Prodajna Faktura nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:195 msgid "Sales Invoice isn't created by user {0}" -msgstr "" +msgstr "Prodajna Faktura nije kreirana od {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:472 msgid "Sales Invoice mode is activated in POS. Please create Sales Invoice instead." @@ -49055,7 +49058,7 @@ msgstr "Odabrani dokument mora biti u podnešenom stanju" #: erpnext/assets/doctype/asset/asset.py:1195 msgid "Selected {0} does not contain the Item Code {1}" -msgstr "" +msgstr "Odabrani {0} ne sadrži Šifru Artikla {1}" #. Option for the 'Pickup Type' (Select) field in DocType 'Shipment' #: erpnext/stock/doctype/shipment/shipment.json @@ -49393,7 +49396,7 @@ msgstr "Serijski broj je već dodijeljen" #: erpnext/assets/doctype/asset_repair/asset_repair.py:296 msgid "Serial No Bundle is mandatory for Item {0}" -msgstr "" +msgstr "Paket Serijskih Brojeva je obavezan za artikal {0}" #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 msgid "Serial No Count" @@ -49458,7 +49461,7 @@ msgstr "Serijski Broj i Šarža" #: erpnext/stock/doctype/stock_settings/stock_settings.js:93 msgid "Serial No and Batch Selector cannot be used when Use Serial / Batch Fields is enabled." -msgstr "" +msgstr "Serijski Broj i birač Šarže ne mogu se koristiti kada je omogućeno Koristi Serijski Broj / Šaržu." #. Name of a report #. Label of a Link in the Stock Workspace @@ -49501,7 +49504,7 @@ msgstr "Serijski Broj {0} ne postoji" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:378 msgid "Serial No {0} is already Delivered. You cannot use it again in Manufacture / Repack entry." -msgstr "" +msgstr "Serijski broj {0} je već dostavljen. Ne možete ga ponovno koristiti u unosu Proizvodnje / Ponovnog pakiranja." #: erpnext/public/js/utils/barcode_scanner.js:435 msgid "Serial No {0} is already added" @@ -49517,11 +49520,11 @@ msgstr "Serijski broj {0} nije u {1} {2}, i ne može se vratiti naspram {1} {2}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:343 msgid "Serial No {0} is under maintenance contract until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod ugovorom o održavanju do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:336 msgid "Serial No {0} is under warranty until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod garancijom do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:322 msgid "Serial No {0} not found" @@ -49657,7 +49660,7 @@ msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mi #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:298 msgid "Serial and Batch Bundle {0} should have voucher type as 'Maintenance Schedule'" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} treba imati tip verifikata kao 'Raspored Održavanja'" #. Label of the section_break_45 (Section Break) field in DocType #. 'Subcontracting Receipt Item' @@ -51136,7 +51139,7 @@ msgstr "Nedostaju neki obavezni podaci o poduzeću Nemate dozvolu da ih ažurira #: erpnext/www/book_appointment/index.js:248 msgid "Something went wrong, please try again" -msgstr "" +msgstr "Nešto nije u redu, pokušaj ponovo" #: erpnext/accounts/doctype/pricing_rule/utils.py:754 msgid "Sorry, this coupon code is no longer valid" @@ -51387,7 +51390,7 @@ msgstr "Raspodijeli proviziju među više prodavača." #: erpnext/buying/doctype/purchase_order/purchase_order.js:600 #: erpnext/public/js/controllers/buying.js:558 msgid "Splitting {0} units of {1}" -msgstr "" +msgstr "Dijeljenje {0} jedinica od {1}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2195 msgid "Splitting {0} {1} into {2} rows as per Payment Terms" @@ -51509,15 +51512,15 @@ msgstr "Poredak" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:73 msgid "Standing scores must be continuous and cover 0 to 100 without gaps or overlaps" -msgstr "" +msgstr "Aktualni rezultati moraju biti neprekidni i obuhvatiti raspon od 0 do 100 bez praznina ili preklapanja" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:76 msgid "Standing scores must cover the full range from 0 to 100" -msgstr "" +msgstr "Aktualni rezultati moraju pokrivati cijeli raspon od 0 do 100." #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:68 msgid "Standing {0} must have a minimum grade lower than its maximum grade" -msgstr "" +msgstr "{0} mora imati najmanje ocjene niže od svoje najviše ocjene" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:54 msgid "Start / Resume" @@ -51525,7 +51528,7 @@ msgstr "Pokreni / Nastavi" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:45 msgid "Start Date cannot be after End Date" -msgstr "" +msgstr "Datum početka ne može biti nakon datuma završetka" #: erpnext/crm/doctype/email_campaign/email_campaign.py:40 msgid "Start Date cannot be before the current date" @@ -51591,7 +51594,7 @@ msgstr "Započet je pozadinski zadatak za kreiranje {1} {0}. {2}" #: erpnext/public/js/bulk_transaction_processing.js:29 msgid "Starting a background job to create {0} {1}" -msgstr "" +msgstr "Započet je pozadinski zadatak za kreiranje {0} {1}" #. Label of the date_dist_from_left_edge (Float) field in DocType 'Cheque Print #. Template' @@ -51802,7 +51805,7 @@ msgstr "Unos Zaključanih Zaliha {0} već postoji za odabrani vremenski raspon" #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:100 msgid "Stock Closing Entry {0} has been queued for processing, the system will take some time to complete it." -msgstr "" +msgstr "Završni Unos Zaliha {0} je stavljen u red za obradu, sistemu će trebati neko vrijeme da ga završi." #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9 msgid "Stock Closing Log" @@ -51885,11 +51888,11 @@ msgstr "Tip Unosa Zaliha" #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.py:65 msgid "Stock Entry Type {0} cannot be set as standard" -msgstr "" +msgstr "Tip Unosa Zaliha {0} ne može se postaviti kao standard" #: erpnext/stock/doctype/pick_list/mapper.py:289 msgid "Stock Entry has already been created against this Pick List" -msgstr "" +msgstr "Unos Zaliha je već kreiran naspram ove Liste Odabira" #: erpnext/stock/doctype/batch/batch.js:138 msgid "Stock Entry {0} created" @@ -51897,7 +51900,7 @@ msgstr "Unos Zaliha {0} je kreiran" #: erpnext/manufacturing/doctype/job_card/job_card.py:1639 msgid "Stock Entry {0} has been created" -msgstr "" +msgstr "Unos Zaliha {0} je stvoren" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:997 msgid "Stock Entry {0} is not submitted" @@ -52510,7 +52513,7 @@ msgstr "Zaliha nije dostupna za Artikal {0} u Skladištu {1}." #: erpnext/selling/page/point_of_sale/pos_controller.js:826 msgid "Stock quantity is not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}." -msgstr "" +msgstr "Količina Zaliha nije dovoljna za Kod Artikla: {0} na skladištu {1}. Dostupna količina {2} {3}." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:255 msgid "Stock transactions before {0} are frozen" @@ -53980,7 +53983,7 @@ msgstr "Ciljna Imovina {0} ne pripada {1}" #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.py:189 msgid "Target Asset {0} needs to be a composite asset" -msgstr "" +msgstr "Ciljana Imovina {0} mora biti objedinjena imovina" #. Name of a DocType #: erpnext/setup/doctype/target_detail/target_detail.json @@ -55090,7 +55093,7 @@ msgstr "Tekst prikazan u finansijskom izvještaju (npr. 'Ukupni Prihod', 'Gotovi #: erpnext/stock/doctype/packing_slip/packing_slip.py:89 msgid "The 'From Package No.' field must not be empty or have a value less than 1." -msgstr "" +msgstr "\"Od Paketa Broj.\" polje ne smije biti prazno niti njegova vrijednost manja od 1." #. Description of the 'Current BOM' (Link) field in DocType 'BOM Update Tool' #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.json @@ -55099,7 +55102,7 @@ msgstr "Sastavnica koja će biti zamijenjena" #: erpnext/controllers/subcontracting_controller.py:1056 msgid "The Batch No {0} has not been supplied against the {1} {2}" -msgstr "" +msgstr "Broj Šarže {0} nije dostavljen protiv {1} {2}" #: erpnext/stock/serial_batch_bundle.py:1557 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." @@ -55107,7 +55110,7 @@ msgstr "Šarža {0} ima negativnu količinu {1}. Da biste to riješili, idite na #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1590 msgid "The Batch {0} of item {1} has negative stock in the warehouse {2}{3}. Please add a stock quantity of {4} to proceed with this entry. If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or in the Stock Settings to proceed. However, enabling this setting may lead to negative stock in the system. So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate." -msgstr "" +msgstr "Šarža {0} artikla {1} ima negativne zalihe u skladištu {2}{3}. Dodaj količinu zaliha od {4} da biste nastavili s ovim unosom. Ako nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' za Šaržu {0} ili u Postavkama Zaliha da biste nastavili. Međutim, omogućavanje ove postavke može dovesti do negativnih zaliha u sistemu. Stoga, molimo vas da osigurate da se nivoi zaliha što prije prilagode kako bi se održala ispravna stopa vrednovanja." #: erpnext/crm/doctype/email_campaign/email_campaign.py:71 msgid "The Campaign '{0}' already exists for the {1} '{2}'" @@ -55135,7 +55138,7 @@ msgstr "Knjigovodstveni Unosi će biti otkazani u pozadini, može potrajati neko #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1156 msgid "The Item {0} does not have Serial No or Batch No" -msgstr "" +msgstr "Artikal {0} nema Serijski niti Šaržni Broj" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:179 msgid "The Loyalty Program isn't valid for the selected company" @@ -55155,11 +55158,11 @@ msgstr "Lista Odabira koja ima Unose Rezervacije Zaliha ne može se ažurirati. #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:127 msgid "The Process Loss Qty has been reset as per the Job Card's Process Loss Qty" -msgstr "" +msgstr "Količinski Gubitak Procesa je poništen prema Radnim Karticama Količinskog Gubitka Procesa" #: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 msgid "The Process Loss Qty has been reset as per the job card's Process Loss Qty" -msgstr "" +msgstr "Količinski Gubitak Procesa je poništen prema Radnim Karticama Količinskog Gubitka Procesa" #: erpnext/setup/doctype/sales_person/sales_person.py:102 msgid "The Sales Person is linked with {0}" @@ -55175,7 +55178,7 @@ msgstr "Serijski Broj {0} je rezervisan naspram {1} {2} i ne može se koristiti #: erpnext/controllers/subcontracting_controller.py:1071 msgid "The Serial Nos {0} have not been supplied against the {1} {2}" -msgstr "" +msgstr "Serijski Brojevi {0} nisu dostavljeni protiv {1} {2}" #: erpnext/stock/doctype/stock_entry/stock_entry.py:950 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" @@ -55231,7 +55234,7 @@ msgstr "Završena količina {0} operacije {1} ne može biti veća od završene k #: erpnext/accounts/doctype/dunning/dunning.py:87 msgid "The currency of invoice {0} ({1}) is different from the currency of this dunning ({2})." -msgstr "" +msgstr "Valuta Fakture {0} ({1}) se razlikuje od valute ove Opomene ({2})." #: erpnext/selling/page/point_of_sale/pos_controller.js:200 msgid "The current POS opening entry is outdated. Please close it and create a new one." @@ -55284,7 +55287,7 @@ msgstr "Polje {0} u redu {1} nije postavljeno" #: erpnext/stock/stock_ledger.py:369 msgid "The field {0} is required for reposting" -msgstr "" +msgstr "Polje {0} je obavezno za ponovno knjiženje" #: erpnext/accounts/doctype/share_transfer/share_transfer.py:188 msgid "The fields From Shareholder and To Shareholder cannot be blank" @@ -55309,7 +55312,7 @@ msgstr "Brojevi Folija nisu usklađeni" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:306 msgid "The following Items, having Putaway Rules, could not be accommodated:" -msgstr "" +msgstr "Sljedeći artikli, koji imaju Pravila Odlaganja na Stranu, nisu mogli biti primjenjene:" #: erpnext/assets/doctype/asset_repair/asset_repair.py:137 msgid "The following Purchase Invoices are not submitted:" @@ -55337,7 +55340,7 @@ msgstr "Sljedeći personal još uvijek podnose izvještaj {0}:" #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py:185 msgid "The following invalid Pricing Rules are deleted:{0}" -msgstr "" +msgstr "Sljedeća nevažeća Pravila Cijena se brišu:{0}" #: erpnext/accounts/doctype/payment_request/payment_request.py:782 msgid "The following payment schedule(s) already exist:\n" @@ -55386,7 +55389,7 @@ msgstr "Artikli {items} nisu označeni kao {type_of} artikli. Možete ih omoguć #: erpnext/manufacturing/doctype/workstation/workstation.py:595 msgid "The job card {0} is in {1} state and you cannot complete it." -msgstr "" +msgstr "Radna Kartica {0} je u {1} stanju i ne možete je završiti." #: erpnext/manufacturing/doctype/workstation/workstation.py:589 msgid "The job card {0} is in {1} state and you cannot start it again." @@ -55424,11 +55427,11 @@ msgstr "Početno stanje se možda nije usklađeno s vašim bankovnim izvodom. Ž #: erpnext/manufacturing/doctype/operation/operation.py:43 msgid "The operation {0} cannot be added multiple times" -msgstr "" +msgstr "Operacija {0} se ne može dodati više puta" #: erpnext/manufacturing/doctype/operation/operation.py:48 msgid "The operation {0} cannot be its own sub-operation" -msgstr "" +msgstr "Operacija {0} ne može biti vlastita podoperacija" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:107 msgid "The original invoice should be consolidated before or along with the return invoice." @@ -55478,7 +55481,7 @@ msgstr "Procenat kojim vam je dozvoljeno prenijeti više naspram naručene koli #: erpnext/stock/doctype/item_price/item_price.py:71 msgid "The price list {0} does not exist or is disabled" -msgstr "" +msgstr "Cjenovnik {0} ne postoji ili je onemogućen" #. Description of the 'Last Purchase Rate' (Float) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -55507,7 +55510,7 @@ msgstr "Odabrane Sastavnice nisu za istu artikal" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:544 msgid "The selected change account {0} does not belong to Company {1}." -msgstr "" +msgstr "Odabrani račun povrata {0} ne pripada {1}." #: erpnext/stock/doctype/batch/batch.py:157 msgid "The selected item cannot have Batch" @@ -55524,7 +55527,7 @@ msgstr "Prodavač i Kupac ne mogu biti isti" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:186 #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:198 msgid "The serial and batch bundle {0} is not linked to {1} {2}" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} nije povezan sa {1} {2}" #: erpnext/stock/doctype/batch/batch.py:386 msgid "The serial no {0} does not belong to item {1}" @@ -55578,7 +55581,7 @@ msgstr "Zadatak je stavljen u red kao pozadinski posao. U slučaju da postoji bi #: erpnext/stock/doctype/material_request/material_request.py:352 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" -msgstr "" +msgstr "Ukupna količina izdavanja / prijenosa {0} u Materijalnom Nalogu {1} ne može biti veća od dozvoljene tražene količine {2} za artikal {3}" #: erpnext/stock/doctype/material_request/material_request.py:359 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" @@ -55658,7 +55661,7 @@ msgstr "{0} {1} se ne poklapa s {0} {2} u {3} {4}" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1730 msgid "The {0} {1} is in submitted state, please cancel it first" -msgstr "" +msgstr "{0} {1} je u podnešenom stanju, prvo ga otkažite" #: erpnext/manufacturing/doctype/job_card/job_card.py:1075 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." @@ -55699,7 +55702,7 @@ msgstr "U sistemu nema unosa kod kojih je datum odobravanja prije datuma knjiže #: erpnext/stock/report/item_variant_details/item_variant_details.py:25 msgid "There are no item variants for the selected item" -msgstr "" +msgstr "Nema artikal varijanti za odabrani artikal" #: erpnext/www/book_appointment/index.js:95 msgid "There are no slots available on this date" @@ -55747,7 +55750,7 @@ msgstr "Postoji jedna neusklađena transakcija prije {0}." #: erpnext/stock/doctype/stock_entry/stock_entry.py:887 msgid "There must be at least 1 Finished Good in this Stock Entry" -msgstr "" +msgstr "U ovom Unosu Zaliha mora biti najmanje jedan gotov proizvod" #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:152 msgid "There was an error creating Bank Account while linking with Plaid." @@ -55759,7 +55762,7 @@ msgstr "Došlo je do greške pri sinhronizaciji transakcija." #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:174 msgid "There was an error updating Bank Account {0} while linking with Plaid." -msgstr "" +msgstr "Došlo je do greške prilikom ažuriranja Bankovnog Računa {0} prilikom povezivanja s Plaid-om." #: banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx:81 msgid "There was an error while importing the bank statement." @@ -55815,7 +55818,7 @@ msgstr "Ovaj Unos Plaćanja je usklađen sa {0}. Otkazivanjem će se automatski #: erpnext/selling/doctype/product_bundle/product_bundle.py:121 msgid "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" -msgstr "" +msgstr "Ovaj Artikal Paket je povezan sa {0}. Morat ćete otkazati ove dokumente kako biste izbrisali ovaj Artikal Paket" #: erpnext/buying/doctype/purchase_order/mapper.py:251 msgid "This Purchase Order has been fully subcontracted." @@ -56156,7 +56159,7 @@ msgstr "Ovo će ograničiti pristup korisnika drugim zapisima zaposlenih" #: erpnext/controllers/selling_controller.py:901 msgid "This {0} will be treated as material transfer." -msgstr "" +msgstr "Ovaj {0} će se tretirati kao prijenos materijala." #. Option for the 'Under Withheld Reason' (Select) field in DocType 'Tax #. Withholding Entry' @@ -56288,7 +56291,7 @@ msgstr "Vremenska Linija" #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher" -msgstr "" +msgstr "Vremensko ograničenje (u sekundama) za svaki pozadinski zadatak koji je stavljen u red čekanja od strane verifikata za zatvaranje knjgovodstvenog perioda" #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:36 #: erpnext/public/js/projects/timer.js:5 @@ -56577,7 +56580,7 @@ msgstr "Do Vremena" #: erpnext/projects/doctype/timesheet_detail/timesheet_detail.py:108 msgid "To Time cannot be before From Time" -msgstr "" +msgstr "Do Vremena ne može biti prije Od Vremena" #. Description of the 'Referral Code' (Data) field in DocType 'Sales Partner' #: erpnext/setup/doctype/sales_partner/sales_partner.json @@ -56633,7 +56636,7 @@ msgstr "Dostava Klijentu" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:276 msgid "To cancel a {0} you need to cancel the POS Closing Entry {1}." -msgstr "" +msgstr "Da biste otkazali {0}, potrebno je da otkažete unos zatvaranja Kase {1}." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:290 msgid "To cancel this Sales Invoice you need to cancel the POS Closing Entry {0}." @@ -56645,7 +56648,7 @@ msgstr "Za kreiranje Zahtjeva Plaćanja obavezan je referentni dokument" #: erpnext/assets/doctype/asset_category/asset_category.py:120 msgid "To enable Capital Work in Progress Accounting, you must select Capital Work in Progress Account in accounts table" -msgstr "" +msgstr "Da biste omogućili knjigovodstvo nedovršenih kapitalnih radova, morate odabrati Račun nedovršenih kapitalnih radova u tabeli računa" #: erpnext/manufacturing/doctype/production_plan/production_plan.js:734 msgid "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked." @@ -56824,19 +56827,19 @@ msgstr "Ukupni Predujam" #: erpnext/public/js/utils.js:250 msgid "Total Advance Paid" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed" #: erpnext/public/js/utils.js:195 msgid "Total Advance Paid: {0}" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed: {0}" #: erpnext/public/js/utils.js:252 msgid "Total Advance Received" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed" #: erpnext/public/js/utils.js:198 msgid "Total Advance Received: {0}" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed: {0}" #. Label of the total_allocated_amount (Currency) field in DocType 'Payment #. Entry' @@ -57495,7 +57498,7 @@ msgstr "Ukupno Vrijeme u minutama" #: erpnext/public/js/utils.js:253 msgid "Total Unpaid" -msgstr "" +msgstr "Ukupno Neplaćeno" #: erpnext/public/js/utils.js:200 msgid "Total Unpaid: {0}" @@ -57595,7 +57598,7 @@ msgstr "Ukupno sati: {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:574 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:190 msgid "Total payments amount can't be greater than {0}" -msgstr "" +msgstr "Ukupan iznos plaćanja ne može biti veći od {0}" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:66 msgid "Total percentage against cost centers should be 100" @@ -57614,7 +57617,7 @@ msgstr "Ukupno {0} ({1})" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:248 msgid "Total {0} for all items is zero, maybe you should change 'Distribute Charges Based On'" -msgstr "" +msgstr "Ukupni iznos {0} a za sve artikle je nula, možda biste trebali promijeniti 'Raspodjeli Troškove na Osnovu'" #: erpnext/controllers/trends.py:25 erpnext/controllers/trends.py:32 msgid "Total(Amt)" @@ -58155,7 +58158,7 @@ msgstr "Probni Bilans Stranke" #: erpnext/accounts/report/trial_balance/trial_balance.py:595 msgid "Trial Balance requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Probni Bilans zahtijeva sinhronizaciju {0} sa DuckDB-om" #. Label of the trial_period_end (Date) field in DocType 'Subscription' #: erpnext/accounts/doctype/subscription/subscription.json @@ -59399,7 +59402,7 @@ msgstr "Korisnik nije primijenio pravilo na fakturi {0}" #: erpnext/crm/frappe_crm_api.py:175 msgid "User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Korisniku nije dozvoljeno sinhroniziranje podataka iz Prodajne Podrške u Sistem. Kontaktiraj Odgovornog Sistema." #: erpnext/setup/doctype/employee/employee.py:298 msgid "User {0} does not exist" @@ -59415,7 +59418,7 @@ msgstr "Korisnik {0} je već dodijeljen {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:62 msgid "User {0} is disabled. Please select valid user/cashier" -msgstr "" +msgstr "Korisnik {0} je onemogućen. Odaberi važećeg korisnika/blagajnika" #: erpnext/setup/doctype/employee/employee.py:365 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." @@ -59763,7 +59766,7 @@ msgstr "Naknade za tip vrijednovanja ne mogu biti označene kao Inkluzivne" #: erpnext/public/js/controllers/accounts.js:228 msgid "Valuation type charges cannot be marked as Inclusive" -msgstr "" +msgstr "Naknade tipa procjene vrijednosti ne mogu biti označene kao uključene." #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:58 msgid "Value (G - D)" @@ -61329,11 +61332,11 @@ msgstr "Sažetka Izvještaja Radnog Naloga" #: erpnext/stock/doctype/material_request/material_request.py:579 msgid "Work Order cannot be created for the following reason:
{0}" -msgstr "" +msgstr "Radni Nalog se ne može kreirati iz sljedećeg razloga:
{0}" #: erpnext/manufacturing/doctype/work_order/work_order.py:857 msgid "Work Order cannot be raised against an Item Template" -msgstr "" +msgstr "Radni Nalog se nemože pokrenuti naspram Šablona Artikla" #: erpnext/manufacturing/doctype/work_order/work_order.py:1123 #: erpnext/manufacturing/doctype/work_order/work_order.py:1170 @@ -61686,7 +61689,7 @@ msgstr "Uvoziš podatke za Listu Koda:" #: erpnext/accounts/services/child_item_update.py:232 msgid "You are not allowed to update as per the conditions set in {0} Workflow." -msgstr "" +msgstr "Nije vam dozvoljeno ažuriranje prema uslovima postavljenim u {0} Radnom Toku." #: erpnext/accounts/services/gl_validator.py:114 msgid "You are not authorized to add or update entries before {0}" @@ -61706,7 +61709,7 @@ msgstr "Birate više od potrebne količine za artikal {0}. Provjerite postoji li #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:111 msgid "You can add the original invoice {0} manually to proceed." -msgstr "" +msgstr "Možete ručno dodati originalnu fakturu {0} da biste nastavili." #: banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx:743 msgid "You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25)." @@ -61718,7 +61721,7 @@ msgstr "Takođe možete kopirati i zalijepiti ovu vezu u svoj pretraživač" #: erpnext/assets/doctype/asset_category/asset_category.py:124 msgid "You can also set default CWIP account in Company {0}" -msgstr "" +msgstr "Također možete postaviti standard Račun Kapitalnog Posla u Toku za {0}" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:761 msgid "You can change the parent account to a Balance Sheet account or select a different account." @@ -61747,7 +61750,7 @@ msgstr "Možete odabrati samo jedan način plaćanja kao standard" #: erpnext/selling/page/point_of_sale/pos_payment.js:595 msgid "You can redeem up to {0}." -msgstr "" +msgstr "Možete iskoristiti do {0}." #: banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx:193 msgid "You can reset the clearing dates of these entries here." @@ -61779,11 +61782,11 @@ msgstr "Ne možete kreirati {0} unutar zatvorenog Knjigovodstvenog Perioda {1}" #: erpnext/accounts/services/gl_validator.py:64 msgid "You cannot create or cancel any accounting entries within the closed Accounting Period {0}" -msgstr "" +msgstr "Ne možete izraditi niti otkazati nikakve knjigovodstvene zapise unutar zatvorenog knjigovodstvenog perioda. {0}" #: erpnext/accounts/services/gl_validator.py:145 msgid "You cannot create/amend any accounting entries until this date." -msgstr "" +msgstr "Ne možete izraditi/izmijeniti bilo koje knjigovodstvene unose do ovog datuma." #: erpnext/accounts/doctype/journal_entry/journal_entry.py:675 msgid "You cannot credit and debit same account at the same time" @@ -61795,7 +61798,7 @@ msgstr "Ne možete izbrisati tip projekta 'Eksterni'" #: erpnext/setup/doctype/department/department.js:19 msgid "You cannot edit the root node." -msgstr "" +msgstr "Ne možete uređivati korijenski čvor." #: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:198 msgid "You cannot enable both the settings '{0}' and '{1}'." @@ -61803,15 +61806,15 @@ msgstr "Ne možete omogućiti i '{0}' i '{1} postavke." #: erpnext/manufacturing/doctype/job_card/job_card.py:1441 msgid "You cannot make any changes to Job Card since Work Order is closed." -msgstr "" +msgstr "Ne možete napraviti nikakve promjene na Radnoj Kartici jer je Radni Nalog zatvoren." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:167 msgid "You cannot outward the following {0} as they are either Delivered, Inactive or located in a different warehouse." -msgstr "" +msgstr "Ne možete poslati sljedeće {0} jer su ili Dostavljeni, Neaktivni ili se nalaze u drugom skladištu." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:229 msgid "You cannot process the serial number {0} as it has already been used in the SABB {1}. {2} If you want to inward the same serial number multiple times, then enable 'Allow existing Serial No to be Manufactured/Received again' in the {3}" -msgstr "" +msgstr "Ne možete obraditi serijski broj {0} jer je već korišten u Serijskom i Šaržnom Paketu {1}. {2} ako želite da primite isti serijski broj više puta, tada omogući 'Dozvoli da se postojeći Serijski Broj ponovo Proizvede/Primi' u {3}" #: erpnext/selling/page/point_of_sale/pos_payment.js:625 msgid "You cannot redeem more than {0}." @@ -61819,7 +61822,7 @@ msgstr "Ne možete iskoristiti više od {0}." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:212 msgid "You cannot repost item valuation before {0}" -msgstr "" +msgstr "Ne možete ponovo knjižiti procjenu vrijednosti artikla prije {0}" #: erpnext/accounts/doctype/subscription/subscription.py:832 msgid "You cannot restart a Subscription that is not cancelled." @@ -61827,7 +61830,7 @@ msgstr "Ne možete ponovo pokrenuti Pretplatu koja nije otkazana." #: erpnext/selling/page/point_of_sale/pos_payment.js:281 msgid "You cannot submit an empty order." -msgstr "" +msgstr "Ne možete podnijeti prazan nalog." #: erpnext/selling/page/point_of_sale/pos_payment.js:280 msgid "You cannot submit the order without payment." @@ -61843,7 +61846,7 @@ msgstr "Ne možete {0} ovaj dokument jer postoji drugi Unos Zatvaranje Perioda { #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:165 msgid "You do not have enough permission to access {0}: {1}" -msgstr "" +msgstr "Nemate dovoljno dozvola za pristup {0}: {1}" #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:82 msgid "You do not have permission to import and submit bank transactions" @@ -61856,7 +61859,7 @@ msgstr "Nemate dozvolu za uvoz bankovnih transakcija" #: erpnext/accounts/services/child_item_update.py:210 msgid "You do not have permissions to {0} items in a {1}." -msgstr "" +msgstr "Nemate dozvole za {0} artikla u {1}." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:187 msgid "You don't have enough Loyalty Points to redeem" @@ -61884,7 +61887,7 @@ msgstr "Nemate dozvolu za ažuriranje ovog dokumenta.Kontaktiraj Odgovornog Sist #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:310 msgid "You had {0} errors while creating opening invoices. Check {1} for more details" -msgstr "" +msgstr "Imali ste {0} grešaka prilikom izrade početnih faktura. Pogledaj {1} za više detalja" #: erpnext/public/js/utils.js:1055 msgid "You have already selected items from {0} {1}" @@ -61904,7 +61907,7 @@ msgstr "Omogućili ste {0} i {1} u {2}. Ovo može dovesti do umetanja cijena iz #: erpnext/stock/doctype/shipment/shipment.js:442 msgid "You have entered a duplicate Delivery Note on row {0}. Please rectify and try again." -msgstr "" +msgstr "Unijeli ste duplikat Dostavnice u red {0}. Ispravi grešku i pokušaj ponovo." #: banking/src/components/features/BankReconciliation/BankPicker.tsx:64 msgid "You have not added any bank accounts to your company." @@ -61928,7 +61931,7 @@ msgstr "Morate odabrati Klijenta prije dodavanja Artikla." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:282 msgid "You need to cancel POS Closing Entry {0} to be able to cancel this document." -msgstr "" +msgstr "Morate otkazati Unos Zatvaranje Kase {0} da biste mogli otkazati ovaj dokument." #: erpnext/accounts/services/taxes.py:277 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." @@ -61984,7 +61987,7 @@ msgstr "Nulto Stanje" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353 msgid "Zero Balance Journal: {0}" -msgstr "" +msgstr "Žurnal Nultog Stanja: {0}" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78 msgid "Zero Rated" @@ -62108,7 +62111,7 @@ msgstr "naziv polja" #: erpnext/setup/doctype/item_group/item_group.py:49 msgid "for tax category {0}" -msgstr "" +msgstr "za PDV kategoriju {0}" #. Option for the 'Service Provider' (Select) field in DocType 'Currency #. Exchange Settings' @@ -62467,7 +62470,7 @@ msgstr "{0} ne može biti negativan" #: erpnext/accounts/doctype/sales_invoice/services/loyalty.py:77 msgid "{0} cannot be cancelled since the Loyalty Points earned has been redeemed. First cancel the {1} No {2}" -msgstr "" +msgstr "{0} se ne može otkazati jer su zarađeni bodovi lojalnosti iskorišteni. Prvo otkaži {1} Broj {2}" #: erpnext/accounts/doctype/pos_settings/pos_settings.py:53 msgid "{0} cannot be changed with opened Opening Entries." @@ -62475,7 +62478,7 @@ msgstr "{0} se ne može mijenjati s otvorenim Početnim Unosima." #: erpnext/public/js/utils/sales_common.js:336 msgid "{0} cannot be greater than 100" -msgstr "" +msgstr "{0} ne može biti veće od 100" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136 msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}" @@ -62544,7 +62547,7 @@ msgstr "{0} je uspješno podnešen" #: erpnext/controllers/buying_controller.py:289 msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return." -msgstr "" +msgstr "{0} je podnio/la imovinu povezanu s njim/njom. Morate otkazati imovinu da biste izradili povrat." #: erpnext/projects/doctype/project/project_dashboard.html:15 msgid "{0} hours" @@ -62556,7 +62559,7 @@ msgstr "{0} u redu {1}" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66 msgid "{0} is a child company." -msgstr "" +msgstr "{0} je podređeno poduzeće." #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465 msgid "{0} is a child table and will be deleted automatically with its parent" @@ -62639,7 +62642,7 @@ msgstr "{0} nije omogućen u {1}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649 msgid "{0} is not running. Cannot trigger events for this document" -msgstr "" +msgstr "{0} se ne izvršava. Nije moguće pokrenuti događaje za ovaj dokument" #: erpnext/stock/doctype/material_request/material_request.py:478 msgid "{0} is not the default supplier for any items." @@ -62647,7 +62650,7 @@ msgstr "{0} nije standard dobavljač za bilo koji artikal." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686 msgid "{0} is on hold until {1}" -msgstr "" +msgstr "{0} je na čekanju do {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68 msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." @@ -62822,11 +62825,11 @@ msgstr "{0} {1} je već povezan sa Zajedničkim Kodom {2}." #: erpnext/accounts/doctype/party_link/party_link.py:53 #: erpnext/accounts/doctype/party_link/party_link.py:63 msgid "{0} {1} is already linked with another {2}" -msgstr "" +msgstr "{0} {1} je već povezan sa drugim {2}" #: erpnext/accounts/doctype/party_link/party_link.py:40 msgid "{0} {1} is already linked with {2} {3}" -msgstr "" +msgstr "{0} {1} je već povezan s {2} {3}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" @@ -62867,7 +62870,7 @@ msgstr "{0} {1} nije aktivan" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452 msgid "{0} {1} is not affecting bank account {2}" -msgstr "" +msgstr "{0} {1} ne utiče na bankovni račun {2}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po index f8f26879689..326be58cd19 100644 --- a/erpnext/locale/fa.po +++ b/erpnext/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-29 20:08\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -14218,7 +14218,7 @@ msgstr "آدرس فعلی" #. Label of the current_accommodation_type (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Current Address Is" -msgstr "آدرس فعلی است" +msgstr "آدرس فعلی" #. Label of the current_amount (Currency) field in DocType 'Stock #. Reconciliation Item' @@ -17767,7 +17767,7 @@ msgstr "سود سهام پرداخت شده" #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Divorced" -msgstr "جدا شده" +msgstr "طلاق گرفته" #. Option for the 'Status' (Select) field in DocType 'Lead' #: erpnext/crm/doctype/lead/lead.json @@ -30241,7 +30241,7 @@ msgstr "متخصص بازاریابی" #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Married" -msgstr "متاهل" +msgstr "متأهل" #: erpnext/setup/setup_wizard/data/marketing_source.txt:7 msgid "Mass Mailing" @@ -34688,7 +34688,7 @@ msgstr "" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Owned" -msgstr "مالکیت" +msgstr "ملکی" #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29 #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:24 @@ -37162,7 +37162,7 @@ msgstr "آدرس دائمی" #. 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Permanent Address Is" -msgstr "آدرس دائمی است" +msgstr "آدرس دائمی" #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:73 #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:77 @@ -44208,7 +44208,7 @@ msgstr "اجاره" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Rented" -msgstr "اجاره شده" +msgstr "استیجاری" #. Label of the reorder_level (Float) field in DocType 'Material Request Item' #: erpnext/stock/doctype/material_request_item/material_request_item.json @@ -50940,7 +50940,7 @@ msgstr "" #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Single" -msgstr "تنها" +msgstr "مجرد" #. Option for the 'Bank Entry Type' (Select) field in DocType 'Bank Transaction #. Rule' @@ -60982,7 +60982,7 @@ msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، ب #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Widowed" -msgstr "بیوه" +msgstr "همسر فوت شده" #. Label of the width (Float) field in DocType 'Shipment Parcel' #. Label of the width (Float) field in DocType 'Shipment Parcel Template' @@ -61884,7 +61884,7 @@ msgstr "تراز صفر" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353 msgid "Zero Balance Journal: {0}" -msgstr "" +msgstr "دفتر تراز صفر: {0}" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78 msgid "Zero Rated" @@ -62008,7 +62008,7 @@ msgstr "fieldname" #: erpnext/setup/doctype/item_group/item_group.py:49 msgid "for tax category {0}" -msgstr "" +msgstr "برای دسته بندی مالیاتی {0}" #. Option for the 'Service Provider' (Select) field in DocType 'Currency #. Exchange Settings' @@ -62375,7 +62375,7 @@ msgstr "" #: erpnext/public/js/utils/sales_common.js:336 msgid "{0} cannot be greater than 100" -msgstr "" +msgstr "{0} نمی‌تواند بزرگتر از ۱۰۰ باشد" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136 msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}" @@ -62444,7 +62444,7 @@ msgstr "{0} با موفقیت ارسال شد" #: erpnext/controllers/buying_controller.py:289 msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return." -msgstr "" +msgstr "{0} دارایی‌های مرتبط با آن را ارسال کرده است. برای ایجاد بازگشت خرید، باید دارایی‌ها را لغو کنید." #: erpnext/projects/doctype/project/project_dashboard.html:15 msgid "{0} hours" @@ -62456,7 +62456,7 @@ msgstr "{0} در ردیف {1}" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66 msgid "{0} is a child company." -msgstr "" +msgstr "{0} یک شرکت فرزند است." #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465 msgid "{0} is a child table and will be deleted automatically with its parent" @@ -62539,7 +62539,7 @@ msgstr "{0} در {1} فعال نیست" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649 msgid "{0} is not running. Cannot trigger events for this document" -msgstr "" +msgstr "{0} در حال اجرا نیست. نمی‌توان رویدادها را برای این سند فعال کرد" #: erpnext/stock/doctype/material_request/material_request.py:478 msgid "{0} is not the default supplier for any items." @@ -62547,7 +62547,7 @@ msgstr "{0} تامین کننده پیش‌فرض هیچ موردی نیست." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686 msgid "{0} is on hold until {1}" -msgstr "" +msgstr "{0} تا زمان {1} در حالت انتظار است" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68 msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." @@ -62722,11 +62722,11 @@ msgstr "" #: erpnext/accounts/doctype/party_link/party_link.py:53 #: erpnext/accounts/doctype/party_link/party_link.py:63 msgid "{0} {1} is already linked with another {2}" -msgstr "" +msgstr "{0} {1} از قبل به {2} دیگری لینک شده است" #: erpnext/accounts/doctype/party_link/party_link.py:40 msgid "{0} {1} is already linked with {2} {3}" -msgstr "" +msgstr "{0} {1} از قبل به {2} {3} لینک شده است" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" @@ -62767,7 +62767,7 @@ msgstr "{0} {1} فعال نیست" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452 msgid "{0} {1} is not affecting bank account {2}" -msgstr "" +msgstr "{0} {1} تاثیری بر حساب بانکی {2} ندارد" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" diff --git a/erpnext/locale/hr.po b/erpnext/locale/hr.po index 71ebf3f6525..221c1d12322 100644 --- a/erpnext/locale/hr.po +++ b/erpnext/locale/hr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-28 20:03\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Croatian\n" "MIME-Version: 1.0\n" @@ -269,7 +269,7 @@ msgstr "'Dozvoli višestruke Prodajne Naloge naspram Nabavnog Naloga Klijenta'" #: erpnext/controllers/trends.py:62 msgid "'Based On' and 'Group By' can not be the same" -msgstr "" +msgstr "'Na Temelju' i 'Grupiraj Po' ne mogu biti isti" #: erpnext/selling/report/inactive_customers/inactive_customers.py:23 msgid "'Days Since Last Order' must be greater than or equal to zero" @@ -295,15 +295,15 @@ msgstr "'Od datuma' mora biti nakon 'Do datuma'" #: erpnext/stock/doctype/item/item.py:466 msgid "'Has Serial No' cannot be 'Yes' for non-stock item" -msgstr "" +msgstr "'Ima Serijski Broj' ne može biti 'Da' za artikal koji nije na zalihama" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:145 msgid "'Inspection Required before Delivery' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Dostave' je onemogućena za artikal {0}, nema potrebe za izradom Kontrole Kvaliteta" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:136 msgid "'Inspection Required before Purchase' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Nabave' je onemogućena za artikal {0}, nema potrebe za izradom Kontrole Kvaliteta" #: erpnext/stock/report/stock_ledger/stock_ledger.py:684 #: erpnext/stock/report/stock_ledger/stock_ledger.py:725 @@ -323,7 +323,7 @@ msgstr "'Do Paketa Broj' ne može biti manje od 'Od Paketa Broj.'" #: erpnext/controllers/sales_and_purchase_return.py:80 msgid "'Update Stock' cannot be checked because items are not delivered via {0}" -msgstr "" +msgstr "'Ažuriraj Zalihe' se ne može provjeriti jer se artikli ne dostavljaju putem {0}" #: erpnext/accounts/doctype/sales_invoice/services/fixed_assets.py:46 msgid "'Update Stock' cannot be checked for fixed asset sale" @@ -827,7 +827,7 @@ msgstr "

Ne možese fakturisati više od predviđenog iznosa za sljedeće arti #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:159 msgid "

Following {0}s do not belong to Company {1}:

" -msgstr "" +msgstr "

Slijedeći {0} ne pripadaju {1} :

" #. Content of the 'html_llwp' (HTML) field in DocType 'Request for Quotation' #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -1042,7 +1042,7 @@ msgstr "A - C" #: erpnext/selling/doctype/customer/customer.py:358 msgid "A Customer Group exists with the same name. Please change the Customer name or rename the Customer Group" -msgstr "" +msgstr "Grupa Klijenta postoji sa istim imenom, molimo promijenite naziv klijenta ili preimenujte Grupu Klijenta" #: erpnext/manufacturing/doctype/workstation/workstation.js:73 msgid "A Holiday List can be added to exclude counting these days for the Workstation." @@ -1054,7 +1054,7 @@ msgstr "Potencijalni Klijent zahtijeva ili ime osobe ili ime tvrtke" #: erpnext/stock/doctype/packing_slip/packing_slip.py:83 msgid "A Packing Slip can only be created for a Draft Delivery Note." -msgstr "" +msgstr "Otpremnica se može kreirati samo za nacrt Dostavnice." #: erpnext/accounts/services/gl_validator.py:123 msgid "A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more." @@ -1312,7 +1312,7 @@ msgstr "Pristupni ključ je potreban za davaoca usluga: {0}" #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:423 msgid "Access to Request for Quotation from the portal is disabled. To allow access, enable it in Portal Settings." -msgstr "" +msgstr "Pristup zahtjevu za ponudu sa portala je onemogućen. Da biste omogućili pristup, omogućite ga u Postavkama Portala." #. Description of the 'Common Code' (Data) field in DocType 'UOM' #: erpnext/setup/doctype/uom/uom.json @@ -2062,7 +2062,7 @@ msgstr "Knjigovodstveni Period" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:49 msgid "Accounting Period cannot be created for a future date. End Date {0} is after today." -msgstr "" +msgstr "Knjigovodstveno razdoblje ne može se izraditi za budući datum. Datum završetka {0} je sutra." #: erpnext/accounts/doctype/accounting_period/accounting_period.py:77 msgid "Accounting Period overlaps with {0}" @@ -2921,7 +2921,7 @@ msgstr "Dodata uloga dobavljača korisniku {0}." #: erpnext/controllers/website_list_for_contact.py:311 msgid "Added {1} role to user {0}." -msgstr "" +msgstr "Dodana je uloga {1} korisniku {0}." #: erpnext/crm/doctype/lead/lead.js:81 msgid "Adding Lead to Prospect..." @@ -3183,7 +3183,7 @@ msgstr "Dodatna Prenesena Količina" #: erpnext/manufacturing/doctype/work_order/work_order.py:591 msgid "Additional Transferred Qty {0} cannot be greater than {1}. To fix this, increase the percentage value of the field 'Transfer Extra Raw Materials to WIP' in Manufacturing Settings." -msgstr "" +msgstr "Dodatna Prenesena Količina {0} ne može biti veća od {1}. Da biste ovo ispravili, povećajte postotnu vrijednostpolja 'Prenesi Dodatne Sirovine u Nedovršenu Proizvodnju'u Postavkama Proizvodnje." #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:630 msgid "Additional {0} {1} of item {2} required as per BOM to complete this transaction" @@ -3779,7 +3779,7 @@ msgstr "Algoritam" #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/selling/doctype/customer/customer.json msgid "Alias" -msgstr "" +msgstr "Nadimak" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185 @@ -3989,7 +3989,7 @@ msgstr "Svi komentari i e-pošta kopirat će se iz jednog dokumenta u drugi novo #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:204 msgid "All the items have already been returned." -msgstr "" +msgstr "Svi artikli su već vraćeni." #: erpnext/manufacturing/doctype/work_order/work_order.js:1272 msgid "All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table." @@ -3997,7 +3997,7 @@ msgstr "Svi obavezni Artikli (sirovine) bit će preuzeti iz Sastavnice i popunje #: erpnext/stock/doctype/delivery_note/mapper.py:82 msgid "All these items have already been invoiced/returned" -msgstr "" +msgstr "Svi ovi artikli su već fakturirani/vraćeni" #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:100 #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:101 @@ -4144,7 +4144,7 @@ msgstr "Dozvoli Alternativni Artikal" #: erpnext/stock/doctype/item_alternative/item_alternative.py:68 msgid "Allow Alternative Item must be checked on Item {0}" -msgstr "" +msgstr "Dozvoli Alternativni Artikal mora biti odabrano za Artikal {0}" #. Label of the material_consumption (Check) field in DocType 'Manufacturing #. Settings' @@ -4536,7 +4536,7 @@ msgstr "Dozvoljena Transakcija sa" #. Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Allowed Users" -msgstr "" +msgstr "Dopušteni Korisnici" #: erpnext/accounts/doctype/party_link/party_link.py:27 msgid "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only." @@ -5459,7 +5459,7 @@ msgstr "Termin s" #: erpnext/www/book_appointment/index.js:237 msgid "Appointment created successfully" -msgstr "" +msgstr "Termin je uspješno zakazan" #: erpnext/crm/doctype/appointment/appointment.py:101 msgid "Appointment was created. But no lead was found. Please check the email to confirm" @@ -5505,11 +5505,11 @@ msgstr "Jeste li sigurni da želite izbrisati sve demo podatke?" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:51 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:100 msgid "Are you sure you want to create Reposting Entries?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:66 msgid "Are you sure you want to create a Reposting Entry?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:480 msgid "Are you sure you want to delete this Item?" @@ -5603,7 +5603,7 @@ msgstr "Pošto ima dovoljno sirovina, Materijalni Nalog nije potreban za Skladi #: erpnext/stock/doctype/stock_settings/stock_settings.py:250 msgid "As there is reserved stock, you cannot disable {0}." -msgstr "" +msgstr "Budući da postoje rezervirane zalihe, ne možete onemogućiti {0}." #: erpnext/stock/doctype/stock_settings/stock_settings.py:224 #: erpnext/stock/doctype/stock_settings/stock_settings.py:236 @@ -6194,7 +6194,7 @@ msgstr "Dodijeli Imenu" #: erpnext/buying/doctype/purchase_order/purchase_order.js:593 #: erpnext/public/js/controllers/buying.js:555 msgid "Assigning {0} to {1} (row {2})" -msgstr "" +msgstr "Dodjeljuje se {0} {1} (red {2})" #. Label of the filters_section (Section Break) field in DocType 'Service Level #. Agreement' @@ -6220,7 +6220,7 @@ msgstr "U Redu {0}: U Serijskom i Šaržnom Paketu {1} mora imati status dokumen #: erpnext/accounts/services/internal_transfer.py:98 msgid "At Row {0}: The field {1} is mandatory for internal transfer" -msgstr "" +msgstr "U redu {0}: Polje {1} je obavezno za interni prijenos" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:85 msgid "At least one account with exchange gain or loss is required" @@ -6253,7 +6253,7 @@ msgstr "Najmanje jedno od Prodaje ili Nabave mora biti odabrano" #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:226 msgid "At least one raw material for Finished Good Item {0} should be customer provided." -msgstr "" +msgstr "Klijent treba osigurati barem jednu sirovinu za Gotov Proizvod {0}." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:60 msgid "At least one raw material item must be present in the stock entry for the type {0}" @@ -6293,7 +6293,7 @@ msgstr "Red {0}: Serijski Broj je obavezan za Artikal {1}" #: erpnext/stock/services/serial_batch_bundle_service.py:498 msgid "At row {0}: Serial and Batch Bundle {1} has already been created. Please remove the values from the serial no or batch no fields." -msgstr "" +msgstr "U redu {0}: Serijski i Šaržni Paket {1} je već izrađen. Ukloni vrijednosti za serijski broj ili broj šarže." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:123 msgid "At row {0}: set Parent Row No for item {1}" @@ -6930,7 +6930,7 @@ msgstr "Sastavnica 1" #: erpnext/manufacturing/doctype/bom/mapper.py:82 msgid "BOM 1 {0} and BOM 2 {1} should not be the same" -msgstr "" +msgstr "Sastavnica 1 {0} i Sastavnica 2 {1} ne smiju biti isti" #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:38 msgid "BOM 2" @@ -7188,7 +7188,7 @@ msgstr "Rekurzija Sastavnice: {1} ne može biti nadređena ili podređena {0}" #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js:81 msgid "BOM update is queued and may take a few minutes. Check {0} for progress." -msgstr "" +msgstr "Ažuriranje Sastavnice je u redu čekanja i može potrajati nekoliko minuta. Provjerite {0} za napredak." #: erpnext/manufacturing/doctype/bom/bom.py:1404 msgid "BOM {0} does not belong to Item {1}" @@ -7351,7 +7351,7 @@ msgstr "Sažetak Bilansa Stanja" #: erpnext/accounts/report/balance_sheet/balance_sheet.py:284 msgid "Balance Sheet requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilansa Stanja zahtijeva da se {0} sinkronizuje s DuckDB-om" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:13 msgid "Balance Stock Qty" @@ -7514,7 +7514,7 @@ msgstr "Tip Bankovnog Računa" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:443 msgid "Bank Account {0} in Bank Transaction {1} is not matching with Bank Account {2}" -msgstr "" +msgstr "Bankovni Račun {0} u Bankovnoj Transakciji {1} ne odgovara Bankovnim Računu {2}" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:15 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:20 @@ -8096,7 +8096,7 @@ msgstr "Broj Šarže je obavezan" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3495 msgid "Batch No {0} does not exist" -msgstr "" +msgstr "Broj Šarže {0} ne postoji" #: erpnext/stock/utils.py:625 msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." @@ -8108,7 +8108,7 @@ msgstr "Broj Šarže {0} nije prisutan u originalnom {1} {2}, stoga ga ne možet #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:658 msgid "Batch No {0} of Item {1} has negative stock of quantity {2} in the warehouse {3}" -msgstr "" +msgstr "Broj Šarže {0} Artikla {1} ima negativnu količinu {2} u skladištu {3}" #. Label of the batch_no (Int) field in DocType 'BOM Update Batch' #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -8177,7 +8177,7 @@ msgstr "Šarža i Serijski Broj" #: erpnext/manufacturing/doctype/work_order/work_order.py:742 msgid "Batch not created for item {0} since it does not have a batch series." -msgstr "" +msgstr "Šarža nije izrađena za artikal {0} jer nema Broj Šarže." #. Description of the 'Automatically Create New Batch' (Check) field in DocType #. 'Item' @@ -8765,7 +8765,7 @@ msgstr "Proknjižena Osnovna Imovina" #: erpnext/accounts/services/gl_validator.py:143 msgid "Books have been closed until the period ending on {0}" -msgstr "" +msgstr "Knjigovodstvo je zatvoreno do kraja razdoblja koje završava {0}" #. Option for the 'Type of Transaction' (Select) field in DocType 'Inventory #. Dimension' @@ -8993,7 +8993,7 @@ msgstr "Proračun se ne može dodijeliti naspram Grupnog Računu {0}" #: erpnext/accounts/doctype/budget/budget.py:165 msgid "Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense" -msgstr "" +msgstr "Proračun se ne može dodijeliti za {0}, jer njegova Kontna Klasa nije Prihod ili Rashod" #: erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py:9 msgid "Budgets" @@ -9347,7 +9347,7 @@ msgstr "Izračunata Razlika Popusta" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:95 msgid "Calculating arrival times" -msgstr "" +msgstr "Izračunavanje vremena dolaska" #. Label of the section_break_11 (Section Break) field in DocType 'Supplier #. Scorecard Period' @@ -9565,7 +9565,7 @@ msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije na #: erpnext/stock/doctype/stock_settings/stock_settings.py:191 msgid "Can't change the valuation method, as there are transactions against some items which do not have their own valuation method" -msgstr "" +msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije za neke artikle koji nemaju vlastiti metod vrijednovanja" #: erpnext/support/doctype/warranty_claim/warranty_claim.py:79 msgid "Cancel Material Visit {0} before cancelling this Warranty Claim" @@ -9644,7 +9644,7 @@ msgstr "Ne može biti artikal fiksne imovine jer je kreiran Registar Zaliha." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:92 #: erpnext/stock/doctype/delivery_trip/delivery_trip.py:219 msgid "Cannot calculate arrival time as the driver address is missing." -msgstr "" +msgstr "Nije moguće izračunati vrijeme dolaska jer nedostaje adresa vozača." #: erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py:117 msgid "Cannot cancel Asset Depreciation Schedule {0} as it has a draft journal entry {1}." @@ -9656,7 +9656,7 @@ msgstr "Ne može se otkazati Unos Zatvaranja Blagajne" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:140 msgid "Cannot cancel Stock Reservation Entry {0}, as it has been used in the work order {1}. Please cancel the work order first or unreserve the stock" -msgstr "" +msgstr "Ne može se otkazati Unos Rezervacije Zaliha {0} jer je korišten u radnom nalogu {1}. Prvo otkaži radni nalog ili poništiti rezervaciju zaliha" #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:275 msgid "Cannot cancel as processing of cancelled documents is pending." @@ -9708,7 +9708,7 @@ msgstr "Nije moguće promijeniti standard valutu tvrtke, jer postoje postojeće #: erpnext/projects/doctype/task/task.py:146 msgid "Cannot complete task {0} as its dependent task {1} is not completed / cancelled." -msgstr "" +msgstr "Nije moguće dovršiti zadatak {0} jer njegov zavisni zadatak {1} nije dovršen / otkazan." #: erpnext/accounts/doctype/cost_center/cost_center.py:61 msgid "Cannot convert Cost Center to ledger as it has child nodes" @@ -9745,7 +9745,7 @@ msgstr "Nije moguće kreirati knjigovodstvene unose naspram onemogućenih račun #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:146 msgid "Cannot create more Subcontracting Orders against the Purchase Order {0}." -msgstr "" +msgstr "Ne može se izraditi više Podugovornih Naloga na osnovu Naloga Nabave {0}." #: erpnext/controllers/sales_and_purchase_return.py:444 msgid "Cannot create return for consolidated invoice {0}." @@ -9840,7 +9840,7 @@ msgstr "Nije moguće spojiti {0} '{1}' u '{2}' jer oba imaju postojeće knjigovo #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:125 msgid "Cannot optimize route as the driver address is missing." -msgstr "" +msgstr "Nije moguće optimizirati rutu jer nedostaje adresa vozača." #: erpnext/manufacturing/doctype/work_order/services/status.py:41 msgid "Cannot produce more Item {0} than Sales Order quantity {1} {2}" @@ -9870,7 +9870,7 @@ msgstr "Ne može se upućivati na broj reda veći ili jednak trenutnom broju red #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:626 msgid "Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

The Allowed Qty is calculated as follows:
  • Actual Qty [Available Qty at Warehouse] = {5}
  • Reserved Stock [Ignore current SRE] = {6}
  • Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}
  • Voucher Qty [Voucher Item Qty] = {8}
  • Delivered Qty [Qty delivered against the Voucher Item] = {9}
  • Total Reserved Qty [Qty reserved against the Voucher Item] = {10}
  • Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}
" -msgstr "" +msgstr "Ne može se rezervirati više od Dopuštene Količine {0} {1} za Artikal {2} za {3} {4}.

Dopuštena Količina izračunava se na sljedeći način:
  • Stvarna Količina [Raspoloživa Količina u Skladištu] = {5}
  • Rezervirana Zaliha [Zanemari Trenutni Unos Rezarvascije Zaliha = {6}
  • Dostupna Količina za Rezervaciju [Stvarna Količina - Rezervirana Zaliha] = {7}
  • Količina Verifikata [Količina Artikla Verifikata] = {8}
  • Dostavljena Količina [Količina Dostavljena na Temelju Artikla Verifikata] = {9}
  • Ukupna Rezerviraa Količina [Količina Rezervirana za Artikal Verifikata] = {10}
  • Dopuštena Količina [Minimum od (Dostupna Količina za Rezervaciju, (Količina Verifikata - Dostavljena Količina - Ukupna Rezervirana Količina))] = {11}
" #: erpnext/accounts/doctype/bank/bank.js:63 msgid "Cannot retrieve link token for update. Check Error Log for more information" @@ -9895,7 +9895,7 @@ msgstr "Nije moguće odabrati tip naknade kao 'Iznos na Prethodnom Redu' ili 'Uk #: erpnext/stock/doctype/item_alternative/item_alternative.py:36 msgid "Cannot set alternative item for the item {0}" -msgstr "" +msgstr "Nije moguće postaviti alternativni artikal za artikal {0}" #: erpnext/selling/doctype/quotation/quotation.py:293 msgid "Cannot set as Lost as Sales Order is made." @@ -10325,7 +10325,7 @@ msgstr "Ručno promijenite ovaj datum da postavite sljedeći datum početka sinh #: erpnext/selling/doctype/customer/customer.py:161 msgid "Changed customer name to '{0}' as '{1}' already exists." -msgstr "" +msgstr "Ime klijenta promijenjeno je u '{0}' jer '{1}' već postoji." #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:160 msgid "Changes in {0}" @@ -10615,7 +10615,7 @@ msgstr "Podređena tablica nije dopuštena" #: erpnext/projects/doctype/task/task.py:319 msgid "Child Task exists for this Task. You cannot delete this Task." -msgstr "" +msgstr "Za ovaj zadatak postoji podređeni zadatak. Ne možete izbrisati ovaj zadatak." #: erpnext/stock/doctype/warehouse/warehouse_tree.js:21 msgid "Child nodes can be only created under 'Group' type nodes" @@ -11795,11 +11795,11 @@ msgstr "Naziv polja poveznice tvrtke koje se koristi za filtriranje (neobavezno #: erpnext/setup/doctype/company/company.js:239 msgid "Company name does not match" -msgstr "" +msgstr "Naziv tvrtke se ne poklapa" #: erpnext/assets/doctype/asset/asset.py:330 msgid "Company of asset {0} and purchase document {1} does not match." -msgstr "" +msgstr "Tvrtka imovine {0} i dokument o nabavi {1} se ne poklapa." #: erpnext/setup/doctype/employee/employee.py:164 msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" @@ -11839,11 +11839,11 @@ msgstr "Tvrtka {0} ne postoji" #: erpnext/setup/setup_wizard/operations/taxes_setup.py:14 msgid "Company {0} does not exist yet. Taxes setup aborted." -msgstr "" +msgstr "Tvrtka {0} još ne postoji. Postavljanje PDV-a je prekinuto." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:579 msgid "Company {0} does not match with POS Profile Company {1}" -msgstr "" +msgstr "Tvrtka {0} ne odgovara Kasa Profilu Tvrtke {1}" #: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:105 msgid "Company {0} is added more than once" @@ -12319,7 +12319,7 @@ msgstr "Potrošena Količina" #: erpnext/manufacturing/doctype/work_order/services/reservation.py:186 msgid "Consumed Qty {0} cannot be greater than Reserved Qty {1} for item {2}" -msgstr "" +msgstr "Potrošena Količina {0} ne može biti veća od Rezervirane Količine {1} za artikal {2}" #. Label of the consumed_quantity (Data) field in DocType 'Asset Repair #. Consumed Item' @@ -13052,11 +13052,11 @@ msgstr "Centar Troškova {0} ne može se koristiti za dodjelu jer se koristi kao #: erpnext/assets/doctype/asset/asset.py:358 msgid "Cost Center {0} does not belong to Company {1}" -msgstr "" +msgstr "Centar Troška {0} ne pripada Tvrtki {1}" #: erpnext/assets/doctype/asset/asset.py:365 msgid "Cost Center {0} is a group cost center and group cost centers cannot be used in transactions" -msgstr "" +msgstr "Centar Troška {0} je grupni centar troška a grupni centri troška ne mogu se koristiti u transakcijama" #: erpnext/accounts/report/financial_statements.py:685 msgid "Cost Center: {0} does not exist" @@ -13181,7 +13181,7 @@ msgstr "Obračun Troškova i Fakturisanje" #: erpnext/projects/doctype/project/project.js:140 msgid "Costing and Billing fields have been updated" -msgstr "" +msgstr "Polja Troškova i Fakturiranja su ažurirana" #: erpnext/setup/demo.py:78 msgid "Could Not Delete Demo Data" @@ -13210,7 +13210,7 @@ msgstr "Nije moguće pronaći odgovarajuću promjenu koja bi odgovarala razlici: #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:46 #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:50 msgid "Could not find path for {0}" -msgstr "" +msgstr "Nije moguće pronaći put za {0}" #: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:101 msgid "Could not re-extract the table." @@ -14335,7 +14335,7 @@ msgstr "Trenutna Sastavnica" #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py:80 msgid "Current BOM and New BOM cannot be the same" -msgstr "" +msgstr "Trenutna i Nova Sastavnica ne mogu biti iste" #. Label of the current_exchange_rate (Float) field in DocType 'Exchange Rate #. Revaluation Account' @@ -17059,11 +17059,11 @@ msgstr "Razlika u kontu stavki u tablici" #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:155 msgid "Difference Account must be an Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" -msgstr "" +msgstr "Račun Razlika mora biti račun tipa Imovina/Obveza (Privremeno Početno), budući da je ovaj unos zaliha početni unos" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:985 msgid "Difference Account must be an Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" -msgstr "" +msgstr "Račun Razlike mora biti račun tipa Imovine/Obveze, budući da je ovo Usklađivanje Zaliha početni unos" #. Label of the difference_amount (Currency) field in DocType 'Payment #. Reconciliation Allocation' @@ -17309,7 +17309,7 @@ msgstr "Onemogućeni artikli se ne mogu odabrati ni u jednoj transakciji." #: erpnext/accounts/services/internal_transfer.py:120 msgid "Disabled pricing rules since this {0} is an internal transfer" -msgstr "" +msgstr "Pravila određivanja cijena onemogućena su jer je ovo {0} interni prijenos" #. Description of the 'Disabled' (Check) field in DocType 'Supplier' #: erpnext/buying/doctype/supplier/supplier.json @@ -17318,7 +17318,7 @@ msgstr "Onemogućeni dobavljači su skriveni od odabira u novim transakcijama, a #: erpnext/accounts/services/internal_transfer.py:136 msgid "Disabled tax included prices since this {0} is an internal transfer" -msgstr "" +msgstr "Cijene s PDV-om onemogućene jer je ovo {0} interni prijenos" #: erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py:82 msgid "Disabled template must not be default template" @@ -17569,7 +17569,7 @@ msgstr "Popust mora biti manji od 100%" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3090 msgid "Discount of {0} applied as per Payment Term" -msgstr "" +msgstr "Popust od {0} primijenjen prema Uvjetima Plaćanja" #. Label of the section_break_18 (Section Break) field in DocType 'Pricing #. Rule' @@ -17934,7 +17934,7 @@ msgstr "Želiš li podnijeti unos zaliha?" #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py:50 #: erpnext/selling/report/sales_partner_commission_summary/test_sales_partner_commission_summary.py:22 msgid "DocType can be one of {0}" -msgstr "" +msgstr "DocType može biti jedan od {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:182 #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:458 @@ -18659,7 +18659,7 @@ msgstr "Verifikacija e-pošte nije uspjela." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:20 msgid "Emails queued" -msgstr "" +msgstr "E-pošta u redu čekanja" #. Label of the emergency_contact_details (Section Break) field in DocType #. 'Employee' @@ -18938,7 +18938,7 @@ msgstr "Omogući Evropski Pristup" #. 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Enable Frappe CRM Data Synchronization" -msgstr "" +msgstr "Omogući sinkronizaciju podataka Prodajne Podrške" #. Label of the enable_fuzzy_matching (Check) field in DocType 'Accounts #. Settings' @@ -19397,7 +19397,7 @@ msgstr "Unesi {0} iznos." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:170 msgid "Enter {0} name." -msgstr "" +msgstr "Unesi {0} ime." #: erpnext/setup/setup_wizard/data/industry_type.txt:22 msgid "Entertainment & Leisure" @@ -19496,15 +19496,15 @@ msgstr "Greška prilikom ponovnog knjiženja vrijednosti artikla" #: erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py:175 msgid "Error: This asset already has {0} depreciation periods booked. The `depreciation start` date must be at least {1} periods after the `available for use` date. Please correct the dates accordingly." -msgstr "" +msgstr "Pogreška: Ova imovina već ima rezerviranih {0} razdoblja amortizacije. Datum `početka amortizacije` mora biti najmanje {1} razdoblja nakon datuma `raspoloživosti za upotrebu`. Molimo ispravite datume u skladu s tim." #: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py:93 msgid "Error: {0}" -msgstr "" +msgstr "Pogreška: {0}" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:980 msgid "Error: {0} is a mandatory field" -msgstr "" +msgstr "Pogreška: {0} je obavezno polje" #. Label of the errors_notification_section (Section Break) field in DocType #. 'Stock Reposting Settings' @@ -20197,7 +20197,7 @@ msgstr "Neuspješni Unosi" #: erpnext/utilities/doctype/video_settings/video_settings.py:35 msgid "Failed to authenticate the API key. Please check the error logs." -msgstr "" +msgstr "Autentifikacija API ključa nije uspjela. Provjerite zapisnike pogrešaka." #: erpnext/setup/setup_wizard/setup_wizard.py:37 #: erpnext/setup/setup_wizard/setup_wizard.py:38 @@ -21176,11 +21176,11 @@ msgstr "Za Radni Nalog" #: erpnext/controllers/status_updater.py:292 msgid "For an item {0}, quantity must be a negative number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti negativan broj" #: erpnext/controllers/status_updater.py:289 msgid "For an item {0}, quantity must be a positive number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti pozitivan broj" #. Description of the 'Income Account' (Link) field in DocType 'Dunning' #: erpnext/accounts/doctype/dunning/dunning.json @@ -21214,11 +21214,11 @@ msgstr "Za individualnog Dobavljača" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:379 msgid "For item {0}, only {1} assets have been created or linked to {2}. Please create or link {3} more assets with the respective document." -msgstr "" +msgstr "Za artikal {0}, samo {1} imovina je stvorena ili povezana s {2}. Stvori ili poveži još {3} imovine s odgovarajućim dokumentom." #: erpnext/controllers/status_updater.py:302 msgid "For item {0}, rate must be a positive number. To allow negative rates, enable {1} in {2}" -msgstr "" +msgstr "Za artikal {0}, cijena mora biti pozitivan broj. Da biste omogućili negativne cijene, omogućite {1} u {2}" #. Description of the 'Do not fetch incoming rate from Serial No' (Check) field #. in DocType 'Stock Reposting Settings' @@ -21232,7 +21232,7 @@ msgstr "Za operaciju {0} u redu {1}, molimo dodajte sirovine ili postavite Sasta #: erpnext/manufacturing/doctype/work_order/mapper.py:379 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity ({2})" -msgstr "" +msgstr "Za Operaciju {0}: Količina ({1}) ne može biti veća od količine na čekanju ({2})" #: erpnext/projects/doctype/project/project.js:208 msgid "For project - {0}, update your status" @@ -21278,7 +21278,7 @@ msgstr "Za praktičnost Klienta, ovi kodovi se mogu koristiti u formatima za isp #: erpnext/stock/serial_batch_bundle.py:1234 msgid "For the item {0}, the Available qty {1} is less than the Required Qty {2} in the warehouse {3}. Please add sufficient qty in the warehouse." -msgstr "" +msgstr "Za artikal {0}, Raspoloživa Količina {1} je manja od Zatražene Količine {2} u skladištu {3}. Dodaj dovoljnu količinu u skladište." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:893 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." @@ -21381,11 +21381,11 @@ msgstr "Podrška Prodaje" #. Name of a DocType #: erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json msgid "Frappe CRM Allowed User" -msgstr "" +msgstr "Dozvoljeni korisnik Prodajne Podrške" #: erpnext/crm/frappe_crm_api.py:168 msgid "Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Sinkronizacija podataka Prodajne Podrške nije omogućena U Sustavu. Obrati se Upravitelju Sustava." #: erpnext/setup/install.py:232 msgid "Frappe School" @@ -22057,7 +22057,7 @@ msgstr "Dužina napomena Knjigovodstvenog Registra" #: erpnext/accounts/report/general_ledger/general_ledger.py:829 msgid "General Ledger requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Knjigovodstveni Registar zahtijeva da se {0} sinkronizuje sa DuckDB-om" #. Label of the gs (Section Break) field in DocType 'Item Group' #: erpnext/setup/doctype/item_group/item_group.json @@ -24098,7 +24098,7 @@ msgstr "Uvezi Fakture" #. Import' #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json msgid "Import MT940 Format" -msgstr "" +msgstr "Uvezi MT940 Format" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:144 msgid "Import Successful" @@ -25495,7 +25495,7 @@ msgstr "Nevažeće Skladište" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:460 msgid "Invalid amount in accounting entries of {0} {1} for Account {2}: {3}" -msgstr "" +msgstr "Nevažeći iznos u knjigovodstvenim unosima {0} {1} za račun {2}: {3}" #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:312 msgid "Invalid condition expression" @@ -27846,7 +27846,7 @@ msgstr "Artikal {0} se nemože naručiti više od {1} u odnosu na Ugovorni Nalog #: erpnext/stock/services/internal_transfer.py:104 msgid "Item {0} cannot be received in more than {1} qty against the {2} {3}" -msgstr "" +msgstr "Artikal {0} ne može se primiti u količini većoj od {1} u odnosu na {2} {3}" #: erpnext/assets/doctype/asset/asset.py:343 #: erpnext/stock/doctype/item/item.py:693 @@ -27892,7 +27892,7 @@ msgstr "Artikal {0} zanemaren jer nije artikal na zalihama" #: erpnext/stock/get_item_details.py:359 msgid "Item {0} is a template, please select one of its variants" -msgstr "" +msgstr "Artikal {0} je predložak, odaberite jednu od njezinih varijanti" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:614 msgid "Item {0} is already reserved/delivered against Sales Order {1}." @@ -28010,7 +28010,7 @@ msgstr "Artikal: {0} ne postoji u sustavu" #: erpnext/manufacturing/doctype/bom/bom.py:970 msgid "Item: {0} with Stock UOM: {1} cannot have fractional process loss qty as UOM {2} is a whole number." -msgstr "" +msgstr "Artikal: {0} s jedinicom zalihe: {1} ne može imati frakcijsku količinu gubitaka u procesu jer je jedinica mjere {2} cijeli broj." #. Label of a Card Break in the Buying Workspace #. Label of a Workspace Sidebar Item @@ -28217,7 +28217,7 @@ msgstr "Radne Kartice {0} je završen" #: erpnext/manufacturing/doctype/job_card/job_card.py:1416 msgid "Job Card {0}: As per the sequence of the operations in the work order {1}, complete the operation {2} before the operation {3}." -msgstr "" +msgstr "Radna Kartica {0}: Prema redoslijedu operacija u radnom nalogu {1}, dovršite operaciju {2} prije operacije {3}." #. Label of the dashboard_tab (Tab Break) field in DocType 'Workstation' #: erpnext/manufacturing/doctype/workstation/workstation.json @@ -28292,11 +28292,11 @@ msgstr "Radna Kartica {0} kreirana" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:106 msgid "Job paused" -msgstr "" +msgstr "Posao Pauziran" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:64 msgid "Job started" -msgstr "" +msgstr "Posao Započet" #: erpnext/utilities/bulk_transaction.py:72 msgid "Job: {0} has been triggered for processing failed transactions" @@ -28645,7 +28645,7 @@ msgstr "Prošla Fiskalna Godina" #: erpnext/accounts/doctype/account/account.py:673 msgid "Last GL Entry update was done {0}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying." -msgstr "" +msgstr "Posljednje ažuriranje Knjigovodstvenog Registra je obavljeno {0}. Ova operacija nije dopuštena dok se sustav aktivno koristi. Pričekaj 5 minuta prije ponovnog pokušaja." #. Label of the last_integration_date (Date) field in DocType 'Bank Account' #: erpnext/accounts/doctype/bank_account/bank_account.json @@ -29163,7 +29163,7 @@ msgstr "Povezivanje s klijentom nije uspjelo. Molimo pokušajte ponovo." #: erpnext/selling/doctype/customer/customer.js:282 msgid "Linking to Supplier failed. Please try again." -msgstr "" +msgstr "Povezivanje sa Dobavljačem nije uspjelo. Pokušaj ponovo." #: erpnext/accounts/report/financial_ratios/financial_ratios.js:55 #: erpnext/accounts/report/financial_ratios/financial_ratios.py:152 @@ -30731,7 +30731,7 @@ msgstr "Materijali su već primljeni naspram {0} {1}" #: erpnext/manufacturing/doctype/job_card/job_card.py:189 #: erpnext/manufacturing/doctype/job_card/job_card.py:903 msgid "Materials need to be transferred to the work in progress warehouse for the job card {0}" -msgstr "" +msgstr "Materijali se moraju prenijeti u skladište nedovršene proizvodnje za radnu karticu {0}" #. Label of the max_amount (Currency) field in DocType 'Bank Transaction Rule' #. Label of the max_amount (Currency) field in DocType 'Promotional Scheme @@ -31618,7 +31618,7 @@ msgstr "Više Računa (Predložak Naloga Knjiženja)" #: erpnext/selling/doctype/customer/customer.py:443 msgid "Multiple Loyalty Programs found for Customer {0}. Please select manually." -msgstr "" +msgstr "Višei Program Vjernosti pronađeno je za Klijenta {0}. Odaberi ručno." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:253 msgid "Multiple POS Opening Entry" @@ -31626,7 +31626,7 @@ msgstr "Višestruki Unos Otvaranja Blagajne" #: erpnext/accounts/doctype/pricing_rule/utils.py:345 msgid "Multiple Price Rules exist with same criteria, please resolve conflict by assigning priority. Price Rules: {0}" -msgstr "" +msgstr "Postoji više pravila o cijenama s istim kriterijima, molimo riješite sukob dodjeljivanjem prioriteta. Pravila o cijenama: {0}" #. Option for the 'Loyalty Program Type' (Select) field in DocType 'Loyalty #. Program' @@ -32289,7 +32289,7 @@ msgstr "Novi Radni Prostor" #: erpnext/selling/doctype/customer/customer.py:408 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be at least {0}" -msgstr "" +msgstr "Novo kreditno ograničenje je manje od trenutnog nepodmirenog iznosa klijenta. Kreditno ograničenje mora biti najmanje {0}" #. Description of the 'Bill Even If Previous Invoice Unpaid' (Check) field in #. DocType 'Subscription' @@ -32299,7 +32299,7 @@ msgstr "Nove fakture će se generirati prema rasporedu čak i ako su trenutne fa #: erpnext/support/doctype/issue/issue.js:126 msgid "New issue created: {0}" -msgstr "" +msgstr "Novi zahtjev stvoren: {0}" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 msgid "New release date should be in the future" @@ -32383,7 +32383,7 @@ msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:146 msgid "No Delivery Note selected for Customer {0}" -msgstr "" +msgstr "Nije odabrana Dostavnica za Klijenta {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:767 msgid "No DocTypes in To Delete list. Please generate or import the list before submitting." @@ -32548,7 +32548,7 @@ msgstr "Nisu pronađeni kontakti s e-poštom." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:164 msgid "No customers found with selected options." -msgstr "" +msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/selling/page/sales_funnel/sales_funnel.js:137 msgid "No data for this period" @@ -32769,7 +32769,7 @@ msgstr "Nije pronađen nijedan zapis" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:22 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:39 msgid "No records for these settings." -msgstr "" +msgstr "Nema zapisa za ove postavke." #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:773 msgid "No records found in Allocation table" @@ -33268,7 +33268,7 @@ msgstr "Numeričke Vrijednosti" #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py:88 msgid "Numero has not been set in the XML file" -msgstr "" +msgstr "Broj nije postavljen u XML datoteci" #. Option for the 'Blood Group' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -33444,11 +33444,11 @@ msgstr "Nakon postavljanja, ova faktura će biti na čekanju do postavljenog dat #: erpnext/manufacturing/doctype/work_order/work_order.js:763 msgid "Once the Work Order is Closed, it cannot be resumed." -msgstr "" +msgstr "Nakon što je Radni Nalog Zatvoren, ne može se ponovo otvoriti." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:39 msgid "One customer can be part of only a single Loyalty Program." -msgstr "" +msgstr "Jedan Klijent može biti dio samo jednog Programa Lojalnosti." #. Option for the 'Status' (Select) field in DocType 'Subcontracting Inward #. Order' @@ -34045,7 +34045,7 @@ msgstr "Operacija {0} ne pripada radnom nalogu {1}" #: erpnext/manufacturing/doctype/workstation/workstation.py:453 msgid "Operation {0} is longer than any available working hours in workstation {1}, break down the operation into multiple operations" -msgstr "" +msgstr "Operacija {0} traje duže od bilo kojeg raspoloživog radnog vremena na radnoj stanici {1}, podijelite operaciju na više operacija" #. Label of the operations (Table) field in DocType 'BOM' #. Label of the operations_section_section (Section Break) field in DocType @@ -34232,7 +34232,7 @@ msgstr "Optimiziraj Rutu" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:128 msgid "Optimizing route" -msgstr "" +msgstr "Optimizacija rute" #: erpnext/manufacturing/doctype/work_order/work_order.js:1029 msgid "Optional. Select a specific manufacture entry to reverse." @@ -34693,7 +34693,7 @@ msgstr "Preko Odbitka" #: erpnext/accounts/services/billing_validation.py:56 msgid "Overbilling of {0} ignored because you have {1} role." -msgstr "" +msgstr "Prekomjerno Fakturiranje {0} zanemareno jer imate {1} ulogu." #: erpnext/controllers/status_updater.py:519 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." @@ -34815,7 +34815,7 @@ msgstr "Verifikat Zatvaranje Perioda" #. Label of the pcv_job_timeout (Int) field in DocType 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "PCV Job Timeout (seconds)" -msgstr "" +msgstr "Vremensko Ograničenje Zadatka Završnog Verifikata Razdoblja (sekunde)" #: erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js:35 msgid "PCV Paused" @@ -34963,7 +34963,7 @@ msgstr "Faktura Blagajne nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:130 msgid "POS Invoice isn't created by user {0}" -msgstr "" +msgstr "Fakturu Blagajne nije kreirao korisnik {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:208 msgid "POS Invoice should have the field {0} checked." @@ -35087,7 +35087,7 @@ msgstr "Korisnik Profila Blagajne" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:124 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:189 msgid "POS Profile doesn't match {0}" -msgstr "" +msgstr "Profil Blagajne ne poklapa se s {0}" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:210 msgid "POS Profile is mandatory to mark this invoice as POS Transaction." @@ -35099,19 +35099,19 @@ msgstr "Kasa Profil {0} ne može se onemogućiti jer su u tijeku Kasa sesije." #: erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py:62 msgid "POS Profile {0} contains Mode of Payment {1}. Please remove them to disable this mode." -msgstr "" +msgstr "Profil Blagajne {0} sadrži ovaj način plaćanja {1}. Uklonite ga da onemogućite ovaj način." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:58 msgid "POS Profile {0} does not belong to company {1}" -msgstr "" +msgstr "Profil Blagajne {0} ne pripada tvrtki {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:47 msgid "POS Profile {0} does not exist." -msgstr "" +msgstr "Profil Blagajne {0} ne postoji." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:54 msgid "POS Profile {0} is disabled." -msgstr "" +msgstr "Profil Blagajne {0} je onemogućen." #. Name of a report #: erpnext/accounts/report/pos_register/pos_register.json @@ -36059,7 +36059,7 @@ msgstr "Stranka je Obavezna" #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:69 msgid "Party is required to create a payment entry." -msgstr "" +msgstr "Stranka je obavezna za izradu unosa plaćanja." #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:66 msgid "Party type is required to create a payment entry." @@ -36766,7 +36766,7 @@ msgstr "Tip Plaćanja" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:624 msgid "Payment Type must be one of Receive, Pay, or Internal Transfer" -msgstr "" +msgstr "Tip Plaćanja mora biti Uplata, Isplata i Interni Prijenos" #. Label of the payment_url (Data) field in DocType 'Payment Request' #: erpnext/accounts/doctype/payment_request/payment_request.json @@ -37723,7 +37723,7 @@ msgstr "Dodaj račun za pravilo bankovnog unosa." #: erpnext/public/js/utils/serial_no_batch_selector.js:663 msgid "Please add at least one Serial No / Batch No" -msgstr "" +msgstr "Dodaj barem jedan Serijski Broj / Broj Šarže" #: erpnext/stock/doctype/item/item.js:914 msgid "Please add at least one row in Item Defaults with a Company before setting opening stock." @@ -37731,7 +37731,7 @@ msgstr "Dodaj barem jedan red u Postavke Artikala sa tvrtkom prije postavljanja #: erpnext/crm/doctype/crm_settings/crm_settings.py:51 msgid "Please add at least one user on Allowed Users to allow Data Synchronization from Frappe CRM site." -msgstr "" +msgstr "Dodaj barem jednog korisnika na popis Dopušteni Porisnici kako biste omogućili Sinkronizaciju Podataka s Prodajnom Podrškom." #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py:84 msgid "Please add the Bank Account column" @@ -37823,7 +37823,7 @@ msgstr "Konfiguriraj račune za pravilo bankovnog unosa." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:345 msgid "Please contact any of the following users for this transaction." -msgstr "" +msgstr "Za ovu transakciju obratite se bilo kojem od sljedećih korisnika." #: erpnext/selling/doctype/customer/customer.py:534 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" @@ -37895,7 +37895,7 @@ msgstr "Omogući {0} u {1}." #: erpnext/controllers/selling_controller.py:872 msgid "Please enable {0} in {1} to allow same item in multiple rows" -msgstr "" +msgstr "Omogući {0} u {1} kako biste dopustili isti artikal u više redova" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:378 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." @@ -37907,11 +37907,11 @@ msgstr "Potvrdi da je {0} račun {1} Troškovni račun. Možete promijeniti vrst #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:758 msgid "Please ensure {0} account is a Balance Sheet account." -msgstr "" +msgstr "Provjeri da li je račun {0} račun Bilance Stanja." #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:768 msgid "Please ensure {0} account {1} is a Receivable account." -msgstr "" +msgstr "Provjeri da li je {0} račun {1} račun Potraživanja." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:140 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" @@ -38118,7 +38118,7 @@ msgstr "Molimo vas da generirate popis za brisanje prije podnošenja" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:67 msgid "Please import accounts against parent company or enable {0} in company master." -msgstr "" +msgstr "Uvezi račune naspram matične tvrtkea ili omogući {0} u Postavkama Tvrtke." #: erpnext/setup/doctype/employee/employee.py:291 msgid "Please make sure the employees above report to another Active employee." @@ -38216,7 +38216,7 @@ msgstr "Odaberi Tvrtku" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:139 #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:76 msgid "Please select Company and Posting Date to get entries" -msgstr "" +msgstr "Odaberi Tvrtku i Datum Knjiženja da biste preuzeli unose" #: erpnext/accounts/doctype/journal_entry/journal_entry.js:435 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:28 @@ -38249,7 +38249,7 @@ msgstr "Odaberi Kod Artikla" #: erpnext/selling/doctype/sales_order/sales_order.js:1756 msgid "Please select Items from the Table" -msgstr "" +msgstr "Odaberi artikle iz Tablice" #: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py:55 msgid "Please select Maintenance Status as Completed or remove Completion Date" @@ -38398,7 +38398,7 @@ msgstr "Odaberi red za kreiranje Unosa Ponovnog Knjiženje" #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:146 msgid "Please select a supplier" -msgstr "" +msgstr "Odaberi Dobavljača" #: erpnext/accounts/report/purchase_register/purchase_register.py:37 msgid "Please select a supplier for fetching payments." @@ -38410,7 +38410,7 @@ msgstr "Odaberi važeći Nalog Nabave koji je konfigurisan za Podugovor." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:200 msgid "Please select a valid document type." -msgstr "" +msgstr "Odaberi valjani tip dokumenta." #: erpnext/selling/doctype/quotation/quotation.js:245 msgid "Please select a value for {0} quotation_to {1}" @@ -38430,7 +38430,7 @@ msgstr "Molimo odaberite barem jedan filter: Šifra Artikla, Šarža ili Serijsk #: erpnext/selling/doctype/sales_order/sales_order.js:1368 msgid "Please select at least one item to continue" -msgstr "" +msgstr "Odaberi jedan artikal za nastavak" #: erpnext/buying/doctype/purchase_order/services/drop_ship.py:17 msgid "Please select at least one item to update delivered quantity." @@ -38438,7 +38438,7 @@ msgstr "Molimo odaberite barem jedan artikal za ažuriranje dostavljene količin #: erpnext/manufacturing/doctype/work_order/work_order.js:392 msgid "Please select at least one operation to create Job Card" -msgstr "" +msgstr "Odaberi barem jednu operaciju za stvaranje Radne Kartice" #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js:33 msgid "Please select at least one row to fix" @@ -38504,7 +38504,7 @@ msgstr "Odaberi Tvrtku" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:65 msgid "Please select the Multiple Tier Program type for more than one collection rule." -msgstr "" +msgstr "Odaberi Tip Višeslojnog Programa za više od jednog pravila prikupljanja." #: erpnext/stock/doctype/item/item.js:433 msgid "Please select the Warehouse first" @@ -38566,7 +38566,7 @@ msgstr "Postavi Račun u Skladištu {0} ili Standard Račun Zaliha u Kompaniji { #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:333 msgid "Please set Accounting Dimension {0} in {1}" -msgstr "" +msgstr "Postavi Knjigovodstvenu Dimenziju {0} u {1}" #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:23 #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:34 @@ -38608,7 +38608,7 @@ msgstr "Postavi Račun Osnovne Imovine u Kategoriju Imovine {0}" #: erpnext/accounts/doctype/purchase_invoice/services/expense_account.py:151 msgid "Please set Fixed Asset Account in {0} against {1}." -msgstr "" +msgstr "Postavi Račun Osnovnih Sredstava u {0} na {1}." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:296 msgid "Please set Parent Row No for item {0}" @@ -38645,7 +38645,7 @@ msgstr "Postavi Tvrtku" #: erpnext/assets/doctype/asset/asset.py:374 msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {0}" -msgstr "" +msgstr "Postavi Centar Troškova za Imovinu ili postavite Centar Troškova Amortizacije za {0}" #: erpnext/stock/doctype/item/item.py:339 #: erpnext/stock/doctype/item/item.py:1623 @@ -38699,11 +38699,11 @@ msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Način Plaćanja {0}" #: erpnext/accounts/doctype/pos_profile/pos_profile.py:207 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:367 msgid "Please set default Cash or Bank account in Mode of Payments {0}" -msgstr "" +msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Načine Plaćanja {0}" #: erpnext/accounts/utils.py:2568 msgid "Please set default Exchange Gain/Loss Account in Company {0}" -msgstr "" +msgstr "Postavi Standard Račun Rezultata od Kursnih Razlika u {0}" #: erpnext/assets/doctype/asset_repair/services/gl_composer.py:92 msgid "Please set default Expense Account in Company {0}" @@ -38842,11 +38842,11 @@ msgstr "Navedi od/Do Raspona" #: erpnext/public/js/controllers/transaction.js:2634 msgid "Please specify {0}. It is needed to fetch Item Details." -msgstr "" +msgstr "Navedi {0}. Potrebno je za preuzimanje Detalja Artikla." #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:142 msgid "Please submit Purchase Order {0} before proceeding." -msgstr "" +msgstr "Podnesite Nalog Nabave {0} prije nego što nastavite." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:276 msgid "Please try again in an hour." @@ -39080,7 +39080,7 @@ msgstr "Datuma Knjiženja" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:263 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:146 msgid "Posting Date cannot be a future date" -msgstr "" +msgstr "Datum Knjiženja ne može biti budući datum" #. Label of the exchange_gain_loss_posting_date (Select) field in DocType #. 'Accounts Settings' @@ -39283,7 +39283,7 @@ msgstr "Uplaćeni Troškovi" #: erpnext/accounts/report/general_ledger/general_ledger.py:682 msgid "Presentation Currency cannot be {0}, when {1} is enabled." -msgstr "" +msgstr "Valuta prikaza ne može biti {0}, kada je omogućen {1}." #: erpnext/setup/setup_wizard/data/designation.txt:24 msgid "President" @@ -39957,7 +39957,7 @@ msgstr "Prioriteti" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:61 msgid "Priority cannot be less than 1." -msgstr "" +msgstr "Prioritet ne može biti manji od 1." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:764 msgid "Priority has been changed to {0}." @@ -40523,7 +40523,7 @@ msgstr "Bilans Uspjeha" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:215 msgid "Profit and Loss Statement requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilanca Uspjeha zahtijeva da se {0} sinkronizuje s DuckDB-om" #. Label of the heading_cppb (Heading) field in DocType 'Bisect Accounting #. Statements' @@ -41282,7 +41282,7 @@ msgstr "Nalog Nabave Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:466 msgid "Purchase Order Required for item {0}" -msgstr "" +msgstr "Nalog Nabave je obavezan za artikal {0}" #. Name of a report #. Label of a chart in the Buying Workspace @@ -41342,7 +41342,7 @@ msgstr "Nalozi Nabave za Primitak" #: erpnext/controllers/accounts_controller.py:1236 msgid "Purchase Orders {0} are unlinked" -msgstr "" +msgstr "Nabavni Nalozi {0} nisu povezani" #: erpnext/stock/report/item_prices/item_prices.py:59 msgid "Purchase Price List" @@ -41432,7 +41432,7 @@ msgstr "Nabavni Račun je Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:488 msgid "Purchase Receipt Required for item {0}" -msgstr "" +msgstr "Račun Nabave je obavezan za artikal {0}" #. Label of a Link in the Buying Workspace #. Name of a report @@ -41452,7 +41452,7 @@ msgstr "Statistika Nabavnog Računa " #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:356 msgid "Purchase Receipt does not have any Item for which Retain Sample is enabled." -msgstr "" +msgstr "Račun Nabave nema nijedan artikal za koju je omogućeno Zadržavanje Uzorka." #: erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py:135 msgid "Purchase Receipt {0} created." @@ -42483,7 +42483,7 @@ msgstr "Količina za Skeniranje" #: erpnext/stock/doctype/stock_entry/stock_entry.py:910 msgid "Quantity {0} should not be greater than allowed quantity {1}" -msgstr "" +msgstr "Količina {0} ne smije biti veća od dopuštene količine {1}" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -42935,7 +42935,7 @@ msgstr "PDV Stopa" #: erpnext/accounts/services/child_item_update.py:515 msgid "Rate of '{0}' items cannot be changed" -msgstr "" +msgstr "Cijena '{0}' artikala ne može se mijenjati" #. Label of the rate_of_depreciation (Percent) field in DocType 'Asset #. Depreciation Schedule' @@ -43674,7 +43674,7 @@ msgstr "Zabilježite prijenos između dva bankovna računa" #: erpnext/stock/doctype/item_alternative/item_alternative.py:84 msgid "Record already exists for the item {0}" -msgstr "" +msgstr "Zapis za artikal {0} već postoji" #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:513 #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:519 @@ -44099,7 +44099,7 @@ msgstr "Odbijeno Skladište" #: erpnext/public/js/utils/serial_no_batch_selector.js:671 msgid "Rejected Warehouse and Accepted Warehouse cannot be the same." -msgstr "" +msgstr "Odbijeno i Prihvaćeno Skladište ne mogu biti ista." #: erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py:23 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py:14 @@ -44528,11 +44528,11 @@ msgstr "Datoteke Podataka Ponovnog Knjiženja" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:47 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:96 msgid "Reposting Entries will change the value of accounts Stock In Hand, and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje unosa promijenit će vrijednost računa Zalihe na Raspolaganju i Troškovi zaliha u izvješću Probna Bilanca, a također će promijeniti i vrijednost stanja u izvješću Stanja Zaliha." #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:62 msgid "Reposting Entry will change the value of accounts Stock In Hand and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje će promijeniti vrijednost računa Zalihe na Raspolaganju i Troškovi Zaliha u izvješću Probna Bilanca, a također će promijeniti i vrijednost stanja u izvješću Stanju Zaliha." #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' @@ -44919,7 +44919,7 @@ msgstr "Rezervno Skladište" #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:178 msgid "Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {0}." -msgstr "" +msgstr "Rezervno Skladište mora biti različito od Dobavljačevog Skladišta za Isporučeni Artikal {0}." #: erpnext/manufacturing/doctype/production_plan/production_plan.js:287 msgid "Reserve for Raw Materials" @@ -45522,7 +45522,7 @@ msgstr "Povrati" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:359 msgid "Revaluation Journal: {0}" -msgstr "" +msgstr "Žurnal Revalorizacije: {0}" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:151 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:113 @@ -46038,7 +46038,7 @@ msgstr "Red #{0}: Broj Šarže {1} je već odabran." #: erpnext/controllers/subcontracting_inward_controller.py:443 msgid "Row #{0}: Batch No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)." -msgstr "" +msgstr "Red #{0}: Šaržni Broj(evi) {1} nije u povezanom Podugovaračkom Nalogu. Odaberi važeće Šaržne broj(eve)." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:880 msgid "Row #{0}: Cannot allocate more than {1} against payment term {2}" @@ -46130,7 +46130,7 @@ msgstr "Red #{0}: Kumulativni prag ne može biti manji od praga pojedinačne tra #: erpnext/assets/doctype/asset_category/asset_category.py:66 msgid "Row #{0}: Currency of {1} - {2} does not match company currency." -msgstr "" +msgstr "Red #{0}: Valuta od {1} do {2} ne odgovara valuti tvrtke." #: erpnext/controllers/subcontracting_inward_controller.py:91 msgid "Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times." @@ -46184,7 +46184,7 @@ msgstr "Red #{0}: Duplikat unosa u Referencama {1} {2}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:113 msgid "Row #{0}: Either Party ID or Party Name is required" -msgstr "" +msgstr "Red #{0}: Obavezan je ili ID Stranke ili Naziv Stranke" #: erpnext/selling/doctype/sales_order/sales_order.py:270 msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" @@ -46200,7 +46200,7 @@ msgstr "Red #{0}: Račun troškova {1} nije važeći za Fakturu Nabave {2}. Dopu #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{0}: Finance Book should not be empty since you're using multiple." -msgstr "" +msgstr "Red #{0}: Finansijski Registar ne smije biti prazan jer ih koristite više." #: erpnext/selling/doctype/sales_order/services/subcontracting.py:40 msgid "Row #{0}: Finished Good Item Qty can not be zero" @@ -46208,7 +46208,7 @@ msgstr "Red #{0}: Količina gotovog proizvoda artikla ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:39 msgid "Row #{0}: Finished Good Item Qty cannot be zero" -msgstr "" +msgstr "Red #{0}: Količina gotovog proizvoda artikla ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:21 #: erpnext/selling/doctype/sales_order/services/subcontracting.py:20 @@ -46259,7 +46259,7 @@ msgstr "Red #{0}: Polja Od i Do su obavezna" #: erpnext/stock/doctype/pick_list/pick_list.py:650 msgid "Row #{0}: Item Code is Mandatory" -msgstr "" +msgstr "Red #{0}: Šifra Artikla je obavezna" #: erpnext/public/js/utils/barcode_scanner.js:427 msgid "Row #{0}: Item added" @@ -46316,15 +46316,15 @@ msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može s #: erpnext/controllers/subcontracting_inward_controller.py:80 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted, add another row instead." -msgstr "" +msgstr "Red #{0}: Artikal {1} se ne slaže. Promjena koda artikla nije dopuštena, umjesto toga dodaj još jedan red." #: erpnext/controllers/subcontracting_inward_controller.py:129 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted." -msgstr "" +msgstr "Red #{0}: Artikal {1} se ne slaže. Promjena koda artikla nije dopuštena." #: erpnext/stock/doctype/stock_entry/services/subcontracting.py:94 msgid "Row #{0}: Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" -msgstr "" +msgstr "Red #{0}: Artikal {1} nije pronađen u tablici 'Isporučene Sirovine' u {2} {3}" #: erpnext/stock/doctype/stock_entry/services/disassemble.py:115 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." @@ -46365,19 +46365,19 @@ msgstr "Red #{0}: Prekomjerna potrošnja Klijent Dostavljenog Artikla {1} u odno #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:92 msgid "Row #{0}: POS Invoice {1} has been {2}" -msgstr "" +msgstr "Red #{0}: Faktura Blagajne {1} je {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:73 msgid "Row #{0}: POS Invoice {1} is not against customer {2}" -msgstr "" +msgstr "Red #{0}: Faktura Blagajne {1} nije naspram klijenta {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:88 msgid "Row #{0}: POS Invoice {1} is not submitted yet" -msgstr "" +msgstr "Red #{0}: Faktura Blagajne {1} još nije podnešena" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:123 msgid "Row #{0}: Party ID is required" -msgstr "" +msgstr "Red #{0}: ID Stranke je obavezan" #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:80 msgid "Row #{0}: Please select Item Code in Assembly Items" @@ -46385,11 +46385,11 @@ msgstr "Red #{0}: Odaberi Kod Artikla u Artiklima Montaže" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:340 msgid "Row #{0}: Please select a valid Quality Inspection with Item Code {1}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete sa Kodom Artikla {1}." #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:333 msgid "Row #{0}: Please select a valid Quality Inspection with Reference Type {1} and Reference Name {2}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete s Tipom Reference {1} i Nazivom Reference {2}." #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:82 msgid "Row #{0}: Please select the BOM No in Assembly Items" @@ -46413,7 +46413,7 @@ msgstr "Red #{0}: Ažuriraj račun odloženih prihoda/troškova u redu artikla i #: erpnext/assets/doctype/asset/asset.py:413 msgid "Row #{0}: Please use a different Finance Book." -msgstr "" +msgstr "Red #{0}: Koristi drugi Finansijski Registar." #: erpnext/manufacturing/doctype/bom/bom.py:378 #, python-format @@ -46435,7 +46435,7 @@ msgstr "Red #{0}: Količina mora biti pozitivan broj" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:429 msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Item {2} against Batch {3} in Warehouse {4}." -msgstr "" +msgstr "Red #{0}: Količina bi trebala biti manja ili jednaka Dostupnoj Količini za Rezervaciju (Stvarna količina - Rezervisana količina) {1} za artikal {2} naspram Šarže {3} u Skladištu {4}." #: erpnext/stock/services/quality_inspection_service.py:77 msgid "Row #{0}: Quality Inspection is required for Item {1}" @@ -46512,7 +46512,10 @@ msgid "Row #{0}: Selling rate for item {1} is lower than its {2}.\n" "\t\t\t\t\tSelling {3} should be at least {4}.

Alternatively,\n" "\t\t\t\t\tyou can disable '{5}' in {6} to bypass\n" "\t\t\t\t\tthis validation." -msgstr "" +msgstr "Red #{0}: Prodajna cijena za artikal {1} je niža od njegove {2}.\n" +"\t\t\t\t\tProdaja {3} treba biti barem {4}.

Alternativno,\n" +"\t\t\t\t\tmožete onemogućiti '{5}' u {6} kako biste zaobišli\n" +"\t\t\t\t\tovu validaciju." #: erpnext/manufacturing/doctype/work_order/work_order.py:348 msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." @@ -46520,7 +46523,7 @@ msgstr "Red #{0}: ID Sekvence mora biti {1} ili {2} za Operaciju {3}." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:528 msgid "Row #{0}: Serial No {1} cannot be returned since it was not transacted in original invoice {2}" -msgstr "" +msgstr "Red #{0}: Serijski Broj {1} ne može se vratiti jer nije naveden u originalnoj fakturi {2}" #: erpnext/stock/services/serial_batch_bundle_service.py:123 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" @@ -46637,11 +46640,11 @@ msgstr "Red #{0}: Šarža {1} je već istekla." #: erpnext/stock/doctype/stock_entry/stock_entry.py:408 msgid "Row #{0}: The job card item reference is missing. Kindly create the stock entry from the job card. If you have added the row manually then you won't be able to add job card item reference." -msgstr "" +msgstr "Red #{0}: Nedostaje referenca artikla na radnoj kartici. Stvori unos zaliha iz radne kartice. Ako ste red dodali ručno, nećete moći dodati referencu artikla na radnu karticu." #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:103 msgid "Row #{0}: The original Invoice {1} of return invoice {2} is not consolidated." -msgstr "" +msgstr "Red #{0}: Izvorna faktura {1} povratne fakture {2} nije konsolidirana." #: erpnext/stock/doctype/item/item.py:599 msgid "Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}" @@ -46649,7 +46652,7 @@ msgstr "Red #{0}: Skladište {1} nije podređeno skladište grupnog skladišta { #: erpnext/manufacturing/doctype/workstation/workstation.py:190 msgid "Row #{0}: Timings conflict with row {1}" -msgstr "" +msgstr "Red #{0}: Vremenski sukob s redom {1}" #: erpnext/assets/doctype/asset/asset.py:656 msgid "Row #{0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" @@ -46673,7 +46676,7 @@ msgstr "Red #{0}: Radni Nalog postoji za punu ili djelomičnu količinu artiikla #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:500 msgid "Row #{0}: You cannot add positive quantities in a return invoice. Please remove item {1} to complete the return." -msgstr "" +msgstr "Redak #{0}: Ne možete dodati pozitivne količine u povratnu fakturu. Ukloni artikal {1} kako biste dovršili povrat." #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:110 msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." @@ -46685,7 +46688,7 @@ msgstr "Red #{0}: Odaberi Imovinu za Artikal {1}." #: erpnext/stock/doctype/pick_list/pick_list.py:235 msgid "Row #{0}: item {1} has been picked already." -msgstr "" +msgstr "Red #{0}: artikal {1} je već odabran." #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:142 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:207 @@ -46694,7 +46697,7 @@ msgstr "Red #{0}: {1}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:142 msgid "Row #{0}: {1} account is not of type {2}" -msgstr "" +msgstr "Red #{0}: {1} račun nije tipa {2}" #: erpnext/public/js/controllers/buying.js:261 msgid "Row #{0}: {1} can not be negative for item {2}" @@ -46714,11 +46717,11 @@ msgstr "Red #{0}: {1} od {2} bi trebao biti {3}. Ažuriraj {1} ili odaberi drugi #: erpnext/stock/doctype/item/item.py:1511 msgid "Row #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}." -msgstr "" +msgstr "Red #{0}: {1} {2} ne pripada tvrtki {3}. Odaberi valjani {4}." #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{0}: {1} {2} does not exist." -msgstr "" +msgstr "Red #{0}: {1} {2} ne postoji." #: erpnext/accounts/services/child_item_update.py:251 msgid "Row #{0}:Quantity for Item {1} cannot be zero." @@ -46895,7 +46898,7 @@ msgstr "Red {0}: Od vremena i do vremena je obavezano." #: erpnext/manufacturing/doctype/job_card/job_card.py:355 msgid "Row {0}: From Time and To Time of {1} are overlapping with {2}" -msgstr "" +msgstr "Red {0}: Vrijeme od i Vrijeme do {1} preklapaju se s {2}" #: erpnext/projects/doctype/timesheet/timesheet.py:225 msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" @@ -46919,7 +46922,7 @@ msgstr "Red {0}: Nevažeća referenca {1}" #: erpnext/controllers/taxes_and_totals.py:134 msgid "Row {0}: Item Tax template for {1} updated as per validity and rate applied" -msgstr "" +msgstr "Red {0}: Predložak PDV-a na Artikal za {1} ažuriran je prema valjanosti i primijenjenoj stopi" #: erpnext/controllers/selling_controller.py:659 msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" @@ -46983,7 +46986,7 @@ msgstr "Red {0}: Odaberi Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:214 msgid "Row {0}: Please select a valid BOM for Item {1}." -msgstr "" +msgstr "Red {0}: Odaberi valjanu Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:208 msgid "Row {0}: Please select an active BOM for Item {1}." @@ -47055,7 +47058,7 @@ msgstr "Red {0}: Cijeli iznos troška za račun {1} u {2} je već dodijeljen." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:103 msgid "Row {0}: The item {1}, quantity must be a positive number" -msgstr "" +msgstr "Red {0}: Artikal {1}, količina mora biti pozitivan broj" #: erpnext/accounts/services/taxes.py:269 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" @@ -47116,7 +47119,7 @@ msgstr "Red {0}: {1} {2} je povezan sa {3}. Odaberi dokument koji pripada {4}." #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:132 msgid "Row {0}: {1} {2} must be submitted" -msgstr "" +msgstr "Red {0}: {1} {2} mora biti podnešen" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:111 msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" @@ -47162,7 +47165,7 @@ msgstr "Redovi: {0} imaju 'Unos Plaćanja' kao Tip Reference. Ovo ne treba posta #: erpnext/controllers/accounts_controller.py:276 msgid "Rows: {0} in {1} section are invalid. Reference Name should point to a valid Payment Entry or Journal Entry." -msgstr "" +msgstr "Redovi: {0} u {1} sekciji su nevažeći. Naziv reference treba da ukazuje na važeći Unos Plaćanja ili Nalog Knjiženja." #. Label of the rule_applied (Check) field in DocType 'Pricing Rule Detail' #: erpnext/accounts/doctype/pricing_rule_detail/pricing_rule_detail.json @@ -47589,7 +47592,7 @@ msgstr "Prodajna Faktura nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:195 msgid "Sales Invoice isn't created by user {0}" -msgstr "" +msgstr "Prodajna Faktura nije izrađena od korisnika {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:472 msgid "Sales Invoice mode is activated in POS. Please create Sales Invoice instead." @@ -49055,7 +49058,7 @@ msgstr "Odabrani dokument mora biti u podnešenom stanju" #: erpnext/assets/doctype/asset/asset.py:1195 msgid "Selected {0} does not contain the Item Code {1}" -msgstr "" +msgstr "Odabrani {0} ne sadrži Kod Artikla {1}" #. Option for the 'Pickup Type' (Select) field in DocType 'Shipment' #: erpnext/stock/doctype/shipment/shipment.json @@ -49393,7 +49396,7 @@ msgstr "Serijski broj je već dodijeljen" #: erpnext/assets/doctype/asset_repair/asset_repair.py:296 msgid "Serial No Bundle is mandatory for Item {0}" -msgstr "" +msgstr "Paket Serijskih Brojeva je obavezan za artikal {0}" #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 msgid "Serial No Count" @@ -49458,7 +49461,7 @@ msgstr "Serijski Broj i Šarža" #: erpnext/stock/doctype/stock_settings/stock_settings.js:93 msgid "Serial No and Batch Selector cannot be used when Use Serial / Batch Fields is enabled." -msgstr "" +msgstr "Serijski Broj i birač Šarže ne mogu se koristiti kada je omogućeno Koristi Serijski Broj / Šaržu." #. Name of a report #. Label of a Link in the Stock Workspace @@ -49501,7 +49504,7 @@ msgstr "Serijski Broj {0} ne postoji" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:378 msgid "Serial No {0} is already Delivered. You cannot use it again in Manufacture / Repack entry." -msgstr "" +msgstr "Serijski broj {0} je već dostavljen. Ne možete ga ponovno koristiti u unosu Proizvodnje / Ponovnog pakiranja." #: erpnext/public/js/utils/barcode_scanner.js:435 msgid "Serial No {0} is already added" @@ -49517,11 +49520,11 @@ msgstr "Serijski broj {0} nije u {1} {2}, i ne može se vratiti naspram {1} {2}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:343 msgid "Serial No {0} is under maintenance contract until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod ugovorom o održavanju do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:336 msgid "Serial No {0} is under warranty until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod jamstvom do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:322 msgid "Serial No {0} not found" @@ -49657,7 +49660,7 @@ msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mi #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:298 msgid "Serial and Batch Bundle {0} should have voucher type as 'Maintenance Schedule'" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} treba imati tip verifikata kao 'Raspored Održavanja'" #. Label of the section_break_45 (Section Break) field in DocType #. 'Subcontracting Receipt Item' @@ -51136,7 +51139,7 @@ msgstr "Nedostaju neki obavezni podaci o tvrtki. Nemate dopuštenje za njihovo a #: erpnext/www/book_appointment/index.js:248 msgid "Something went wrong, please try again" -msgstr "" +msgstr "Nešto nije u redu, pokušajte ponovo" #: erpnext/accounts/doctype/pricing_rule/utils.py:754 msgid "Sorry, this coupon code is no longer valid" @@ -51387,7 +51390,7 @@ msgstr "Raspodijeli proviziju među više prodavača." #: erpnext/buying/doctype/purchase_order/purchase_order.js:600 #: erpnext/public/js/controllers/buying.js:558 msgid "Splitting {0} units of {1}" -msgstr "" +msgstr "Dijeljenje {0} jedinica od {1}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2195 msgid "Splitting {0} {1} into {2} rows as per Payment Terms" @@ -51509,15 +51512,15 @@ msgstr "Poredak" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:73 msgid "Standing scores must be continuous and cover 0 to 100 without gaps or overlaps" -msgstr "" +msgstr "Trenutni rezultati moraju biti kontinuirani i pokrivati od 0 do 100 bez praznina ili preklapanja" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:76 msgid "Standing scores must cover the full range from 0 to 100" -msgstr "" +msgstr "Trenutni rezultati moraju pokrivati cijeli raspon od 0 do 100" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:68 msgid "Standing {0} must have a minimum grade lower than its maximum grade" -msgstr "" +msgstr "{0} mora imati minimalnu ocjenu nižu od maksimalne ocjene" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:54 msgid "Start / Resume" @@ -51525,7 +51528,7 @@ msgstr "Pokreni / Nastavi" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:45 msgid "Start Date cannot be after End Date" -msgstr "" +msgstr "Datum početka ne može biti nakon datuma završetka" #: erpnext/crm/doctype/email_campaign/email_campaign.py:40 msgid "Start Date cannot be before the current date" @@ -51591,7 +51594,7 @@ msgstr "Pokrenut je pozadinski zadatak za stvaranje {1} {0}. {2}" #: erpnext/public/js/bulk_transaction_processing.js:29 msgid "Starting a background job to create {0} {1}" -msgstr "" +msgstr "Pokretanje pozadinskog zadatka za stvaranje {0} {1}" #. Label of the date_dist_from_left_edge (Float) field in DocType 'Cheque Print #. Template' @@ -51802,7 +51805,7 @@ msgstr "Unos Zaključanih Zaliha {0} već postoji za odabrani vremenski raspon" #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:100 msgid "Stock Closing Entry {0} has been queued for processing, the system will take some time to complete it." -msgstr "" +msgstr "Završni Unos Zaliha {0} je stavljen u red za obradu, sustavu će trebati neko vrijeme da ga dovrši." #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9 msgid "Stock Closing Log" @@ -51885,11 +51888,11 @@ msgstr "Tip Unosa Zaliha" #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.py:65 msgid "Stock Entry Type {0} cannot be set as standard" -msgstr "" +msgstr "Tip Unosa Zaliha {0} ne može se postaviti kao standard" #: erpnext/stock/doctype/pick_list/mapper.py:289 msgid "Stock Entry has already been created against this Pick List" -msgstr "" +msgstr "Unos Zaliha je već izrađen naspram ove Liste Odabira" #: erpnext/stock/doctype/batch/batch.js:138 msgid "Stock Entry {0} created" @@ -51897,7 +51900,7 @@ msgstr "Unos Zaliha {0} je kreiran" #: erpnext/manufacturing/doctype/job_card/job_card.py:1639 msgid "Stock Entry {0} has been created" -msgstr "" +msgstr "Unos Zaliha {0} je izrađen" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:997 msgid "Stock Entry {0} is not submitted" @@ -52510,7 +52513,7 @@ msgstr "Zaliha nije dostupna za Artikal {0} u Skladištu {1}." #: erpnext/selling/page/point_of_sale/pos_controller.js:826 msgid "Stock quantity is not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}." -msgstr "" +msgstr "Količina na zalihi nije dovoljna za Artikal Kod: {0} u skladištu {1}. Dostupna količina {2} {3}." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:255 msgid "Stock transactions before {0} are frozen" @@ -53980,7 +53983,7 @@ msgstr "Ciljna Imovina {0} ne pripada tvrtki {1}" #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.py:189 msgid "Target Asset {0} needs to be a composite asset" -msgstr "" +msgstr "Ciljana Imovina {0} mora biti složena imovina" #. Name of a DocType #: erpnext/setup/doctype/target_detail/target_detail.json @@ -55090,7 +55093,7 @@ msgstr "Tekst prikazan u financijskom izvješću (npr. 'Ukupni Prihod', 'Gotovin #: erpnext/stock/doctype/packing_slip/packing_slip.py:89 msgid "The 'From Package No.' field must not be empty or have a value less than 1." -msgstr "" +msgstr "\"Od Paketa Broj.\" polje ne smije biti prazno niti njegova vrijednost manja od 1." #. Description of the 'Current BOM' (Link) field in DocType 'BOM Update Tool' #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.json @@ -55099,7 +55102,7 @@ msgstr "Sastavnica koja će biti zamijenjena" #: erpnext/controllers/subcontracting_controller.py:1056 msgid "The Batch No {0} has not been supplied against the {1} {2}" -msgstr "" +msgstr "Broj Šarže {0} nije dostavljen naspram {1} {2}" #: erpnext/stock/serial_batch_bundle.py:1557 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." @@ -55107,7 +55110,7 @@ msgstr "Šarža {0} ima negativnu količinu {1}. Da biste to riješili, idite na #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1590 msgid "The Batch {0} of item {1} has negative stock in the warehouse {2}{3}. Please add a stock quantity of {4} to proceed with this entry. If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or in the Stock Settings to proceed. However, enabling this setting may lead to negative stock in the system. So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate." -msgstr "" +msgstr "Šarža {0} artikla {1} ima negativne zalihe u skladištu {2}{3}. Dodaj količinu zaliha od {4} da biste nastavili s ovim unosom. Ako nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' za Šaržu {0} ili u Postavkama Zaliha da biste nastavili. Međutim, omogućavanje ove postavke može dovesti do negativnih zaliha u sustavu. Stoga, molimo vas da osigurate da se razina zaliha što prije prilagode kako bi se održala ispravna stopa vrednovanja." #: erpnext/crm/doctype/email_campaign/email_campaign.py:71 msgid "The Campaign '{0}' already exists for the {1} '{2}'" @@ -55135,7 +55138,7 @@ msgstr "Knjigovodstveni Unosi će biti otkazani u pozadini, može potrajati neko #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1156 msgid "The Item {0} does not have Serial No or Batch No" -msgstr "" +msgstr "Artikal {0} nema Serijski niti Šaržni Broj" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:179 msgid "The Loyalty Program isn't valid for the selected company" @@ -55155,11 +55158,11 @@ msgstr "Lista Odabira koja ima Unose Rezervacije Zaliha ne može se ažurirati. #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:127 msgid "The Process Loss Qty has been reset as per the Job Card's Process Loss Qty" -msgstr "" +msgstr "Količina gubitaka procesa poništena je prema količini gubitaka procesa na radnoj kartici" #: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 msgid "The Process Loss Qty has been reset as per the job card's Process Loss Qty" -msgstr "" +msgstr "Količina gubitaka procesa poništena je prema količini gubitaka procesa na radnoj kartici" #: erpnext/setup/doctype/sales_person/sales_person.py:102 msgid "The Sales Person is linked with {0}" @@ -55175,7 +55178,7 @@ msgstr "Serijski Broj {0} je rezervisan naspram {1} {2} i ne može se koristiti #: erpnext/controllers/subcontracting_controller.py:1071 msgid "The Serial Nos {0} have not been supplied against the {1} {2}" -msgstr "" +msgstr "Serijski Brojevi {0} nisu dostavljeni naspram {1} {2}" #: erpnext/stock/doctype/stock_entry/stock_entry.py:950 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" @@ -55231,7 +55234,7 @@ msgstr "Završena količina {0} operacije {1} ne može biti veća od završene k #: erpnext/accounts/doctype/dunning/dunning.py:87 msgid "The currency of invoice {0} ({1}) is different from the currency of this dunning ({2})." -msgstr "" +msgstr "Valuta Fakture {0} ({1}) razlikuje se od valute ove opomene ({2})." #: erpnext/selling/page/point_of_sale/pos_controller.js:200 msgid "The current POS opening entry is outdated. Please close it and create a new one." @@ -55284,7 +55287,7 @@ msgstr "Polje {0} u redu {1} nije postavljeno" #: erpnext/stock/stock_ledger.py:369 msgid "The field {0} is required for reposting" -msgstr "" +msgstr "Polje {0} je obavezno za ponovno knjiženje" #: erpnext/accounts/doctype/share_transfer/share_transfer.py:188 msgid "The fields From Shareholder and To Shareholder cannot be blank" @@ -55309,7 +55312,7 @@ msgstr "Brojevi Folija nisu usklađeni" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:306 msgid "The following Items, having Putaway Rules, could not be accommodated:" -msgstr "" +msgstr "Sljedeći artikli, koji imaju Pravila Odlaganja na Stranu, nisu mogli biti primjenjene:" #: erpnext/assets/doctype/asset_repair/asset_repair.py:137 msgid "The following Purchase Invoices are not submitted:" @@ -55337,7 +55340,7 @@ msgstr "Sljedeće osoblje još uvijek podnosi izvješća {0}:" #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py:185 msgid "The following invalid Pricing Rules are deleted:{0}" -msgstr "" +msgstr "Sljedeća nevažeća pravila određivanja cijena se brišu:{0}" #: erpnext/accounts/doctype/payment_request/payment_request.py:782 msgid "The following payment schedule(s) already exist:\n" @@ -55386,7 +55389,7 @@ msgstr "Artikli {items} nisu označeni kao {type_of} artikli. Možete ih omoguć #: erpnext/manufacturing/doctype/workstation/workstation.py:595 msgid "The job card {0} is in {1} state and you cannot complete it." -msgstr "" +msgstr "Radna Kartica {0} je u {1} stanju i ne možete je dovršiti." #: erpnext/manufacturing/doctype/workstation/workstation.py:589 msgid "The job card {0} is in {1} state and you cannot start it again." @@ -55424,11 +55427,11 @@ msgstr "Početno stanje možda ne odgovara vašem bankovnom izvodu. Želite li i #: erpnext/manufacturing/doctype/operation/operation.py:43 msgid "The operation {0} cannot be added multiple times" -msgstr "" +msgstr "Operacija {0} ne može se dodati više puta" #: erpnext/manufacturing/doctype/operation/operation.py:48 msgid "The operation {0} cannot be its own sub-operation" -msgstr "" +msgstr "Operacija {0} ne može biti vlastita podoperacija" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:107 msgid "The original invoice should be consolidated before or along with the return invoice." @@ -55478,7 +55481,7 @@ msgstr "Procenat kojim vam je dozvoljeno prenijeti više naspram naručene koli #: erpnext/stock/doctype/item_price/item_price.py:71 msgid "The price list {0} does not exist or is disabled" -msgstr "" +msgstr "Cjenik {0} ne postoji ili je onemogućen" #. Description of the 'Last Purchase Rate' (Float) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -55507,7 +55510,7 @@ msgstr "Odabrane Sastavnice nisu za istu artikal" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:544 msgid "The selected change account {0} does not belong to Company {1}." -msgstr "" +msgstr "Odabrani račun povrata {0} ne pripada {1}." #: erpnext/stock/doctype/batch/batch.py:157 msgid "The selected item cannot have Batch" @@ -55524,7 +55527,7 @@ msgstr "Prodavač i Kupac ne mogu biti isti" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:186 #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:198 msgid "The serial and batch bundle {0} is not linked to {1} {2}" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} nije povezan sa {1} {2}" #: erpnext/stock/doctype/batch/batch.py:386 msgid "The serial no {0} does not belong to item {1}" @@ -55578,7 +55581,7 @@ msgstr "Zadatak je stavljen u red kao pozadinski posao. U slučaju da postoji bi #: erpnext/stock/doctype/material_request/material_request.py:352 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" -msgstr "" +msgstr "Ukupna količina izdavanja / prijenosa {0} u Materijalnom Nalogu {1} ne može biti veća od dopuštene tražene količine {2} za artikal {3}" #: erpnext/stock/doctype/material_request/material_request.py:359 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" @@ -55658,7 +55661,7 @@ msgstr "{0} {1} se ne poklapa s {0} {2} u {3} {4}" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1730 msgid "The {0} {1} is in submitted state, please cancel it first" -msgstr "" +msgstr "{0} {1} je u podnešenom stanju, prvo ga otkažite" #: erpnext/manufacturing/doctype/job_card/job_card.py:1075 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." @@ -55699,7 +55702,7 @@ msgstr "U sustavu nema unosa kod kojih je datum odobravanja prije datuma knjiže #: erpnext/stock/report/item_variant_details/item_variant_details.py:25 msgid "There are no item variants for the selected item" -msgstr "" +msgstr "Nema varijanti artikla za odabrani artikal" #: erpnext/www/book_appointment/index.js:95 msgid "There are no slots available on this date" @@ -55747,7 +55750,7 @@ msgstr "Postoji jedna neusklađena transakcija prije {0}." #: erpnext/stock/doctype/stock_entry/stock_entry.py:887 msgid "There must be at least 1 Finished Good in this Stock Entry" -msgstr "" +msgstr "U ovom unosu zaliha mora biti barem jedan gotov proizvod" #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:152 msgid "There was an error creating Bank Account while linking with Plaid." @@ -55759,7 +55762,7 @@ msgstr "Došlo je do greške pri sinhronizaciji transakcija." #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:174 msgid "There was an error updating Bank Account {0} while linking with Plaid." -msgstr "" +msgstr "Došlo je do pogreške prilikom ažuriranja bankovnog računa {0} prilikom povezivanja s Plaidom." #: banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx:81 msgid "There was an error while importing the bank statement." @@ -55815,7 +55818,7 @@ msgstr "Ovaj unos plaćanja usklađen je s {0}. Otkazivanje će ga automatski po #: erpnext/selling/doctype/product_bundle/product_bundle.py:121 msgid "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" -msgstr "" +msgstr "Ovaj Artikal Paket je povezan sa {0}. Morat ćete otkazati ove dokumente kako biste izbrisali ovaj Artikal Paket" #: erpnext/buying/doctype/purchase_order/mapper.py:251 msgid "This Purchase Order has been fully subcontracted." @@ -56156,7 +56159,7 @@ msgstr "Ovo će ograničiti pristup korisnika drugim zapisima zaposlenih" #: erpnext/controllers/selling_controller.py:901 msgid "This {0} will be treated as material transfer." -msgstr "" +msgstr "Ovo {0} će se tretirati kao prijenos materijala." #. Option for the 'Under Withheld Reason' (Select) field in DocType 'Tax #. Withholding Entry' @@ -56288,7 +56291,7 @@ msgstr "Vremenska Linija" #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher" -msgstr "" +msgstr "Vremensko ograničenje (u sekundama) za svaki pozadinski zadatak stavljen u red čekanja prema verifikatu za zatvaranje knjigovodstvenog razdoblja" #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:36 #: erpnext/public/js/projects/timer.js:5 @@ -56577,7 +56580,7 @@ msgstr "Do Vremena" #: erpnext/projects/doctype/timesheet_detail/timesheet_detail.py:108 msgid "To Time cannot be before From Time" -msgstr "" +msgstr "Do Vremena ne može biti prije Od Vremena" #. Description of the 'Referral Code' (Data) field in DocType 'Sales Partner' #: erpnext/setup/doctype/sales_partner/sales_partner.json @@ -56633,7 +56636,7 @@ msgstr "Dostava Klijentu" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:276 msgid "To cancel a {0} you need to cancel the POS Closing Entry {1}." -msgstr "" +msgstr "Za otkazivanje {0} morate otkazati unos zatvaranja Blagajne {1}." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:290 msgid "To cancel this Sales Invoice you need to cancel the POS Closing Entry {0}." @@ -56645,7 +56648,7 @@ msgstr "Za kreiranje Zahtjeva Plaćanja obavezan je referentni dokument" #: erpnext/assets/doctype/asset_category/asset_category.py:120 msgid "To enable Capital Work in Progress Accounting, you must select Capital Work in Progress Account in accounts table" -msgstr "" +msgstr "Da biste omogućili knjigovodstvo nedovršenih kapitalnih radova, morate odabrati Račun nedovršenih kapitalnih radova u tablici računa" #: erpnext/manufacturing/doctype/production_plan/production_plan.js:734 msgid "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked." @@ -56824,19 +56827,19 @@ msgstr "Ukupni Predujam" #: erpnext/public/js/utils.js:250 msgid "Total Advance Paid" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed" #: erpnext/public/js/utils.js:195 msgid "Total Advance Paid: {0}" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed: {0}" #: erpnext/public/js/utils.js:252 msgid "Total Advance Received" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed" #: erpnext/public/js/utils.js:198 msgid "Total Advance Received: {0}" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed: {0}" #. Label of the total_allocated_amount (Currency) field in DocType 'Payment #. Entry' @@ -57495,7 +57498,7 @@ msgstr "Ukupno Vrijeme u minutama" #: erpnext/public/js/utils.js:253 msgid "Total Unpaid" -msgstr "" +msgstr "Ukupno Neplaćeno" #: erpnext/public/js/utils.js:200 msgid "Total Unpaid: {0}" @@ -57595,7 +57598,7 @@ msgstr "Ukupno sati: {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:574 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:190 msgid "Total payments amount can't be greater than {0}" -msgstr "" +msgstr "Ukupni iznos plaćanja ne može biti veći od {0}" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:66 msgid "Total percentage against cost centers should be 100" @@ -57614,7 +57617,7 @@ msgstr "Ukupno {0} ({1})" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:248 msgid "Total {0} for all items is zero, maybe you should change 'Distribute Charges Based On'" -msgstr "" +msgstr "Ukupno {0} za sve artikle je nula, možda biste trebali promijeniti 'Raspodjeli Naknade na Temelju'" #: erpnext/controllers/trends.py:25 erpnext/controllers/trends.py:32 msgid "Total(Amt)" @@ -58155,7 +58158,7 @@ msgstr "Probna Bilanca Stranke" #: erpnext/accounts/report/trial_balance/trial_balance.py:595 msgid "Trial Balance requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Probna Bilanca zahtijeva sinhronizaciju {0} sa DuckDB-om" #. Label of the trial_period_end (Date) field in DocType 'Subscription' #: erpnext/accounts/doctype/subscription/subscription.json @@ -59399,7 +59402,7 @@ msgstr "Korisnik nije primijenio pravilo na fakturi {0}" #: erpnext/crm/frappe_crm_api.py:175 msgid "User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Korisniku nije dopuštena sinkronizacija podataka iz Prodajne Podrške u Sustav. Obratite se Upravitelju Sustava." #: erpnext/setup/doctype/employee/employee.py:298 msgid "User {0} does not exist" @@ -59415,7 +59418,7 @@ msgstr "Korisnik {0} je već dodijeljen {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:62 msgid "User {0} is disabled. Please select valid user/cashier" -msgstr "" +msgstr "Korisnik {0} je onemogućen. Odaberi valjanog korisnika/blagajnika" #: erpnext/setup/doctype/employee/employee.py:365 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." @@ -59763,7 +59766,7 @@ msgstr "Naknade za tip vrijednovanja ne mogu biti označene kao Inkluzivne" #: erpnext/public/js/controllers/accounts.js:228 msgid "Valuation type charges cannot be marked as Inclusive" -msgstr "" +msgstr "Naknade tipa procjene vrijednosti ne mogu biti označene kao uključene" #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:58 msgid "Value (G - D)" @@ -61329,11 +61332,11 @@ msgstr "Sažetka Izvješća Radnog Naloga" #: erpnext/stock/doctype/material_request/material_request.py:579 msgid "Work Order cannot be created for the following reason:
{0}" -msgstr "" +msgstr "Radni Nalog se ne može izraditi iz sljedećeg razloga:
{0}" #: erpnext/manufacturing/doctype/work_order/work_order.py:857 msgid "Work Order cannot be raised against an Item Template" -msgstr "" +msgstr "Radni Nalog ne može se pokrenuti na temelju Predloška Artikla" #: erpnext/manufacturing/doctype/work_order/work_order.py:1123 #: erpnext/manufacturing/doctype/work_order/work_order.py:1170 @@ -61686,7 +61689,7 @@ msgstr "Uvoziš podatke za Listu Koda:" #: erpnext/accounts/services/child_item_update.py:232 msgid "You are not allowed to update as per the conditions set in {0} Workflow." -msgstr "" +msgstr "Nije vam dopušteno ažuriranje prema uvjetima postavljenim u {0} Radnom Tijeku." #: erpnext/accounts/services/gl_validator.py:114 msgid "You are not authorized to add or update entries before {0}" @@ -61706,7 +61709,7 @@ msgstr "Birate više od potrebne količine za artikal {0}. Provjerite postoji li #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:111 msgid "You can add the original invoice {0} manually to proceed." -msgstr "" +msgstr "Izvornu Fakturu {0} možete dodati ručno da biste nastavili." #: banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx:743 msgid "You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25)." @@ -61718,7 +61721,7 @@ msgstr "Takođe možete kopirati i zalijepiti ovu vezu u svoj pretraživač" #: erpnext/assets/doctype/asset_category/asset_category.py:124 msgid "You can also set default CWIP account in Company {0}" -msgstr "" +msgstr "Također možete postaviti standard Račun Kapitalnog Posla u Toku u {0}" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:761 msgid "You can change the parent account to a Balance Sheet account or select a different account." @@ -61747,7 +61750,7 @@ msgstr "Možete odabrati samo jedan način plaćanja kao standard" #: erpnext/selling/page/point_of_sale/pos_payment.js:595 msgid "You can redeem up to {0}." -msgstr "" +msgstr "Možete iskoristiti do {0}." #: banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx:193 msgid "You can reset the clearing dates of these entries here." @@ -61779,11 +61782,11 @@ msgstr "Ne možete kreirati {0} unutar zatvorenog Knjigovodstvenog Perioda {1}" #: erpnext/accounts/services/gl_validator.py:64 msgid "You cannot create or cancel any accounting entries within the closed Accounting Period {0}" -msgstr "" +msgstr "Ne možete izraditi niti otkazati nikakve knjigovodstvene zapise unutar zatvorenog knjigovodstvenog perioda. {0}" #: erpnext/accounts/services/gl_validator.py:145 msgid "You cannot create/amend any accounting entries until this date." -msgstr "" +msgstr "Ne možete izraditi/izmijeniti bilo koje knjigovodstvene unose do ovog datuma." #: erpnext/accounts/doctype/journal_entry/journal_entry.py:675 msgid "You cannot credit and debit same account at the same time" @@ -61795,7 +61798,7 @@ msgstr "Ne možete izbrisati tip projekta 'Eksterni'" #: erpnext/setup/doctype/department/department.js:19 msgid "You cannot edit the root node." -msgstr "" +msgstr "Ne možete uređivati korijenski čvor." #: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:198 msgid "You cannot enable both the settings '{0}' and '{1}'." @@ -61803,15 +61806,15 @@ msgstr "Ne možete omogućiti i '{0}' i '{1} postavke." #: erpnext/manufacturing/doctype/job_card/job_card.py:1441 msgid "You cannot make any changes to Job Card since Work Order is closed." -msgstr "" +msgstr "Ne možete unositi nikakve promjene na Radnoj Kartici jer je Radni Nalog zatvoren." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:167 msgid "You cannot outward the following {0} as they are either Delivered, Inactive or located in a different warehouse." -msgstr "" +msgstr "Ne možete poslati sljedeće {0} jer su ili Isporučeno, Neaktivno ili se nalaze u drugom skladištu." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:229 msgid "You cannot process the serial number {0} as it has already been used in the SABB {1}. {2} If you want to inward the same serial number multiple times, then enable 'Allow existing Serial No to be Manufactured/Received again' in the {3}" -msgstr "" +msgstr "Ne možete obraditi serijski broj {0} jer je već korišten u Serijskom i Šaržnom Paketu {1}. {2} ako želite da primite isti serijski broj više puta, tada omogućite 'Dozvoli da se postojeći Serijski Broj ponovo Proizvede/Primi' u {3}" #: erpnext/selling/page/point_of_sale/pos_payment.js:625 msgid "You cannot redeem more than {0}." @@ -61819,7 +61822,7 @@ msgstr "Ne možete iskoristiti više od {0}." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:212 msgid "You cannot repost item valuation before {0}" -msgstr "" +msgstr "Ne možete ponovo knjižiti procjenu vrijednosti artikla prije {0}" #: erpnext/accounts/doctype/subscription/subscription.py:832 msgid "You cannot restart a Subscription that is not cancelled." @@ -61827,7 +61830,7 @@ msgstr "Ne možete ponovo pokrenuti Pretplatu koja nije otkazana." #: erpnext/selling/page/point_of_sale/pos_payment.js:281 msgid "You cannot submit an empty order." -msgstr "" +msgstr "Ne možete podnijeti prazan nalog." #: erpnext/selling/page/point_of_sale/pos_payment.js:280 msgid "You cannot submit the order without payment." @@ -61843,7 +61846,7 @@ msgstr "Ne možete {0} ovaj dokument jer postoji drugi Unos Zatvaranje Perioda { #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:165 msgid "You do not have enough permission to access {0}: {1}" -msgstr "" +msgstr "Nemate dovoljno dopuštenja za pristup {0}: {1}" #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:82 msgid "You do not have permission to import and submit bank transactions" @@ -61856,7 +61859,7 @@ msgstr "Nemate dopuštenje za uvoz bankovnih transakcija" #: erpnext/accounts/services/child_item_update.py:210 msgid "You do not have permissions to {0} items in a {1}." -msgstr "" +msgstr "Nemate dopuštenja za {0} artikala u {1}." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:187 msgid "You don't have enough Loyalty Points to redeem" @@ -61884,7 +61887,7 @@ msgstr "Nemate dopuštenje za ažuriranje ovog dokumenta. Obratite se Upravitelj #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:310 msgid "You had {0} errors while creating opening invoices. Check {1} for more details" -msgstr "" +msgstr "Imali ste {0} pogrešaka prilikom izrade početnih računa. Pogledajte {1} za više detalja" #: erpnext/public/js/utils.js:1055 msgid "You have already selected items from {0} {1}" @@ -61904,7 +61907,7 @@ msgstr "Omogućili ste {0} i {1} u {2}. To može dovesti do umetanja cijena iz z #: erpnext/stock/doctype/shipment/shipment.js:442 msgid "You have entered a duplicate Delivery Note on row {0}. Please rectify and try again." -msgstr "" +msgstr "Unijeli ste duplikat Dostavnice u red {0}. Ispravi grešku i pokušaj ponovo." #: banking/src/components/features/BankReconciliation/BankPicker.tsx:64 msgid "You have not added any bank accounts to your company." @@ -61928,7 +61931,7 @@ msgstr "Morate odabrati Klijenta prije dodavanja Artikla." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:282 msgid "You need to cancel POS Closing Entry {0} to be able to cancel this document." -msgstr "" +msgstr "Morate otkazati Unos Zatvaranje Blagajne {0} da biste mogli otkazati ovaj dokument." #: erpnext/accounts/services/taxes.py:277 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." @@ -61984,7 +61987,7 @@ msgstr "Nulto Stanje" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353 msgid "Zero Balance Journal: {0}" -msgstr "" +msgstr "Žurnal Nultog Stanja: {0}" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78 msgid "Zero Rated" @@ -62108,7 +62111,7 @@ msgstr "naziv polja" #: erpnext/setup/doctype/item_group/item_group.py:49 msgid "for tax category {0}" -msgstr "" +msgstr "za PDV kategoriju {0}" #. Option for the 'Service Provider' (Select) field in DocType 'Currency #. Exchange Settings' @@ -62467,7 +62470,7 @@ msgstr "{0} ne može biti negativan" #: erpnext/accounts/doctype/sales_invoice/services/loyalty.py:77 msgid "{0} cannot be cancelled since the Loyalty Points earned has been redeemed. First cancel the {1} No {2}" -msgstr "" +msgstr "{0} se ne može otkazati jer su osvojeni bodovi vjernosti iskorišteni. Prvo otkažite {1} Ne {2}" #: erpnext/accounts/doctype/pos_settings/pos_settings.py:53 msgid "{0} cannot be changed with opened Opening Entries." @@ -62475,7 +62478,7 @@ msgstr "{0} se ne može mijenjati s otvorenim Početnim Unosima." #: erpnext/public/js/utils/sales_common.js:336 msgid "{0} cannot be greater than 100" -msgstr "" +msgstr "{0} ne može biti veće od 100" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136 msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}" @@ -62544,7 +62547,7 @@ msgstr "{0} je uspješno podnešen" #: erpnext/controllers/buying_controller.py:289 msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return." -msgstr "" +msgstr "{0} je podnio/la imovinu povezanu s njim/njom. Morate otkazati imovinu da biste izradili povrat." #: erpnext/projects/doctype/project/project_dashboard.html:15 msgid "{0} hours" @@ -62556,7 +62559,7 @@ msgstr "{0} u redu {1}" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66 msgid "{0} is a child company." -msgstr "" +msgstr "{0} je podređena tvrtka." #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465 msgid "{0} is a child table and will be deleted automatically with its parent" @@ -62639,7 +62642,7 @@ msgstr "{0} nije omogućen u {1}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649 msgid "{0} is not running. Cannot trigger events for this document" -msgstr "" +msgstr "{0} se ne izvršava. Ne može pokrenuti događaje za ovaj dokument" #: erpnext/stock/doctype/material_request/material_request.py:478 msgid "{0} is not the default supplier for any items." @@ -62647,7 +62650,7 @@ msgstr "{0} nije standard dobavljač za bilo koji artikal." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686 msgid "{0} is on hold until {1}" -msgstr "" +msgstr "{0} je na čekanju do {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68 msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." @@ -62822,11 +62825,11 @@ msgstr "{0} {1} je već povezan sa Zajedničkim Kodom {2}." #: erpnext/accounts/doctype/party_link/party_link.py:53 #: erpnext/accounts/doctype/party_link/party_link.py:63 msgid "{0} {1} is already linked with another {2}" -msgstr "" +msgstr "{0} {1} je već povezan s drugim {2}" #: erpnext/accounts/doctype/party_link/party_link.py:40 msgid "{0} {1} is already linked with {2} {3}" -msgstr "" +msgstr "{0} {1} je već povezan s {2} {3}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" @@ -62867,7 +62870,7 @@ msgstr "{0} {1} nije aktivan" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452 msgid "{0} {1} is not affecting bank account {2}" -msgstr "" +msgstr "{0} {1} ne utječe na bankovni račun {2}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" diff --git a/erpnext/locale/sv.po b/erpnext/locale/sv.po index 13dd28de655..5c7e61ce3b7 100644 --- a/erpnext/locale/sv.po +++ b/erpnext/locale/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-29 20:08\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -8227,7 +8227,7 @@ msgstr "Saldo Historik per Parti" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:183 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:86 msgid "Batchwise Valuation" -msgstr "Partivis Värdering" +msgstr "Partibaserad Värdering" #. Label of the section_break_3 (Section Break) field in DocType 'Stock #. Reconciliation Item' @@ -9319,7 +9319,7 @@ msgstr "Beräkna Uppskatade Ankomst Tider" #. Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json msgid "Calculate Product Bundle price based on child Item's rates" -msgstr "Beräkna Artikel Paket pris baserat på priser för underordnade artiklar" +msgstr "Beräkna Artikel Paket pris baserat på priser för paket artiklar" #. Description of the 'Hidden Line (Internal Use Only)' (Check) field in #. DocType 'Financial Report Row' @@ -15680,7 +15680,7 @@ msgstr "Avdraget från" #. Deduction Certificate' #: erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json msgid "Deductee Details" -msgstr "Avdragsberättigad Detaljer" +msgstr "Avdragstagare Detaljer" #. Label of a Workspace Sidebar Item #: erpnext/workspace_sidebar/taxes.json @@ -17908,7 +17908,7 @@ msgstr "Uppdatera inte Varianter vid Spara" #. Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "Do not use Batch-wise Valuation" -msgstr "Använd inte Partivis Värdering" +msgstr "Använd inte Partibaserad Värdering" #: erpnext/assets/doctype/asset/asset.js:957 msgid "Do you really want to restore this scrapped asset?" @@ -19193,9 +19193,9 @@ msgid "Enabling this will do the following:\n" msgstr "Om du aktiverar detta kommer följande att hända:\n" "
    \n" "
  • Pris Kolumn i alla Artikel Paket tabeller redigerbar.
  • \n" -"
  • Beräkna priser för alla artikel paket i Artikel tabell, baserat på priser för dess underordnade artiklar, som anges i Artikel Paket tabell.
  • \n" +"
  • Beräkna priser för alla Artikel Paket i Artikel tabell, baserat på priser för paket artiklar, som anges i Artikel Paket tabell.
  • \n" "
\n" -"Observera: Om detta är aktiverat kommer uppdatering av pris för artikel paket i artikel tabell inte att ändra dess pris. Det kommer att återställas till det pris som baseras på dess underordnade artiklar när dokumentet sparas." +"Observera: Om detta är aktiverat kommer uppdatering av pris för artikel paket i artikel tabell inte att ändra deras pris. Det kommer att återställas till pris som baseras på paket artiklar när dokument sparas." #. Label of the encashment_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -33834,7 +33834,7 @@ msgstr "Öppning Faktura Verktyg" #: erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py:832 #: erpnext/accounts/doctype/sales_invoice/services/gl_composer.py:642 msgid "Opening Invoice has rounding adjustment of {0}.

'{1}' account is required to post these values. Please set it in Company: {2}.

Or, '{3}' can be enabled to not post any rounding adjustment." -msgstr "Öppning Fakturan har avrundning justering på {0}.

'{1}' konto erfordras för att bokföra dessa värden. Ange det i Bolag: {2}.

Eller så kan '{3}' aktiveras för att inte bokföra någon avrundning justering." +msgstr "Öppning Faktura har avrundning justering på {0}.

'{1}' konto erfordras för att bokföra dessa värden. Ange det i Bolag: {2}.

Eller så kan '{3}' aktiveras för att inte bokföra någon avrundning justering." #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html:8 msgid "Opening Invoices" @@ -59194,7 +59194,7 @@ msgstr "Använd Python filter för att hämta Konton" #. Label of the use_batchwise_valuation (Check) field in DocType 'Batch' #: erpnext/stock/doctype/batch/batch.json msgid "Use Batch-wise Valuation" -msgstr "Använd Partivis Värdering" +msgstr "Använd Partibaserad Värdering" #. Label of the use_csv_sniffer (Check) field in DocType 'Bank Statement #. Import' @@ -60825,7 +60825,7 @@ msgstr "Garanti Utgång (Serienummer)" #: erpnext/stock/doctype/serial_no/serial_no.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Warranty Expiry Date" -msgstr "Garanti Utgångsdatum" +msgstr "Garanti Utgång Datum" #. Label of the warranty_period (Int) field in DocType 'Serial No' #: erpnext/stock/doctype/serial_no/serial_no.json From ed72732bb2ccc04ce2f519aef4d2d722be0d1356 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:21:36 +0530 Subject: [PATCH 188/195] test: Accounts Payable Summary report coverage --- .../test_accounts_payable_summary.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py diff --git a/erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py new file mode 100644 index 00000000000..46491b1ad37 --- /dev/null +++ b/erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py @@ -0,0 +1,144 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.accounts_payable_summary.accounts_payable_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestAccountsPayableSummary(ERPNextTestSuite): + """Payable Summary is a thin wrapper over AccountsReceivableSummary with + account_type=Payable; these tests lock the supplier-side output: invoiced, + advance, paid, outstanding, ageing buckets and the optional GL-balance / + future-payment columns.""" + + def setUp(self): + frappe.set_user("Administrator") + self.maxDiff = None + self.company = "_Test Company" + self.supplier = "_Test Supplier" + + def _filters(self, **overrides): + filters = { + "company": self.company, + "supplier": self.supplier, + "posting_date": today(), + "range": "30, 60, 90, 120", + } + filters.update(overrides) + return filters + + def _make_invoice(self, rate=200): + return make_purchase_invoice( + company=self.company, + supplier=self.supplier, + qty=1, + rate=rate, + price_list_rate=rate, + posting_date=today(), + ) + + def _expected_row(self, pi, **overrides): + supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group") + row = { + "party_type": "Supplier", + "advance": 0, + "party": self.supplier, + "invoiced": 200.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 200.0, + "range1": 200.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 200.0, + "future_amount": 0.0, + "sales_person": [], + "currency": pi.currency, + "supplier_group": supplier_group, + } + row.update(overrides) + return row + + def test_01_payable_summary_output(self): + """Invoiced -> advance -> partial payment progression for a single supplier.""" + filters = self._filters() + pi = self._make_invoice() + + expected = self._expected_row(pi) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # advance payment: pay 50 but allocate nothing against the invoice + pe = get_payment_entry(pi.doctype, pi.name) + pe.paid_amount = 50 + pe.references[0].allocated_amount = 0 + pe.save().submit() + + expected.update({"advance": 50.0, "outstanding": 150.0, "range1": 150.0, "total_due": 150.0}) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # partial payment allocated against the invoice + pe = get_payment_entry(pi.doctype, pi.name) + pe.paid_amount = 125 + pe.references[0].allocated_amount = 125 + pe.save().submit() + + expected.update( + {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0} + ) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + @ERPNextTestSuite.change_settings("Buying Settings", {"supp_master_name": "Naming Series"}) + def test_02_gl_balance_and_future_payment_columns(self): + """Naming-series naming adds party_name; show_gl_balance / show_future_payments + add their columns; a fully-paid invoice drops out of the report.""" + filters = self._filters() + pi = self._make_invoice() + + pe = get_payment_entry(pi.doctype, pi.name) + pe.paid_amount = 150 + pe.references[0].allocated_amount = 150 + pe.save().submit() + + expected = self._expected_row( + pi, + party_name=frappe.db.get_value("Supplier", self.supplier, "supplier_name"), + paid=150.0, + outstanding=50.0, + range1=50.0, + total_due=50.0, + ) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # GL balance reconciliation columns + filters.update({"show_gl_balance": True}) + expected.update({"gl_balance": 50.0, "diff": 0.0}) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # future payment columns + filters.update({"show_future_payments": True}) + expected.update({"remaining_balance": 50.0}) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # clear the remaining balance -> supplier drops out of the summary entirely + get_payment_entry(pi.doctype, pi.name).save().submit() + rows = execute(filters)[1] + self.assertEqual(len(rows), 0) From 7034dc71e7f5fc5643fd4d9860b7042b91978252 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:24:12 +0530 Subject: [PATCH 189/195] test: Dimension-wise Accounts Balance report coverage --- ..._dimension_wise_accounts_balance_report.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py new file mode 100644 index 00000000000..fb87342da66 --- /dev/null +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py @@ -0,0 +1,82 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.dimension_wise_accounts_balance_report.dimension_wise_accounts_balance_report import ( + execute, +) +from erpnext.accounts.utils import get_fiscal_year +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDimensionWiseAccountsBalance(ERPNextTestSuite): + """Balances accounts one column per value of an accounting dimension (here + Cost Center). Locks the two behaviours that matter: an entry lands in its + own dimension column as debit - credit, and children roll up into parents.""" + + def setUp(self): + frappe.set_user("Administrator") + self.company = "_Test Company" + self.expense_account = "_Test Account Cost for Goods Sold - _TC" + self.cash_account = "Cash - _TC" + + def _make_cost_center(self, name): + full_name = f"{name} - _TC" + if not frappe.db.exists("Cost Center", full_name): + frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": name, + "parent_cost_center": "_Test Company - _TC", + "company": self.company, + "is_group": 0, + } + ).insert() + return full_name + + def _filters(self, **overrides): + filters = frappe._dict( + { + "company": self.company, + "dimension": "Cost Center", + "fiscal_year": get_fiscal_year(today(), company=self.company)[0], + } + ) + filters.update(overrides) + return filters + + def test_dimension_column_and_rollup(self): + # a dedicated cost center isolates our column from any other posted data + cost_center = self._make_cost_center("Test Dimension CC") + make_journal_entry( + self.expense_account, + self.cash_account, + 300, + cost_center=cost_center, + posting_date=today(), + submit=True, + ) + + columns, data = execute(self._filters()) + column = frappe.scrub(cost_center) + self.assertIn(column, [c["fieldname"] for c in columns]) + + rows = {row["account"]: row for row in data} + + # the entry shows as debit - credit under its own dimension column + self.assertEqual(rows[self.expense_account][column], 300.0) + self.assertEqual(rows[self.cash_account][column], -300.0) + + # and rolls up into each account's parent (isolated to our cost center) + expense_parent = frappe.db.get_value("Account", self.expense_account, "parent_account") + cash_parent = frappe.db.get_value("Account", self.cash_account, "parent_account") + self.assertEqual(rows[expense_parent][column], 300.0) + self.assertEqual(rows[cash_parent][column], -300.0) + + def test_requires_fiscal_year(self): + filters = self._filters() + filters.pop("fiscal_year") + self.assertRaises(frappe.ValidationError, execute, filters) From 4feaacc649092a355fb424579d15cfd1be3bbf95 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:26:38 +0530 Subject: [PATCH 190/195] test: Custom Financial Statement report coverage --- .../test_custom_financial_statement.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py diff --git a/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py b/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py new file mode 100644 index 00000000000..8fa3e530fcc --- /dev/null +++ b/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py @@ -0,0 +1,92 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.custom_financial_statement.custom_financial_statement import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCustomFinancialStatement(ERPNextTestSuite): + """The report renders a Financial Report Template through FinancialReportEngine. + These tests exercise its own entry point: a template with an account-data row + and a calculated row, and the guard that returns nothing without a template.""" + + def setUp(self): + frappe.set_user("Administrator") + self.company = "_Test Company" + self.expense_account = "_Test Account Cost for Goods Sold - _TC" + self.cash_account = "Cash - _TC" + + def _make_template(self): + # rows filter by exact account name so the value is isolated from other data + template_name = f"Test Custom FS {frappe.generate_hash()[:8]}" + return frappe.get_doc( + { + "doctype": "Financial Report Template", + "template_name": template_name, + "report_type": "Profit and Loss Statement", + "rows": [ + { + "reference_code": "EXP", + "display_name": "Test Expense", + "indentation_level": 0, + "data_source": "Account Data", + "balance_type": "Closing Balance", + "calculation_formula": f'["name", "=", "{self.expense_account}"]', + }, + { + "reference_code": "EXP_X2", + "display_name": "Expense Doubled", + "indentation_level": 0, + "data_source": "Calculated Amount", + "calculation_formula": "EXP * 2", + }, + ], + } + ).insert() + + def _filters(self, template_name): + return frappe._dict( + { + "company": self.company, + "report_template": template_name, + "from_fiscal_year": "2024", + "to_fiscal_year": "2024", + "period_start_date": "2024-01-01", + "period_end_date": "2024-12-31", + "filter_based_on": "Date Range", + "periodicity": "Yearly", + "accumulated_values": 0, + } + ) + + def test_account_and_calculated_rows(self): + make_journal_entry( + self.expense_account, + self.cash_account, + 2000, + posting_date="2024-06-15", + company=self.company, + submit=True, + ) + template = self._make_template() + + columns, data = execute(self._filters(template.template_name))[:2] + self.assertTrue(columns) + + rows = {row.get("account_name"): row for row in data} + self.assertIn("Test Expense", rows) + self.assertIn("Expense Doubled", rows) + + period_key = rows["Test Expense"].get("_segment_info", {}).get("period_keys", [])[0] + + # the account-data row picks up the posted expense; the calculated row doubles it + self.assertEqual(flt(rows["Test Expense"][period_key]), 2000.0) + self.assertEqual(flt(rows["Expense Doubled"][period_key]), 4000.0) + + def test_no_template_returns_nothing(self): + """Without a report_template the report short-circuits and returns None.""" + self.assertIsNone(execute(frappe._dict({"company": self.company}))) From 0ba43a17c158a17c2e0049d77a1c2fa1cd89ac44 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:29:48 +0530 Subject: [PATCH 191/195] test: strengthen price_per_unit assertion, drop no-op quotation guard --- .../test_supplier_quotation_comparison.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py index 7eaed09cd14..d32a7cabfcc 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py @@ -14,7 +14,10 @@ class TestSupplierQuotationComparison(ERPNextTestSuite): """The report lists Supplier Quotation item lines so quotes for the same item can be compared across suppliers.""" - def make_quotation(self, supplier, qty, rate): + def make_quotation(self, supplier, qty, rate, uom=None): + item = {"item_code": ITEM, "qty": qty, "rate": rate, "warehouse": "_Test Warehouse - _TC"} + if uom: + item["uom"] = uom sq = frappe.get_doc( { "doctype": "Supplier Quotation", @@ -22,9 +25,7 @@ class TestSupplierQuotationComparison(ERPNextTestSuite): "company": COMPANY, "currency": "INR", "transaction_date": "2026-06-01", - "items": [ - {"item_code": ITEM, "qty": qty, "rate": rate, "warehouse": "_Test Warehouse - _TC"} - ], + "items": [item], } ) sq.insert() @@ -40,7 +41,9 @@ class TestSupplierQuotationComparison(ERPNextTestSuite): self.assertEqual(execute(None)[1], []) def test_quotation_line_listed_with_price(self): - sq = self.make_quotation("_Test Supplier", qty=10, rate=100) + # _Test UOM 1 converts at 10 stock units per qty, so price_per_unit + # (amount / stock_qty) diverges from base_rate and the division path is tested + sq = self.make_quotation("_Test Supplier", qty=10, rate=100, uom="_Test UOM 1") rows = [r for r in self.run_report(item_code=ITEM) if r.get("quotation") == sq.name] self.assertTrue(rows, "Supplier Quotation line missing from report") @@ -49,13 +52,14 @@ class TestSupplierQuotationComparison(ERPNextTestSuite): self.assertEqual(row["qty"], 10) self.assertEqual(row["base_rate"], 100) self.assertEqual(row["base_amount"], 1000) - self.assertEqual(row["price_per_unit"], 100) + # 1000 amount / (10 qty * 10 conversion) = 10, distinct from the 100 base_rate + self.assertEqual(row["price_per_unit"], 10) def test_compares_multiple_suppliers_for_item(self): sq1 = self.make_quotation("_Test Supplier", qty=10, rate=100) sq2 = self.make_quotation("_Test Supplier 1", qty=10, rate=120) - quotes = {r["quotation"]: r for r in self.run_report(item_code=ITEM) if r.get("quotation")} + quotes = {r["quotation"]: r for r in self.run_report(item_code=ITEM)} self.assertIn(sq1.name, quotes) self.assertIn(sq2.name, quotes) self.assertEqual(quotes[sq1.name]["base_rate"], 100) From f0434cadd4f5899aee68ae8ccbc854b7d11d5e76 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:30:34 +0530 Subject: [PATCH 192/195] test: guard against missing owner row before subscripting --- .../report/lead_owner_efficiency/test_lead_owner_efficiency.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py b/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py index f1063c2e8e7..745fc85aec6 100644 --- a/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py +++ b/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py @@ -64,6 +64,7 @@ class TestLeadOwnerEfficiency(ERPNextTestSuite): ).insert() row = self.owner_row(self.run_report()) + self.assertIsNotNone(row, "Lead owner missing from report") self.assertEqual(row["lead_count"], 1) self.assertEqual(row["opp_count"], 1) # one opportunity from one lead -> 100% opp/lead conversion From 21a9f2754ed1f180902ceb692b2ebd7d20bf0839 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:31:13 +0530 Subject: [PATCH 193/195] test: unpack report_summary tuple and key labels via _() --- .../report/project_summary/test_project_summary.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/projects/report/project_summary/test_project_summary.py b/erpnext/projects/report/project_summary/test_project_summary.py index 9f0ef5c7a4a..66dff87e1e6 100644 --- a/erpnext/projects/report/project_summary/test_project_summary.py +++ b/erpnext/projects/report/project_summary/test_project_summary.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe import _ from erpnext.projects.report.project_summary.project_summary import execute from erpnext.tests.utils import ERPNextTestSuite @@ -57,8 +58,8 @@ class TestProjectSummary(ERPNextTestSuite): self.make_task(project, "Completed") self.make_task(project, "Open") - report_summary = self.run_report(project)[4] + _columns, _data, _message, _chart, report_summary = self.run_report(project) summary = {s["label"]: s["value"] for s in report_summary} - self.assertEqual(summary["Total Tasks"], 2) - self.assertEqual(summary["Completed Tasks"], 1) - self.assertEqual(summary["Overdue Tasks"], 0) + self.assertEqual(summary[_("Total Tasks")], 2) + self.assertEqual(summary[_("Completed Tasks")], 1) + self.assertEqual(summary[_("Overdue Tasks")], 0) From 9c53a91b82a3ac213271e730b188b633a661f63c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 23:32:02 +0530 Subject: [PATCH 194/195] test: add simulate=True to draft timesheet for overlap safety --- .../test_timesheet_billing_summary.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py b/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py index 5526d5db01c..c4ae6ecd9b3 100644 --- a/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py +++ b/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py @@ -54,7 +54,9 @@ class TestTimesheetBillingSummary(ERPNextTestSuite): self.assertEqual(group_rows[0]["hours"], 2) def test_draft_excluded_unless_requested(self): - ts = make_timesheet(self.employee, is_billable=1, project=self.project.name, do_not_submit=True) + ts = make_timesheet( + self.employee, simulate=True, is_billable=1, project=self.project.name, do_not_submit=True + ) # submitted-only by default: the draft timesheet is absent self.assertNotIn(ts.name, {r.get("timesheet") for r in self.run_report()}) From 42c6768b4c6a9e0ca0a695264570fd30c49697d0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 3 Jul 2026 00:16:06 +0530 Subject: [PATCH 195/195] test: guard period_keys index access for clearer failure --- .../test_custom_financial_statement.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py b/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py index 8fa3e530fcc..5d981b77c38 100644 --- a/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py +++ b/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py @@ -81,7 +81,9 @@ class TestCustomFinancialStatement(ERPNextTestSuite): self.assertIn("Test Expense", rows) self.assertIn("Expense Doubled", rows) - period_key = rows["Test Expense"].get("_segment_info", {}).get("period_keys", [])[0] + period_keys = rows["Test Expense"].get("_segment_info", {}).get("period_keys", []) + self.assertTrue(period_keys, "expected at least one period key in _segment_info") + period_key = period_keys[0] # the account-data row picks up the posted expense; the calculated row doubles it self.assertEqual(flt(rows["Test Expense"][period_key]), 2000.0)