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:
parent
b8180a06a1
commit
6bd867cbe7
18 changed files with 424 additions and 40 deletions
|
|
@ -2,6 +2,10 @@
|
|||
(function ($) {
|
||||
var recipeBase = @json(url('/admin/inventory/api/products'));
|
||||
var existingLines = @json(($existingLines ?? []) === [] ? (object) [] : $existingLines);
|
||||
// Prüfung AP-53 (AP53-01): Toleranzregel kommt aus ProductionService — eine Quelle für Ampel und Server.
|
||||
var TOLERANCE_RATIO = {{ \App\Services\ProductionService::TOLERANCE_RATIO }};
|
||||
var TOLERANCE_FLOOR = {{ \App\Services\ProductionService::TOLERANCE_FLOOR }};
|
||||
var COMPARE_EPSILON = {{ \App\Services\ProductionService::COMPARE_EPSILON }};
|
||||
var excludeProductionId = @json($excludeProductionId ?? null);
|
||||
|
||||
var lineIndex = 0;
|
||||
|
|
@ -134,10 +138,10 @@
|
|||
});
|
||||
// AP-39: Orange = nichts eingetragen; Grün = eingetragen und ±5 % vom Soll;
|
||||
// Rot = eingetragen, aber > 5 % daneben (Zahlendreher-Schutz).
|
||||
var tolerance = Math.max(soll * 0.05, 0.05);
|
||||
var tolerance = Math.max(soll * TOLERANCE_RATIO, TOLERANCE_FLOOR);
|
||||
if (!anyEntered) {
|
||||
$dot.css('color', '#fd7e14').attr('title', '{{ __('Noch nichts eingetragen') }}');
|
||||
} else if (Math.abs(sum - soll) <= tolerance) {
|
||||
} else if (Math.abs(sum - soll) <= tolerance + COMPARE_EPSILON) {
|
||||
$dot.css('color', '#28a745').attr('title', '{{ __('Soll erfüllt (±5 %)') }}');
|
||||
} else {
|
||||
$dot.css('color', '#dc3545').attr('title', '{{ __('Abweichung über 5 % – bitte prüfen') }}');
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@
|
|||
<td>{{ $group['suppliers']->isNotEmpty() ? $group['suppliers']->implode(', ') : '—' }}</td>
|
||||
<td>{{ $group['locations']->isNotEmpty() ? $group['locations']->implode(', ') : '—' }}</td>
|
||||
<td>{{ $group['best_before']?->format('d.m.Y') ?? '—' }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($group['remaining'], 0) }} g</td>
|
||||
<td class="text-right">{{ formatWawiQuantity($group['remaining'], 0) }} g</td>
|
||||
</tr>
|
||||
@if($group['entries']->count() > 1)
|
||||
@foreach($group['entries'] as $charge)
|
||||
|
|
@ -292,7 +292,7 @@
|
|||
@if(isset($remainingByLocation[$location->id]) && $remainingByLocation[$location->id] > 0)
|
||||
<tr>
|
||||
<td colspan="4" class="text-right text-muted">{{ __('Bestand') }} {{ $location->name }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($remainingByLocation[$location->id], 0) }} g</td>
|
||||
<td class="text-right">{{ formatWawiQuantity($remainingByLocation[$location->id], 0) }} g</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@
|
|||
}
|
||||
if ($('#ordered_unit').val() === 'ml' && hasDensity) {
|
||||
var ml = parseNumber($('#ordered_quantity').val());
|
||||
var g = ml != null ? formatNumber(ml * density) : '?';
|
||||
$hint.text('{{ __('Erfassung in ml — entspricht ca.') }} ' + g + ' {{ __('g (Dichte') }} ' + formatNumber(density) + ' g/ml).');
|
||||
var g = ml != null ? formatQuantity(ml * density) : '?';
|
||||
$hint.text('{{ __('Erfassung in ml — entspricht ca.') }} ' + g + ' {{ __('g (Dichte') }} ' + formatDensity(density) + ' g/ml).');
|
||||
} else if (hasDensity) {
|
||||
$hint.text('{{ __('In Gramm — oder Einheit auf ml stellen (Dichte hinterlegt).') }}');
|
||||
} else {
|
||||
|
|
@ -116,11 +116,19 @@
|
|||
return parseFloat(normalized);
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
// AP-53: bis zu drei Nachkommastellen, mindestens zwei.
|
||||
// Prüfung AP-53 (4.3): Mengen (3 Stellen), Preise (2 Stellen) und Dichte (4 Stellen) getrennt formatieren.
|
||||
function formatQuantity(value) {
|
||||
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: false});
|
||||
}
|
||||
|
||||
function formatPrice(value) {
|
||||
return Number(value).toFixed(2).replace('.', ',');
|
||||
}
|
||||
|
||||
function formatDensity(value) {
|
||||
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 4, useGrouping: false});
|
||||
}
|
||||
|
||||
function currentFactor() {
|
||||
var percent = parseFloat($('#tax_rate_id option:selected').data('percent')) || 0;
|
||||
return 1 + percent / 100;
|
||||
|
|
@ -131,7 +139,7 @@
|
|||
if (net === null) {
|
||||
return;
|
||||
}
|
||||
$('#price_per_kg_gross').val(formatNumber(net * currentFactor()));
|
||||
$('#price_per_kg_gross').val(formatPrice(net * currentFactor()));
|
||||
}
|
||||
|
||||
function recalcFromGross() {
|
||||
|
|
@ -140,7 +148,7 @@
|
|||
return;
|
||||
}
|
||||
var factor = currentFactor();
|
||||
$('#price_per_kg').val(formatNumber(factor > 0 ? gross / factor : gross));
|
||||
$('#price_per_kg').val(formatPrice(factor > 0 ? gross / factor : gross));
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue