mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 15:11:52 +00:00
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:
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user