Guide: Extending Core with Regional Compliance
How to “plug in” regional logic into business-m libraries without modifying core code.
1. The Strategy: “Don’t Touch the Core”
Section titled “1. The Strategy: “Don’t Touch the Core””The goal of the Port/Adapter architecture is to keep m-core, wms, and finance completely agnostic of regional rules.
Instead of adding an if region == 'india': block in wms, we use Events and Mixins.
2. Extending DocTypes (Mixins + Registry)
Section titled “2. Extending DocTypes (Mixins + Registry)”Most regional modules need extra fields on standard DocTypes (e.g., gstin on Address). We use Standard Python Mixins for the developer UX, and the framework’s MetadataDecoratorRegistry for system stability.
The Developer Action (App Level)
Section titled “The Developer Action (App Level)”class IndiaAddressMixin: gstin: str | None = None place_of_supply: str | None = NoneThe “Invisible” Infrastructure (Framework Level)
Section titled “The “Invisible” Infrastructure (Framework Level)”During bootstrap, the framework uses the MetadataDecoratorRegistry (which you implemented) to bake these into the model:
from m_india.doctypes.mixins import IndiaAddressMixinfrom framework_m.metadata import MetadataDecoratorRegistry
registry = MetadataDecoratorRegistry.get_instance()
# Register the transition. This informs the registry how to handle these fields.registry.register_property("Address", "gstin", str, persistence="json")registry.register_property("Address", "place_of_supply", str, persistence="json")The Value of the “Overengineering”:
Section titled “The Value of the “Overengineering”:”- Model Stability: When you call
registry.bake(Address), you get a single unified model even if multiple regional apps are active. - Transparent Persistence: Fields with
persistence="json"are stored in the sharedcustom_fieldscolumn, avoiding day-1 migrations. - No-Cliff Scaling: The same
Addressmodel is distributed to all macroservices in Enterprise mode, ensuring they all share the exact same schema.
3. Hooking into Business Logic (Events)
Section titled “3. Hooking into Business Logic (Events)”When a core event happens (like SalesInvoice.on_submit), regional adapters can listen and perform compliance actions.
Example: E-Way Bill on DeliveryNote Submission
Section titled “Example: E-Way Bill on DeliveryNote Submission”In wms, the core logic simply publishes an event.
class DeliveryNoteController(BaseController): async def on_submit(self, doc): # 1. Update stock await self.update_stock(doc)
# 2. Publish compliance event await self.publish("compliance.transaction.submitted", doc)In m-india, we subscribe to this event.
@subscribe("compliance.transaction.submitted")async def handle_delivery_note(doc): if doc.doctype == "DeliveryNote" and doc.total_amount > 50000: # Trigger India-specific E-Way Bill generation await india_compliance.generate_eway_bill(doc)4. Port Dispatcher
Section titled “4. Port Dispatcher”For synchronous logic like tax calculation, we use the RegionalPort dispatcher.
Example: Calculating Taxes on SalesInvoice
Section titled “Example: Calculating Taxes on SalesInvoice”In finance, the logic calls the m-regional port.
from m_regional import get_active_provider
async def calculate_bill_taxes(inv: SalesInvoice): # This automatically finds 'm-india' or 'm-usa' based on context. provider = await get_active_provider("tax", context=inv)
if provider: inv.taxes = await provider.calculate_taxes(inv)
inv.save()5. Summary of Regional Touchpoints
Section titled “5. Summary of Regional Touchpoints”| Context | Core Lib | Hook/Event | Regional Action |
|---|---|---|---|
| Procurement | finance |
PurchaseInvoice.on_save |
Calculate GST/TDS (India) |
| Sales | finance |
SalesInvoice.on_submit |
Generate IRN (India) |
| Logistics | wms |
DeliveryNote.on_submit |
Generate E-Way Bill (India) |
| Payroll | people |
SalarySlip.on_calculate |
Deduct PF/PT (India) |