docs: Core Plugin & Dependency concept — is_core flag, topological sort, report_generator, ERP roadmap
This commit is contained in:
@@ -0,0 +1,336 @@
|
|||||||
|
# LeoCRM — Core Plugin & Dependency Konzept
|
||||||
|
|
||||||
|
## 1. Ziel
|
||||||
|
|
||||||
|
LeoCRM soll modular zu einem ERP ausgebaut werden. Die Plugin-Architektur ist bereits vorhanden (Manifest, BasePlugin, Registry), aber es fehlen:
|
||||||
|
|
||||||
|
1. **Core Plugins** — unverzichtbare Basis-Plugins die immer aktiv sind
|
||||||
|
2. **Dependency Resolution** — Plugins können Abhängigkeiten deklarieren und diese werden durchgesetzt
|
||||||
|
3. **ERP-Module** — fachliche Erweiterungen die auf Core-Plugins aufbauen
|
||||||
|
|
||||||
|
## 2. Plugin-Kategorien
|
||||||
|
|
||||||
|
### 2.1 Core Plugins (`is_core: true`)
|
||||||
|
|
||||||
|
- **Immer aktiv** — können nicht deaktiviert oder deinstalliert werden
|
||||||
|
- **Werden als erste geladen** — vor allen Nicht-Core-Plugins
|
||||||
|
- **Basis-Funktionalität** die andere Plugins voraussetzen
|
||||||
|
- **Beispiele:**
|
||||||
|
- `permissions` — RBAC-System
|
||||||
|
- `entity_links` — Querverweise zwischen Entitäten
|
||||||
|
- `tags` — Tagging-System
|
||||||
|
- `report_generator` — Report- & Dokumentgenerator
|
||||||
|
- `audit` — Audit-Log (bereits im Core, nicht als Plugin)
|
||||||
|
|
||||||
|
### 2.2 Builtin Plugins
|
||||||
|
|
||||||
|
- **Mitgeliefert aber optional** — können deaktiviert werden
|
||||||
|
- **Dürfen Core-Dependencies deklarieren**
|
||||||
|
- **Beispiele:** `dms`, `mail`, `calendar`
|
||||||
|
|
||||||
|
### 2.3 Custom Plugins
|
||||||
|
|
||||||
|
- **Nutzer-/Drittanbieter-Plugins** — zur Laufzeit installierbar
|
||||||
|
- **Müssen Dependencies explizit deklarieren**
|
||||||
|
- **Beispiele:** ERP-Module (Invoicing, Inventory, HR, etc.)
|
||||||
|
|
||||||
|
## 3. Manifest-Erweiterung
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PluginManifest(BaseModel):
|
||||||
|
# ... bestehende Felder ...
|
||||||
|
|
||||||
|
# NEU: Core-Plugin Flag
|
||||||
|
is_core: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Core plugins cannot be deactivated and load first"
|
||||||
|
)
|
||||||
|
|
||||||
|
# NEU: Mindestversion für Dependencies
|
||||||
|
dependencies: list[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Plugin names this plugin requires (must be installed and active)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ERWEITERT: Semantic dependency with version
|
||||||
|
# dependency: str = "permissions>=1.0.0"
|
||||||
|
# Format: plugin_name[>=|>|<=|<|==version]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Dependency Resolution
|
||||||
|
|
||||||
|
### 4.1 Topological Sort
|
||||||
|
|
||||||
|
Die Registry muss Plugins in Abhängigkeits-Reihenfolge laden:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. permissions (is_core, keine deps)
|
||||||
|
2. entity_links (is_core, deps: [permissions])
|
||||||
|
3. tags (is_core, deps: [permissions])
|
||||||
|
4. report_generator (is_core, deps: [permissions, entity_links])
|
||||||
|
5. dms (deps: [permissions])
|
||||||
|
6. mail (deps: [permissions])
|
||||||
|
7. calendar (deps: [permissions])
|
||||||
|
8. invoicing (deps: [permissions, report_generator]) ← ERP-Modul
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Algorithmus
|
||||||
|
|
||||||
|
```python
|
||||||
|
def resolve_load_order(plugins: dict[str, BasePlugin]) -> list[str]:
|
||||||
|
"""Topological sort: Core first, then by dependency order."""
|
||||||
|
# 1. Kahn's Algorithm oder DFS-based topo sort
|
||||||
|
# 2. Core-Plugins bekommen Priorität bei gleichrangigen Abhängigkeiten
|
||||||
|
# 3. Zyklus-Erkennung: RuntimeError bei zirkulären Dependencies
|
||||||
|
# 4. Fehlende Dependency: RuntimeError mit klarer Meldung
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Validierung beim Installieren
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def validate_dependencies(plugin: BasePlugin, db: AsyncSession) -> None:
|
||||||
|
"""Prüft vor Installation ob alle Dependencies erfüllt sind."""
|
||||||
|
for dep_name in plugin.manifest.dependencies:
|
||||||
|
dep = await get_plugin_record(db, dep_name)
|
||||||
|
if dep is None:
|
||||||
|
raise PluginDependencyError(
|
||||||
|
f"Plugin '{plugin.name}' requires '{dep_name}' which is not installed"
|
||||||
|
)
|
||||||
|
if not dep.active:
|
||||||
|
raise PluginDependencyError(
|
||||||
|
f"Plugin '{plugin.name}' requires '{dep_name}' to be active"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Deaktivierungs-Schutz
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def deactivate_plugin(name: str, db: AsyncSession) -> None:
|
||||||
|
"""Verhindert Deaktivierung wenn andere Plugins abhängen."""
|
||||||
|
# 1. Prüfe ob Plugin is_core → Fehler
|
||||||
|
# 2. Prüfe ob andere aktive Plugins dieses Plugin als Dependency haben → Fehler
|
||||||
|
dependents = await get_dependent_plugins(db, name)
|
||||||
|
if dependents:
|
||||||
|
raise PluginDependencyError(
|
||||||
|
f"Cannot deactivate '{name}': {dependents} still depend on it"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Report Generator als Core Plugin
|
||||||
|
|
||||||
|
### 5.1 Konzept
|
||||||
|
|
||||||
|
Der Report Generator ist ein **Core Plugin** das Dokumente und Reports erzeugt. ERP-Module (Invoicing, Inventory, etc.) nutzen ihn als Dependency.
|
||||||
|
|
||||||
|
### 5.2 Manifest
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ReportGeneratorPlugin(BasePlugin):
|
||||||
|
manifest = PluginManifest(
|
||||||
|
name="report_generator",
|
||||||
|
version="1.0.0",
|
||||||
|
display_name="Report Generator",
|
||||||
|
description="Generates PDF/Excel/CSV reports from templates and data sources",
|
||||||
|
is_core=True,
|
||||||
|
dependencies=["permissions", "entity_links"],
|
||||||
|
routes=[
|
||||||
|
PluginRouteDef(
|
||||||
|
path="/api/v1/reports",
|
||||||
|
module="app.plugins.builtins.report_generator.routes",
|
||||||
|
router_attr="router",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
events=["report.requested", "report.generated"],
|
||||||
|
migrations=["0001_initial.sql"],
|
||||||
|
permissions=["reports.read", "reports.generate", "reports.manage_templates"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Funktionalität
|
||||||
|
|
||||||
|
- **Template Engine** — Jinja2-basierte Templates für PDF/Excel/CSV
|
||||||
|
- **Data Sources** — SQL-Queries oder Python-Funktionen als Datenquelle
|
||||||
|
- **Scheduling** — Cron-basierte Report-Generierung
|
||||||
|
- **Output** — PDF (WeasyPrint), Excel (openpyxl), CSV, JSON
|
||||||
|
- **Storage** — Reports werden im DMS gespeichert (wenn DMS aktiv)
|
||||||
|
- **Distribution** — E-Mail-Versand, Download, API
|
||||||
|
|
||||||
|
### 5.4 API Endpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/reports/templates — Liste aller Templates
|
||||||
|
POST /api/v1/reports/templates — Template erstellen
|
||||||
|
GET /api/v1/reports/templates/{id} — Template Details
|
||||||
|
PUT /api/v1/reports/templates/{id} — Template aktualisieren
|
||||||
|
DELETE /api/v1/reports/templates/{id} — Template löschen
|
||||||
|
|
||||||
|
POST /api/v1/reports/generate — Report generieren (async)
|
||||||
|
GET /api/v1/reports/{id} — Report Status/Download
|
||||||
|
GET /api/v1/reports/{id}/download — Report herunterladen
|
||||||
|
|
||||||
|
GET /api/v1/reports/scheduled — Geplante Reports
|
||||||
|
POST /api/v1/reports/scheduled — Report planen
|
||||||
|
DELETE /api/v1/reports/scheduled/{id} — Geplanten Report löschen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 ERP-Nutzung
|
||||||
|
|
||||||
|
Ein ERP-Modul "Invoicing" würde den Report Generator nutzen:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class InvoicingPlugin(BasePlugin):
|
||||||
|
manifest = PluginManifest(
|
||||||
|
name="invoicing",
|
||||||
|
version="1.0.0",
|
||||||
|
display_name="Invoicing",
|
||||||
|
description="Invoice management with PDF generation",
|
||||||
|
dependencies=["permissions", "report_generator", "currencies", "taxes"],
|
||||||
|
routes=[
|
||||||
|
PluginRouteDef(
|
||||||
|
path="/api/v1/invoicing",
|
||||||
|
module="app.plugins.builtins.invoicing.routes",
|
||||||
|
router_attr="router",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
events=["invoice.created", "invoice.sent", "invoice.paid"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_invoice_created(self, payload: dict) -> None:
|
||||||
|
"""When an invoice is created, generate PDF via report_generator."""
|
||||||
|
# Ruft report_generator API auf: POST /api/v1/reports/generate
|
||||||
|
# Template: "invoice_template"
|
||||||
|
# Data: payload (invoice data)
|
||||||
|
# Output: PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. ERP-Aufbau-Strategie
|
||||||
|
|
||||||
|
### 6.1 Phasen
|
||||||
|
|
||||||
|
| Phase | Plugins | Funktionalität |
|
||||||
|
|---|---|---|
|
||||||
|
| **Phase 1** | `report_generator` (Core) | Report- & Dokumentgenerator |
|
||||||
|
| **Phase 2** | `invoicing` | Rechnungen mit PDF-Generierung |
|
||||||
|
| **Phase 3** | `inventory` | Lagerverwaltung, Bestandsführung |
|
||||||
|
| **Phase 4** | `purchase_orders` | Bestellungen, Lieferanten |
|
||||||
|
| **Phase 5** | `hr` | Mitarbeiter, Gehalt, Urlaub |
|
||||||
|
| **Phase 6** | `accounting` | Buchhaltung, Buchungen, Bilanz |
|
||||||
|
|
||||||
|
### 6.2 Abhängigkeits-Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
permissions (Core)
|
||||||
|
├── entity_links (Core)
|
||||||
|
├── tags (Core)
|
||||||
|
├── report_generator (Core)
|
||||||
|
│ ├── invoicing
|
||||||
|
│ │ └── accounting
|
||||||
|
│ ├── purchase_orders
|
||||||
|
│ │ └── accounting
|
||||||
|
│ └── hr
|
||||||
|
├── currencies (Core, bereits als Basis-Feature)
|
||||||
|
├── taxes (Core, bereits als Basis-Feature)
|
||||||
|
├── dms (Builtin)
|
||||||
|
├── mail (Builtin)
|
||||||
|
└── calendar (Builtin)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Implementierungs-Prinzipien
|
||||||
|
|
||||||
|
1. **Jedes ERP-Modul ist ein Plugin** — kein festcodiertes ERP
|
||||||
|
2. **Core-Plugins sind stabil** — brechen nie andere Plugins
|
||||||
|
3. **Versionierte Dependencies** — `dependencies=["report_generator>=1.0.0"]`
|
||||||
|
4. **Event-Driven** — Plugins kommunizieren über Events, nicht direkte Aufrufe
|
||||||
|
5. **Tenant-Isolated** — Jedes Plugin respektiert Tenant-Grenzen
|
||||||
|
6. **Frontend-Modular** — Plugin-Frontends werden dynamisch geladen
|
||||||
|
|
||||||
|
## 7. Technische Umsetzung
|
||||||
|
|
||||||
|
### 7.1 Registry-Erweiterung
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app/plugins/registry.py — neue Methoden
|
||||||
|
|
||||||
|
class PluginRegistry:
|
||||||
|
# ... bestehend ...
|
||||||
|
|
||||||
|
def resolve_load_order(self) -> list[str]:
|
||||||
|
"""Topological sort of all discovered plugins."""
|
||||||
|
# 1. Build dependency graph
|
||||||
|
# 2. Core-Plugins first
|
||||||
|
# 3. Topological sort (Kahn's algorithm)
|
||||||
|
# 4. Cycle detection
|
||||||
|
# 5. Missing dependency detection
|
||||||
|
|
||||||
|
async def validate_dependencies(
|
||||||
|
self, plugin_name: str, db: AsyncSession
|
||||||
|
) -> list[str]:
|
||||||
|
"""Check if all dependencies are installed and active."""
|
||||||
|
# Returns list of missing/unmet dependencies
|
||||||
|
|
||||||
|
async def get_dependents(
|
||||||
|
self, plugin_name: str, db: AsyncSession
|
||||||
|
) -> list[str]:
|
||||||
|
"""Find all plugins that depend on this one."""
|
||||||
|
# For deactivation protection
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Plugin Model Erweiterung
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app/models/plugin.py — neues Feld
|
||||||
|
class Plugin(BaseModel):
|
||||||
|
# ... bestehende Felder ...
|
||||||
|
is_core: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Startup-Sequenz (main.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Aktuell: Alle Builtins werden automatisch installiert+aktiviert
|
||||||
|
# Neu:
|
||||||
|
# 1. Discover all plugins
|
||||||
|
# 2. resolve_load_order() → [permissions, entity_links, tags, report_generator, dms, mail, calendar]
|
||||||
|
# 3. For each in order:
|
||||||
|
# a. Check if in DB → if not, create record (is_core=True for core plugins)
|
||||||
|
# b. Run migrations
|
||||||
|
# c. Activate (skip for non-core if deactivated by admin)
|
||||||
|
# d. Register routes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Frontend-Anpassung
|
||||||
|
|
||||||
|
### 8.1 Sidebar dynamisch
|
||||||
|
|
||||||
|
Die Sidebar sollte Plugins nicht mehr hardcoded listen, sondern dynamisch aus der Plugin-API laden:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// GET /api/v1/plugins/active → [{name, display_name, icon, route_prefix, is_core}]
|
||||||
|
// Sidebar rendert nur aktive Plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Plugin-Frontend-Laden
|
||||||
|
|
||||||
|
Jedes Plugin kann ein Frontend-Modul mitbringen:
|
||||||
|
|
||||||
|
```
|
||||||
|
app/plugins/builtins/invoicing/
|
||||||
|
├── __init__.py
|
||||||
|
├── plugin.py # Backend Plugin
|
||||||
|
├── routes.py # API Routes
|
||||||
|
├── manifest.py # (in plugin.py)
|
||||||
|
├── migrations/ # SQL Migrations
|
||||||
|
└── frontend/ # Frontend Module
|
||||||
|
├── index.tsx # Plugin Entry Point
|
||||||
|
├── pages/ # Plugin Pages
|
||||||
|
└── components/ # Plugin Components
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Nächste Schritte
|
||||||
|
|
||||||
|
1. **Manifest erweitern** — `is_core` Feld hinzufügen
|
||||||
|
2. **Registry erweitern** — `resolve_load_order()`, `validate_dependencies()`, `get_dependents()`
|
||||||
|
3. **Plugin Model erweitern** — `is_core` Spalte
|
||||||
|
4. **Startup-Sequenz anpassen** — Core-First, Topological Sort
|
||||||
|
5. **Report Generator Plugin bauen** — Templates, PDF/Excel, Scheduling
|
||||||
|
6. **ERP-Module starten** — Invoicing als erstes Modul
|
||||||
Reference in New Issue
Block a user