Files
ApiCore/ApiCore-Backend/src/erp-export/harvest-years.ts
T
Nick SzaunigandClaude Opus 5.5 68f81fabd4 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]>
2026-10-12 11:43:57 +02:00

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),
}));
}