gruene-seele/resources/views/admin/inventory/stock-entries/_scripts.blade.php
Kevin Adametz 736cb087ae Warenwirtschaft: Einkäufe auch nach Buchung korrigierbar, mit Änderungsprotokoll
Bisher ließen sich nur offene Bestellungen bearbeiten; ein Tippfehler in
einem gebuchten Wareneingang war nicht korrigierbar (Anforderung 14.09.2026).

- Bearbeiten/Korrigieren für gebuchte Eingänge (Admin): Bestell- und
  Eingangsdaten (Menge, Datum, Charge, MHD, Qualität, Preise, Lieferant,
  Lagerort). Menge wirkt sofort auf den Bestand; ml-Eingänge werden in ml
  korrigiert und über die gespeicherte Dichte in Gramm gebucht.
- Schutz: Menge nicht unter den bereits verbrauchten Anteil (Rohstoff je
  Eingang, Verpackung je Artikel); bei Verbrauch sind Art und Rohstoff/
  Artikel gesperrt (Formular + Server).
- Änderungsprotokoll (stock_entry_changes): wer/wann/Grund + geänderte
  Felder alt → neu; Anzeige auf der Detailseite. Grund bei gebuchten
  Eingängen Pflicht, bei offenen optional. Speichern ohne Änderung
  erzeugt keine Zeile.
- Tests (6 neue), In-App-Doku (Hinweise + Leitfaden 3.2) nachgezogen.

Migration: 2026_09_14_110000_create_stock_entry_changes_table.
Suite: 396 grün.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 14:51:27 +00:00

