Warenwirtschaft: AP-52 — Verpackungsvorschau mit Bestand, Differenz und Lagerort (Kundennachtrag 09.09.2026)

Je Verpackungsartikel Bestand (Stück), Differenz zu Bedarf (rot/grün wie bei
Rohstoffen, AP-40) und Lagerplatz; Spalte einheitlich „Lagerort“ für Rohstoffe
(INCI-Tabelle, vorher „Ablage“) und Verpackung.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-09 08:48:30 +00:00
parent 22e218d570
commit 3abb9e644e
5 changed files with 103 additions and 16 deletions

View file

@ -141,7 +141,8 @@
<th>{{ __('Qualität') }}</th>
<th class="text-right">{{ __('Anteil %') }}</th>
<th class="text-right">{{ __('Gramm (gesamt)') }}</th>
<th>{{ __('Ablage') }}</th>
{{-- AP-52: „Lagerort" einheitlich für Rohstoffe und Verpackung (Kundenwunsch 09.09.2026) --}}
<th>{{ __('Lagerort') }}</th>
<th class="text-right">{{ __('Bestand') }}</th>
<th class="text-right">{{ __('Differenz') }}</th>
</tr>
@ -157,9 +158,18 @@
<h6>{{ __('Verpackung (Vorschau)') }} <small class="text-muted font-weight-normal">{{ __('aus dem Produktionsergebnis') }}</small></h6>
<div class="table-responsive border rounded mb-3">
<table class="table table-sm mb-0">
<thead><tr><th>{{ __('Artikel') }}</th><th>{{ __('Stück gesamt') }}</th></tr></thead>
{{-- AP-52: Bestand/Differenz/Lagerort je Verpackungsartikel — direkt bei der Planung sehen, ob alles da ist --}}
<thead>
<tr>
<th>{{ __('Artikel') }}</th>
<th class="text-right">{{ __('Stück gesamt') }}</th>
<th>{{ __('Lagerort') }}</th>
<th class="text-right">{{ __('Bestand') }}</th>
<th class="text-right">{{ __('Differenz') }}</th>
</tr>
</thead>
<tbody id="recipe-packaging-preview"></tbody>
<tfoot><tr><td colspan="2" class="text-muted small" id="recipe-packaging-hint"></td></tr></tfoot>
<tfoot><tr><td colspan="5" class="text-muted small" id="recipe-packaging-hint"></td></tr></tfoot>
</table>
</div>
@ -198,7 +208,7 @@
<div class="col-sm-3">{{ __('Chargen-Nr.') }}</div>
<div class="col-sm-1"></div>
</div>
<div id="production-outputs" data-initial='@json($initialOutputs)' data-packaging-bom='@json(($packagingBom ?? []) === [] ? (object) [] : $packagingBom)'
<div id="production-outputs" data-initial='@json($initialOutputs)' data-packaging-bom='@json(($packagingBom ?? []) === [] ? (object) [] : $packagingBom)' data-packaging-info='@json(($packagingInfo ?? []) === [] ? (object) [] : $packagingInfo)'
data-packaging-snapshot='@json($packagingSnapshot ?? [])' data-packaging-snapshot-applies='{{ ! empty($packagingSnapshotApplies) ? 1 : 0 }}' data-initial-output-totals='@json(($initialOutputTotals ?? []) === [] ? (object) [] : $initialOutputTotals)'></div>
<button type="button" class="btn btn-sm btn-outline-secondary mb-3" id="btn-add-output">{{ __('+ weitere Ergebniszeile') }}</button>

View file

@ -33,6 +33,11 @@
return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
// AP-52: Stückzahlen (Verpackung) ohne Nachkommastellen.
function fmtPieces(value) {
return Number(value).toLocaleString('de-DE', {maximumFractionDigits: 0});
}
function parseGerman(value) {
value = String(value || '').trim();
if (!value) {
@ -395,17 +400,37 @@
return true;
}
// AP-52: Bestand + Lagerort je Verpackungsartikel (vom Server), Differenz = Bestand − Bedarf.
var packagingInfo = $('#production-outputs').data('packaging-info') || {};
function packagingRow(id, name, pieces, withDiff) {
var info = packagingInfo[String(id)] || {};
var stock = info.stock != null ? Number(info.stock) : null;
var $row = $('<tr></tr>')
.append($('<td></td>').text(name))
.append($('<td class="text-right"></td>').text(pieces))
.append($('<td></td>').text(info.storage_label || '—'))
.append($('<td class="text-right"></td>').text(stock != null ? fmtPieces(stock) : '—'));
var $diff = $('<td class="text-right"></td>');
if (withDiff && stock != null) {
// Wie AP-40 bei den Rohstoffen: negativ rot = Fehlmenge, sonst grün.
var d = stock - pieces;
$diff.text((d >= 0 ? '+' : '') + fmtPieces(d)).css('color', d < 0 ? '#dc3545' : '#28a745');
} else {
$diff.text('—').attr('title', withDiff ? '' : '{{ __('Bereits gebucht — Bestand enthält den Abzug') }}');
}
return $row.append($diff);
}
function renderPackagingPreview() {
var $hint = $('#recipe-packaging-hint');
if (packagingSnapshotApplies && sameTotals(currentOutputTotals(), initialOutputTotals)) {
var $snap = $('#recipe-packaging-preview').empty();
packagingSnapshot.forEach(function (pk) {
$snap.append($('<tr></tr>')
.append($('<td></td>').text(pk.name))
.append($('<td></td>').text(pk.pieces)));
$snap.append(packagingRow(pk.id, pk.name, pk.pieces, false));
});
if (!packagingSnapshot.length) {
$snap.append('<tr><td colspan="2" class="text-muted">{{ __('Kein Verpackungsverbrauch gebucht.') }}</td></tr>');
$snap.append('<tr><td colspan="5" class="text-muted">{{ __('Kein Verpackungsverbrauch gebucht.') }}</td></tr>');
}
$hint.text('{{ __('Gespeicherter Verpackungsverbrauch dieser Produktion — bleibt erhalten, solange Ergebnisprodukte und Stückzahlen unverändert sind.') }}');
return;
@ -429,12 +454,10 @@
});
var $pk = $('#recipe-packaging-preview').empty();
order.forEach(function (id) {
$pk.append($('<tr></tr>')
.append($('<td></td>').text(totals[id].name))
.append($('<td></td>').text(Math.round(totals[id].pieces))));
$pk.append(packagingRow(id, totals[id].name, Math.round(totals[id].pieces), true));
});
if (!order.length) {
$pk.append('<tr><td colspan="2" class="text-muted">{{ __('Keine Verpackung hinterlegt.') }}</td></tr>');
$pk.append('<tr><td colspan="5" class="text-muted">{{ __('Keine Verpackung hinterlegt.') }}</td></tr>');
}
}