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>
231 lines
9.5 KiB
PHP
231 lines
9.5 KiB
PHP
<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>
|