231 lines
9.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script>
(function ($) {
function toggleBlocks() {
var t = $('#entry_type').val();
var isIng = t === 'ingredient';
$('#stock-entry-ingredient-block').toggle(isIng);
$('#stock-entry-quality-block').toggle(isIng);
$('#stock-entry-packaging-block').toggle(!isIng);
$('#price-per-kg-block').toggle(isIng);
$('#price-total-block').toggle(!isIng);
$('#received-ingredient-block').toggle(isIng);
}
// Korrektur eines gebuchten Eingangs: die eingegangene Menge folgt derselben
// Einheit wie die Bestellung (Hinweis + ml→g-Vorschau am Feld).
function applyReceivedUnit(isIng, unit, density, hasDensity) {
var $rq = $('#received_quantity');
if (!$rq.length) {
return;
}
var $hint = $('#received-quantity-hint');
var $preview = $('#received-quantity-preview');
$preview.hide().text('');
if (!isIng) {
$hint.text('{{ __('Angabe in Stück.') }}');
return;
}
if (unit === 'ml' && hasDensity) {
$hint.text('{{ __('Angabe in ml — der Bestand wird über die Dichte in Gramm gebucht.') }}');
var ml = parseNumber($rq.val());
if (ml != null && ml > 0) {
$preview.text(formatQuantity(ml) + ' ml × ' + formatDensity(density) + ' g/ml = ' + formatQuantity(ml * density) + ' g').show();
}
} else if (unit === 'gram') {
$hint.text('{{ __('Angabe in Gramm.') }}');
} else {
$hint.text('{{ __('Bitte zuerst oben die Einheit wählen.') }}');
}
}
function initIngredientSelect2() {
var $el = $('#ingredient_id');
if ($el.data('select2')) {
$el.select2('destroy');
}
$el.select2({
theme: 'default',
width: '100%',
placeholder: '{{ __('Inhaltsstoff suchen…') }}',
allowClear: true,
ajax: {
url: $el.data('search-url'),
dataType: 'json',
delay: 250,
data: function (params) {
return {q: params.term || ''};
},
processResults: function (data) {
return {results: data.results || []};
},
cache: true
},
minimumInputLength: 1
});
}
function initPackagingSelect2() {
var $el = $('#packaging_item_id');
if ($el.data('select2')) {
$el.select2('destroy');
}
$el.select2({
theme: 'default',
width: '100%',
placeholder: '{{ __('Verpackungsartikel suchen…') }}',
allowClear: true,
ajax: {
url: $el.data('search-url'),
dataType: 'json',
delay: 250,
data: function (params) {
return {
q: params.term || '',
entry_type: $('#entry_type').val()
};
},
processResults: function (data) {
return {results: data.results || []};
},
cache: true
},
minimumInputLength: 0
});
}
// AP-42: aktuelle Dichte des gewählten Rohstoffs (aus dem AJAX-Ergebnis oder
// — bei vorbelegtem Rohstoff — aus der data-density-Option).
function currentDensity() {
var sel = $('#ingredient_id').select2('data');
if (sel && sel[0] && sel[0].density != null && sel[0].density !== '') {
return parseFloat(sel[0].density);
}
var d = $('#ingredient_id option:selected').data('density');
return (d != null && d !== '') ? parseFloat(d) : null;
}
function applyOrderedUnit() {
var isIng = $('#entry_type').val() === 'ingredient';
var $unit = $('#ordered_unit');
// Einheit ist eigenes Pflichtfeld vor der Menge — nur bei Rohstoffen sichtbar
// und nur dann required (ein verstecktes required-Feld blockiert das Absenden).
$('#ordered-unit-wrap').toggle(isIng);
$unit.prop('required', isIng);
var density = isIng ? currentDensity() : null;
var hasDensity = density != null && density > 0;
$unit.find('option[value="ml"]').prop('disabled', !hasDensity);
if (!hasDensity && $unit.val() === 'ml') {
$unit.val('');
}
var $unitHint = $('#ordered-unit-hint');
var $hint = $('#ordered-quantity-hint');
var $preview = $('#ordered-quantity-preview');
$preview.hide().text('');
applyReceivedUnit(isIng, $unit.val(), density, hasDensity);
if (!isIng) {
$hint.text('{{ __('Bei Verpackung in Stück.') }}');
return;
}
if (!$('#ingredient_id').val()) {
$unitHint.text('{{ __('Zuerst den Inhaltsstoff wählen — davon hängt ab, ob ml möglich ist.') }}');
} else if (hasDensity) {
$unitHint.text('{{ __('Dichte hinterlegt') }} (' + formatDensity(density) + ' g/ml) — {{ __('Erfassung in Gramm oder Milliliter möglich.') }}');
} else {
$unitHint.text('{{ __('Keine Dichte am Rohstoff hinterlegt — nur Gramm möglich. Für ml zuerst die Dichte pflegen.') }}');
}
var unit = $unit.val();
if (unit === 'ml' && hasDensity) {
$hint.text('{{ __('Erfassung in ml — der Bestand wird über die Dichte in Gramm gebucht.') }}');
var ml = parseNumber($('#ordered_quantity').val());
if (ml != null && ml > 0) {
// AP-42: Live-Vorschau der ml→g-Umrechnung direkt am Mengenfeld.
$preview.text(formatQuantity(ml) + ' ml × ' + formatDensity(density) + ' g/ml = ' + formatQuantity(ml * density) + ' g').show();
}
} else if (unit === 'gram') {
$hint.text('{{ __('Erfassung in Gramm.') }}');
} else {
$hint.text('{{ __('Bitte zuerst oben die Einheit wählen.') }}');
}
}
function parseNumber(value) {
if (value === null || value === undefined) {
return null;
}
// 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;
}
return parseFloat(normalized);
}
// 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;
}
function recalcFromNet() {
var net = parseNumber($('#price_per_kg').val());
if (net === null) {
return;
}
$('#price_per_kg_gross').val(formatPrice(net * currentFactor()));
}
function recalcFromGross() {
var gross = parseNumber($('#price_per_kg_gross').val());
if (gross === null) {
return;
}
var factor = currentFactor();
$('#price_per_kg').val(formatPrice(factor > 0 ? gross / factor : gross));
}
$(document).ready(function () {
toggleBlocks();
initIngredientSelect2();
initPackagingSelect2();
applyOrderedUnit();
$('#entry_type').on('change', function () {
toggleBlocks();
applyOrderedUnit();
$('#packaging_item_id').val(null).trigger('change');
initPackagingSelect2();
});
$('#ingredient_id').on('change', applyOrderedUnit);
$('#ordered_unit').on('change', applyOrderedUnit);
$('#ordered_quantity').on('input', applyOrderedUnit);
$('#received_quantity').on('input', applyOrderedUnit);
$('#price_per_kg').on('input', recalcFromNet);
$('#price_per_kg_gross').on('input', recalcFromGross);
$('#tax_rate_id').on('change', function () {
if (parseNumber($('#price_per_kg').val()) !== null) {
recalcFromNet();
} else {
recalcFromGross();
}
});
});
})(jQuery);
</script>