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]>
73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
/**
|
|
* 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),
|
|
}));
|
|
}
|