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]>
97 lines
2.5 KiB
TypeScript
97 lines
2.5 KiB
TypeScript
import { summarizeHarvestYears } from './harvest-years';
|
|
|
|
function harvest(date: string, netWeight: number | null) {
|
|
return { date: new Date(date), netWeight };
|
|
}
|
|
|
|
describe('summarizeHarvestYears', () => {
|
|
it('summiert das Nettogewicht je Kalenderjahr (UTC), älteste Jahre zuerst', () => {
|
|
const years = summarizeHarvestYears([
|
|
harvest('2026-07-14T00:00:00.000Z', 41),
|
|
harvest('2025-06-01T00:00:00.000Z', 12.5),
|
|
harvest('2026-05-20T00:00:00.000Z', 18.25),
|
|
]);
|
|
|
|
expect(years).toEqual([
|
|
{
|
|
year: 2025,
|
|
netWeightKg: 12.5,
|
|
harvestCount: 1,
|
|
harvestsWithoutNetWeight: 0,
|
|
},
|
|
{
|
|
year: 2026,
|
|
netWeightKg: 59.25,
|
|
harvestCount: 2,
|
|
harvestsWithoutNetWeight: 0,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('zählt Ernten ohne Nettogewicht mit, rechnet sie aber nicht ein', () => {
|
|
const [year] = summarizeHarvestYears([
|
|
harvest('2026-07-14T00:00:00.000Z', 20),
|
|
harvest('2026-08-01T00:00:00.000Z', null),
|
|
]);
|
|
expect(year).toEqual({
|
|
year: 2026,
|
|
netWeightKg: 20,
|
|
harvestCount: 2,
|
|
harvestsWithoutNetWeight: 1,
|
|
});
|
|
});
|
|
|
|
it('rundet auf Gramm, damit Gleitkomma-Reste nicht im Vertrag landen', () => {
|
|
const [year] = summarizeHarvestYears([
|
|
harvest('2026-07-14T00:00:00.000Z', 0.1),
|
|
harvest('2026-07-15T00:00:00.000Z', 0.2),
|
|
harvest('2026-07-16T00:00:00.000Z', 12.3456),
|
|
]);
|
|
expect(year.netWeightKg).toBe(12.646);
|
|
});
|
|
|
|
it('ordnet Silvester und Neujahr nach UTC zu', () => {
|
|
const years = summarizeHarvestYears([
|
|
harvest('2025-12-31T23:59:59.999Z', 1),
|
|
harvest('2026-01-01T00:00:00.000Z', 2),
|
|
]);
|
|
expect(years.map((y) => [y.year, y.netWeightKg])).toEqual([
|
|
[2025, 1],
|
|
[2026, 2],
|
|
]);
|
|
});
|
|
|
|
it('liefert für ein angefragtes Jahr ohne Ernten eine Nullzeile statt nichts', () => {
|
|
expect(summarizeHarvestYears([], 2024)).toEqual([
|
|
{
|
|
year: 2024,
|
|
netWeightKg: 0,
|
|
harvestCount: 0,
|
|
harvestsWithoutNetWeight: 0,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('beschränkt sich auf das angefragte Jahr', () => {
|
|
const years = summarizeHarvestYears(
|
|
[
|
|
harvest('2025-06-01T00:00:00.000Z', 5),
|
|
harvest('2026-06-01T00:00:00.000Z', 7),
|
|
],
|
|
2026,
|
|
);
|
|
expect(years).toEqual([
|
|
{
|
|
year: 2026,
|
|
netWeightKg: 7,
|
|
harvestCount: 1,
|
|
harvestsWithoutNetWeight: 0,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('liefert ohne Ernten und ohne Jahr eine leere Liste', () => {
|
|
expect(summarizeHarvestYears([])).toEqual([]);
|
|
});
|
|
});
|