A Product Lifecycle Management (PLM) web app for a small aerospace/defense manufacturer's EN 9100 quality management system: parts, BOMs, documents, production batches, serial number genealogy, and RMAs. Django + DRF backend, React (TypeScript, Vite, Mantine) frontend.
Licensed under Apache 2.0. Contributions welcome — see CONTRIBUTING.md.
Django REST Framework backend, one app per domain area, mirroring PLAN.md's architecture; React (TypeScript, Vite, Mantine) SPA frontend talking to it over session-authenticated JSON — see API.md for the full REST reference.
A few choices that shape the rest of the codebase:
- Attach-anywhere documents and comments —
DocumentandCommentuse aGenericForeignKeyrather than one FK column per attachable node type, so a document can hang off a Part, PartRevision, Project, or Serial Number without a schema change each time a new node type needs it. - Per-part/batch/serial custom fields — firmware revision and similar
fields the company iterates on live in a
JSONField(custom_field_definitionsonPart,dynamic_fieldsonProductionBatch/SerialNumber), not fixed columns. - Audit trail — every core model carries
django-simple-history(full field-level history, who/when/what), plus explicit append-only event tables (SerialEvent,BatchPhaseChange) for the genealogy timelines EN 9100 traceability actually gets queried against. - State machines are admin-configurable —
ProductionBatch,RMA,SerialNumber, andDocumenteach drive a state machine with a hardcoded default transition graph, butRMAandProductionBatchconsult theWorkflowTransitiontable first, so an Admin can tighten or loosen allowed transitions from the Administration page without a code change. - Numbering is race-safe, not just unique — batch and RMA numbers
increment via a
SequenceCounterrow updated insideselect_for_update(), self-healing against the table's actual max so legacy-imported records that bypass the counter can't cause a collision on the next suggestion.
The core domain objects and how they relate (admin/config/audit models —
WorkflowTransition, SiteSettings, ImportRun, Subscription — and
the generic Document/Comment attach points omitted for clarity; see
API.md for those):
erDiagram
PROJECT ||--o{ PART : "top-level parts (optional)"
PART ||--o{ PART_REVISION : has
PART_REVISION ||--o{ BOM_ITEM : children
PART ||--o{ BOM_ITEM : "used as child"
VENDOR ||--o{ PART : supplies
PROJECT ||--o{ PRODUCTION_BATCH : batches
PART_REVISION ||--o{ PRODUCTION_BATCH : "frozen anchor"
PRODUCTION_BATCH ||--o{ SERIAL_NUMBER : generates
PART_REVISION ||--o{ SERIAL_NUMBER : "current revision"
SERIAL_NUMBER ||--o{ RMA_ITEM : "tracked unit (optional)"
RMA ||--o{ RMA_ITEM : items
CUSTOMER ||--o{ RMA : reports
CUSTOMER ||--o{ CUSTOMER_DIVISION : divisions
Part is either internal (owned, lives under a Project) or
purchased (sourced from a Vendor) — never both, enforced in
Part.clean(). A PartRevision becomes immutable once released; that's
the pointer a ProductionBatch freezes onto at production start, which
transitively pins the BOM tree and every attached document. SerialNumber
starts pointing at its batch's frozen revision but can be upgraded
per-unit independently (logged as a SerialEvent). An RMAItem links
either a tracked SerialNumber or a free-text reference for a unit
outside the system (legacy stock, third-party serials) — never both.
One-time machine setup (installs git, Docker, Python, Node — needs your sudo password, run it yourself in an interactive shell):
wsl -d Debian
bash /mnt/d/D/lvdplm/scripts/setup-wsl-debian.sh
# log out/in, or `newgrp docker`, so the docker group membership appliesThen, from a WSL Debian shell in this directory (/mnt/d/D/lvdplm):
cp backend/.env.example backend/.env
docker compose up --build- Backend: http://localhost:8000 (Django admin at
/admin/) - Frontend: http://localhost:5173
First-time database setup, in another terminal while docker compose up is running:
docker compose exec backend python manage.py migrate
docker compose exec backend python manage.py seed_groups
docker compose exec backend python manage.py seed_document_types
docker compose exec backend python manage.py createsuperuserdocker compose exec backend python manage.py test
docker compose exec backend ruff check .
docker compose exec frontend npm run lint
docker compose exec frontend npm run buildSee DEPLOYMENT.md — docker-compose.prod.yml (MySQL +
gunicorn + nginx), environment variable reference, TLS setup, backups,
and a from-scratch Proxmox LXC (Debian) walkthrough.