From 0d8c65a013cddc1952f0075b2d04f9a28aa42d02 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:50:03 +0000 Subject: [PATCH 01/21] ci(mergify): upgrade configuration to current format --- .mergify.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 5e558062048..95763b27cb2 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -88,7 +88,6 @@ pull_request_rules: actions: merge: method: squash - commit_message_template: | - {{ title }} (#{{ number }}) - - {{ body }} + commit_message_format: + title: pr-title + body: pr-body From d387155e162ca8f3a3918f721eb1cd4a4e2fd3f8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 9 Jul 2026 18:26:14 +0530 Subject: [PATCH 02/21] test: seed current-dated USD<->INR exchange rate in bootstrap Tests that create USD documents dated today() (e.g. Sales Order in test_advance_payment_ledger_entry, USD BOM in test_routing) rely on get_exchange_rate() finding a USD->INR Currency Exchange record. The only seeded records are dated 2016, so the lookup misses and falls back to an external API that is blocked in CI, returning 0. That surfaces as "Exchange Rate is mandatory" on Sales Order validation and a ZeroDivisionError in BOM.get_routing (hour_rate / conversion_rate). Whether it passes depends on which shard incidentally committed the 2016 records first, making it an order-dependent flake that unrelated PRs trip by shifting test distribution. Seed today()-dated USD<->INR rates once in BootStrapTestData so the lookup resolves deterministically without the external API. Rates mirror the latest Currency Exchange test_records to keep cost calculations unchanged. --- erpnext/tests/utils.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index deeb8310d9c..61800eef0a0 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -181,6 +181,7 @@ class BootStrapTestData: self.make_location() self.make_price_list() self.make_item_price() + self.make_currency_exchange() self.make_loyalty_program() self.make_shareholder() self.make_sales_taxes_template() @@ -2533,6 +2534,38 @@ class BootStrapTestData: ] self.make_records(["item_code", "price_list", "price_list_rate"], records) + def make_currency_exchange(self): + """Seed current-dated USD<->INR rates so foreign-currency documents + transacted on ``today()`` resolve an exchange rate deterministically. + + Without this, ``get_exchange_rate`` finds no in-window Currency Exchange + record and falls back to an external API that is unreachable in CI, + returning ``0`` and breaking tests that create USD documents. The rates + mirror the latest values in the Currency Exchange ``test_records`` so + cost calculations stay unchanged regardless of which record is picked. + """ + records = [ + { + "doctype": "Currency Exchange", + "date": today(), + "from_currency": "USD", + "to_currency": "INR", + "exchange_rate": 62.9, + "for_buying": 1, + "for_selling": 1, + }, + { + "doctype": "Currency Exchange", + "date": today(), + "from_currency": "INR", + "to_currency": "USD", + "exchange_rate": 0.0167, + "for_buying": 1, + "for_selling": 1, + }, + ] + self.make_records(["from_currency", "to_currency", "date"], records) + def make_operation(self): records = [ {"doctype": "Operation", "name": "_Test Operation 1", "workstation": "_Test Workstation 1"} From e1e56b6920b6e9095e3a6c366870d305a6d440b3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 12 Jul 2026 21:12:29 +0530 Subject: [PATCH 03/21] test: adjust currency tests for deterministic seeded exchange rate Seeding a current-dated USD->INR rate makes get_exchange_rate resolve 62.9 on today() instead of hitting the live API, which exposed three tests that implicitly relied on a different/undefined current rate: - customer: dropped its own colliding current-dated seed (ignored via ignore_if_duplicate, and its cleanup deleted the shared seed) and now asserts the quotation resolves the seeded rate via get_exchange_rate. - exchange_rate_revaluation: the revalued rate (62.9) is now below the booked 80, so the revaluation is a loss (debited) rather than a gain; derive the gain/loss column from the sign instead of assuming a gain. - purchase_invoice: the receipt rate was an accidental tuple (70,) that got discarded and recomputed to the seed; set explicit rates with the receipt above the invoice so the stock exchange difference is a credit, matching the asserted column. --- .../test_exchange_rate_revaluation.py | 7 ++++++- .../purchase_invoice/test_purchase_invoice.py | 4 ++-- .../selling/doctype/customer/test_customer.py | 20 +++++-------------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 5a37bccaafb..3e5b08d069d 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -348,10 +348,15 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): je.reload() self.assertEqual(je.voucher_type, "Exchange Rate Revaluation") self.assertEqual(len(je.accounts), 3) + # A gain is credited to the gain/loss account, a loss is debited. The current + # exchange rate (from master data) may sit either side of the booked rate, so + # derive the column from the sign instead of assuming a gain. + gain_loss_debit = abs(total_gain_loss) if total_gain_loss < 0 else 0.0 + gain_loss_credit = total_gain_loss if total_gain_loss > 0 else 0.0 expected = [ (usd_account, new_balance, 0.0, 100.0, 0.0), (usd_account, 0.0, old_balance, 0.0, 100.0), - (gain_loss_account, 0.0, total_gain_loss, 0.0, total_gain_loss), + (gain_loss_account, gain_loss_debit, gain_loss_credit, gain_loss_debit, gain_loss_credit), ] actual = [] for acc in je.accounts: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 17afc03dde1..e60d3f4614c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -472,7 +472,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): pr = frappe.new_doc("Purchase Receipt") pr.currency = "USD" pr.company = "_Test Company with perpetual inventory" - pr.conversion_rate = (70,) + pr.conversion_rate = 80 pr.supplier = "_Test Supplier USD" pr.append( "items", @@ -491,7 +491,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): # Createing purchase invoice against Purchase Receipt pi = create_purchase_invoice(pr.name) - pi.conversion_rate = 80 + pi.conversion_rate = 70 pi.credit_to = "_Test Payable USD - TCP1" pi.insert() pi.submit() diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index a1b15a1e867..c1315fe518b 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -17,6 +17,7 @@ from erpnext.selling.doctype.customer.mapper import ( make_quotation, parse_full_name, ) +from erpnext.setup.utils import get_exchange_rate from erpnext.tests.utils import ERPNextTestSuite @@ -29,20 +30,9 @@ class TestCustomer(ERPNextTestSuite): 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) + # Master data seeds a current-dated exchange rate, so make_quotation should + # resolve that rate instead of falling back to the default conversion rate of 1.0. + expected_rate = get_exchange_rate(foreign_currency, company_currency, nowdate()) customer = frappe.get_doc( { @@ -59,7 +49,7 @@ class TestCustomer(ERPNextTestSuite): 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) + self.assertEqual(flt(quotation.conversion_rate), flt(expected_rate)) def test_get_customer_name_dedupes_with_numeric_suffix(self): # When a customer name already exists, get_customer_name appends "- ". The From 3e8784f596b49fc5af6d3c9d1ecf201af0b5ee43 Mon Sep 17 00:00:00 2001 From: S Sakthivel Murugan Date: Sat, 4 Jul 2026 17:46:19 +0530 Subject: [PATCH 04/21] fix: validate mandatory date filters in reports --- .../tds_computation_summary.py | 16 +++++++++++++--- .../batch_wise_balance_history.py | 5 +++++ .../cogs_by_item_group/cogs_by_item_group.py | 13 ++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 3ab3986b013..b6fc77fd1c4 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -21,11 +21,21 @@ class TDSComputationSummaryReport(TaxWithholdingDetailsReport): AGGREGATE_FIELDS = ("total_amount", "tax_amount") def validate_filters(self): - if self.filters.from_date > self.filters.to_date: + from_date = self.filters.from_date + to_date = self.filters.to_date + if not from_date or not to_date: + frappe.throw( + _("{0} and {1} are mandatory").format( + frappe.bold(_("From Date")), + frappe.bold(_("To Date")), + ) + ) + + if from_date > to_date: frappe.throw(_("From Date must be before To Date")) - from_year = get_fiscal_year(self.filters.from_date)[0] - to_year = get_fiscal_year(self.filters.to_date)[0] + from_year = get_fiscal_year(from_date)[0] + to_year = get_fiscal_year(to_date)[0] if from_year != to_year: frappe.throw(_("From Date and To Date lie in different Fiscal Year")) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index fb49b060fb7..01533e9d414 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -30,6 +30,11 @@ def execute(filters=None): _("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.") ) + if not filters.from_date or not filters.to_date: + frappe.throw( + _("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))) + ) + if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 000aca9f43e..a325a6ca89e 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -34,7 +34,18 @@ def update_filters_with_account(filters: Filters) -> None: def validate_filters(filters: Filters) -> None: - if filters.from_date > filters.to_date: + from_date = filters.from_date + to_date = filters.to_date + + if not from_date or not to_date: + frappe.throw( + _("{0} and {1} are mandatory").format( + frappe.bold(_("From Date")), + frappe.bold(_("To Date")), + ) + ) + + if from_date > to_date: frappe.throw(_("From Date must be before To Date")) From ac99d28100e9db1c65906879fff6f64c9698790d Mon Sep 17 00:00:00 2001 From: sokumon Date: Tue, 14 Jul 2026 14:27:39 +0530 Subject: [PATCH 05/21] chore: merge erpnext workspaces --- .../workspace/accounting/accounting.json | 652 ++++++++++++++++++ .../accounts_setup/accounts_setup.json | 329 --------- .../accounts/workspace/banking/banking.json | 222 ------ .../workspace/budgeting/budgeting.json | 104 --- .../accounts/workspace/payments/payments.json | 192 +++++- .../share_management/share_management.json | 86 --- .../subscriptions/subscriptions.json | 121 ---- erpnext/accounts/workspace/taxes/taxes.json | 188 ----- erpnext/buying/workspace/buying/buying.json | 118 +++- .../erpnext_settings/erpnext_settings.json | 112 ++- .../workspace/organization/organization.json | 204 ------ .../subcontracting/subcontracting.json | 415 ----------- erpnext/workspace_sidebar/accounts_setup.json | 312 --------- erpnext/workspace_sidebar/banking.json | 190 ----- erpnext/workspace_sidebar/budgeting.json | 82 --- erpnext/workspace_sidebar/organization.json | 116 ---- .../workspace_sidebar/share_management.json | 65 -- erpnext/workspace_sidebar/subcontracting.json | 241 ------- erpnext/workspace_sidebar/subscriptions.json | 104 --- erpnext/workspace_sidebar/taxes.json | 159 ----- 20 files changed, 1070 insertions(+), 2942 deletions(-) create mode 100644 erpnext/accounts/workspace/accounting/accounting.json delete mode 100644 erpnext/accounts/workspace/accounts_setup/accounts_setup.json delete mode 100644 erpnext/accounts/workspace/banking/banking.json delete mode 100644 erpnext/accounts/workspace/budgeting/budgeting.json delete mode 100644 erpnext/accounts/workspace/share_management/share_management.json delete mode 100644 erpnext/accounts/workspace/subscriptions/subscriptions.json delete mode 100644 erpnext/accounts/workspace/taxes/taxes.json delete mode 100644 erpnext/setup/workspace/organization/organization.json delete mode 100644 erpnext/subcontracting/workspace/subcontracting/subcontracting.json delete mode 100644 erpnext/workspace_sidebar/accounts_setup.json delete mode 100644 erpnext/workspace_sidebar/banking.json delete mode 100644 erpnext/workspace_sidebar/budgeting.json delete mode 100644 erpnext/workspace_sidebar/organization.json delete mode 100644 erpnext/workspace_sidebar/share_management.json delete mode 100644 erpnext/workspace_sidebar/subcontracting.json delete mode 100644 erpnext/workspace_sidebar/subscriptions.json delete mode 100644 erpnext/workspace_sidebar/taxes.json diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json new file mode 100644 index 00000000000..4af2a59f5de --- /dev/null +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -0,0 +1,652 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-07-14 12:00:00.000000", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "landmark", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Accounting", + "link_type": "DocType", + "links": [], + "modified": "2026-07-14 12:00:00.000000", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Accounting", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 4.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "house", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Accounting", + "link_type": "Workspace", + "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": "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": 0, + "collapsible": 1, + "icon": "book-open-check", + "indent": 1, + "keep_closed": 1, + "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": "coins", + "indent": 1, + "keep_closed": 1, + "label": "Taxes", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "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": 1, + "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": 1, + "collapsible": 1, + "icon": "package", + "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": 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": 1, + "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": "wallet", + "indent": 1, + "keep_closed": 1, + "label": "Budgeting", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "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": 1, + "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, + "icon": "coins", + "indent": 1, + "keep_closed": 1, + "label": "Share Management", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "user", + "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": 0, + "collapsible": 1, + "icon": "repeat", + "indent": 1, + "keep_closed": 1, + "label": "Subscriptions", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "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": 1, + "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": 1, + "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": "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": "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" + }, + { + "child": 1, + "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": 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" + }, + { + "child": 0, + "collapsible": 1, + "icon": "wrench", + "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": "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/accounts_setup/accounts_setup.json b/erpnext/accounts/workspace/accounts_setup/accounts_setup.json deleted file mode 100644 index 88dd071b131..00000000000 --- a/erpnext/accounts/workspace/accounts_setup/accounts_setup.json +++ /dev/null @@ -1,329 +0,0 @@ -{ - "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 deleted file mode 100644 index d4ff8487759..00000000000 --- a/erpnext/accounts/workspace/banking/banking.json +++ /dev/null @@ -1,222 +0,0 @@ -{ - "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-07-03 13:43:50.924019", - "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": "wrench", - "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/budgeting/budgeting.json b/erpnext/accounts/workspace/budgeting/budgeting.json deleted file mode 100644 index c5ea717fe52..00000000000 --- a/erpnext/accounts/workspace/budgeting/budgeting.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "app": "erpnext", - "charts": [], - "content": "[]", - "creation": "2026-06-14 14:38:20.315394", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "wallet", - "idx": 0, - "indicator_color": "green", - "is_hidden": 0, - "label": "Budgeting", - "link_type": "DocType", - "links": [], - "modified": "2026-07-03 04:24:48.116724", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Budgeting", - "number_cards": [], - "owner": "Administrator", - "public": 1, - "quick_lists": [], - "roles": [], - "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": "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": "wallet", - "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": "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/payments/payments.json b/erpnext/accounts/workspace/payments/payments.json index fc29978e9e9..0553e0de207 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-07-03 13:43:50.184761", + "modified": "2026-07-14 12:00:00.000000", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", @@ -25,9 +25,23 @@ "public": 1, "quick_lists": [], "roles": [], - "sequence_id": 47.0, + "sequence_id": 3.0, "shortcuts": [], "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "house", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Payments", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, { "child": 0, "collapsible": 1, @@ -161,6 +175,180 @@ "show_arrow": 0, "type": "Link" }, + { + "child": 0, + "collapsible": 1, + "icon": "circle-dollar-sign", + "indent": 1, + "keep_closed": 0, + "label": "Banking", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "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": 1, + "collapsible": 1, + "icon": "wrench", + "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": 1, + "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": "database", + "indent": 1, + "keep_closed": 1, + "label": "Banking 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": "receipt-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" + }, { "child": 0, "collapsible": 1, diff --git a/erpnext/accounts/workspace/share_management/share_management.json b/erpnext/accounts/workspace/share_management/share_management.json deleted file mode 100644 index c48bec275ce..00000000000 --- a/erpnext/accounts/workspace/share_management/share_management.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "app": "erpnext", - "charts": [], - "content": "[]", - "creation": "2026-06-11 11:51:22.831729", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "coins", - "idx": 0, - "indicator_color": "green", - "is_hidden": 0, - "label": "Share Management", - "link_type": "DocType", - "links": [], - "modified": "2026-07-03 13:43:51.040978", - "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": "user", - "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/subscriptions/subscriptions.json b/erpnext/accounts/workspace/subscriptions/subscriptions.json deleted file mode 100644 index f97c4a09b95..00000000000 --- a/erpnext/accounts/workspace/subscriptions/subscriptions.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "app": "erpnext", - "charts": [], - "content": "[]", - "creation": "2026-06-14 14:08:36.817393", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "wallet", - "idx": 0, - "indicator_color": "green", - "is_hidden": 0, - "label": "Subscriptions", - "link_type": "DocType", - "links": [], - "modified": "2026-07-03 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 deleted file mode 100644 index e94bacb66d3..00000000000 --- a/erpnext/accounts/workspace/taxes/taxes.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "app": "erpnext", - "charts": [], - "content": "[]", - "creation": "2026-06-11 11:51:22.649582", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "coins", - "idx": 0, - "indicator_color": "green", - "is_hidden": 0, - "label": "Taxes", - "link_type": "DocType", - "links": [], - "modified": "2026-07-03 13:43:50.894825", - "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": "package", - "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/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json index cfd480b3312..4fdfd1fe342 100644 --- a/erpnext/buying/workspace/buying/buying.json +++ b/erpnext/buying/workspace/buying/buying.json @@ -501,7 +501,7 @@ "type": "Link" } ], - "modified": "2026-07-03 13:43:50.509039", + "modified": "2026-07-14 12:00:00.000000", "modified_by": "Administrator", "module": "Buying", "module_onboarding": "Buying Onboarding", @@ -754,6 +754,83 @@ "show_arrow": 0, "type": "Link" }, + { + "child": 0, + "collapsible": 1, + "icon": "rocket", + "indent": 1, + "keep_closed": 1, + "label": "Subcontracting", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "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": "", + "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": 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, @@ -910,6 +987,45 @@ "show_arrow": 0, "type": "Link" }, + { + "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, diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 57e558c0e7d..d930956d516 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-07-03 13:43:50.429297", + "modified": "2026-07-14 12:00:00.000000", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -355,6 +355,116 @@ "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "building-2", + "indent": 1, + "keep_closed": 1, + "label": "Organization", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 1, + "icon": "building-2", + "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, + "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": 1, + "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": 1, + "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": 1, + "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": 1, + "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": 1, + "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" } ], "standard": 1, diff --git a/erpnext/setup/workspace/organization/organization.json b/erpnext/setup/workspace/organization/organization.json deleted file mode 100644 index 45ca544db31..00000000000 --- a/erpnext/setup/workspace/organization/organization.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "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": "[]", - "creation": "2026-06-11 11:51:21.789012", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "building-2", - "idx": 0, - "indicator_color": "green", - "is_hidden": 0, - "label": "Organization", - "link_type": "DocType", - "links": [], - "modified": "2026-07-03 00:45:57.595188", - "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, - "default_workspace": 1, - "icon": "building-2", - "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" - } - ], - "standard": 1, - "title": "Organization", - "type": "Workspace" -} diff --git a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json deleted file mode 100644 index 672d6ae28fc..00000000000 --- a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json +++ /dev/null @@ -1,415 +0,0 @@ -{ - "app": "erpnext", - "charts": [ - { - "chart_name": "Subcontracting Order", - "label": "Subcontracting Outward Order" - } - ], - "content": "[{\"id\":\"ednT7K5OAg\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Subcontracting Outward Order\",\"col\":12}},{\"id\":\"IlzVs7JD8u\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Subcontracting Outward Order Count\",\"col\":4}},{\"id\":\"wB9idWUvTB\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Subcontracting Inward Order Count\",\"col\":4}},{\"id\":\"4QwMfBRGk8\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Active Subcontracted Items\",\"col\":4}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Subcontracting Inward and Outward\",\"col\":12}},{\"id\":\"PXXMxfhCfA\",\"type\":\"card\",\"data\":{\"card_name\":\"Subcontracting Inward Order\",\"col\":4}},{\"id\":\"ir3NsTvngO\",\"type\":\"card\",\"data\":{\"card_name\":\"Subcontracting Outward Order\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", - "creation": "2020-03-02 17:11:37.032604", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "rocket", - "idx": 2, - "is_hidden": 0, - "label": "Subcontracting", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 0, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 1, - "label": "Subcontract Order Summary", - "link_count": 0, - "link_to": "Subcontract Order Summary", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Subcontracted Item To Be Received", - "link_count": 0, - "link_to": "Subcontracted Item To Be Received", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 1, - "label": "Subcontracted Raw Materials To Be Transferred", - "link_count": 0, - "link_to": "Subcontracted Raw Materials To Be Transferred", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Subcontracting Inward Order", - "link_count": 3, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Sales Order", - "link_count": 0, - "link_to": "Sales Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Subcontracting Inward Order", - "link_count": 0, - "link_to": "Subcontracting Inward Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Subcontracting Delivery", - "link_count": 0, - "link_to": "Stock Entry", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Subcontracting Outward Order", - "link_count": 3, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Purchase Order", - "link_count": 0, - "link_to": "Purchase Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Subcontracting Outward Order", - "link_count": 0, - "link_to": "Subcontracting Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Subcontracting Receipt", - "link_count": 0, - "link_to": "Subcontracting Receipt", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2026-07-03 13:43:50.289920", - "modified_by": "Administrator", - "module": "Subcontracting", - "module_onboarding": "Subcontracting Onboarding", - "name": "Subcontracting", - "number_cards": [ - { - "label": "Subcontracting Outward Order Count", - "number_card_name": "Subcontracting Outward Order Count" - }, - { - "label": "Active Subcontracted Items", - "number_card_name": "Active Subcontracted Items" - }, - { - "label": "Subcontracting Inward Order Count", - "number_card_name": "Subcontracting Inward Order Count" - } - ], - "owner": "Administrator", - "parent_page": "", - "public": 1, - "quick_lists": [], - "restrict_to_domain": "", - "roles": [], - "sequence_id": 8.0, - "shortcuts": [], - "sidebar_items": [ - { - "child": 0, - "collapsible": 1, - "icon": "house", - "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/workspace_sidebar/accounts_setup.json b/erpnext/workspace_sidebar/accounts_setup.json deleted file mode 100644 index 93a436ee15b..00000000000 --- a/erpnext/workspace_sidebar/accounts_setup.json +++ /dev/null @@ -1,312 +0,0 @@ -{ - "app": "erpnext", - "creation": "2026-01-23 14:36:51.659571", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "database", - "idx": 1, - "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" - } - ], - "modified": "2026-06-12 14:50:50.262533", - "modified_by": "Administrator", - "module": "Accounts", - "module_onboarding": "Accounting Onboarding", - "name": "Accounts Setup", - "owner": "Administrator", - "standard": 1, - "title": "Accounts Setup" -} diff --git a/erpnext/workspace_sidebar/banking.json b/erpnext/workspace_sidebar/banking.json deleted file mode 100644 index 90578e81f80..00000000000 --- a/erpnext/workspace_sidebar/banking.json +++ /dev/null @@ -1,190 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-12 14:55:28.092635", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "circle-dollar-sign", - "idx": 0, - "items": [ - { - "child": 0, - "collapsible": 1, - "icon": "book-open-check", - "indent": 0, - "keep_closed": 0, - "label": "Bank Clearance", - "link_to": "Bank Clearance", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "wrench", - "indent": 0, - "keep_closed": 0, - "label": "Bank Reconciliation", - "link_to": "Bank Reconciliation Tool", - "link_type": "DocType", - "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", - "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", - "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", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "database", - "indent": 1, - "keep_closed": 1, - "label": "Setup", - "link_type": "DocType", - "show_arrow": 0, - "type": "Section Break" - }, - { - "child": 1, - "collapsible": 1, - "icon": "", - "indent": 0, - "keep_closed": 0, - "label": "Bank", - "link_to": "Bank", - "link_type": "DocType", - "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", - "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", - "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", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Bank Guarantee", - "link_to": "Bank Guarantee", - "link_type": "DocType", - "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", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "scroll-text", - "indent": 1, - "keep_closed": 1, - "label": "Dunning", - "link_type": "DocType", - "show_arrow": 0, - "type": "Section Break" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Dunning", - "link_to": "Dunning", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Dunning Type", - "link_to": "Dunning Type", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - } - ], - "modified": "2026-07-03 00:06:13.017457", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Banking", - "owner": "Administrator", - "standard": 1, - "title": "Banking" -} diff --git a/erpnext/workspace_sidebar/budgeting.json b/erpnext/workspace_sidebar/budgeting.json deleted file mode 100644 index 3da32884f8d..00000000000 --- a/erpnext/workspace_sidebar/budgeting.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-10 16:53:45.409587", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "accounting", - "idx": 0, - "items": [ - { - "child": 0, - "collapsible": 1, - "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, - "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, - "icon": "wallet", - "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, - "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, - "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" - } - ], - "modified": "2026-07-03 00:06:13.032297", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Budgeting", - "owner": "Administrator", - "standard": 1, - "title": "Budgeting" -} diff --git a/erpnext/workspace_sidebar/organization.json b/erpnext/workspace_sidebar/organization.json deleted file mode 100644 index ab3f8470cb9..00000000000 --- a/erpnext/workspace_sidebar/organization.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "app": "erpnext", - "creation": "2026-02-24 17:39:43.793115", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "organization", - "idx": 1, - "items": [ - { - "child": 0, - "collapsible": 1, - "default_workspace": 1, - "icon": "building-2", - "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-07-03 00:37:22.942285", - "modified_by": "Administrator", - "module": "Setup", - "module_onboarding": "Organization Onboarding", - "name": "Organization", - "owner": "Administrator", - "standard": 1, - "title": "Organization" -} diff --git a/erpnext/workspace_sidebar/share_management.json b/erpnext/workspace_sidebar/share_management.json deleted file mode 100644 index 34ab0ef0db0..00000000000 --- a/erpnext/workspace_sidebar/share_management.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-10 16:49:07.269956", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "", - "idx": 0, - "items": [ - { - "child": 1, - "collapsible": 1, - "icon": "user", - "indent": 0, - "keep_closed": 0, - "label": "Shareholder", - "link_to": "Shareholder", - "link_type": "DocType", - "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", - "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", - "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", - "show_arrow": 0, - "type": "Link" - } - ], - "modified": "2026-07-03 00:06:13.040767", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Share Management", - "owner": "Administrator", - "standard": 1, - "title": "Share Management" -} diff --git a/erpnext/workspace_sidebar/subcontracting.json b/erpnext/workspace_sidebar/subcontracting.json deleted file mode 100644 index 5587e19f608..00000000000 --- a/erpnext/workspace_sidebar/subcontracting.json +++ /dev/null @@ -1,241 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-17 14:49:59.811213", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "getting-started", - "idx": 0, - "items": [ - { - "child": 0, - "collapsible": 1, - "icon": "house", - "indent": 0, - "keep_closed": 0, - "label": "Home", - "link_to": "Subcontracting", - "link_type": "Workspace", - "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", - "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", - "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", - "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", - "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", - "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", - "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", - "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", - "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", - "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", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "database", - "indent": 1, - "keep_closed": 0, - "label": "Setup", - "link_type": "DocType", - "show_arrow": 0, - "type": "Section Break" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Item", - "link_to": "Item", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Bill of Materials", - "link_to": "BOM", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "notepad-text", - "indent": 1, - "keep_closed": 1, - "label": "Reports", - "link_type": "DocType", - "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", - "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", - "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", - "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", - "show_arrow": 0, - "type": "Link" - } - ], - "modified": "2026-07-03 20:22:17.130321", - "modified_by": "Administrator", - "module": "Buying", - "module_onboarding": "Subcontracting Onboarding", - "name": "Subcontracting", - "owner": "Administrator", - "standard": 1, - "title": "Subcontracting" -} diff --git a/erpnext/workspace_sidebar/subscriptions.json b/erpnext/workspace_sidebar/subscriptions.json deleted file mode 100644 index ec188edf169..00000000000 --- a/erpnext/workspace_sidebar/subscriptions.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-10 16:08:50.904116", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "accounting", - "idx": 0, - "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" - } - ], - "modified": "2026-01-10 00:06:13.048591", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Subscriptions", - "owner": "Administrator", - "standard": 1, - "title": "Subscriptions" -} diff --git a/erpnext/workspace_sidebar/taxes.json b/erpnext/workspace_sidebar/taxes.json deleted file mode 100644 index 09061ee1452..00000000000 --- a/erpnext/workspace_sidebar/taxes.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-12 15:03:06.180114", - "docstatus": 0, - "doctype": "Workspace Sidebar", - "header_icon": "money-coins-1", - "idx": 0, - "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": "", - "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", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "package", - "indent": 0, - "keep_closed": 0, - "label": "Item Tax Template", - "link_to": "Item Tax Template", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "database", - "indent": 1, - "keep_closed": 1, - "label": "Setup", - "link_type": "DocType", - "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", - "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", - "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", - "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", - "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", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "sheet", - "indent": 1, - "keep_closed": 1, - "label": "Reports", - "link_to": "", - "link_type": "DocType", - "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", - "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", - "show_arrow": 0, - "type": "Link" - } - ], - "modified": "2026-07-03 18:36:08.105306", - "modified_by": "Administrator", - "module": "Accounts", - "module_onboarding": "Accounting Onboarding", - "name": "Taxes", - "owner": "Administrator", - "standard": 1, - "title": "Taxes" -} From 2d6f89a7f58856b7ee52b646f696adac2483331c Mon Sep 17 00:00:00 2001 From: SandraFrappe Date: Tue, 14 Jul 2026 14:32:06 +0530 Subject: [PATCH 06/21] fix: prevent duplicate material request items in purchase order --- .../doctype/purchase_order/purchase_order.py | 1 + .../purchase_order/test_purchase_order.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index be27000db2b..0a28177ba74 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -259,6 +259,7 @@ class PurchaseOrder(BuyingController): "ref_dn_field": "material_request_item", "compare_fields": mri_compare_fields, "is_child_table": True, + "allow_duplicate_prev_row_id": True, }, } ) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 37ccf275cdc..dbb3a38676c 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -220,6 +220,23 @@ class TestPurchaseOrder(ERPNextTestSuite): frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0) frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) + def test_duplicate_material_request_item_row_allowed(self): + """Splitting a Material Request Item's qty across multiple PO rows must be + allowed, mirroring how Sales Order allows duplicate Quotation Item rows.""" + mr = make_material_request(qty=10) + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + + duplicate_row = po.items[0].as_dict() + duplicate_row.qty = 4 + po.items[0].qty = 6 + + po.append("items", duplicate_row) + po.save() + + self.assertEqual(len(po.items), 2) + self.assertEqual(po.items[0].material_request_item, po.items[1].material_request_item) + def test_update_remove_child_linked_to_mr(self): """Test impact on linked PO and MR on deleting/updating row.""" mr = make_material_request(qty=10) From b6cce627a8cfd7bfac24d5402cac2a84314ffc1a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 14 Jul 2026 16:00:47 +0530 Subject: [PATCH 07/21] feat: company-wise restriction for Item, Customer and Supplier masters (#57124) --- erpnext/buying/doctype/supplier/supplier.js | 3 + erpnext/buying/doctype/supplier/supplier.json | 18 ++- erpnext/buying/doctype/supplier/supplier.py | 4 + erpnext/hooks.py | 12 ++ erpnext/selling/doctype/customer/customer.js | 3 + .../selling/doctype/customer/customer.json | 25 +++- erpnext/selling/doctype/customer/customer.py | 4 + .../global_defaults/global_defaults.json | 33 ++++- .../global_defaults/global_defaults.py | 2 + .../doctype/company_restriction/__init__.py | 0 .../company_restriction.json | 39 ++++++ .../company_restriction.py | 116 ++++++++++++++++++ erpnext/stock/doctype/item/item.js | 3 + erpnext/stock/doctype/item/item.json | 31 +++-- erpnext/stock/doctype/item/item.py | 4 + 15 files changed, 282 insertions(+), 15 deletions(-) create mode 100644 erpnext/stock/doctype/company_restriction/__init__.py create mode 100644 erpnext/stock/doctype/company_restriction/company_restriction.json create mode 100644 erpnext/stock/doctype/company_restriction/company_restriction.py diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 4d2d64cfcc1..acdbed969e8 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -3,6 +3,9 @@ frappe.ui.form.on("Supplier", { setup: function (frm) { + frm.set_query("allowed_companies", () => ({ + query: "erpnext.stock.doctype.company_restriction.company_restriction.company_query", + })); frm.set_query("default_price_list", { buying: 1 }); if (frm.doc.__islocal == 1) { frm.set_value("represents_company", ""); diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 12a40cbca7b..caee355c57c 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -54,6 +54,8 @@ "tax_withholding_category", "tax_withholding_group", "settings_tab", + "company_restrictions_section", + "allowed_companies", "invoice_settings_section", "is_transporter", "allow_purchase_invoice_creation_without_purchase_order", @@ -425,6 +427,20 @@ "fieldtype": "Tab Break", "label": "Settings" }, + { + "fieldname": "company_restrictions_section", + "fieldtype": "Section Break", + "label": "Company Restrictions", + "description": "If set, this Supplier is only available for transactions in the listed companies. Leave empty for no restriction.", + "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + }, + { + "fieldname": "allowed_companies", + "fieldtype": "Table MultiSelect", + "label": "Allowed Companies", + "options": "Company Restriction", + "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + }, { "fieldname": "contact_and_address_tab", "fieldtype": "Tab Break", @@ -562,7 +578,7 @@ "link_fieldname": "party" } ], - "modified": "2026-06-27 16:12:33.190257", + "modified": "2026-07-14 21:00:00.000000", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 1de54ed9313..dfed0be4198 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -17,6 +17,7 @@ from erpnext.accounts.party import ( validate_party_currency_before_merging, ) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user +from erpnext.stock.doctype.company_restriction.company_restriction import validate_allowed_companies from erpnext.utilities.transaction_base import TransactionBase @@ -36,12 +37,14 @@ class Supplier(TransactionBase): from erpnext.buying.doctype.customer_number_at_supplier.customer_number_at_supplier import ( CustomerNumberAtSupplier, ) + from erpnext.stock.doctype.company_restriction.company_restriction import CompanyRestriction from erpnext.utilities.doctype.portal_user.portal_user import PortalUser accounts: DF.Table[PartyAccount] alias: DF.Data | None allow_purchase_invoice_creation_without_purchase_order: DF.Check allow_purchase_invoice_creation_without_purchase_receipt: DF.Check + allowed_companies: DF.TableMultiSelect[CompanyRestriction] companies: DF.Table[AllowedToTransactWith] country: DF.Link | None customer_numbers: DF.Table[CustomerNumberAtSupplier] @@ -146,6 +149,7 @@ class Supplier(TransactionBase): self.validate_internal_supplier() self.add_role_for_user() self.validate_currency_for_receivable_payable_and_advance_account() + validate_allowed_companies(self) @frappe.whitelist() def get_supplier_group_details(self): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e783d9e0fc4..38fb883d3a3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -307,6 +307,18 @@ sounds = [ has_upload_permission = {"Employee": "erpnext.setup.doctype.employee.employee.has_upload_permission"} +permission_query_conditions = { + "Item": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions", + "Customer": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions", + "Supplier": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions", +} + +has_permission = { + "Item": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission", + "Customer": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission", + "Supplier": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission", +} + has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", "Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index a21cc00b991..5ee6dd871c2 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -3,6 +3,9 @@ frappe.ui.form.on("Customer", { setup: function (frm) { + frm.set_query("allowed_companies", () => ({ + query: "erpnext.stock.doctype.company_restriction.company_restriction.company_query", + })); frm.custom_make_buttons = { Opportunity: "Opportunity", Quotation: "Quotation", diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 6dd308d319d..848d99e6a8f 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -4,7 +4,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", - "creation": "2013-06-11 14:26:44", + "creation": "2026-07-14 12:46:50.256889", "description": "Buyer of Goods and Services.", "doctype": "DocType", "document_type": "Setup", @@ -65,6 +65,9 @@ "tax_withholding_group", "tax_withholding_category", "settings_tab", + "company_restrictions_section", + "allowed_companies", + "section_break_ario", "so_required", "dn_required", "column_break_53", @@ -512,6 +515,20 @@ "fieldtype": "Tab Break", "label": "Settings" }, + { + "description": "If set, this Customer is only available for transactions in the listed companies. Leave empty for no restriction.", + "fieldname": "company_restrictions_section", + "fieldtype": "Section Break", + "label": "Company Restrictions", + "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + }, + { + "fieldname": "allowed_companies", + "fieldtype": "Table MultiSelect", + "label": "Allowed Companies", + "options": "Company Restriction", + "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + }, { "collapsible": 1, "collapsible_depends_on": "default_sales_partner", @@ -683,6 +700,10 @@ "label": "Alias", "no_copy": 1, "unique": 1 + }, + { + "fieldname": "section_break_ario", + "fieldtype": "Section Break" } ], "icon": "fa fa-user", @@ -696,7 +717,7 @@ "link_fieldname": "party" } ], - "modified": "2026-06-27 16:12:10.457900", + "modified": "2026-07-14 21:00:00.000000", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index fd16c5d7aed..064e3068716 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -25,6 +25,7 @@ from erpnext.accounts.party import ( validate_party_currency_before_merging, ) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user +from erpnext.stock.doctype.company_restriction.company_restriction import validate_allowed_companies from erpnext.utilities.transaction_base import TransactionBase from .mapper import ( @@ -51,11 +52,13 @@ class Customer(TransactionBase): from erpnext.selling.doctype.supplier_number_at_customer.supplier_number_at_customer import ( SupplierNumberAtCustomer, ) + from erpnext.stock.doctype.company_restriction.company_restriction import CompanyRestriction from erpnext.utilities.doctype.portal_user.portal_user import PortalUser account_manager: DF.Link | None accounts: DF.Table[PartyAccount] alias: DF.Data | None + allowed_companies: DF.TableMultiSelect[CompanyRestriction] companies: DF.Table[AllowedToTransactWith] credit_limits: DF.Table[CustomerCreditLimit] customer_details: DF.Text | None @@ -186,6 +189,7 @@ class Customer(TransactionBase): self.validate_internal_customer() self.add_role_for_user() self.validate_currency_for_receivable_payable_and_advance_account() + validate_allowed_companies(self) # set loyalty program tier if not self.is_new() and (customer := self.get_doc_before_save()): diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index 55ff08d21fe..908da5ff912 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.json +++ b/erpnext/setup/doctype/global_defaults/global_defaults.json @@ -5,16 +5,20 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "defaults_section", "default_company", "country", - "default_distance_unit", "column_break_8", "default_currency", + "default_distance_unit", + "demo_company", + "general_settings_section", "hide_currency_symbol", "disable_rounded_total", "disable_in_words", + "column_break_hnew", "use_posting_datetime_for_naming_documents", - "demo_company" + "enable_company_wise_masters" ], "fields": [ { @@ -27,7 +31,7 @@ { "fieldname": "country", "fieldtype": "Link", - "label": "Country", + "label": "Default Country", "options": "Country" }, { @@ -88,6 +92,27 @@ "fieldname": "use_posting_datetime_for_naming_documents", "fieldtype": "Check", "label": "Use Posting Datetime for Naming Documents" + }, + { + "default": "0", + "description": "When enabled, Supplier, Customer, and Item records can be restricted to specific companies via their Allowed Companies table. Transactions will only show masters configured for the selected company.", + "fieldname": "enable_company_wise_masters", + "fieldtype": "Check", + "label": "Enable Company-wise Master Filtering" + }, + { + "fieldname": "defaults_section", + "fieldtype": "Section Break", + "label": "Defaults" + }, + { + "fieldname": "general_settings_section", + "fieldtype": "Section Break", + "label": "General Settings" + }, + { + "fieldname": "column_break_hnew", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -96,7 +121,7 @@ "in_create": 1, "issingle": 1, "links": [], - "modified": "2026-07-14 13:37:46.177444", + "modified": "2026-07-14 15:18:25.829886", "modified_by": "Administrator", "module": "Setup", "name": "Global Defaults", diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index a85b04530b0..911888b095f 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -18,6 +18,7 @@ keydict = { "account_url": "account_url", "disable_rounded_total": "disable_rounded_total", "disable_in_words": "disable_in_words", + "enable_company_wise_masters": "enable_company_wise_masters", } ROUNDED_TOTAL_DOCTYPES = ( @@ -51,6 +52,7 @@ class GlobalDefaults(Document): demo_company: DF.Link | None disable_in_words: DF.Check disable_rounded_total: DF.Check + enable_company_wise_masters: DF.Check hide_currency_symbol: DF.Literal["", "No", "Yes"] use_posting_datetime_for_naming_documents: DF.Check # end: auto-generated types diff --git a/erpnext/stock/doctype/company_restriction/__init__.py b/erpnext/stock/doctype/company_restriction/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/company_restriction/company_restriction.json b/erpnext/stock/doctype/company_restriction/company_restriction.json new file mode 100644 index 00000000000..2c7c0c804cf --- /dev/null +++ b/erpnext/stock/doctype/company_restriction/company_restriction.json @@ -0,0 +1,39 @@ +{ + "actions": [], + "allow_bulk_edit": 1, + "allow_rename": 1, + "creation": "2026-07-13 21:39:49.805859", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company" + ], + "fields": [ + { + "allow_on_submit": 1, + "fieldname": "company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-07-14 00:15:00.000000", + "modified_by": "Administrator", + "module": "Stock", + "name": "Company Restriction", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/stock/doctype/company_restriction/company_restriction.py b/erpnext/stock/doctype/company_restriction/company_restriction.py new file mode 100644 index 00000000000..6b995e75a33 --- /dev/null +++ b/erpnext/stock/doctype/company_restriction/company_restriction.py @@ -0,0 +1,116 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from pypika.terms import Bracket, ExistsCriterion + + +class CompanyRestriction(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + company: DF.Link + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + +def get_allowed_companies(user, doctype): + from frappe.permissions import get_allowed_docs_for_doctype, get_user_permissions + + if not frappe.get_single_value("Global Defaults", "enable_company_wise_masters"): + return None + + user_permissions = get_user_permissions(user or frappe.session.user) + if "Company" not in user_permissions: + return None + return get_allowed_docs_for_doctype(user_permissions["Company"], doctype) or None + + +def get_permission_query_conditions(user, doctype=None): + if not doctype: + return None + + allowed_companies = get_allowed_companies(user, doctype) + if not allowed_companies: + return None + + parent = frappe.qb.DocType(doctype) + restriction = frappe.qb.DocType("Company Restriction") + restriction_rows = ( + frappe.qb.from_(restriction) + .select(restriction.name) + .where( + (restriction.parenttype == doctype) + & (restriction.parentfield == "allowed_companies") + & (restriction.parent == parent.name) + ) + ) + allowed_rows = restriction_rows.where(restriction.company.isin(allowed_companies)) + return Bracket(ExistsCriterion(allowed_rows) | ExistsCriterion(restriction_rows).negate()) + + +def has_permission(doc, ptype=None, user=None): + allowed_companies = get_allowed_companies(user, doc.doctype) + if not allowed_companies: + return True + + companies = [row.company for row in doc.get("allowed_companies") or []] + if not companies: + return True + return any(company in allowed_companies for company in companies) + + +def validate_allowed_companies(doc): + if doc.flags.ignore_permissions: + return + + allowed_companies = get_allowed_companies(frappe.session.user, doc.doctype) + if not allowed_companies: + return + + previous_companies = set() + if previous_doc := doc.get_doc_before_save(): + previous_companies = {row.company for row in previous_doc.get("allowed_companies") or []} + + current_companies = {row.company for row in doc.get("allowed_companies") or []} + for company in current_companies.symmetric_difference(previous_companies): + if company not in allowed_companies: + frappe.throw( + _("You are not permitted to add or remove Company {0} in Allowed Companies").format(company), + frappe.PermissionError, + ) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def company_query( + doctype: str, + txt: str, + searchfield: str, + start: int, + page_len: int, + filters: dict | str | None = None, +): + filters = frappe.parse_json(filters) if filters else {} + if isinstance(filters, list): + filters.append(["Company", "name", "like", f"%{txt}%"]) + else: + filters["name"] = ("like", f"%{txt}%") + + return frappe.get_list( + "Company", + filters=filters, + limit_start=start, + limit_page_length=page_len, + order_by="name", + as_list=True, + ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index b20f53f74e7..eb8034b57c4 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -59,6 +59,9 @@ frappe.ui.form.on("Item", { }, setup: function (frm) { + frm.set_query("allowed_companies", () => ({ + query: "erpnext.stock.doctype.company_restriction.company_restriction.company_query", + })); frm.add_fetch("attribute", "numeric_values", "numeric_values"); frm.add_fetch("attribute", "from_range", "from_range"); frm.add_fetch("attribute", "to_range", "to_range"); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 6da7ec333b9..81975cd50f1 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "field:item_code", - "creation": "2026-02-02 14:41:23.105228", + "creation": "2026-07-13 23:00:47.512490", "description": "A Product or a Service that is bought, sold or kept in stock.", "doctype": "DocType", "document_type": "Setup", @@ -40,6 +40,8 @@ "over_delivery_receipt_allowance", "column_break_wugd", "over_billing_allowance", + "company_restrictions_section", + "allowed_companies", "section_break_11", "brand", "description", @@ -240,7 +242,6 @@ "description": "ERPNext will make a stock ledger entry for each transaction of this item. Keep unchecked for non-stock or service items.", "fieldname": "is_stock_item", "fieldtype": "Check", - "in_list_view": 0, "label": "Maintain Stock", "oldfieldname": "is_stock_item", "oldfieldtype": "Select", @@ -281,9 +282,9 @@ "description": "Enable if this item is a company asset like machinery or furniture.", "fieldname": "is_fixed_asset", "fieldtype": "Check", + "in_list_view": 1, "label": "Is Fixed Asset", - "read_only_depends_on": "eval:doc.is_stock_item", - "in_list_view": 1 + "read_only_depends_on": "eval:doc.is_stock_item" }, { "allow_in_quick_entry": 1, @@ -596,7 +597,7 @@ "oldfieldtype": "Currency" }, { - "description": "Minimum stock level to maintain as a buffer. Used to calculate recommended reorder level: Reorder Level = Safety Stock + (Average Daily Consumption × Lead Time).", + "description": "Minimum stock level to maintain as a buffer. Used to calculate recommended reorder level: Reorder Level = Safety Stock + (Average Daily Consumption \u00d7 Lead Time).", "fieldname": "safety_stock", "fieldtype": "Float", "label": "Safety Stock", @@ -699,9 +700,9 @@ "description": "Allow this item to be used in sales transactions.", "fieldname": "is_sales_item", "fieldtype": "Check", + "in_list_view": 1, "label": "Allow Sales", - "show_description_on_click": 1, - "in_list_view": 1 + "show_description_on_click": 1 }, { "fieldname": "column_break3", @@ -1084,6 +1085,20 @@ "fieldname": "item_prices_column", "fieldtype": "Column Break", "label": "Item Prices" + }, + { + "fieldname": "company_restrictions_section", + "fieldtype": "Section Break", + "label": "Company Restrictions", + "description": "If set, this Item is only available for transactions in the listed companies. Leave empty for no restriction.", + "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + }, + { + "fieldname": "allowed_companies", + "fieldtype": "Table MultiSelect", + "label": "Allowed Companies", + "options": "Company Restriction", + "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" } ], "icon": "fa fa-tag", @@ -1091,7 +1106,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-07-05 23:24:45.734144", + "modified": "2026-07-14 21:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3fff0cb1c28..1fc62169daa 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -30,6 +30,7 @@ from erpnext.controllers.item_variant import ( make_variant_item_code, validate_item_variant_attributes, ) +from erpnext.stock.doctype.company_restriction.company_restriction import validate_allowed_companies from erpnext.stock.doctype.item_default.item_default import ItemDefault from erpnext.stock.serial_batch_bundle import SerialBatchCreation from erpnext.stock.utils import get_valuation_method @@ -60,6 +61,7 @@ class Item(Document): if TYPE_CHECKING: from frappe.types import DF + from erpnext.stock.doctype.company_restriction.company_restriction import CompanyRestriction from erpnext.stock.doctype.item_barcode.item_barcode import ItemBarcode from erpnext.stock.doctype.item_customer_detail.item_customer_detail import ItemCustomerDetail from erpnext.stock.doctype.item_default.item_default import ItemDefault @@ -71,6 +73,7 @@ class Item(Document): allow_alternative_item: DF.Check allow_negative_stock: DF.Check + allowed_companies: DF.TableMultiSelect[CompanyRestriction] asset_category: DF.Link | None asset_naming_series: DF.Literal[None] attributes: DF.Table[ItemVariantAttribute] @@ -242,6 +245,7 @@ class Item(Document): self.validate_serialized_change_with_bundle() self.validate_standard_cost_change() self.validate_item_tax_net_rate_range() + validate_allowed_companies(self) if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") From f2e8c7b664f96073702cb84be7f019292e51ab4b Mon Sep 17 00:00:00 2001 From: sokumon Date: Tue, 14 Jul 2026 16:05:44 +0530 Subject: [PATCH 08/21] fix: add sequence for erpnext --- .../workspace/accounting/accounting.json | 95 +++++++++++++++++-- erpnext/hooks.py | 1 + 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index 4af2a59f5de..e7dcefb59f3 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -1,8 +1,29 @@ { "app": "erpnext", - "charts": [], - "content": "[]", - "creation": "2026-07-14 12:00:00.000000", + "charts": [ + { + "chart_name": "Profit and Loss", + "label": "Profit and Loss" + }, + { + "chart_name": "Accounts Receivable Ageing", + "label": "Accounts Receivable Ageing" + }, + { + "chart_name": "Accounts Payable Ageing", + "label": "Accounts Payable Ageing" + }, + { + "chart_name": "Bank Balance", + "label": "Bank Balance" + }, + { + "chart_name": "Budget Variance", + "label": "Budget Variance" + } + ], + "content": "[{\"id\":\"acc_ov_hdr1\",\"type\":\"header\",\"data\":{\"text\":\"Accounting Overview\",\"col\":12}},{\"id\":\"acc_ov_nc01\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Bills\",\"col\":3}},{\"id\":\"acc_ov_nc02\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Bills\",\"col\":3}},{\"id\":\"acc_ov_nc03\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Payment\",\"col\":3}},{\"id\":\"acc_ov_nc04\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Payment\",\"col\":3}},{\"id\":\"acc_ov_ch01\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"acc_ov_ch02\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Accounts Receivable Ageing\",\"col\":6}},{\"id\":\"acc_ov_ch03\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Accounts Payable Ageing\",\"col\":6}},{\"id\":\"acc_ov_ch04\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Bank Balance\",\"col\":6}},{\"id\":\"acc_ov_ch05\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Budget Variance\",\"col\":6}}]", + "creation": "2026-07-14 12:00:00", "custom_blocks": [], "docstatus": 0, "doctype": "Workspace", @@ -15,12 +36,29 @@ "label": "Accounting", "link_type": "DocType", "links": [], - "modified": "2026-07-14 12:00:00.000000", + "modified": "2026-07-14 14:28:55.763394", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", "name": "Accounting", - "number_cards": [], + "number_cards": [ + { + "label": "Outgoing Bills", + "number_card_name": "Total Outgoing Bills" + }, + { + "label": "Incoming Bills", + "number_card_name": "Total Incoming Bills" + }, + { + "label": "Incoming Payment", + "number_card_name": "Total Incoming Payment" + }, + { + "label": "Outgoing Payment", + "number_card_name": "Total Outgoing Payment" + } + ], "owner": "Administrator", "public": 1, "quick_lists": [], @@ -45,6 +83,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "database", "indent": 1, "keep_closed": 0, @@ -57,6 +96,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Chart of Accounts", @@ -69,6 +109,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Chart of Cost Centers", @@ -81,6 +122,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Account Category", @@ -93,6 +135,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Accounting Dimension", @@ -105,6 +148,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Currency", @@ -117,6 +161,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Currency Exchange", @@ -129,6 +174,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Finance Book", @@ -141,6 +187,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Mode of Payment", @@ -153,6 +200,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Payment Term", @@ -165,6 +213,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Journal Entry Template", @@ -177,6 +226,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Terms and Conditions", @@ -189,6 +239,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Company", @@ -201,6 +252,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Fiscal Year", @@ -213,6 +265,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-open-check", "indent": 1, "keep_closed": 1, @@ -225,6 +278,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -238,6 +292,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -251,6 +306,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -264,6 +320,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -277,6 +334,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -290,6 +348,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "coins", "indent": 1, "keep_closed": 1, @@ -302,6 +361,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "panel-bottom-close", "indent": 0, "keep_closed": 0, @@ -316,6 +376,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "panel-top-close", "indent": 0, "keep_closed": 0, @@ -329,6 +390,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "package", "indent": 0, "keep_closed": 0, @@ -342,6 +404,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "triangle", "indent": 0, "keep_closed": 0, @@ -355,6 +418,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "book-open-text", "indent": 0, "keep_closed": 0, @@ -368,6 +432,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "book-text", "indent": 0, "keep_closed": 0, @@ -381,6 +446,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Tax Withholding Group", @@ -393,6 +459,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "notebook-text", "indent": 0, "keep_closed": 0, @@ -406,6 +473,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "wallet", "indent": 1, "keep_closed": 1, @@ -446,6 +514,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "coins", "indent": 1, "keep_closed": 1, @@ -458,6 +527,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "user", "indent": 0, "keep_closed": 0, @@ -471,6 +541,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "move-horizontal", "indent": 0, "keep_closed": 0, @@ -484,6 +555,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "repeat", "indent": 1, "keep_closed": 1, @@ -496,6 +568,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "circle-dollar-sign", "indent": 0, "keep_closed": 0, @@ -509,6 +582,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "receipt-text", "indent": 0, "keep_closed": 0, @@ -522,6 +596,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "settings", "indent": 0, "keep_closed": 0, @@ -535,6 +610,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "sheet", "indent": 1, "keep_closed": 1, @@ -547,6 +623,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "TDS Computation Summary", @@ -559,6 +636,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Tax Withholding Details", @@ -585,6 +663,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "list", "indent": 0, "keep_closed": 0, @@ -598,6 +677,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "notepad-text", "indent": 0, "keep_closed": 0, @@ -611,6 +691,7 @@ { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "wrench", "indent": 1, "keep_closed": 1, @@ -623,6 +704,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "icon": "", "indent": 0, "keep_closed": 0, @@ -636,6 +718,7 @@ { "child": 1, "collapsible": 1, + "default_workspace": 0, "indent": 0, "keep_closed": 0, "label": "Currency Exchange Settings", @@ -647,6 +730,6 @@ } ], "standard": 1, - "title": "Accounts Setup", + "title": "Accounting", "type": "Workspace" } diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e783d9e0fc4..1d353ed6b09 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -17,6 +17,7 @@ add_to_apps_screen = [ "title": app_title, "route": app_home, "has_permission": "erpnext.check_app_permission", + "sequence_id": 1, } ] From b2ec906ff3663fec4710890b874d33755f002273 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 14 Jul 2026 16:18:53 +0530 Subject: [PATCH 09/21] test: remove test --- .../purchase_order/test_purchase_order.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index dbb3a38676c..37ccf275cdc 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -220,23 +220,6 @@ class TestPurchaseOrder(ERPNextTestSuite): frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0) frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) - def test_duplicate_material_request_item_row_allowed(self): - """Splitting a Material Request Item's qty across multiple PO rows must be - allowed, mirroring how Sales Order allows duplicate Quotation Item rows.""" - mr = make_material_request(qty=10) - po = make_purchase_order(mr.name) - po.supplier = "_Test Supplier" - - duplicate_row = po.items[0].as_dict() - duplicate_row.qty = 4 - po.items[0].qty = 6 - - po.append("items", duplicate_row) - po.save() - - self.assertEqual(len(po.items), 2) - self.assertEqual(po.items[0].material_request_item, po.items[1].material_request_item) - def test_update_remove_child_linked_to_mr(self): """Test impact on linked PO and MR on deleting/updating row.""" mr = make_material_request(qty=10) From d7f4524cddb14d4444fe6d5886ece83a011b406f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 14 Jul 2026 16:40:07 +0530 Subject: [PATCH 10/21] refactor: convert Hide Currency Symbol in Global Defaults to a Check field (#57135) --- erpnext/patches.txt | 3 ++- .../v16_0/convert_hide_currency_symbol_to_check.py | 9 +++++++++ .../setup/doctype/global_defaults/global_defaults.json | 8 ++++---- erpnext/setup/doctype/global_defaults/global_defaults.py | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 erpnext/patches/v16_0/convert_hide_currency_symbol_to_check.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 12c13f8aaee..6a9632dc51f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -261,6 +261,7 @@ erpnext.patches.v14_0.update_proprietorship_to_individual erpnext.patches.v15_0.rename_subcontracting_fields erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage erpnext.patches.v16_0.convert_commission_rate_to_percent +erpnext.patches.v16_0.convert_hide_currency_symbol_to_check [post_model_sync] erpnext.patches.v15_0.rename_gross_purchase_amount_to_net_purchase_amount @@ -496,4 +497,4 @@ erpnext.patches.v16_0.backfill_pick_list_transferred_qty erpnext.patches.v16_0.create_shop_floor_roles erpnext.patches.v15_0.backfill_sla_link_filters_on_custom_field erpnext.patches.v15_0.backfill_sla_link_filters_on_docfield -erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm \ No newline at end of file +erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm diff --git a/erpnext/patches/v16_0/convert_hide_currency_symbol_to_check.py b/erpnext/patches/v16_0/convert_hide_currency_symbol_to_check.py new file mode 100644 index 00000000000..d3ed8ca5a31 --- /dev/null +++ b/erpnext/patches/v16_0/convert_hide_currency_symbol_to_check.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + # runs pre_model_sync: field is still a Select, so this returns the raw "Yes"/"No" + old_value = frappe.db.get_single_value("Global Defaults", "hide_currency_symbol") + new_value = 1 if old_value == "Yes" else 0 + frappe.db.set_single_value("Global Defaults", "hide_currency_symbol", new_value) + frappe.db.set_default("hide_currency_symbol", new_value) diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index 908da5ff912..305972a5cea 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.json +++ b/erpnext/setup/doctype/global_defaults/global_defaults.json @@ -55,12 +55,12 @@ "reqd": 1 }, { + "default": "0", "description": "Do not show any symbol like $ etc next to currencies.", "fieldname": "hide_currency_symbol", - "fieldtype": "Select", + "fieldtype": "Check", "in_list_view": 1, - "label": "Hide Currency Symbol", - "options": "\nNo\nYes" + "label": "Hide Currency Symbol" }, { "default": "0", @@ -121,7 +121,7 @@ "in_create": 1, "issingle": 1, "links": [], - "modified": "2026-07-14 15:18:25.829886", + "modified": "2026-07-14 18:30:00.000000", "modified_by": "Administrator", "module": "Setup", "name": "Global Defaults", diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index 911888b095f..9684566d3a9 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -53,7 +53,7 @@ class GlobalDefaults(Document): disable_in_words: DF.Check disable_rounded_total: DF.Check enable_company_wise_masters: DF.Check - hide_currency_symbol: DF.Literal["", "No", "Yes"] + hide_currency_symbol: DF.Check use_posting_datetime_for_naming_documents: DF.Check # end: auto-generated types From 672fadaa78befee144cc81895698b7ae86226085 Mon Sep 17 00:00:00 2001 From: Poovitha Palanivelu Date: Tue, 14 Jul 2026 15:08:59 +0530 Subject: [PATCH 11/21] feat: add on hold status to project --- erpnext/projects/doctype/project/project.json | 4 ++-- erpnext/projects/doctype/project/project.py | 6 +++--- erpnext/projects/doctype/project/project_list.js | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 8f5a9b03813..d40bb75595b 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -86,7 +86,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nCompleted\nCancelled", + "options": "Open\nOn hold\nCompleted\nCancelled", "search_index": 1 }, { @@ -482,7 +482,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2026-05-22 16:45:50.762759", + "modified": "2026-07-14 14:20:50.418911", "modified_by": "Administrator", "module": "Projects", "name": "Project", diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 14c4345ea78..63c75d61f4c 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -59,7 +59,7 @@ class Project(Document): project_type: DF.Link | None sales_order: DF.Link | None second_email: DF.Time | None - status: DF.Literal["Open", "Completed", "Cancelled"] + status: DF.Literal["Open", "On hold", "Completed", "Cancelled"] subject: DF.Data | None to_time: DF.Time | None total_billable_amount: DF.Currency @@ -311,8 +311,8 @@ class Project(Document): pct_complete += row["progress"] * frappe.utils.safe_div(row["task_weight"], weight_sum) self.percent_complete = flt(flt(pct_complete), 2) - # don't update status if it is cancelled - if self.status == "Cancelled": + # don't update status if it is manually set to cancelled or on hold + if self.status in ("Cancelled", "On hold"): return self.status = "Completed" if self.percent_complete == 100 else "Open" diff --git a/erpnext/projects/doctype/project/project_list.js b/erpnext/projects/doctype/project/project_list.js index 1503b1ee5d3..28a774524d4 100644 --- a/erpnext/projects/doctype/project/project_list.js +++ b/erpnext/projects/doctype/project/project_list.js @@ -4,6 +4,8 @@ frappe.listview_settings["Project"] = { get_indicator: function (doc) { if (doc.status == "Open" && doc.percent_complete) { return [__("{0}%", [cint(doc.percent_complete)]), "orange", "percent_complete,>,0|status,=,Open"]; + } else if (doc.status == "On hold") { + return [__("On hold"), "blue", "status,=,On hold"]; } else { return [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; } From 1fd2faa68d0b4960d9e2e48ab782be9cc6b1b644 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 14 Jul 2026 17:52:48 +0530 Subject: [PATCH 12/21] fix: permission issue (#57112) --- erpnext/controllers/stock_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 733a7160da8..64d2a0bd62a 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -565,6 +565,7 @@ def show_accounting_ledger_preview(company: str, doctype: str, docname: str): filters = frappe._dict(company=company, include_dimensions=1) doc = frappe.get_lazy_doc(doctype, docname) + doc.check_permission("read") doc.run_method("before_gl_preview") gl_columns, gl_data = get_accounting_ledger_preview(doc, filters) @@ -580,6 +581,7 @@ def show_stock_ledger_preview(company: str, doctype: str, docname: str): filters = frappe._dict(company=company) doc = frappe.get_lazy_doc(doctype, docname) + doc.check_permission("read") doc.run_method("before_sl_preview") sl_columns, sl_data = get_stock_ledger_preview(doc, filters) From 5133ba47b7f7d7b05691252b7df1caf2f877c085 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 14 Jul 2026 18:02:59 +0530 Subject: [PATCH 13/21] fix: make currency exchange truly idempotent against any pre-existing state Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- erpnext/tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 61800eef0a0..aebb7a22650 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2564,7 +2564,7 @@ class BootStrapTestData: "for_selling": 1, }, ] - self.make_records(["from_currency", "to_currency", "date"], records) + self.make_records(["from_currency", "to_currency", "date", "for_buying", "for_selling"], records) def make_operation(self): records = [ From 4705909ceee075fcfa777fc8157784bc3d1482ca Mon Sep 17 00:00:00 2001 From: pandiyan Date: Tue, 14 Jul 2026 21:44:48 +0530 Subject: [PATCH 14/21] fix: batch BOM source warehouse lookups to avoid n+1 queries in production plan work order creation --- .../services/work_order_planning.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/services/work_order_planning.py b/erpnext/manufacturing/doctype/production_plan/services/work_order_planning.py index ae4611e9fe3..3897eddaa1d 100644 --- a/erpnext/manufacturing/doctype/production_plan/services/work_order_planning.py +++ b/erpnext/manufacturing/doctype/production_plan/services/work_order_planning.py @@ -38,9 +38,10 @@ class WorkOrderCreationService: self.doc = doc def get_production_items(self): + bom_warehouse_map = self.get_bom_source_warehouse_map(self.doc.po_items) item_dict = {} for d in self.doc.po_items: - item_details = self._production_item_details(d) + item_details = self._production_item_details(d, bom_warehouse_map) if self.doc.get_items_from == "Material Request": item_details["qty"] = d.planned_qty key = (d.item_code, d.material_request_item, d.warehouse, d.planned_start_date) @@ -52,7 +53,20 @@ class WorkOrderCreationService: item_dict[key] = item_details return item_dict - def _production_item_details(self, d): + def get_bom_source_warehouse_map(self, rows): + bom_names = {row.bom_no for row in rows if row.bom_no} + if not bom_names: + return {} + return dict( + frappe.get_all( + "BOM", + filters={"name": ["in", list(bom_names)]}, + fields=["name", "default_source_warehouse"], + as_list=True, + ) + ) + + def _production_item_details(self, d, bom_warehouse_map): details = { "production_item": d.item_code, "use_multi_level_bom": d.include_exploded_items, @@ -70,7 +84,7 @@ class WorkOrderCreationService: "product_bundle_item": d.product_bundle_item, "planned_start_date": d.planned_start_date, "project": self.doc.project, - "source_warehouse": frappe.get_value("BOM", d.bom_no, "default_source_warehouse"), + "source_warehouse": bom_warehouse_map.get(d.bom_no), } if not details["project"] and d.sales_order: details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") @@ -112,6 +126,7 @@ class WorkOrderCreationService: wo_list.append(work_order) def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): + bom_warehouse_map = self.get_bom_source_warehouse_map(self.doc.sub_assembly_items) for row in self.doc.sub_assembly_items: if row.type_of_manufacturing == "Subcontract": subcontracted_po.setdefault(row.supplier, []).append(row) @@ -119,16 +134,16 @@ class WorkOrderCreationService: if row.type_of_manufacturing == "Material Request": continue - work_order = self._sub_assembly_work_order(row, default_warehouses) + work_order = self._sub_assembly_work_order(row, default_warehouses, bom_warehouse_map) if work_order: wo_list.append(work_order) - def _sub_assembly_work_order(self, row, default_warehouses): + def _sub_assembly_work_order(self, row, default_warehouses, bom_warehouse_map): if flt(row.qty) <= flt(row.ordered_qty): return None work_order_data = { - "source_warehouse": frappe.get_value("BOM", row.bom_no, "default_source_warehouse"), + "source_warehouse": bom_warehouse_map.get(row.bom_no), "wip_warehouse": default_warehouses.get("wip_warehouse"), "fg_warehouse": default_warehouses.get("fg_warehouse"), "scrap_warehouse": default_warehouses.get("scrap_warehouse"), From f44bcae47d7780eba76ef1822af04aa09a9b839f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jul 2026 10:31:07 +0530 Subject: [PATCH 15/21] fix: hide job card field in purchase order item --- .../doctype/purchase_order_item/purchase_order_item.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index b0c75c49d9e..b405c0b0be5 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -913,8 +913,10 @@ "fieldname": "job_card", "fieldtype": "Link", "label": "Job Card", + "no_copy": 1, "options": "Job Card", - "search_index": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "distributed_discount_amount", @@ -941,7 +943,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-06-08 21:00:00.000000", + "modified": "2026-07-15 10:30:04.600510", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 27672851cdbc2fe8d5addb628a94b2215768ce11 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jul 2026 10:34:07 +0530 Subject: [PATCH 16/21] fix: set correct currency in supplier quotation net rate field --- .../supplier_quotation_item/supplier_quotation_item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index c131439463f..31efaa6690b 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -307,6 +307,7 @@ "fieldname": "net_rate", "fieldtype": "Currency", "label": "Net Rate", + "options": "currency", "print_hide": 1, "read_only": 1 }, @@ -613,7 +614,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-06-17 12:05:52.441645", + "modified": "2026-07-15 10:33:24.855979", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", From 2310c4c0059f9bc23696349a7f9fb4b55fedf4b0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jul 2026 10:59:52 +0530 Subject: [PATCH 17/21] fix: allow delivery when a batch is reserved across multiple sales orders validate_reserved_batches compared the voucher's own qty against the remaining batch qty, so delivering one order's reserved unit threw Reserved Batch Conflict whenever the remainder exactly matched another order's reservation. Compare the remaining batch qty against the aggregated outstanding reserved qty (qty - delivered_qty) of other vouchers instead, excluding reservations the voucher itself delivers. --- .../test_stock_reservation_entry.py | 86 +++++++++++++++++ .../services/serial_batch_bundle_service.py | 94 +++++++++---------- 2 files changed, 130 insertions(+), 50 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index a8529efcd19..e6969815c27 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -538,6 +538,65 @@ class TestStockReservationEntry(ERPNextTestSuite): self.assertEqual(row.delivered_qty, 0, "DN cancel must restore the serial/batch reservation") self.assertEqual(row.status, "Reserved") + @ERPNextTestSuite.change_settings( + "Stock Settings", + { + "allow_negative_stock": 0, + "enable_stock_reservation": 1, + "auto_reserve_serial_and_batch": 1, + "pick_serial_and_batch_based_on": "FIFO", + "use_serial_batch_fields": 1, + }, + ) + def test_batch_shared_across_sales_orders_can_be_delivered(self) -> None: + # Regression (#57159): one batch reserved by two Sales Orders. Delivering each order's own + # reserved unit must not raise Reserved Batch Conflict — the remainder covers the other order. + item_doc = make_batch_item() + create_material_receipt(items={item_doc.name: item_doc}, warehouse=self.warehouse, qty=2) + + orders = [] + for _i in range(2): + so = make_sales_order(item_code=item_doc.name, warehouse=self.warehouse, qty=1, rate=100) + so.create_stock_reservation_entries() + orders.append(so) + + self.assertEqual( + len(get_reserved_batch_nos(orders[0].name) | get_reserved_batch_nos(orders[1].name)), 1 + ) + + for so in orders: + dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + dn.save() + dn.submit() + self.assertEqual(dn.docstatus, 1) + + @ERPNextTestSuite.change_settings( + "Stock Settings", + { + "allow_negative_stock": 0, + "enable_stock_reservation": 1, + "auto_reserve_serial_and_batch": 1, + "pick_serial_and_batch_based_on": "FIFO", + "use_serial_batch_fields": 1, + }, + ) + def test_delivery_draining_a_batch_reserved_for_another_sales_order_is_blocked(self) -> None: + # Guard for #57159 fix: an order without a reservation must still be blocked from draining + # a batch below what another order has reserved from it, even if other batches have stock. + item_doc = make_batch_item() + create_material_receipt(items={item_doc.name: item_doc}, warehouse=self.warehouse, qty=2) + create_material_receipt(items={item_doc.name: item_doc}, warehouse=self.warehouse, qty=2) + + so_a = make_sales_order(item_code=item_doc.name, warehouse=self.warehouse, qty=2, rate=100) + so_a.create_stock_reservation_entries() + (reserved_batch_no,) = get_reserved_batch_nos(so_a.name) + + so_b = make_sales_order(item_code=item_doc.name, warehouse=self.warehouse, qty=2, rate=100) + dn = make_delivery_note(so_b.name) + dn.items[0].batch_no = reserved_batch_no + dn.save() + self.assertRaisesRegex(frappe.ValidationError, "is reserved for", dn.submit) + @ERPNextTestSuite.change_settings( "Stock Settings", { @@ -893,6 +952,33 @@ def create_items() -> dict: return items +def make_batch_item(): + return make_item( + properties={ + "is_stock_item": 1, + "valuation_rate": 100, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SRBI-.#####.", + } + ) + + +def get_reserved_batch_nos(sales_order: str) -> set: + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + + batch_nos = ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select(sb_entry.batch_no) + .where((sre.voucher_no == sales_order) & (sre.docstatus == 1)) + ).run(pluck=True) + + return set(batch_nos) + + def create_material_receipt( items: dict, warehouse: str = "_Test Warehouse - _TC", qty: float = 100 ) -> StockEntry: diff --git a/erpnext/stock/services/serial_batch_bundle_service.py b/erpnext/stock/services/serial_batch_bundle_service.py index 2e752371ed5..2699c7e025f 100644 --- a/erpnext/stock/services/serial_batch_bundle_service.py +++ b/erpnext/stock/services/serial_batch_bundle_service.py @@ -9,6 +9,8 @@ delegators for methods reached from other doctypes / ``run_method``; internal helpers live here only. """ +from collections import defaultdict + import frappe from frappe import _, bold from frappe.utils import cstr, flt, get_link_to_form, getdate @@ -604,66 +606,57 @@ class SerialBatchBundleService: if not batches: return - field_mapper = { - "Sales Invoice": [["Sales Order", "sales_order"]], - "Delivery Note": [["Sales Order", "against_sales_order"]], - "Stock Entry": [ - ["Work Order", "work_order"], - ["Subcontracting Inward Order", "subcontracting_inward_order"], - ], + reference_fields = { + "Sales Invoice": ["sales_order"], + "Delivery Note": ["against_sales_order"], + "Stock Entry": ["work_order", "subcontracting_inward_order"], }.get(self.doc.doctype) - qty_field = { - "Sales Invoice": "qty", - "Delivery Note": "qty", - "Stock Entry": "fg_completed_qty", - }.get(self.doc.doctype) - - reserved_batches_data = self.get_reserved_batches(batches) items = self.doc.items if self.doc.doctype == "Stock Entry": items = [self.doc] - for item in items: - for field in field_mapper: - if not item.get(field[1]): - continue + own_vouchers = {item.get(field) for item in items for field in reference_fields if item.get(field)} - value = item.get(field[1]) - for row in reserved_batches_data: - if self.doc.doctype in ["Sales Invoice", "Delivery Note"] and row.item_code != item.get( - "item_code" - ): - continue + outstanding_qty = defaultdict(float) + reservations = {} + for row in self.get_reserved_batches(batches): + if row.voucher_no in own_vouchers: + continue - if row.voucher_no == value: - continue + key = (row.batch_no, row.warehouse) + outstanding_qty[key] += flt(row.qty) - flt(row.delivered_qty) + reservations.setdefault(key, row) - batch_qty = get_batch_qty( - row.batch_no, - row.warehouse, - posting_date=self.doc.posting_date, - posting_time=self.doc.posting_time, - consider_negative_batches=True, - ) + for (batch_no, warehouse), reserved_qty in outstanding_qty.items(): + if reserved_qty <= 0: + continue - if item.get(qty_field) < batch_qty: - continue + batch_qty = get_batch_qty( + batch_no, + warehouse, + posting_date=self.doc.posting_date, + posting_time=self.doc.posting_time, + consider_negative_batches=True, + ) - frappe.throw( - _( - "The batch {0} is already reserved in {1} {2}. So, cannot proceed with the {3} {4}, which is created against the {5} {6}." - ).format( - frappe.bold(row.batch_no), - frappe.bold(row.voucher_type), - frappe.bold(row.voucher_no), - frappe.bold(self.doc.doctype), - frappe.bold(self.doc.name), - frappe.bold(field[0]), - frappe.bold(value), - ), - title=_("Reserved Batch Conflict"), - ) + if flt(batch_qty, 6) >= flt(reserved_qty, 6): + continue + + row = reservations[(batch_no, warehouse)] + frappe.throw( + _( + "The batch {0} is reserved for {1} {2} in the warehouse {3} and the remaining quantity is not enough to cover the reservation. So, cannot proceed with the {4} {5}." + ).format( + frappe.bold(batch_no), + frappe.bold(row.voucher_type), + frappe.bold(row.voucher_no), + frappe.bold(warehouse), + frappe.bold(self.doc.doctype), + frappe.bold(self.doc.name), + ), + title=_("Reserved Batch Conflict"), + ) def get_reserved_batches(self, batches): doctype = frappe.qb.DocType("Stock Reservation Entry") @@ -675,9 +668,10 @@ class SerialBatchBundleService: .on(doctype.name == child_doc.parent) .select( child_doc.batch_no, + child_doc.qty, + child_doc.delivered_qty, doctype.voucher_type, doctype.voucher_no, - doctype.item_code, doctype.warehouse, ) .where((doctype.docstatus == 1) & (child_doc.batch_no.isin(batches))) From 1d6edf967430158efb720d9544fdde894a6711c8 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jul 2026 11:43:32 +0530 Subject: [PATCH 18/21] fix: name every conflicting voucher in the reserved batch error (#57174) * fix: name every conflicting voucher in the reserved batch error * fix: exclude fully-delivered reservations from the conflict message * fix: round outstanding qty guard consistently with the conflict gate --- .../services/serial_batch_bundle_service.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/services/serial_batch_bundle_service.py b/erpnext/stock/services/serial_batch_bundle_service.py index 2699c7e025f..a3e2d2060b2 100644 --- a/erpnext/stock/services/serial_batch_bundle_service.py +++ b/erpnext/stock/services/serial_batch_bundle_service.py @@ -619,17 +619,19 @@ class SerialBatchBundleService: own_vouchers = {item.get(field) for item in items for field in reference_fields if item.get(field)} outstanding_qty = defaultdict(float) - reservations = {} + reservations = defaultdict(list) for row in self.get_reserved_batches(batches): if row.voucher_no in own_vouchers: continue key = (row.batch_no, row.warehouse) - outstanding_qty[key] += flt(row.qty) - flt(row.delivered_qty) - reservations.setdefault(key, row) + outstanding = flt(row.qty) - flt(row.delivered_qty) + outstanding_qty[key] += outstanding + if outstanding > 0: + reservations[key].append(row) for (batch_no, warehouse), reserved_qty in outstanding_qty.items(): - if reserved_qty <= 0: + if flt(reserved_qty, 6) <= 0: continue batch_qty = get_batch_qty( @@ -643,14 +645,18 @@ class SerialBatchBundleService: if flt(batch_qty, 6) >= flt(reserved_qty, 6): continue - row = reservations[(batch_no, warehouse)] + vouchers = ", ".join( + f"{frappe.bold(voucher_type)} {frappe.bold(voucher_no)}" + for voucher_type, voucher_no in dict.fromkeys( + (row.voucher_type, row.voucher_no) for row in reservations[(batch_no, warehouse)] + ) + ) frappe.throw( _( - "The batch {0} is reserved for {1} {2} in the warehouse {3} and the remaining quantity is not enough to cover the reservation. So, cannot proceed with the {4} {5}." + "The batch {0} is reserved for {1} in the warehouse {2} and the remaining quantity is not enough to cover the reservations. So, cannot proceed with the {3} {4}." ).format( frappe.bold(batch_no), - frappe.bold(row.voucher_type), - frappe.bold(row.voucher_no), + vouchers, frappe.bold(warehouse), frappe.bold(self.doc.doctype), frappe.bold(self.doc.name), From e99966a38ec83555919ea1d0f7155e53f6b00a4e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 15 Jul 2026 12:09:34 +0530 Subject: [PATCH 19/21] fix: skip redundant reposting of dependent items (#57092) * fix: skip redundant reposting of dependent items Co-Authored-By: Claude Opus 4.8 * fix: use earliest cascade datetime and batch repost item lookup Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .../repost_item_valuation.py | 144 +++++++++++++++++- .../test_repost_item_valuation.py | 124 ++++++++++++++- erpnext/stock/stock_ledger.py | 32 +++- 3 files changed, 297 insertions(+), 3 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 2d94a892aeb..47c55e37680 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -10,7 +10,7 @@ from frappe.exceptions import QueryDeadlockError, QueryTimeoutError from frappe.model.document import Document from frappe.query_builder import DocType, Interval from frappe.query_builder.functions import CombineDatetime, Max, Now -from frappe.utils import cint, get_link_to_form, get_weekday, getdate, now, nowtime +from frappe.utils import cint, get_datetime, get_link_to_form, get_weekday, getdate, now, nowtime from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException @@ -19,6 +19,7 @@ from erpnext.accounts.services.gl_validator import validate_accounting_period from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers from erpnext.stock.stock_ledger import ( get_affected_transactions, + get_item_wh_first_reposted_from_reposting_data, get_items_to_be_repost, repost_future_sle, ) @@ -343,6 +344,21 @@ class RepostItemValuation(Document): ) ).run() + def skip_reposts_covered_by_dependents(self): + if self.repost_only_accounting_ledgers: + return + + coverage = get_item_wh_first_reposted_from_reposting_data(self) + if not coverage: + return + + source_datetime = get_combine_datetime(self.posting_date, self.posting_time) + mark_covered_item_reposts(self.name, coverage, source_datetime) + + affected = get_affected_transactions(self) + if affected: + mark_covered_transaction_reposts(self, coverage, affected) + def _recalculate_valuation_rate(self): doc = frappe.get_doc(self.voucher_type, self.voucher_no) if doc.get("is_internal_supplier"): @@ -376,6 +392,130 @@ def bulk_restart_reposting(names: str | list): frappe.msgprint(_("Repost Item Valuation restarted for selected failed records.")) +def repost_coverage_cache_key(name): + return f"riv_dependent_coverage::{name}" + + +def get_queued_item_reposts(source_name, item_codes): + return frappe.get_all( + "Repost Item Valuation", + filters={ + "name": ("!=", source_name), + "based_on": "Item and Warehouse", + "status": "Queued", + "docstatus": 1, + "recalculate_valuation_rate": 0, + "recreate_stock_ledgers": 0, + "via_landed_cost_voucher": 0, + "item_code": ("in", item_codes), + }, + fields=["name", "item_code", "warehouse", "posting_date", "posting_time"], + ) + + +def mark_covered_item_reposts(source_name, coverage, source_datetime): + item_codes = {item_code for item_code, _ in coverage} + + for row in get_queued_item_reposts(source_name, list(item_codes)): + from_datetime = coverage.get((row.item_code, row.warehouse)) + if not from_datetime: + continue + + row_datetime = get_combine_datetime(row.posting_date, row.posting_time) + if get_datetime(row_datetime) < get_datetime(source_datetime): + continue + + if get_datetime(from_datetime) <= get_datetime(row_datetime): + frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped") + + +def get_queued_transaction_reposts(source_name, voucher_nos): + return frappe.get_all( + "Repost Item Valuation", + filters={ + "name": ("!=", source_name), + "based_on": "Transaction", + "status": "Queued", + "docstatus": 1, + "repost_only_accounting_ledgers": 0, + "recalculate_valuation_rate": 0, + "recreate_stock_ledgers": 0, + "via_landed_cost_voucher": 0, + "voucher_no": ("in", list(voucher_nos)), + }, + fields=["name", "voucher_type", "voucher_no", "posting_date", "posting_time"], + ) + + +def accumulate_repost_coverage(row_name, coverage, row_datetime): + cache_key = repost_coverage_cache_key(row_name) + acc = frappe.cache().get_value(cache_key) or {} + + for key, from_datetime in coverage.items(): + if get_datetime(from_datetime) > get_datetime(row_datetime): + continue + + existing = acc.get(key) + if not existing or get_datetime(from_datetime) < get_datetime(existing): + acc[key] = from_datetime + + frappe.cache().set_value(cache_key, acc, expires_in_sec=86400) + return acc + + +def get_repost_items_by_voucher(rows): + voucher_nos = {row.voucher_no for row in rows} + if not voucher_nos: + return {} + + items_by_voucher = {} + for sle in frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": ("in", list(voucher_nos))}, + fields=["voucher_type", "voucher_no", "item_code", "warehouse"], + distinct=True, + ): + items_by_voucher.setdefault((sle.voucher_type, sle.voucher_no), set()).add( + (sle.item_code, sle.warehouse) + ) + + return items_by_voucher + + +def is_transaction_repost_covered(items, acc, row_datetime): + if not items: + return False + + for key in items: + covered = acc.get(key) + if not covered or get_datetime(covered) > get_datetime(row_datetime): + return False + + return True + + +def mark_covered_transaction_reposts(source, coverage, affected): + source_datetime = get_combine_datetime(source.posting_date, source.posting_time) + voucher_nos = {voucher_no for _, voucher_no in affected} + + rows = get_queued_transaction_reposts(source.name, voucher_nos) + items_by_voucher = get_repost_items_by_voucher(rows) + + for row in rows: + if (row.voucher_type, row.voucher_no) not in affected: + continue + + row_datetime = get_combine_datetime(row.posting_date, row.posting_time) + if get_datetime(row_datetime) < get_datetime(source_datetime): + continue + + acc = accumulate_repost_coverage(row.name, coverage, row_datetime) + items = items_by_voucher.get((row.voucher_type, row.voucher_no)) + if is_transaction_repost_covered(items, acc, row_datetime): + frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped") + frappe.cache().delete_value(repost_coverage_cache_key(row.name)) + + def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -407,6 +547,8 @@ def repost(doc): repost_gl_entries(doc) + doc.skip_reposts_covered_by_dependents() + doc.set_status("Completed") doc.db_set("reposting_data_file", None) remove_attached_file(doc.name) 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 d2c5eaa6096..fe7b4bfd7c1 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 @@ -14,10 +14,11 @@ 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.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, + mark_covered_transaction_reposts, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.tests.test_utils import StockTestMixin -from erpnext.stock.utils import PendingRepostingError +from erpnext.stock.utils import PendingRepostingError, get_combine_datetime from erpnext.tests.utils import ERPNextTestSuite @@ -171,6 +172,127 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin): riv4.set_status("Skipped") riv3.set_status("Skipped") + def _make_queued_transaction_riv(self, voucher): + riv = frappe.get_doc( + doctype="Repost Item Valuation", + based_on="Transaction", + voucher_type=voucher.doctype, + voucher_no=voucher.name, + posting_date=voucher.posting_date, + posting_time="00:00:00", + ) + riv.flags.dont_run_in_test = True + riv.submit() + return riv + + def test_skip_transaction_repost_covered_by_dependent(self): + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + + covered_pr = make_purchase_receipt( + company=company, warehouse=warehouse, item_code="_Test Item", qty=5 + ) + other_pr = make_purchase_receipt( + company=company, warehouse=warehouse, item_code="_Test Item 2", qty=5 + ) + + covered_riv = self._make_queued_transaction_riv(covered_pr) + other_riv = self._make_queued_transaction_riv(other_pr) + + earlier_date = add_days(covered_pr.posting_date, -1) + source = frappe._dict(name="__test_source_riv__", posting_date=earlier_date, posting_time="00:00:00") + coverage = {("_Test Item", warehouse): get_combine_datetime(earlier_date, "00:00:00")} + affected = {("Purchase Receipt", covered_pr.name), ("Purchase Receipt", other_pr.name)} + + mark_covered_transaction_reposts(source, coverage, affected) + + covered_riv.reload() + other_riv.reload() + self.assertEqual(covered_riv.status, "Skipped") + self.assertEqual(other_riv.status, "Queued") + + other_riv.db_set("status", "Skipped") + + def _make_dependent_repack(self, company, consumed_items, source_wh, fg_item, fg_wh, qty, posting_date): + se = frappe.new_doc("Stock Entry") + se.stock_entry_type = "Repack" + se.company = company + se.set_posting_time = 1 + se.posting_date = posting_date + for item_code in consumed_items: + se.append("items", {"item_code": item_code, "s_warehouse": source_wh, "qty": qty}) + se.append("items", {"item_code": fg_item, "t_warehouse": fg_wh, "qty": qty, "is_finished_item": 1}) + se.insert() + se.submit() + return se + + def test_backdated_manufacture_repost_skips_redundant_dependent(self): + from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( + execute_reposting_entry, + ) + + frappe.flags.dont_execute_stock_reposts = True + self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts", None) + + original_setting = frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting") + frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 1) + self.addCleanup( + frappe.db.set_single_value, "Stock Reposting Settings", "item_based_reposting", original_setting + ) + + company = "_Test Company with perpetual inventory" + source_wh = "Stores - TCP1" + fg_wh = "Finished Goods - TCP1" + + item_a = make_item(properties={"valuation_method": "FIFO"}).name + item_b = make_item(properties={"valuation_method": "FIFO"}).name + item_c = make_item(properties={"valuation_method": "FIFO"}).name + + def _day(days): + return add_days(nowdate(), days) + + make_stock_entry( + item_code=item_a, to_warehouse=source_wh, qty=10, rate=100, posting_date=_day(2), company=company + ) + make_stock_entry( + item_code=item_b, to_warehouse=source_wh, qty=10, rate=100, posting_date=_day(3), company=company + ) + self._make_dependent_repack(company, [item_a, item_b], source_wh, item_c, fg_wh, 5, _day(10)) + + make_stock_entry( + item_code=item_a, to_warehouse=source_wh, qty=10, rate=200, posting_date=_day(1), company=company + ) + make_stock_entry( + item_code=item_b, to_warehouse=source_wh, qty=10, rate=200, posting_date=_day(1), company=company + ) + self._make_dependent_repack(company, [item_a, item_b], source_wh, item_c, fg_wh, 5, _day(5)) + + rivs = frappe.get_all( + "Repost Item Valuation", + filters={ + "docstatus": 1, + "based_on": "Item and Warehouse", + "status": "Queued", + "item_code": ("in", [item_a, item_b, item_c]), + }, + fields=["name", "item_code", "warehouse"], + order_by="posting_date asc, posting_time asc, creation asc", + ) + self.assertTrue( + any(r.item_code == item_c and r.warehouse == fg_wh for r in rivs), + msg="Expected a queued repost for the finished good", + ) + + for r in rivs: + execute_reposting_entry(r.name) + + fg_repost_status = frappe.db.get_value( + "Repost Item Valuation", + {"based_on": "Item and Warehouse", "item_code": item_c, "warehouse": fg_wh, "docstatus": 1}, + "status", + ) + self.assertEqual(fg_repost_status, "Skipped") + def test_stock_freeze_validation(self): today = nowdate() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8b28897df60..c1ce66317bc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -306,6 +306,7 @@ def repost_future_sle( resume_item_wh_wise_last_posted_sle = ( get_item_wh_wise_last_posted_sle_from_reposting_data(doc, reposting_data) or {} ) + item_wh_first_reposted = get_item_wh_first_reposted_from_reposting_data(doc, reposting_data) or {} if not items_to_be_repost: return @@ -328,6 +329,7 @@ def repost_future_sle( "repost_doc": doc, "repost_affected_transaction": repost_affected_transaction, "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, + "item_wh_first_reposted": item_wh_first_reposted, }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, @@ -337,7 +339,14 @@ def repost_future_sle( resume_item_wh_wise_last_posted_sle = {} repost_affected_transaction.update(obj.repost_affected_transaction) - update_args_in_repost_item_valuation(doc, index, items_to_be_repost, repost_affected_transaction) + item_wh_first_reposted = obj.item_wh_first_reposted + update_args_in_repost_item_valuation( + doc, + index, + items_to_be_repost, + repost_affected_transaction, + item_wh_first_reposted=item_wh_first_reposted, + ) def update_args_in_repost_item_valuation( @@ -346,11 +355,15 @@ def update_args_in_repost_item_valuation( items_to_be_repost, repost_affected_transaction, item_wh_wise_last_posted_sle=None, + item_wh_first_reposted=None, ): file_name = "" if not item_wh_wise_last_posted_sle: item_wh_wise_last_posted_sle = {} + if not item_wh_first_reposted: + item_wh_first_reposted = {} + if doc.reposting_data_file: file_name = get_reposting_file_name(doc.doctype, doc.name) # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) @@ -360,6 +373,7 @@ def update_args_in_repost_item_valuation( "repost_affected_transaction": repost_affected_transaction, "item_wh_wise_last_posted_sle": {str(k): v for k, v in item_wh_wise_last_posted_sle.items()} or {}, + "item_wh_first_reposted": {str(k): v for k, v in item_wh_first_reposted.items()}, }, doc, file_name, @@ -495,6 +509,16 @@ def get_item_wh_wise_last_posted_sle_from_reposting_data(doc, reposting_data=Non return frappe._dict() +def get_item_wh_first_reposted_from_reposting_data(doc, reposting_data=None): + if not reposting_data and doc and doc.reposting_data_file: + reposting_data = get_reposting_data(doc.reposting_data_file) + + if not reposting_data or not reposting_data.get("item_wh_first_reposted"): + return {} + + return {frappe.safe_eval(key): value for key, value in reposting_data.item_wh_first_reposted.items()} + + def get_reposting_data(file_path) -> dict: file_name = frappe.db.get_value( "File", @@ -688,6 +712,7 @@ class update_entries_after: self.distinct_sles = set() self.distinct_dependant_item_wh = set() self.prev_sle_dict = frappe._dict({}) + self.item_wh_first_reposted = dict(self.args.get("item_wh_first_reposted") or {}) def get_item_wh_wise_last_posted_sle(self): if self.args and self.args.get("item_wh_wise_last_posted_sle"): @@ -738,6 +763,10 @@ class update_entries_after: i += 1 item_wh_key = (sle.item_code, sle.warehouse) + sle_datetime = sle.posting_datetime or get_combine_datetime(sle.posting_date, sle.posting_time) + existing_datetime = self.item_wh_first_reposted.get(item_wh_key) + if not existing_datetime or get_datetime(sle_datetime) < get_datetime(existing_datetime): + self.item_wh_first_reposted[item_wh_key] = sle_datetime if item_wh_key not in self.prev_sle_dict: self.prev_sle_dict[item_wh_key] = get_previous_sle_of_current_voucher(sle) @@ -832,6 +861,7 @@ class update_entries_after: self.items_to_be_repost, self.repost_affected_transaction, self.item_wh_wise_last_posted_sle, + self.item_wh_first_reposted, ) if not frappe.in_test: From 72b72a81fa8085af3d56c3827ebef70e459fc85c Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 15 Jul 2026 12:20:27 +0530 Subject: [PATCH 20/21] fix(project): improved access control for project users (#56675) * fix: permission check for `get_task_html` and `get_timesheet_html` * fix(project): enabled project access control for users without `Projects User` Role * fix(portal): validate user permissions for project portal * fix: patch to add docshare for the project users * fix(patch): selecting correct column on the query * fix(project): grant access to all the current users for new project * fix(portal): fixed condition to display timesheets on project * test(portal): add access control tests for project user * fix(project): using `frappe.has_permission` instead of `self.has_permission` to validate user permissions * fix(project): granting docshare access for every ProjectUser Roles for an User can be removed any time or an User Permission can be added which might restrict the access to the Project. * fix(patch): create docshare documents for non-cancelled projects and users who have no docshare documents * test(project): removed `test_control_access_does_not_touch_users_with_real_permission` --- erpnext/patches.txt | 1 + .../v16_0/access_control_for_project_users.py | 34 +++++++++ erpnext/projects/doctype/project/project.json | 18 ++++- erpnext/projects/doctype/project/project.py | 30 ++++++++ .../projects/doctype/project/test_project.py | 55 ++++++++++++++ erpnext/templates/pages/projects.py | 22 +++--- erpnext/templates/pages/test_projects.py | 72 +++++++++++++++++++ 7 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 erpnext/patches/v16_0/access_control_for_project_users.py create mode 100644 erpnext/templates/pages/test_projects.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6a9632dc51f..e748cab0008 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -498,3 +498,4 @@ erpnext.patches.v16_0.create_shop_floor_roles erpnext.patches.v15_0.backfill_sla_link_filters_on_custom_field erpnext.patches.v15_0.backfill_sla_link_filters_on_docfield erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm +erpnext.patches.v16_0.access_control_for_project_users diff --git a/erpnext/patches/v16_0/access_control_for_project_users.py b/erpnext/patches/v16_0/access_control_for_project_users.py new file mode 100644 index 00000000000..7202e6c71ea --- /dev/null +++ b/erpnext/patches/v16_0/access_control_for_project_users.py @@ -0,0 +1,34 @@ +import frappe + + +def execute(): + Project = frappe.qb.DocType("Project") + ProjectUser = frappe.qb.DocType("Project User") + + query = ( + frappe.qb.from_(Project) + .join(ProjectUser) + .on(Project.name == ProjectUser.parent) + .select(Project.name, ProjectUser.user) + .where(Project.status != "Cancelled") # Not considering cancelled Projects. + ) + + proj_users = query.run(as_dict=1) + + project_mapped_users = get_project_mapped_users(proj_users) + + for d in proj_users: + if d.user in project_mapped_users[d.name]: + continue + + frappe.share.add_docshare("Project", d.name, user=d.user) + + +def get_project_mapped_users(proj_users): + projects = set([d.name for d in proj_users]) + project_mapped_users = {} + + for d in projects: + project_mapped_users[d] = [d.user for d in frappe.share.get_users("Project", d)] + + return project_mapped_users diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index d40bb75595b..b55cec332bd 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -210,13 +210,15 @@ "fieldname": "users", "fieldtype": "Table", "label": "Users", - "options": "Project User" + "options": "Project User", + "permlevel": 1 }, { "fieldname": "copied_from", "fieldtype": "Data", "hidden": 1, "label": "Copied From", + "permlevel": 1, "read_only": 1 }, { @@ -482,13 +484,25 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2026-07-14 14:20:50.418911", + "modified": "2026-07-14 14:32:11.328347", "modified_by": "Administrator", "module": "Projects", "name": "Project", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects Manager", + "share": 1, + "write": 1 + }, { "create": 1, "delete": 1, diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 63c75d61f4c..fc85099bf6c 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -90,6 +90,7 @@ class Project(Document): def validate(self): if not self.is_new(): self.copy_from_template() + self.control_access_for_project_users() self.send_welcome_email() self.update_costing() self.update_percent_complete() @@ -239,6 +240,7 @@ class Project(Document): def after_insert(self): self.copy_from_template("after_insert") self.link_with_sales_order() + self.control_access_for_project_users() def link_with_sales_order(self) -> None: """Back-link the source Sales Order to this project. @@ -434,6 +436,34 @@ class Project(Document): ) user.welcome_email_sent = 1 + def control_access_for_project_users(self): + def revoke_access_for_project_users(removed_users): + users = set([d.user for d in frappe.share.get_users(self.doctype, self.name)]) + for user in removed_users: + if user not in users: + continue + + frappe.share.remove(self.doctype, self.name, user) + + def grant_access_for_project_users(new_users): + for user in new_users: + frappe.share.add_docshare(self.doctype, self.name, user=user) + + current_users = set([d.user for d in self.users]) + old_doc = self.get_doc_before_save() + + if not old_doc: + grant_access_for_project_users(current_users) + return + + previous_users = set([d.user for d in old_doc.users]) + + new_users = current_users - previous_users + removed_users = previous_users - current_users + + revoke_access_for_project_users(removed_users) + grant_access_for_project_users(new_users) + def get_timeline_data(doctype: str, name: str) -> dict[int, int]: """Return timeline for attendance""" diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 90e8d78f60e..d8d11f3ffa0 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -436,6 +436,61 @@ class TestProject(ERPNextTestSuite): self.assertEqual(project.total_consumed_material_cost, sum(row.amount for row in issue.items)) self.assertGreater(project.total_consumed_material_cost, 0) + def _create_portal_user(self, email): + """A user with no Project-related role, so read access can only come from + control_access_for_project_users() sharing the doc with them.""" + if not frappe.db.exists("User", email): + frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": "Portal", + "send_welcome_email": 0, + } + ).insert(ignore_permissions=True) + return email + + def test_new_project_grants_access_to_its_users(self): + member = self._create_portal_user(f"new_proj_member_{frappe.generate_hash(length=6)}@example.com") + + project = frappe.get_doc( + doctype="Project", + project_name=f"_Test New Project Access {frappe.generate_hash(length=6)}", + status="Open", + company="_Test Company", + ) + project.append("users", {"user": member, "welcome_email_sent": 1}) + project.insert() # must not raise + + self.assertTrue(project.has_permission(user=member)) + shared_with = [d.user for d in frappe.share.get_users("Project", project.name)] + self.assertIn(member, shared_with) + + def test_adding_and_removing_project_user_updates_access(self): + stays = self._create_portal_user(f"stays_{frappe.generate_hash(length=6)}@example.com") + leaves = self._create_portal_user(f"leaves_{frappe.generate_hash(length=6)}@example.com") + + project = frappe.get_doc( + doctype="Project", + project_name=f"_Test Project User Membership {frappe.generate_hash(length=6)}", + status="Open", + company="_Test Company", + ) + project.append("users", {"user": stays, "welcome_email_sent": 1}) + project.insert() + self.assertTrue(project.has_permission(user=stays)) + + # adding a user on update (not insert) must also grant them access + project.append("users", {"user": leaves, "welcome_email_sent": 1}) + project.save() + self.assertTrue(project.has_permission(user=leaves)) + + # removing a user must revoke the share that was granted for membership + project.users = [d for d in project.users if d.user != leaves] + project.save() + self.assertFalse(project.has_permission(user=leaves)) + self.assertTrue(project.has_permission(user=stays)) + def get_project(name, template): project = frappe.get_doc( diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index 46ad25ed6ed..646e2085ace 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -6,21 +6,12 @@ import frappe def get_context(context): - project_user = frappe.db.get_value( - "Project User", - {"parent": frappe.form_dict.project, "user": frappe.session.user}, - ["user", "view_attachments", "hide_timesheets"], - as_dict=True, - ) - if frappe.session.user != "Administrator" and (not project_user or frappe.session.user == "Guest"): - raise frappe.PermissionError + project_user = validate_and_get_project_user(project=frappe.form_dict.project) context.no_cache = 1 context.show_sidebar = True project = frappe.get_doc("Project", frappe.form_dict.project) - project.has_permission("read") - project.tasks = get_tasks( project.name, start=0, item_status="open", search=frappe.form_dict.get("search") ) @@ -66,6 +57,7 @@ def get_tasks(project, start=0, search=None, item_status=None): @frappe.whitelist() def get_task_html(project: str, start: int = 0, item_status: str | None = None): + validate_and_get_project_user(project=project) return frappe.render_template( "erpnext/templates/includes/projects/project_tasks.html", { @@ -106,6 +98,7 @@ def get_timesheets(project, start=0, search=None): @frappe.whitelist() def get_timesheet_html(project: str, start: int = 0): + validate_and_get_project_user(project=project) return frappe.render_template( "erpnext/templates/includes/projects/project_timesheets.html", {"doc": {"timesheets": get_timesheets(project, start)}}, @@ -119,3 +112,12 @@ def get_attachments(project): filters={"attached_to_name": project, "attached_to_doctype": "Project", "is_private": 0}, fields=["file_name", "file_url", "file_size"], ) + + +def validate_and_get_project_user(project: str): + project_doc = frappe.get_doc("Project", project) + project_doc.check_permission() + + project_user = next((d for d in project_doc.users if d.user == frappe.session.user), None) + + return project_user diff --git a/erpnext/templates/pages/test_projects.py b/erpnext/templates/pages/test_projects.py new file mode 100644 index 00000000000..8d66ce95bc5 --- /dev/null +++ b/erpnext/templates/pages/test_projects.py @@ -0,0 +1,72 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +from erpnext.projects.doctype.project.test_project import make_project +from erpnext.templates.pages.projects import validate_and_get_project_user +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProjectsPage(ERPNextTestSuite): + """validate_and_get_project_user() gates the /projects portal page. It must raise + frappe.PermissionError for a user who can't read the Project, and otherwise return + that user's Project User row (or None if they're permitted but not listed as one -- + e.g. an internal Projects Manager browsing the portal).""" + + def _create_user(self, email): + if not frappe.db.exists("User", email): + frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": "Portal", + "send_welcome_email": 0, + } + ).insert(ignore_permissions=True) + return email + + def test_raises_permission_error_for_user_without_access(self): + project = make_project({"project_name": f"_Test Portal Access {frappe.generate_hash(length=6)}"}) + outsider = self._create_user(f"outsider_{frappe.generate_hash(length=6)}@example.com") + + with self.set_user(outsider): + self.assertRaises(frappe.PermissionError, validate_and_get_project_user, project.name) + + def test_allows_user_listed_as_project_user_and_returns_their_row(self): + # Being a Project User shares the Project with that user (see + # Project.control_access_for_project_users), which is what lets them past + # check_permission() here. + member = self._create_user(f"member_{frappe.generate_hash(length=6)}@example.com") + + project = frappe.get_doc( + doctype="Project", + project_name=f"_Test Portal Access {frappe.generate_hash(length=6)}", + status="Open", + company="_Test Company", + ) + project.append( + "users", {"user": member, "view_attachments": 1, "hide_timesheets": 1, "welcome_email_sent": 1} + ) + project.insert() + + with self.set_user(member): + project_user = validate_and_get_project_user(project.name) + + self.assertIsNotNone(project_user) + self.assertEqual(project_user.user, member) + self.assertEqual(project_user.view_attachments, 1) + self.assertEqual(project_user.hide_timesheets, 1) + + def test_allows_internally_permitted_user_not_listed_as_project_user(self): + # The permission gate must be the real permission system (check_permission()), + # not "is this user in the Project's users child table" -- a Projects Manager + # can open any project's portal page without ever being added as its user. + project = make_project({"project_name": f"_Test Portal Access {frappe.generate_hash(length=6)}"}) + manager = self._create_user(f"manager_{frappe.generate_hash(length=6)}@example.com") + frappe.get_doc("User", manager).add_roles("Projects Manager") + + with self.set_user(manager): + project_user = validate_and_get_project_user(project.name) + + self.assertIsNone(project_user) From fee3a6e0fd017a287507d535b0795c28b4fe90ae Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 15 Jul 2026 12:22:02 +0530 Subject: [PATCH 21/21] fix(accounts): update AU standard chart of accounts (#57145) Co-authored-by: Jebajebas --- .../verified/au_standard_chart_of_accounts.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/au_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/au_standard_chart_of_accounts.json index 515a1e4de9d..a55dd3a183d 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/au_standard_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/au_standard_chart_of_accounts.json @@ -24,7 +24,8 @@ "account_number": "11530" }, "account_number": "115", - "is_group": 1 + "is_group": 1, + "account_type": "Bank" }, "Trade Receivables": { "Trade Debtors": { @@ -529,6 +530,13 @@ "account_number": "630", "is_group": 1 }, + "Accrued Manufacturing Expenses": { + "Accrued Expenses - Manufacturing": { + "account_number": "63510" + }, + "account_number": "635", + "is_group": 1 + }, "account_number": "63", "is_group": 1 }, @@ -814,4 +822,4 @@ "root_type": "Expense" } } -} \ No newline at end of file +}