m-core: Foundation Module
Base composition elements for any Framework M business application.
This is NOT an ERP monolith. These are reusable primitives.
🏗️ Architectural Rule: Bounded Contexts (Composition)
Section titled “🏗️ Architectural Rule: Bounded Contexts (Composition)”CRITICAL: m-core acts strictly as an Identity Provider and Global Registry. It stores ONLY what is universally true across ALL apps.
- NO domain-specific fields are allowed in
m-coreDocTypes. - Examples:
credit_limitbelongs in the finance app (asFinanceCustomer).lead_scorebelongs in the CRM app (asCRMCustomerProfile).departmentbelongs in the people app (asEmployeeProfile). - Extension apps MUST define their own peripheral DocTypes that maintain a
foreign_keylink back tom-coreidentities, rather than adding columns to the core tables.
Overview
Section titled “Overview”m-core provides:
- Party — Abstract base for all party types
- Address — Reusable address structure
- Contact — Contact details with party link
- Item — Generic item/product/SKU
- UOM — Units of measure
- Currency — Multi-currency support
1. Party (Abstract Base)
Section titled “1. Party (Abstract Base)”class Party(BaseDocType): """Abstract base for all party types.
Specialized as: Supplier, Customer, Employee, Member, etc. """
# Identity party_type: str # Supplier, Customer, Employee, Member party_name: str = Field(max_length=200) party_code: str | None = Field(max_length=50, unique=True)
# Contact email: str | None = None phone: str | None = None mobile: str | None = None
# Addresses (links) primary_address: str | None = None # FK to Address billing_address: str | None = None shipping_address: str | None = None
# Status is_active: bool = True
# Extensible attributes (JSON for custom fields) attributes: dict = Field(default_factory=dict)
class Meta: abstract = True # Not a table, just a baseSpecialized Party Types
Section titled “Specialized Party Types”class Supplier(Party): """Vendor who supplies goods/services.
NOTE: Financial fields (payment_terms, default_currency) are managed by the finance module via a linked FinanceSupplier DocType. """ party_type: str = "Supplier"
# Core Identity Only supplier_group: str | None = None
class Meta: table_name = "supplier"
class Customer(Party): """Entity who purchases goods/services.
NOTE: Financial fields (credit_limit, tax_id) are managed by the finance module via a linked FinanceCustomer DocType. Sales fields (lead_score) are managed by CRM. """ party_type: str = "Customer"
# Core Identity Only customer_group: str | None = None
class Meta: table_name = "customer"
class Employee(Party): """Internal employee.
NOTE: HR / People fields (department, designation, leave_balance) are managed by the people module via a linked EmployeeProfile DocType. """ party_type: str = "Employee"
# Core Identity Only employee_id: str date_of_joining: date | None = None
class Meta: table_name = "employee"
class Member(Party): """Beneficiary/member (e.g., Legacy ERP beneficiary).""" party_type: str = "Member"
# Core Identity Only member_id: str valid_from: date | None = None valid_until: date | None = None
class Meta: table_name = "member"2. Address
Section titled “2. Address”class Address(BaseDocType): """Reusable address, linked to any party."""
address_title: str | None = None address_type: str = "Billing" # Billing, Shipping, Office, etc.
# Address lines address_line_1: str address_line_2: str | None = None
# Location city: str state: str country: str = "India" pincode: str
# Coordinates (optional) latitude: Decimal | None = None longitude: Decimal | None = None
# Links (polymorphic) link_doctype: str | None = None # "Supplier", "Customer", etc. link_name: str | None = None # Party ID
class Meta: table_name = "address"3. Contact
Section titled “3. Contact”class Contact(BaseDocType): """Contact person, linked to any party."""
first_name: str last_name: str | None = None
email: str | None = None phone: str | None = None mobile: str | None = None
designation: str | None = None is_primary: bool = False
# Links (polymorphic) link_doctype: str | None = None link_name: str | None = None
class Meta: table_name = "contact"4. Item
Section titled “4. Item”class Item(BaseDocType): """Generic item/product/SKU."""
item_code: str = Field(max_length=50, unique=True) item_name: str = Field(max_length=200)
# Classification item_group: str | None = None item_type: str = "Product" # Product, Service, Asset
# Units stock_uom: str = "Nos" # FK to UOM
# Pricing (base) standard_rate: Decimal = Decimal("0")
# Inventory is_stock_item: bool = True
# Barcodes barcode: str | None = None
# Attributes (flexible) attributes: dict = Field(default_factory=dict)
# Status is_active: bool = True
class Meta: table_name = "item" # bind_key = "mongo" # Optional: different DB5. UOM (Unit of Measure)
Section titled “5. UOM (Unit of Measure)”class UOM(BaseDocType): """Unit of Measure."""
uom_name: str = Field(max_length=50, unique=True) symbol: str | None = None # "kg", "pcs", "ltr"
# Conversion is_base_uom: bool = False conversion_factor: Decimal = Decimal("1")
class Meta: table_name = "uom"6. Currency
Section titled “6. Currency”class Currency(BaseDocType): """Currency master."""
currency_code: str = Field(max_length=3, unique=True) # INR, USD currency_name: str symbol: str # ₹, $
# Formatting decimal_places: int = 2 symbol_position: str = "before" # before, after
# Exchange exchange_rate: Decimal = Decimal("1") # vs base currency
is_enabled: bool = True
class Meta: table_name = "currency"Database Flexibility Patterns
Section titled “Database Flexibility Patterns”Framework M supports flexible database wiring — not locked to a single database.
Pattern 1: Multiple SQL Databases (bind_key)
Section titled “Pattern 1: Multiple SQL Databases (bind_key)”For SQL databases, use SQLAlchemy’s bind_key:
DB_BINDS = { "default": "postgresql://...", # Main DB "hr_db": "postgresql://hr_host/...", # HR on separate Postgres}
# doctypes/employee.pyclass Employee(BaseDocType): class Meta: table_name = "employee" bind_key = "hr_db" # Goes to HR databasePattern 2: Custom Adapters (NoSQL)
Section titled “Pattern 2: Custom Adapters (NoSQL)”For non-SQL databases (MongoDB, Elasticsearch), use adapter pattern:
class ItemCatalogAdapter: """MongoDB adapter for item search."""
def __init__(self, mongo_uri: str): self.client = MongoClient(mongo_uri) self.collection = self.client.wms.items
async def search(self, query: str) -> list[dict]: return await self.collection.find({"$text": {"$search": query}})
async def sync_from_postgres(self, item: Item): """Sync Item DocType to Mongo for search.""" await self.collection.update_one( {"_id": item.id}, {"$set": item.model_dump()}, upsert=True, )
# Usage in servicecatalog = ItemCatalogAdapter(settings.MONGO_URI)results = await catalog.search("rice basmati")Pattern 3: Internal Microservices (Specialized DBs)
Section titled “Pattern 3: Internal Microservices (Specialized DBs)”For specialized data, use internal microservices (under apps/):
| Data Type | Service | Database |
|---|---|---|
| Ledger/Accounting | Book Keeper | TigerBeetle |
| Permissions | Permissions Service | SpiceDB |
| Policies | Policy Service | Cedar |
| Cache/Session | (built-in) | Redis |
# Ledger via Book Keeper serviceawait book_keeper.transfer( debit_account="stock_in_hand", credit_account="supplier_payable", amount=invoice.grand_total,)
# Permissions via SpiceDBcan_approve = await permissions.check( subject=f"user:{current_user.id}", permission="approve", resource=f"purchase_order:{po.id}",)Summary
Section titled “Summary”| Database Type | Pattern | When to Use |
|---|---|---|
| Multiple Postgres/MySQL | bind_key |
Multi-tenant, sharding |
| MongoDB | Custom adapter | Search, flexible schema |
| Elasticsearch | Custom adapter | Full-text search |
| TigerBeetle | Book Keeper service | Financial ledger |
| SpiceDB | Permissions service | Fine-grained authz |
| Redis | Built-in | Cache, session, queue |
Legacy ERP Overlay (Terminology)
Section titled “Legacy ERP Overlay (Terminology)”Legacy ERP uses m-core with aliases:
| m-core | Legacy ERP Term | Notes |
|---|---|---|
| Supplier | Firm/Supplier | Same |
| Customer | - | Not used (B2B2C model) |
| Member | Beneficiary | Defense personnel |
| Item | Item/SKU | Same |
Events Published
Section titled “Events Published”| Event | When |
|---|---|
party.created |
New party added |
party.updated |
Party modified |
item.created |
New item added |
address.linked |
Address linked to party |
- Create Party abstract base
- Create Supplier, Customer, Employee, Member
- Create Address with polymorphic links
- Create Contact with polymorphic links
- Create Item with attributes
- Create UOM with conversions
- Create Currency master
- Seed base data (UOMs, Currencies)
Time Estimate
Section titled “Time Estimate”| Task | Duration |
|---|---|
| Party + specializations | 0.5 day |
| Address + Contact | 0.5 day |
| Item + UOM + Currency | 0.5 day |
| Seeding | 0.5 day |
| Total | 2 days |