Skip to content

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-core DocTypes.
  • Examples: credit_limit belongs in the finance app (as FinanceCustomer). lead_score belongs in the CRM app (as CRMCustomerProfile). department belongs in the people app (as EmployeeProfile).
  • Extension apps MUST define their own peripheral DocTypes that maintain a foreign_key link back to m-core identities, rather than adding columns to the core tables.

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

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 base
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"

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"

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"

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 DB

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"

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"

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:

config.py
DB_BINDS = {
"default": "postgresql://...", # Main DB
"hr_db": "postgresql://hr_host/...", # HR on separate Postgres
}
# doctypes/employee.py
class Employee(BaseDocType):
class Meta:
table_name = "employee"
bind_key = "hr_db" # Goes to HR database

For non-SQL databases (MongoDB, Elasticsearch), use adapter pattern:

adapters/item_catalog.py
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 service
catalog = 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 service
await book_keeper.transfer(
debit_account="stock_in_hand",
credit_account="supplier_payable",
amount=invoice.grand_total,
)
# Permissions via SpiceDB
can_approve = await permissions.check(
subject=f"user:{current_user.id}",
permission="approve",
resource=f"purchase_order:{po.id}",
)
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 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

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)

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