Warenwirtschaft: AP-53 — Nacharbeit zur Prüfung 10.09.2026 (Toleranzregel zentral, Sockelrand, Chargenzeile, Ausschuss-Meldung, Auflösung/Obergrenze)

Prüfbericht pruefung-ap53-mengenpraezision-2026-09-10.md, alle Punkte umgesetzt:
- AP53-01: Toleranz nur noch in ProductionService (TOLERANCE_RATIO 0,05, TOLERANCE_FLOOR 0,005, COMPARE_EPSILON); das Produktions-JS rendert die Konstanten aus dem Service statt eigener Zahlen (Ampel hatte noch Sockel 0,05 g).
- AP53-02: withinTolerance() vergleicht mit Float-Ausgleich 1e-9 — 0,025 g auf 0,020 g Soll ist wieder zulässig, 0,026 g nicht; Sollwerte werden nicht gerundet.
- AP53-03: Chargen-Hauptzeile und Standortsumme im Rohstoff-Detail mit formatWawiQuantity(…, 0) (0,125 g statt 0 g).
- AP53-04: Ausschuss-Fehlermeldungen (Request ohne/mit Charge, Controller-Wiederholung) mit formatWawiQuantity().
- 4.1: Obergrenze decimal(12,3) bewusst akzeptiert (Live max. 49.000 g) und als InventoryService::QUANTITY_MAX validiert; Bereichsprüfung + Rollback-Hinweis in der Deploy-Checkliste.
- 4.2: normalizeWawiQuantity() (parsen + drei Stellen) in Bestell-/Wareneingangs-/Ausschuss-Requests, parseQuantity() rundet kanonisch für batch_key- und stock_entry_id-Zeilen; Untergrenze 0,001 g; ml-Eingaben, die umgerechnet 0,000 g ergeben, werden abgelehnt (Store/Update/Receive).
- 4.3: Einkaufs-JS trennt formatQuantity (3), formatPrice (2, wie vor AP-53) und formatDensity (4).
- 4.4: Hinweise präzisiert (Übersichten ganzzahlig, Rundungs-/Untergrenzregel), Leitfaden §26 mit Sockelrand-, Hauptzeilen- und Meldungs-Prüfschritten, Plan V7 und README aktualisiert, Prüfbericht Abschnitt 6 mit Nacharbeitsstand.

Tests: QuantityPrecisionTest +6 (AP53-01…04, 4.2 Rundung/Untergrenze/Obergrenze, ml→0 g). Suite 384 grün / 1.592 Assertions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-10 07:25:11 +00:00
parent b8180a06a1
commit 6bd867cbe7
18 changed files with 424 additions and 40 deletions

View file

@ -19,6 +19,9 @@ class InventoryService
*/
public const QUANTITY_DECIMALS = 3;
/** Obergrenze der decimal(12,3)-Spalten (Prüfung AP-53, Punkt 4.1) — wird validiert. */
public const QUANTITY_MAX = 999999999.999;
public function __construct(
protected ProductionService $productionService,
) {}

View file

@ -14,6 +14,21 @@ use Illuminate\Validation\ValidationException;
class ProductionService
{
/** AP-39/AP-53: Soll-Ist-Toleranz ±5 % mit absolutem Sockel 0,005 g (Prüfung AP-53: eine Quelle für PHP und JS). */
public const TOLERANCE_RATIO = 0.05;
public const TOLERANCE_FLOOR = 0.005;
/** Ausgleich für binäre Float-Darstellung beim Grenzvergleich (AP53-02: 0,025 auf 0,020 ist exakt zulässig). */
public const COMPARE_EPSILON = 1e-9;
public static function withinTolerance(float $sum, float $required): bool
{
$tolerance = max($required * self::TOLERANCE_RATIO, self::TOLERANCE_FLOOR);
return abs($sum - $required) <= $tolerance + self::COMPARE_EPSILON;
}
public function __construct(
protected ProductStockService $productStockService,
) {}
@ -291,8 +306,7 @@ class ProductionService
// passen — gebucht wird der reale Ist-Wert. Toleranz ±5 % (mit kleinem
// absolutem Sockel gegen Float-/Kleinmengen-Effekte); größere
// Abweichungen fängt der Zahlendreher-Schutz weiterhin ab.
$tolerance = max((float) $req * 0.05, 0.005); // AP-53: Sockel 5 mg
if (abs($sum - $req) > $tolerance) {
if (! self::withinTolerance((float) $sum, (float) $req)) {
throw ValidationException::withMessages([
'ingredient_lines' => __('Die Ist-Menge je INCI darf höchstens 5 % vom Soll abweichen (INCI :id: Soll :req g, Ist :sum g).', [
'id' => $iid,
@ -817,13 +831,15 @@ class ProductionService
// Audit N-7: Zahlen (interne Aufrufer) direkt; Strings konsequent im
// deutschen Format parsen — vorher wich „1.500" (Server: 1,5) von der
// UI-Ampel (1500) ab.
// Prüfung AP-53 (4.2): kanonische Menge — immer auf drei Stellen gerundet,
// egal ob die Zeile per batch_key oder stock_entry_id kommt.
if (is_int($value) || is_float($value)) {
return (float) $value;
return roundWawiQuantity($value);
}
$n = reFormatWawiNumber((string) $value);
return $n !== null && $n !== '' && is_numeric($n) ? (float) $n : 0.0;
return $n !== null && $n !== '' && is_numeric($n) ? roundWawiQuantity($n) : 0.0;
}
/**