Warenwirtschaft: AP-53 — Gramm-Mengen mit drei Nachkommastellen (Produktion, Wareneingang, Ausschuss)

Kundennachtrag 09.09.2026: Ist-Mengen je Charge in der Produktion sollen auf das Milligramm eingegeben werden können (z. B. 0,125 g). Bisher rundeten Datenbank (decimal 12,2), Bestandslogik, Anzeige und Toleranzsockel auf zwei Stellen; „0,125" wurde still zu 0,13, „0.125" nach der Punkt-Regel zu 125 g.

- Migration 2026_09_09_100000: production_ingredients.quantity_used, stock_entries.ordered_quantity/received_quantity, stock_disposals.quantity → decimal(12,3); Casts decimal:3. Auf der Dev-DB (Live-Kopie) ausgeführt, 53/78/7 Zeilen unverändert.
- InventoryService::QUANTITY_DECIMALS = 3; roundWawiQuantity() ersetzt alle round(…, 2) in InventoryService, ProductionService, StockDisposalController/-Request, StockEntryRepository/-Controller, RawMaterialStockController.
- formatWawiQuantity($v, $min = 2): bis drei Stellen, mindestens zwei — Produktionsdetail, Einkauf Liste/Detail/Formular, Ausschuss-Liste, Chargen-Rest im Rohstoff-Detail.
- Eingaberegel (reFormatWawiNumber, JS parseGerman/parseNumber): zusätzlich „0.xxx" als Dezimalzahl; „1.500" bleibt 1500. Produktions-JS: fmt() bis drei Stellen, fmtInput() schreibt gespeicherte Zahlen deutsch ins Feld (1,125 statt 1.125 = 1125).
- Soll-Ist-Toleranz: Sockel 0,05 → 0,005 g; Ausschuss min 0,001.
- Doku: Hinweise (Neu/geändert), Leitfaden Version 3.1 mit Abschnitt 26, Plan V7 AP-53, README (Deploy-Checkliste, Suite 378).

Tests: QuantityPrecisionTest (6) — Parsing, Anzeige, Produktion 0,125 g exakt + Rest 9,875 g, Entwurf 1,125 wiederöffnen, Toleranz Kleinmengen, Ausschuss 0,005 g.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-09 15:54:29 +00:00
parent 021edbfcc7
commit b8180a06a1
25 changed files with 378 additions and 71 deletions

View file

@ -29,8 +29,18 @@
return url;
}
// AP-53: Gramm-Mengen mit bis zu drei Nachkommastellen (0,125), mindestens zwei (12,50).
function fmt(value) {
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 3});
}
// AP-53: Zahl → Eingabefeld (deutsches Komma, ohne Tausenderpunkt, max. 3 Stellen),
// damit gespeicherte Werte wie 1,125 beim Wiederöffnen nicht als 1.125 (= 1125) gelesen werden.
function fmtInput(value) {
if (typeof value !== 'number') {
return value == null ? '' : String(value);
}
return Number(value).toLocaleString('de-DE', {maximumFractionDigits: 3, useGrouping: false});
}
// AP-52: Stückzahlen (Verpackung) ohne Nachkommastellen.
@ -44,8 +54,9 @@
return 0;
}
// Audit N-6: einzelner Punkt mit 1–2 Nachkommastellen = Dezimaltrenner
// ("5.1" = 5,1) — identische Regel wie serverseitig (reFormatWawiNumber).
if (/^-?\d+\.\d{1,2}$/.test(value)) {
// ("5.1" = 5,1); AP-53: zusätzlich "0.125" — identische Regel wie
// serverseitig (reFormatWawiNumber). "1.500" bleibt 1500.
if (/^-?(\d+\.\d{1,2}|0\.\d{3})$/.test(value)) {
return parseFloat(value) || 0;
}
return parseFloat(value.replace(/\./g, '').replace(',', '.')) || 0;
@ -96,7 +107,7 @@
var $colQty = $('<div class="col-12 col-sm-5 mb-1"></div>');
var $group = $('<div class="input-group input-group-sm"></div>');
$group.append($('<input type="text" class="form-control recipe-qty-input" required placeholder="0" name="ingredient_lines[' + idx + '][quantity_used]">').val(preselectedQty || ''));
$group.append($('<input type="text" class="form-control recipe-qty-input" required placeholder="0" name="ingredient_lines[' + idx + '][quantity_used]" inputmode="decimal">').val(fmtInput(preselectedQty)));
$group.append('<div class="input-group-append"><span class="input-group-text">g</span></div>');
$colQty.append($group);
$row.append($colQty);

View file

@ -124,7 +124,7 @@
(MHD {{ $line->stockEntry->best_before->format('d.m.Y') }})
@endif
</td>
<td>{{ \App\Services\Util::formatNumber($line->quantity_used) }}</td>
<td>{{ formatWawiQuantity($line->quantity_used) }}</td>
</tr>
@endforeach
</tbody>

View file

@ -276,7 +276,7 @@
<td>{{ $charge->supplier?->name ?? '—' }}</td>
<td>{{ $charge->location?->name ?? '—' }}</td>
<td>{{ $charge->best_before?->format('d.m.Y') ?? '—' }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($charge->getAttribute('remaining_quantity'), 0) }} g</td>
<td class="text-right">{{ formatWawiQuantity($charge->getAttribute('remaining_quantity'), 0) }} g</td>
</tr>
@endforeach
@endif

View file

@ -65,7 +65,7 @@
</td>
<td>{{ $disposal->location?->name ?? '—' }}</td>
<td class="text-right text-danger font-weight-bold">
−{{ \App\Services\Util::formatNumber($disposal->quantity, $disposal->unit === 'piece' ? 0 : 2) }}
−{{ $disposal->unit === 'piece' ? \App\Services\Util::formatNumber($disposal->quantity, 0) : formatWawiQuantity($disposal->quantity) }}
{{ $disposal->unit === 'piece' ? __('Stück') : 'g' }}
</td>
<td>{{ $disposal->reason }}</td>

View file

@ -109,7 +109,7 @@
// über die gespeicherte Dichte); Bestand bleibt intern in Gramm.
$orderedIsMl = $model->purchase_unit === 'ml' && $model->density_used;
$orderedDisplay = $model->ordered_quantity !== null
? \App\Services\Util::formatNumber($orderedIsMl ? $model->ordered_quantity / $model->density_used : $model->ordered_quantity)
? formatWawiQuantity($orderedIsMl ? $model->ordered_quantity / $model->density_used : $model->ordered_quantity)
: '';
@endphp
<div class="form-group">

View file

@ -107,7 +107,9 @@
if (value === null || value === undefined) {
return null;
}
var normalized = String(value).trim().replace(/\./g, '').replace(',', '.');
// AP-53: gleiche Regel wie reFormatWawiNumber — Punkt mit 1–2 Stellen oder "0.xxx" = Komma.
var raw = String(value).trim();
var normalized = /^-?(\d+\.\d{1,2}|0\.\d{3})$/.test(raw) ? raw : raw.replace(/\./g, '').replace(',', '.');
if (normalized === '' || isNaN(normalized)) {
return null;
}
@ -115,7 +117,8 @@
}
function formatNumber(value) {
return value.toFixed(2).replace('.', ',');
// AP-53: bis zu drei Nachkommastellen, mindestens zwei.
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: false});
}
function currentFactor() {

View file

@ -73,7 +73,7 @@
<td>{{ $row->supplier?->name ?? '—' }}</td>
<td>
@if($row->unit === 'gram')
{{ \App\Services\Util::formatNumber($row->ordered_quantity) }} g
{{ formatWawiQuantity($row->ordered_quantity) }} g
@else
{{ \App\Services\Util::formatNumber($row->ordered_quantity, 0) }} {{ __('Stk.') }}
@endif

View file

@ -84,9 +84,9 @@
<span class="wawi-deflist__label">{{ __('Bestellte Menge') }}</span>
<span class="wawi-deflist__value">
@if($model->unit === 'gram')
{{ \App\Services\Util::formatNumber($model->ordered_quantity) }} g
{{ formatWawiQuantity($model->ordered_quantity) }} g
@if($model->purchase_unit === 'ml' && $model->density_used)
<span class="text-muted">(= {{ \App\Services\Util::formatNumber($model->ordered_quantity / $model->density_used) }} ml)</span>
<span class="text-muted">(= {{ formatWawiQuantity($model->ordered_quantity / $model->density_used) }} ml)</span>
@endif
@else
{{ \App\Services\Util::formatNumber($model->ordered_quantity, 0) }} {{ __('Stk.') }}
@ -136,9 +136,9 @@
<span class="wawi-deflist__label">{{ __('Eingegangene Menge') }}</span>
<span class="wawi-deflist__value">
@if($model->unit === 'gram')
{{ \App\Services\Util::formatNumber($model->received_quantity) }} g
{{ formatWawiQuantity($model->received_quantity) }} g
@if($model->purchase_unit === 'ml' && $model->density_used && $model->received_quantity !== null)
<span class="text-muted">(= {{ \App\Services\Util::formatNumber($model->received_quantity / $model->density_used) }} ml)</span>
<span class="text-muted">(= {{ formatWawiQuantity($model->received_quantity / $model->density_used) }} ml)</span>
@endif
@else
{{ \App\Services\Util::formatNumber($model->received_quantity, 0) }} {{ __('Stk.') }}
@ -195,8 +195,8 @@
// und über die gespeicherte Dichte in Gramm umgerechnet.
$receiveIsMl = $model->purchase_unit === 'ml' && $model->density_used;
$receiveDefault = $receiveIsMl
? \App\Services\Util::formatNumber($model->ordered_quantity / $model->density_used)
: \App\Services\Util::formatNumber($model->ordered_quantity);
? formatWawiQuantity($model->ordered_quantity / $model->density_used)
: formatWawiQuantity($model->ordered_quantity);
@endphp
<div class="form-group">
<label for="received_quantity">{{ __('Eingegangene Menge') }} <span class="text-danger">*</span></label>