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:
parent
021edbfcc7
commit
b8180a06a1
25 changed files with 378 additions and 71 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue