Compare commits

...

2 Commits

Author SHA1 Message Date
b89fdedeae KI-AGENT: Positive Kontobuchungen mit BWA-Ausgaben verrechnen
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 21s
Build and Push Docker Images / build-frontend (push) Successful in 1m10s
Build and Push Docker Images / build-website (push) Successful in 22s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 1m20s
2026-09-07 20:50:24 +02:00
e4fcd79c8e KI-AGENT: Lagersystem fachlich und technisch konzipieren 2026-09-07 20:44:04 +02:00
5 changed files with 379 additions and 19 deletions

View File

@@ -5,5 +5,6 @@ Diese Dokumentation unterstützt dich bei der täglichen Nutzung von FEDEO.
## Einstieg
- [Bedienung](./bedienung/README.md)
- [Fach- und Technikkonzept für das Lagersystem](./lagersystem-konzept.md)
- [Kommunikationslösung auf Basis des Matrix-Standards](./kommunikationslösung-matrix.md)
- [Zentraler Push-Server für Selfhost-Instanzen](./zentraler-push-server.md)

355
docs/lagersystem-konzept.md Normal file
View File

@@ -0,0 +1,355 @@
# Fach- und Technikkonzept für das FEDEO-Lagersystem
## Zielbild
FEDEO soll Artikelbestände je Lagerplatz nachvollziehbar führen und die täglichen Vorgänge Wareneingang, Umlagerung, Reservierung, Projektverbrauch, Rückgabe und Inventur unterstützen. Jede Bestandsänderung wird als unveränderliche Buchung protokolliert. Der aktuelle Bestand ist damit kein frei editierbares Feld, sondern das Ergebnis aller bestätigten Lagerbuchungen.
Das Zielbild trennt drei heute teilweise vermischte Begriffe:
- **Artikel** (`products`) beschreiben einen wiederverwendbaren Material- oder Handelsartikel, zum Beispiel „Kabel NYM-J 3×1,5“.
- **Bestand** beschreibt, welche Menge eines Artikels sich an welchem Lagerplatz befindet.
- **Inventarartikel** (`inventoryitems`) sind individuell verwaltete Betriebsmittel oder Anlagegüter, zum Beispiel ein Messgerät oder Fahrzeugzubehör. Sie können eine Seriennummer und einen aktuellen Standort haben, sind aber kein mengenmäßig geführter Lagerbestand.
Kundeninventar bleibt ebenfalls ein eigener Bereich. Ein beim Kunden verbautes oder betreutes Gerät ist nicht automatisch eigener verfügbarer Lagerbestand.
## Vorhandene Basis und Handlungsbedarf
FEDEO bringt bereits wesentliche Bausteine mit:
- Artikelstammdaten mit Einheit, Artikelnummer, EAN, Barcodes, Preisen, Hersteller und Lieferantenzuordnung
- hierarchische eigene Lagerplätze und Kundenlagerplätze
- eigenes Inventar und Kundeninventar einschließlich Seriennummern
- Projekte und Belege, die später als Herkunft oder Ziel einer Lagerbuchung dienen können
- Rollen, Mandantentrennung, Änderungshistorie und Nummernkreise
- die Tabelle `movements` mit Artikel, Menge, Lagerplatz, Projekt und Seriennummern
Die vorhandene Tabelle `movements` ist jedoch noch kein belastbares Lagerbuch. Ihr fehlen unter anderem Buchungsart und -status, Gegenlager bei Umlagerungen, Belegreferenz, Stornobeziehung, Chargenmodell, Reservierungen und Schutz vor doppelter Verarbeitung. Zudem wird sie aktuell weder über eine eigene Route noch über eine Oberfläche genutzt.
Die Mengenfelder von Inventar und Kundeninventar sollten nicht zur Bestandsführung von Artikeln verwendet werden. Andernfalls entstehen parallele Wahrheiten zwischen Artikel, Inventar und Lagerbewegung.
## Fachliche Leitlinien
1. **Buchungen statt Überschreiben:** Bestände werden ausschließlich durch bestätigte Lagerbuchungen verändert.
2. **Keine stillen Korrekturen:** Fehlerhafte Buchungen werden durch eine Gegenbuchung storniert, nicht gelöscht oder nachträglich verändert.
3. **Ein Vorgang, eine Transaktion:** Alle Positionen eines Wareneingangs oder einer Umlagerung werden vollständig oder gar nicht gebucht.
4. **Mandantentrennung in jeder Abfrage:** Artikel, Lagerplätze, Buchungen und Referenzen müssen zum aktiven Mandanten gehören.
5. **Nachvollziehbare Herkunft:** Jede Buchung speichert Benutzer, Zeitpunkt, Buchungsgrund und optional Projekt, Lieferant oder FEDEO-Beleg.
6. **Reserviert ist nicht entnommen:** Reservierungen reduzieren den verfügbaren, aber nicht den physischen Bestand.
7. **Seriennummern und Chargen sind Bewegungsdaten:** Sie gehören zu konkreten Bestandseinheiten und nicht als Liste in eine allgemeine Buchungszeile.
8. **Bestätigen ist endgültig:** Entwürfe sind änderbar; bestätigte Buchungen sind unveränderlich.
## Fachliches Datenmodell
### Artikelstamm erweitern
`products` bleibt der zentrale Artikelstamm und erhält lagerbezogene Einstellungen:
| Feld | Bedeutung |
| --- | --- |
| `stockManaged` | Artikel wird im Lager geführt |
| `trackingMode` | `none`, `lot` oder `serial` |
| `allowNegativeStock` | negativer Bestand erlaubt; standardmäßig `false` |
| `minimumStock` | mandantenweiter Mindestbestand, optional |
| `reorderPoint` | Meldebestand, optional |
| `defaultWarehouse` | bevorzugtes Lager, optional |
| `valuationMethod` | zunächst fest `moving_average`, für spätere Erweiterung vorbereitet |
Die bestehende `unit` ist die Bestandsbasiseinheit. Mengen sollten als `numeric`, nicht als `bigint`, gespeichert werden, da Einheiten wie Meter, Stunden oder Kilogramm Dezimalmengen benötigen. Die erlaubte Genauigkeit kann aus `units.step` abgeleitet werden.
### Lagerstruktur
Die bestehenden `spaces` können als hierarchische Lagerstruktur weiterverwendet werden. Ergänzt werden sollten:
| Feld | Bedeutung |
| --- | --- |
| `kind` | `warehouse`, `zone`, `bin`, `vehicle`, `transit`, `scrap` |
| `stockEnabled` | an diesem Knoten darf Bestand liegen |
| `barcode` | scanbare eindeutige Kennung |
| `sortOrder` | stabile Reihenfolge in der Lageransicht |
Ein typischer Baum lautet beispielsweise `Hauptlager > Halle A > Regal 03 > Fach 02`. Nur Blattknoten beziehungsweise ausdrücklich aktivierte Knoten dürfen Bestand tragen. Die Kombination aus `tenant` und `space_number` sowie ein gesetzter Barcode müssen eindeutig sein. Ein Lagerplatz darf nicht sich selbst oder einen seiner Nachfolger als übergeordneten Lagerplatz erhalten.
### Lagerbeleg und Buchungszeilen
Für die belastbare Umsetzung empfiehlt sich ein Belegkopf mit mehreren Buchungszeilen:
#### `stock_transactions`
- `id` als UUID
- `tenant`
- `number` als lesbare Lagerbelegnummer
- `type`: `opening`, `receipt`, `transfer`, `issue`, `return`, `adjustment`, `count`, `production`, `reversal`
- `status`: `draft`, `posted`, `reversed`
- `bookingDate`
- `notes`
- optionale Referenzen auf `project`, `vendor`, `createddocument` und einen späteren Einkaufsbeleg
- `externalReference` für Lieferschein- oder Fremdbelegnummern
- `idempotencyKey` zum Schutz vor Doppelbuchungen durch API, Scanner oder Wiederholungsversuche
- `reversalOf` beziehungsweise `reversedBy`
- `createdAt`, `createdBy`, `postedAt`, `postedBy`
#### `stock_transaction_lines`
- `id` als UUID
- `transactionId`
- `position`
- `productId`
- `quantity` als positiver `numeric`-Wert
- `fromSpaceId`, optional
- `toSpaceId`, optional
- `unitCost`, optional
- `lotId`, optional
- `notes`, optional
Mindestens eine Seite einer Zeile muss gesetzt sein. Ein Wareneingang besitzt nur `toSpaceId`, eine Entnahme nur `fromSpaceId`, eine Umlagerung beide Seiten. Die Buchungsart bestimmt zusätzlich, welche Kombination erlaubt ist. Dadurch lässt sich eine Umlagerung atomar abbilden, ohne zwei lose Bewegungen erzeugen zu müssen.
### Chargen und Seriennummern
`stock_lots` bildet Chargen ab:
- Artikel, Chargennummer, optional Lieferantencharge
- Herstellungs- und Ablaufdatum
- optionale Lieferanten- und Wareneingangsreferenz
- mandantenweit eindeutige Kombination aus Artikel und Chargennummer
`stock_serials` bildet einzelne Seriennummern ab:
- Artikel und Seriennummer
- Status: `in_stock`, `reserved`, `issued`, `installed`, `scrapped`
- aktueller Lagerplatz als performanter Cache
- optionale Verbindung zu Inventar oder Kundeninventar, wenn ein ausgegebenes Gerät dort weitergeführt wird
Bei Seriennummern entspricht jede gebuchte Einheit genau einer Seriennummer. Bei Chargen muss die Summe der Chargenmengen der Buchungsmenge entsprechen.
### Bestände und Reservierungen
`stock_balances` ist eine technisch gepflegte Projektion für schnelle Listen und Prüfungen:
- `tenant`, `productId`, `spaceId`, optional `lotId`
- `quantityOnHand`
- `quantityReserved`
- `averageUnitCost`
- `updatedAt`
`quantityAvailable` wird als `quantityOnHand - quantityReserved` berechnet. Die eindeutige Kombination verhindert doppelte Bestandszeilen. Die Buchungszeilen bleiben die fachliche Wahrheit; `stock_balances` kann aus ihnen jederzeit neu aufgebaut und geprüft werden.
`stock_reservations` enthält reservierte Mengen für ein Projekt, eine Aufgabe oder einen Beleg. Zustände sind `active`, `partially_fulfilled`, `fulfilled`, `cancelled` und `expired`. Eine spätere Entnahme erfüllt die Reservierung in derselben Datenbanktransaktion.
Für das MVP kann auf Chargen, Seriennummern und Reservierungen zunächst verzichtet werden. Das Schema der Buchungen sollte diese Erweiterungen aber bereits ermöglichen.
## Buchungslogik
Beim Bestätigen eines Lagerbelegs führt das Backend in **einer Datenbanktransaktion** folgende Schritte aus:
1. Mandant, Berechtigung, Status und Eindeutigkeit des `idempotencyKey` prüfen.
2. Alle Artikel und Lagerplätze laden und ihre Zugehörigkeit zum Mandanten validieren.
3. Einheiten, Trackingregeln, Mengen und Pflichtangaben prüfen.
4. Betroffene `stock_balances` in einer stabilen Reihenfolge sperren.
5. Verfügbarkeit prüfen; bei einer Umlagerung ist die Quellseite maßgeblich.
6. Beleg und Zeilen als `posted` speichern und Bestandsprojektion aktualisieren.
7. Seriennummern, Chargen, Reservierungen und gleitenden Durchschnittspreis aktualisieren.
8. Einen verständlichen Historieneintrag erzeugen.
Bestandsrelevante Endpunkte dürfen nicht über die generische CRUD-Route umgesetzt werden, weil dort die fachlichen Invarianten, Sperren und Statusübergänge nicht sicher erzwungen werden können. Dafür wird ein eigener Lagermodul-Service verwendet.
Parallelzugriffe müssen über Zeilensperren oder ein atomisches SQL-Update wie `quantity_on_hand >= benötigte_menge` abgesichert werden. Eine reine Prüfung im Frontend verhindert keine Überentnahme.
### Bewertung
Für die erste Version bietet sich der **gleitende Durchschnittspreis** an:
```text
neuer Durchschnitt =
(alter Bestand × alter Durchschnitt + Zugangsmenge × Zugangspreis)
/ (alter Bestand + Zugangsmenge)
```
Abgänge werden mit dem zum Buchungszeitpunkt gültigen Durchschnitt bewertet. Der Wert wird auf der Buchungszeile festgeschrieben, damit spätere Preisänderungen historische Projektkosten nicht verändern. Stornozeilen übernehmen exakt Menge und Wert der Ursprungsbuchung mit umgekehrter Wirkung.
## Kernabläufe
### Wareneingang
1. Lieferant und optionale Lieferscheinnummer wählen.
2. Artikel scannen oder suchen, Menge, Zielplatz und Einkaufspreis erfassen.
3. Bei Bedarf Charge oder Seriennummern erfassen.
4. Als Entwurf speichern oder direkt bestätigen.
5. Bestand und gleitenden Durchschnittspreis erhöhen; optional Etiketten drucken.
Ein Eingangsrechnungsimport darf einen Wareneingang vorbereiten, aber nicht automatisch bestätigen. Rechnung und tatsächliche Warenannahme können zeitlich und mengenmäßig abweichen.
### Umlagerung
Quelle scannen, Artikel und Menge erfassen, Ziel scannen und bestätigen. Quellabgang und Zielzugang sind eine gemeinsame Buchung. Für mobile Lagerarbeit sollte dieser Ablauf mit wenigen Eingaben und großen Scanflächen umgesetzt werden.
### Projektentnahme und Rückgabe
Eine Entnahme reduziert Bestand und schreibt Menge sowie Wert einem Projekt zu. Rückgaben referenzieren nach Möglichkeit die ursprüngliche Entnahme. Damit können FEDEO-Projekte tatsächliche Materialkosten statt nur kalkulierter Materialpreise ausweisen.
### Reservierung
Material wird für ein Projekt vorgemerkt. Der physische Bestand bleibt unverändert, der verfügbare Bestand sinkt. Beim Kommissionieren oder bei der Entnahme wird die Reservierung ganz oder teilweise erfüllt.
### Inventur
1. Inventurzählung für Lager oder Teilbaum eröffnen und optional Plätze für normale Buchungen sperren.
2. Sollmengen als unveränderlichen Start-Snapshot speichern.
3. Zählmengen mobil erfassen; bei Bedarf Vier-Augen-Freigabe ab einer Differenzschwelle.
4. Differenzen prüfen und freigeben.
5. Nur die Differenzen als `count`- beziehungsweise `adjustment`-Buchung verbuchen.
Der gezählte Wert überschreibt niemals direkt den Bestand.
### Storno
Ein bestätigter Beleg erhält eine Gegenbuchung mit Referenz auf das Original. Ein erneutes Storno desselben Belegs ist ausgeschlossen. Kann die Gegenbuchung wegen zwischenzeitlich fehlenden Bestands nicht durchgeführt werden, braucht es eine berechtigte Korrekturbuchung statt einer stillen negativen Menge.
## API-Schnittstellen
Vorgeschlagene dedizierte Endpunkte:
| Methode und Pfad | Zweck |
| --- | --- |
| `GET /stock/overview` | Bestände, verfügbar/reserviert, Wert und Meldebestand |
| `GET /stock/products/:id` | Bestand eines Artikels nach Platz und Charge |
| `GET /stock/spaces/:id` | Inhalt eines Lagerplatzes einschließlich Unterplätzen |
| `GET /stock/transactions` | paginierbares Lagerjournal |
| `GET /stock/transactions/:id` | Lagerbeleg mit Zeilen und Historie |
| `POST /stock/transactions` | Entwurf anlegen |
| `PATCH /stock/transactions/:id` | Entwurf ändern |
| `POST /stock/transactions/:id/post` | atomar bestätigen |
| `POST /stock/transactions/:id/reverse` | Gegenbuchung erzeugen |
| `POST /stock/reservations` | Bestand reservieren |
| `POST /stock/counts` | Inventur eröffnen |
| `POST /stock/counts/:id/complete` | Differenzen verbuchen |
| `GET /stock/scan/:code` | Artikel, Seriennummer oder Lagerplatz auflösen |
Schreibende Aufrufe akzeptieren einen Idempotenzschlüssel. Fehlerantworten sollen fachlich verwertbare Codes liefern, zum Beispiel `INSUFFICIENT_STOCK`, `SERIAL_ALREADY_IN_STOCK`, `LOCATION_NOT_STOCK_ENABLED` oder `TRANSACTION_ALREADY_POSTED`.
Die wichtigsten Lese- und Buchungsfunktionen können anschließend auch als MCP-Werkzeuge registriert werden. Schreibende MCP-Werkzeuge sollten denselben Service und dieselben Berechtigungsprüfungen wie die HTTP-API verwenden.
## Oberfläche
Unter dem bestehenden Navigationspunkt **Lager** werden folgende Bereiche ergänzt:
- **Übersicht:** Bestandswert, Artikel unter Meldebestand, offene Reservierungen und letzte Buchungen
- **Bestände:** Artikelsicht und Lagerplatzsicht mit Suche, Filtern und Export
- **Buchen:** Wareneingang, Entnahme, Rückgabe, Umlagerung und Korrektur
- **Reservierungen:** offene und teilweise erfüllte Reservierungen
- **Inventur:** Zähllisten, Fortschritt, Differenzen und Freigabe
- **Lagerjournal:** unveränderliche Historie aller bestätigten Buchungen
- **Stammdaten:** vorhandene Artikel, Lagerplätze, Inventar und Kundeninventar
Die Bestandsansicht zeigt mindestens Ist-Bestand, reserviert, verfügbar, Durchschnittspreis, Bestandswert und Meldebestandsstatus. Von Artikel, Lagerplatz, Projekt und Beleg führt jeweils ein Link in die zugehörige Detailansicht.
Für Smartphone und Tablet sind Kamera-Barcodescan und optional RFID sinnvoll. Ein Scan-Endpunkt sollte Codes typisiert auflösen, damit derselbe Ablauf Artikelbarcodes, Lagerplatzcodes und Seriennummern unterscheiden kann. Die vorhandene Geräteanbindung kann später für stationäre Scanner und Etikettendruck genutzt werden.
## Rollen und Berechtigungen
Die bisherige Sammelberechtigung `inventory` sollte mindestens in folgende Rechte aufgeteilt werden:
- `stock-view`
- `stock-receive`
- `stock-transfer`
- `stock-issue`
- `stock-reserve`
- `stock-adjust`
- `stock-count`
- `stock-count-approve`
- `stock-reverse`
- `stock-value-view`
- `stock-settings`
Einkaufs- und Bestandswerte können damit vor operativen Lagerbenutzern verborgen werden. Korrektur, Inventurfreigabe und Storno sollten restriktiver vergeben werden als normale Ein- und Auslagerungen.
## Abgrenzung zu Belegen und Inventar
- Ein Angebot reserviert und bucht keinen Bestand.
- Ein Auftrag kann optional reservieren.
- Ein Lieferschein beziehungsweise eine ausdrücklich bestätigte Materialausgabe löst die Entnahme aus.
- Eine Rechnung bucht nicht nochmals, wenn bereits ein Lieferschein oder eine Materialausgabe gebucht wurde.
- Eine Stornorechnung ist nicht automatisch ein physischer Warenrückgang; die tatsächliche Rücknahme wird gesondert gebucht.
- Seriengeführte Ware kann bei Ausgabe optional einen Inventar- oder Kundeninventardatensatz erzeugen. Die Verbindung bleibt nachvollziehbar, die beiden Bereiche werden aber nicht zu einer Tabelle verschmolzen.
Diese Trennung verhindert doppelte Buchungen und bildet kaufmännische sowie physische Ereignisse korrekt ab.
## Datenintegrität und Betrieb
Wichtige Datenbankregeln und Indizes:
- Eindeutigkeit von Lagerbelegnummer und Idempotenzschlüssel je Mandant
- Eindeutigkeit von Lagerplatznummer und Lagerplatzbarcode je Mandant
- positive Buchungsmenge und voneinander verschiedene Quell- und Zielplätze
- Seriennummer je Mandant und Artikel eindeutig
- Indizes auf `(tenant, productId, spaceId)`, `(tenant, bookingDate)`, Projekt- und Belegreferenzen
- Fremdschlüssel möglichst mit `RESTRICT`; gebuchte Stammdaten werden archiviert, nicht gelöscht
- `posted`-Buchungen auf Datenbank- beziehungsweise Serviceebene unveränderlich
Ein periodischer Integritätsjob vergleicht `stock_balances` mit der Summe des Lagerjournals und meldet Abweichungen. Backups müssen Buchungen und Projektion gemeinsam erfassen; die Projektion bleibt dennoch vollständig rekonstruierbar.
## Einführung in Stufen
### Stufe 1 belastbares MVP
- Artikel als lagergeführt kennzeichnen
- Lagerplätze um Typ, Bestandsfreigabe und Barcode ergänzen
- Lagerbeleg, Buchungszeilen und Bestandsprojektion einführen
- Anfangsbestand, Wareneingang, Entnahme, Rückgabe, Umlagerung und Storno
- Bestandsübersicht und Lagerjournal
- Projektbezug, negativer Bestand standardmäßig gesperrt
- Rollen und serverseitige Berechtigungen
Damit ist bereits ein produktiv nutzbares Lager mit vollständiger Nachvollziehbarkeit vorhanden.
### Stufe 2 operative Unterstützung
- Reservierungen und Kommissionierung
- Inventurworkflow
- Meldebestand und Benachrichtigungen
- Kamera-Scan, Lagerplatz- und Artikeletiketten
- Materialkosten in Projekten und vorbereitete Buchung aus Belegen
- Im- und Export für Anfangsbestände
### Stufe 3 erweiterte Logistik
- Chargen, Ablaufdaten und Seriennummern
- mehrere Lager beziehungsweise Niederlassungen und Fahrzeuglager
- Bestellvorschläge und Einkaufsprozess
- Kunden- oder Konsignationsbestand mit explizitem Eigentümer
- FIFO als optionale Bewertungsmethode
- RFID- und Geräteagent-Integration
## Migration des aktuellen Datenbestands
Vor der Einführung wird pro Mandant ausgewertet, wie `inventoryitems.quantity`, `inventoryitems.currentSpace` und die vorhandene Tabelle `movements` tatsächlich verwendet wurden.
Empfohlenes Vorgehen:
1. Bestehende Artikel und Lagerplätze bereinigen und Lagerplätze eindeutig nummerieren.
2. Inventarartikel klassifizieren: individuelles Betriebsmittel, mengenmäßiger Altbestand oder fehlerhafte Dublette.
3. Echte Betriebsmittel im Inventar belassen; mengenmäßige Bestände einem `product` zuordnen.
4. Zu einem gemeinsam festgelegten Stichtag je Artikel und Lagerplatz eine bestätigte Anfangsbestandsbuchung erzeugen.
5. Historische `movements` nur übernehmen, wenn ihre Semantik und Vollständigkeit verlässlich geklärt werden können; andernfalls revisionssicher als Altbestand erhalten.
6. Summen, Bestandswerte und Seriennummern mit einem Migrationsbericht abstimmen.
7. Danach direkte Mengenänderungen am Inventar nicht mehr als Lagerbuchung zulassen.
Eine automatische Eins-zu-eins-Migration von `inventoryitems` wäre riskant, weil ein Datensatz heute sowohl ein einzelnes Gerät als auch eine Menge ähnlicher Gegenstände beschreiben kann.
## Abnahmekriterien für das MVP
- Zwei parallele Entnahmen können gemeinsam nie mehr als den verfügbaren Bestand ausgeben.
- Jede Bestandszahl lässt sich bis zu ihren bestätigten Buchungen zurückverfolgen.
- Eine Umlagerung verändert den Gesamtbestand nicht und wird atomar gebucht.
- Ein Storno stellt Menge und Wertwirkung der Ursprungsbuchung exakt wieder her.
- Wiederholte API-Anfragen mit demselben Idempotenzschlüssel erzeugen keine Doppelbuchung.
- Benutzer ohne Wertberechtigung sehen weder Einkaufspreis noch Bestandswert.
- Artikel und Lagerplätze eines anderen Mandanten können weder gelesen noch verbucht werden.
- Dezimalmengen werden entsprechend der Artikeleinheit korrekt gebucht.
- Aus dem Lagerjournal neu berechnete Bestände entsprechen vollständig `stock_balances`.
- Projektentnahmen erscheinen mit dem zum Buchungszeitpunkt festgeschriebenen Materialwert am Projekt.
## Empfohlene erste Umsetzung
Als erster vertikaler Schnitt sollte **manueller Wareneingang plus Umlagerung plus Projektentnahme** umgesetzt werden. Dieser Schnitt erzwingt bereits das richtige Buchungsmodell, die Bestandsprüfung, Mandantensicherheit, Berechtigungen und eine brauchbare Oberfläche. Reservierungen, Inventur und Tracking lassen sich danach ergänzen, ohne das Fundament erneut umzubauen.
Die vorhandene Tabelle `movements` sollte dabei nicht schrittweise zu einer universellen CRUD-Tabelle ausgebaut werden. Sauberer ist eine neue, transaktionale Struktur aus `stock_transactions`, `stock_transaction_lines` und `stock_balances`. `movements` bleibt während der Migration lesbar und kann nach einer geprüften Übernahme später außer Betrieb genommen werden.

