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 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 +} 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/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/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json new file mode 100644 index 00000000000..e7dcefb59f3 --- /dev/null +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -0,0 +1,735 @@ +{ + "app": "erpnext", + "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", + "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 14:28:55.763394", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Accounting", + "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": [], + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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, + "default_workspace": 0, + "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": "Accounting", + "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/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_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", 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/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", 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/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) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e783d9e0fc4..1806aaa9c0a 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, } ] @@ -307,6 +308,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/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"), diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 12c13f8aaee..e748cab0008 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,5 @@ 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 +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/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/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 8f5a9b03813..b55cec332bd 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 }, { @@ -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-05-22 16:45:50.762759", + "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 14c4345ea78..fc85099bf6c 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 @@ -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. @@ -311,8 +313,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" @@ -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/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]; } 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/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/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 diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index 55ff08d21fe..305972a5cea 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" }, { @@ -51,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", @@ -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 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 a85b04530b0..9684566d3a9 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,7 +52,8 @@ class GlobalDefaults(Document): demo_company: DF.Link | None disable_in_words: DF.Check disable_rounded_total: DF.Check - hide_currency_symbol: DF.Literal["", "No", "Yes"] + enable_company_wise_masters: DF.Check + hide_currency_symbol: DF.Check use_posting_datetime_for_naming_documents: DF.Check # end: auto-generated types 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/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") 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/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/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")) diff --git a/erpnext/stock/services/serial_batch_bundle_service.py b/erpnext/stock/services/serial_batch_bundle_service.py index 2e752371ed5..a3e2d2060b2 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,63 @@ 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 = defaultdict(list) + 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 = flt(row.qty) - flt(row.delivered_qty) + outstanding_qty[key] += outstanding + if outstanding > 0: + reservations[key].append(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 flt(reserved_qty, 6) <= 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 + + 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} 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), + vouchers, + 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 +674,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))) 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: 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/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) diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index deeb8310d9c..aebb7a22650 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", "for_buying", "for_selling"], records) + def make_operation(self): records = [ {"doctype": "Operation", "name": "_Test Operation 1", "workstation": "_Test Workstation 1"} 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" -}