refactor: enforce company restrictions on any doctype with a Company link

Replace the manually maintained transaction allowlist with a wildcard
validate hook: any doctype carrying a Company link field is checked, so
new doctypes are covered automatically. System-managed doctypes (ledger
entries, reposts, bundles, bins, POS consolidation, bank feeds) are
exempted so cancel, repost and reconciliation of documents created
before a restriction changed keep working; that guarantee is pinned by
a cancel-after-restriction test.
This commit is contained in:
Mihir Kandoi
2026-07-22 14:36:48 +05:30
parent ce01fa0e34
commit 1982816a70
3 changed files with 53 additions and 42 deletions

View File

@@ -364,44 +364,12 @@ pre_submit_validation_doctypes = [
"Sales Order",
]
company_restricted_transaction_doctypes = [
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
"Material Request",
"Request for Quotation",
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
"Stock Entry",
"Stock Reconciliation",
"Payment Entry",
"Journal Entry",
"Subcontracting Order",
"Subcontracting Receipt",
"BOM",
"Work Order",
"Job Card",
"Production Plan",
"Pick List",
"Blanket Order",
"Asset Capitalization",
"Asset Repair",
"Dunning",
"Installation Note",
"Maintenance Schedule",
"Maintenance Visit",
"Warranty Claim",
]
doc_events = {
"*": {
"validate": [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
"erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job",
"erpnext.stock.doctype.company_restriction.company_restriction.validate_transaction_company",
],
},
tuple(period_closing_doctypes): {
@@ -410,9 +378,6 @@ doc_events = {
tuple(pre_submit_validation_doctypes): {
"validate": "erpnext.accounts.utils.pre_submit_validation",
},
tuple(company_restricted_transaction_doctypes): {
"validate": "erpnext.stock.doctype.company_restriction.company_restriction.validate_transaction_company",
},
("Item", "Customer", "Supplier"): {
"validate": "erpnext.stock.doctype.company_restriction.company_restriction.validate_allowed_companies",
},

View File

@@ -11,6 +11,33 @@ from pypika.terms import Bracket, ExistsCriterion
RESTRICTABLE_MASTER_DOCTYPES = ("Item", "Customer", "Supplier")
COMPANY_RESTRICTION_EXEMPT_DOCTYPES = frozenset(
{
"Advance Payment Ledger Entry",
"Asset",
"Bank Transaction",
"Bin",
"Exchange Rate Revaluation",
"GL Entry",
"Landed Cost Voucher",
"Loyalty Point Entry",
"POS Closing Entry",
"POS Invoice Merge Log",
"Payment Ledger Entry",
"Payment Reconciliation",
"Process Payment Reconciliation",
"Repost Accounting Ledger",
"Repost Item Valuation",
"Repost Payment Ledger",
"Serial No",
"Serial and Batch Bundle",
"Stock Closing Balance",
"Stock Ledger Entry",
"Stock Reservation Entry",
"Unreconcile Payment",
}
)
class CompanyRestrictionError(frappe.ValidationError):
pass
@@ -109,6 +136,13 @@ def validate_allowed_companies(doc, method=None):
def validate_transaction_company(doc, method=None):
if doc.doctype in COMPANY_RESTRICTION_EXEMPT_DOCTYPES:
return
company_field = doc.meta.get_field("company")
if not company_field or company_field.fieldtype != "Link" or company_field.options != "Company":
return
company = doc.get("company")
if not company:
return

View File

@@ -60,10 +60,22 @@ class TestCompanyRestriction(ERPNextTestSuite):
item.restrict_to_companies = 1
self.assertRaises(frappe.MandatoryError, item.save)
def test_hooked_doctypes_have_company_field(self):
from erpnext.hooks import company_restricted_transaction_doctypes
def test_exempt_doctypes_exist(self):
from erpnext.stock.doctype.company_restriction.company_restriction import (
COMPANY_RESTRICTION_EXEMPT_DOCTYPES,
)
for doctype in company_restricted_transaction_doctypes:
self.assertTrue(
frappe.get_meta(doctype).has_field("company"), f"{doctype} has no company field"
)
for doctype in COMPANY_RESTRICTION_EXEMPT_DOCTYPES:
self.assertTrue(frappe.db.exists("DocType", doctype), f"{doctype} is not a DocType")
def test_cancel_works_after_restriction_change(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item = make_item()
stock_entry = make_stock_entry(
item_code=item.name, qty=5, to_warehouse="_Test Warehouse - _TC", rate=100
)
self.restrict_to_companies("Item", item.name, ["_Test Company 1"])
stock_entry.reload()
stock_entry.cancel()