View File

@@ -7,7 +7,8 @@ import {
import {
getIncomingInvoiceDepreciationRows,
getIncomingInvoiceImmediateExpenseNet,
getStatementAllocationImmediateExpenseAmount
getStatementAllocationImmediateExpenseAmount,
getStatementAllocationImmediateExpenseImpact
} from "~/composables/useDepreciation"
const loading = ref(true)
@@ -72,16 +73,12 @@ const loadSummary = async () => {
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
})
const directExpenses = (allocations || []).filter((allocation: any) => {
if (allocation?.account === null || typeof allocation?.account === "undefined") {
return false
}
const directAccountAllocations = (allocations || []).filter((allocation: any) => {
const statementDate = allocation?.bankstatement?.date || allocation?.bankstatement?.valueDate || allocation?.date || allocation?.created_at
const date = dayjs(statementDate)
const amount = getStatementAllocationImmediateExpenseAmount(allocation)
const impact = getStatementAllocationImmediateExpenseImpact(allocation)
return amount > 0 && date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
return impact !== 0 && date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
})
const income = outputDocs.reduce((sum: number, doc: any) => {
@@ -102,8 +99,8 @@ const loadSummary = async () => {
return sum + getIncomingInvoiceImmediateExpenseNet(invoice)
}, 0)
const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => {
return sum + Number(getStatementAllocationImmediateExpenseAmount(allocation) || 0)
const directAccountExpenses = directAccountAllocations.reduce((sum: number, allocation: any) => {
return sum + Number(getStatementAllocationImmediateExpenseImpact(allocation) || 0)
}, 0)
const depreciationRows = [
@@ -132,7 +129,7 @@ const loadSummary = async () => {
result: Number((income - expenses).toFixed(2)),
taxBalance: Number((outputTax - inputTax).toFixed(2)),
incomeCount: outputDocs.length,
expenseCount: inputDocs.filter((invoice: any) => getIncomingInvoiceImmediateExpenseNet(invoice) > 0).length + directExpenses.filter((allocation: any) => getStatementAllocationImmediateExpenseAmount(allocation) > 0).length,
expenseCount: inputDocs.filter((invoice: any) => getIncomingInvoiceImmediateExpenseNet(invoice) > 0).length + directAccountAllocations.filter((allocation: any) => getStatementAllocationImmediateExpenseAmount(allocation) > 0).length,
depreciationCount: depreciationRows.length
}
} finally {

View File

@@ -301,14 +301,20 @@ export const getIncomingInvoiceDepreciationRows = (invoice: any, rangeStart: any
.filter(Boolean)
}
export const getStatementAllocationImmediateExpenseAmount = (allocation: any) => {
export const getStatementAllocationImmediateExpenseImpact = (allocation: any) => {
const hasLinkedDocument = allocation?.incominginvoice || allocation?.createddocument || allocation?.ii_id || allocation?.cd_id
if (hasLinkedDocument) return 0
if (allocation?.account === null || allocation?.account === undefined) return 0
const mode = normalizeExpenseBookingMode(allocation?.bookingMode)
const amount = Number(allocation?.amount || 0)
if (mode !== "expense" || amount >= 0) return 0
return Number(Math.abs(amount).toFixed(2))
if (mode !== "expense") return 0
return Number((-amount).toFixed(2))
}
export const getStatementAllocationImmediateExpenseAmount = (allocation: any) => {
return Math.max(0, getStatementAllocationImmediateExpenseImpact(allocation))
}
export const getStatementAllocationDepreciationAmount = (allocation: any, rangeStart: any, rangeEnd: any) => {

View File

@@ -10,7 +10,8 @@ import {
getIncomingInvoiceImmediateExpenseNet,
isDepreciationBookingMode,
normalizeIncomingInvoiceAccount,
getStatementAllocationImmediateExpenseAmount
getStatementAllocationImmediateExpenseAmount,
getStatementAllocationImmediateExpenseImpact
} from "~/composables/useDepreciation"
const router = useRouter()
@@ -190,8 +191,8 @@ const expenseNetTotal = computed(() => {
return sum + getIncomingInvoiceImmediateExpenseNet(invoice)
}, 0)
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
return sum + Number(getStatementAllocationImmediateExpenseAmount(allocation) || 0)
const directAccountExpenses = filteredStatementAllocations.value.reduce((sum, allocation) => {
return sum + Number(getStatementAllocationImmediateExpenseImpact(allocation) || 0)
}, 0)
const depreciations = depreciationTotal.value
@@ -202,8 +203,8 @@ const expenseNetTotal = computed(() => {
const expenseGrossTotal = computed(() => {
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => sum + getIncomingInvoiceImmediateExpenseGross(invoice), 0)
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
return sum + Number(getStatementAllocationImmediateExpenseAmount(allocation) || 0)
const directAccountExpenses = filteredStatementAllocations.value.reduce((sum, allocation) => {
return sum + Number(getStatementAllocationImmediateExpenseImpact(allocation) || 0)
}, 0)
const depreciations = depreciationTotal.value