Skip to content

Architecture

ERP DLM is a complete ERP built with Python FastAPI + Vue 3 + PrimeVue 4 + PostgreSQL 16, containerised with Docker Compose.

Layer Technology
Backend Python 3.13, FastAPI, SQLAlchemy 2.0 async, asyncpg, Alembic, Pydantic v2
Frontend Vue 3 Composition API, TypeScript, PrimeVue 4, Pinia, vue-i18n, Vite
Database PostgreSQL 16 (UUID PKs, soft deletes, timestamps, JSONB)
Infrastructure Docker Compose (postgres, redis, backend, frontend), Caddy reverse proxy
erp-dlm/
├── backend/
│ └── app/
│ ├── core/ # Auth, DB, module_loader, core services
│ └── modules/ # ~50 functional modules
├── frontend/
│ └── src/
│ ├── core/ # Layouts, reusable components, API client, tours
│ ├── modules/ # Module views
│ ├── locales/ # es.json, en.json
│ └── router/ # Vue Router with lazy loading
├── docs/ # Additional technical documentation
└── docs-site/ # This documentation site (Astro + Starlight)

Each functional module lives in backend/app/modules/<name>/ and typically contains:

  • __init__.py — may be empty.
  • api.py — router = APIRouter(prefix="", ...).
  • models.py — SQLAlchemy tables.
  • schemas.py — Pydantic models.
  • service.py — business logic.

backend/app/core/module_loader.py scans backend/app/modules/ at startup and auto-registers each module with the prefix /api/{module-name}.

Important rules:

  • Each api.py router must use prefix="".
  • The loader converts underscores to hyphens: sales_invoices is mounted as /api/sales-invoices.
  • Some modules are excluded from auto-discovery and registered manually in main.py:
    • bank_reconciliation → /api/bank-reconciliation
    • mcp_server → /api/mcp
    • client_portal → /api/portal
    • accounting/reports_api.py → /api
    • saas_manager → /api/saas

The system supports multi-tenancy through the Host header.

  • erp.ovlicuo.com → default instance.
  • acme-sl.erp.ovlicuo.com → tenant acme-sl.

The middleware (backend/app/core/tenant_middleware.py) extracts the subdomain, looks it up in the SaaS manager database and creates a cached SQLAlchemy engine for the tenant database. The value request.state.tenant_subdomain is propagated to get_db().

Service File Responsibility
accounting_service backend/app/core/accounting_service.py Generates accounting entries from invoices and defines VAT/RE account mapping
numbering backend/app/core/numbering.py Automatic document numbering: {SERIES}-{YEAR}-{SEQUENCE:05d}
pdf_service backend/app/core/pdf_service.py PDF generation with ReportLab
stock_service backend/app/core/stock_service.py Warehouse stock, e-commerce reservations and transfers
tax_reports backend/app/core/tax_reports.py Spanish fiscal models: 303, 347, 349, 111, 115, 390, 190, 130
document_workflow backend/app/core/document_workflow.py Conversion between sales/purchase documents

The backend uses JWT:

  • Login: POST /api/auth/login returns access_token and refresh_token.
  • Refresh: POST /api/auth/refresh.
  • Profile: GET /api/auth/me.
  • Permissions: GET /api/auth/me/permissions.

For external integrations there is an api_tokens module, which allows creating tokens with scopes (api_access) and filtering by company_id in multi-tenant environments.

  • Views orchestrate pages, Components receive props and emit events, Composables hold logic.
  • apiClient has baseURL: '/api': calls use routes without duplicating the prefix (apiClient.get('/agents')).
  • PrimeVue 4: use Select (not Dropdown), ToggleSwitch (not InputSwitch) and Tabs/TabList/Tab (not TabView).
  • Every new router view under MainLayout requires a tour (core/tours/<routeName>.ts), registry entry (core/tours/registry.ts), guide section (core/guide/guide-content.ts) and i18n keys in both es.json and en.json.