feat(erp-export): nur lesender Inventar-Spiegel für ERPNext (T8)
Datenquelle für den nächtlichen Inventar-Abgleich in ERPNext (ERPNext- Ausbau T8, Entscheidungen E2/E3). Neues Recht erp-inventory:read, auch als API-Schlüssel-Scope; es öffnet nur diese vier GET-Endpunkte: - /api/v1/export/inventar/produkte: Artikel (ohne Beschreibung) - .../chargen: Abfüllungen und Herstellungschargen mit Losnummer, MHD, Ausgangsmenge, Rest aus dem Bewegungsbuch, Füllmenge g und Sorte - .../bewegungen?seit=&nach=&limit=: Shop-Verkauf, Retoure, Entnahmen mit Grund (HOFLADEN = Vertrauensschrank, EIGENBEDARF, BRUCH, PROBE, SONSTIGES) und Inventur; Cursor über (createdAt, id), Bewegungen erst nach 5 Minuten Wartezeit, damit der Cursor keine offene Buchung überholt - .../ernten?jahr=: Nettogewicht kg je Kalenderjahr, wie die Statistik Alle Listen stabil sortiert, seitenweise (1-500), Parameter mit class-validator geprüft, Cursor je Endpunkt streng validiert. Feld- Allowlists: keine Notizen, Benutzer, Standorte, Rohstoffe. Die Gründe gab es im Datenmodell schon - keine Migration; das Recht legt der Katalog- Sync beim Start an und gibt es der Rolle Administrator. Vertrag mit Feldern, Einheiten, Fehlercodes und Beispielen: docs/erp-inventar-export.md. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
1a1e70c94b
commit
68f81fabd4
17 files changed
+2569
-4
No files matched your search
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Geerntete Honigmenge je Kalenderjahr - Grundlage für die Honigkosten je kg
|
||||
* in ERPNext (Imkerei-Ausgaben des Jahres / geerntete kg).
|
||||
*
|
||||
* Gleiche Definition wie die Statistik in ApiCore (Honigertrag je Jahr):
|
||||
* Summe der Nettogewichte (kg) der Ernten, Jahr aus dem Erntedatum. Das
|
||||
* Jahr wird in UTC bestimmt, weil Erntedaten als Kalendertag (Mitternacht
|
||||
* UTC) gespeichert werden. Ernten ohne Nettogewicht zählen nicht zur Menge,
|
||||
* werden aber ausgewiesen - sonst sähe eine unvollständig erfasste Ernte
|
||||
* wie ein schlechtes Jahr aus und die Kosten je kg wären zu hoch.
|
||||
*/
|
||||
|
||||
export interface HarvestWeight {
|
||||
date: Date;
|
||||
/** Kilogramm; null, wenn nicht erfasst. */
|
||||
netWeight: number | null;
|
||||
}
|
||||
|
||||
export interface HarvestYear {
|
||||
year: number;
|
||||
/** Summe der Nettogewichte in kg, auf Gramm gerundet. */
|
||||
netWeightKg: number;
|
||||
harvestCount: number;
|
||||
harvestsWithoutNetWeight: number;
|
||||
}
|
||||
|
||||
const GRAMS_PER_KG = 1000;
|
||||
|
||||
function roundToGrams(kg: number): number {
|
||||
return Math.round(kg * GRAMS_PER_KG) / GRAMS_PER_KG;
|
||||
}
|
||||
|
||||
function emptyYear(year: number): HarvestYear {
|
||||
return {
|
||||
year,
|
||||
netWeightKg: 0,
|
||||
harvestCount: 0,
|
||||
harvestsWithoutNetWeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fasst Ernten je Jahr zusammen, älteste Jahre zuerst. Mit `onlyYear` nur
|
||||
* dieses Jahr, und zwar auch dann, wenn es keine Ernte hat (Nullzeile).
|
||||
*/
|
||||
export function summarizeHarvestYears(
|
||||
harvests: readonly HarvestWeight[],
|
||||
onlyYear?: number,
|
||||
): HarvestYear[] {
|
||||
const byYear = new Map<number, HarvestYear>();
|
||||
if (onlyYear !== undefined) {
|
||||
byYear.set(onlyYear, emptyYear(onlyYear));
|
||||
}
|
||||
for (const harvest of harvests) {
|
||||
const year = harvest.date.getUTCFullYear();
|
||||
if (onlyYear !== undefined && year !== onlyYear) continue;
|
||||
const current = byYear.get(year) ?? emptyYear(year);
|
||||
byYear.set(year, {
|
||||
year,
|
||||
netWeightKg: current.netWeightKg + (harvest.netWeight ?? 0),
|
||||
harvestCount: current.harvestCount + 1,
|
||||
harvestsWithoutNetWeight:
|
||||
current.harvestsWithoutNetWeight + (harvest.netWeight === null ? 1 : 0),
|
||||
});
|
||||
}
|
||||
return [...byYear.values()]
|
||||
.sort((a, b) => a.year - b.year)
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
netWeightKg: roundToGrams(entry.netWeightKg),
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user