Warenwirtschaft: AP-35 + AP-39 — Produktionsmengen auf Füllgewicht + ±5 % Abwiegen
AP-35 (Mengenberechnung): Die Hersteller-Rezeptur ist in Prozent hinterlegt; die Sollmengen skalieren jetzt auf das Füllgewicht statt fest auf ~100 g. Neuer Faktor recipeGramScale = Füllgewicht/100. Basis = neues, optionales Produkt-Feld "fill_weight" (g); ist es leer, gilt bei ml-Produkten (unit=1) die Füllmenge (contents_total) als Gramm (1 g/ml). Ohne Massenbasis (z. B. Stück-Produkte) bleibt es beim Altverhalten (Anteil = Gramm), sodass Bestandsprodukte/Tests unverändert rechnen. Skala wird an die Maske durchgereicht (recipe_gram_scale), damit Client und Server identisch rechnen (Service + 4 JS-Stellen). AP-39 (Abwiegen): Die Ist-Menge je INCI muss nicht mehr aufs Gramm passen — assertLinesMatchRecipe erlaubt ±5 % (kleiner absoluter Sockel), gebucht wird der reale Ist-Wert. Status-Ampel in der Maske: Orange = nichts eingetragen, Grün = eingetragen und ±5 %, Rot = >5 % daneben (Zahlendreher-Schutz). Migration add_fill_weight_to_products auf Dev-/Live-DB ausgeführt (nullable). Product fillable/cast + Produktformular-Feld ergänzt. 5 Tests, Suite 310 grün. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d60947c34d
commit
744b36cdcb
6 changed files with 238 additions and 11 deletions
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
var lineIndex = 0;
|
||||
var recipeIngredients = [];
|
||||
// AP-35: Umrechnung Anteil (%) → Gramm/Stück (Füllgewicht / 100; 1,0 ohne Basis).
|
||||
var recipeGramScale = 1;
|
||||
var loadedKey = null;
|
||||
|
||||
function currentKey() {
|
||||
|
|
@ -92,14 +94,25 @@
|
|||
return;
|
||||
}
|
||||
var qty = parseInt($('#quantity').val(), 10) || 1;
|
||||
var soll = ing.gram * (ing.factor || 1) * qty;
|
||||
var soll = ing.gram * recipeGramScale * (ing.factor || 1) * qty;
|
||||
var sum = 0;
|
||||
var anyEntered = false;
|
||||
$wrap.find('.recipe-qty-input').each(function () {
|
||||
if (String($(this).val()).trim() !== '') {
|
||||
anyEntered = true;
|
||||
}
|
||||
sum += parseGerman($(this).val());
|
||||
});
|
||||
var ok = Math.abs(sum - soll) <= 0.02;
|
||||
$dot.css('color', ok ? '#28a745' : '#fd7e14')
|
||||
.attr('title', ok ? '{{ __('Soll erfüllt') }}' : '{{ __('Summe weicht vom Soll ab') }}');
|
||||
// AP-39: Orange = nichts eingetragen; Grün = eingetragen und ±5 % vom Soll;
|
||||
// Rot = eingetragen, aber > 5 % daneben (Zahlendreher-Schutz).
|
||||
var tolerance = Math.max(soll * 0.05, 0.05);
|
||||
if (!anyEntered) {
|
||||
$dot.css('color', '#fd7e14').attr('title', '{{ __('Noch nichts eingetragen') }}');
|
||||
} else if (Math.abs(sum - soll) <= tolerance) {
|
||||
$dot.css('color', '#28a745').attr('title', '{{ __('Soll erfüllt (±5 %)') }}');
|
||||
} else {
|
||||
$dot.css('color', '#dc3545').attr('title', '{{ __('Abweichung über 5 % – bitte prüfen') }}');
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllStatuses() {
|
||||
|
|
@ -116,7 +129,7 @@
|
|||
if (!ing || ing.gram == null) {
|
||||
return;
|
||||
}
|
||||
var soll = ing.gram * (ing.factor || 1) * qty;
|
||||
var soll = ing.gram * recipeGramScale * (ing.factor || 1) * qty;
|
||||
$wrap.find('.recipe-soll').text(fmt(soll));
|
||||
});
|
||||
renderInciTable();
|
||||
|
|
@ -135,7 +148,7 @@
|
|||
var totalPercent = 0;
|
||||
var totalGrams = 0;
|
||||
recipeIngredients.forEach(function (ing) {
|
||||
var grams = ing.gram != null ? ing.gram * (ing.factor || 1) * qty : null;
|
||||
var grams = ing.gram != null ? ing.gram * recipeGramScale * (ing.factor || 1) * qty : null;
|
||||
if (ing.gram != null) {
|
||||
totalPercent += ing.gram;
|
||||
}
|
||||
|
|
@ -189,7 +202,7 @@
|
|||
|
||||
function renderIngredientWrap(ing, $container) {
|
||||
var qty = parseInt($('#quantity').val(), 10) || 1;
|
||||
var soll = ing.gram * (ing.factor || 1) * qty;
|
||||
var soll = ing.gram * recipeGramScale * (ing.factor || 1) * qty;
|
||||
var $wrap = $('<div class="border rounded p-2 mb-2" data-ingredient-id="' + ing.id + '"></div>');
|
||||
$wrap.data('recipe-ing', ing);
|
||||
var $head = $('<div class="d-flex justify-content-between flex-wrap"></div>');
|
||||
|
|
@ -218,6 +231,7 @@
|
|||
|
||||
function renderRecipe(data) {
|
||||
recipeIngredients = data.ingredients || [];
|
||||
recipeGramScale = data.recipe_gram_scale != null ? data.recipe_gram_scale : 1;
|
||||
$('#recipe-ingredient-lines').empty();
|
||||
lineIndex = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -530,6 +530,11 @@
|
|||
<label class="form-label" for="contents_total">{{ __('Gesamter Inhalt (ml / g) als Zahl') }}</label>
|
||||
{{ Form::text('contents_total', $product->contents_total, ['placeholder' => __('Bsp: 150'), 'class' => 'form-control', 'id' => 'contents_total']) }}
|
||||
</div>
|
||||
<div class="form-group col-sm-3">
|
||||
<label class="form-label" for="fill_weight">{{ __('Füllgewicht (g) – für Produktion') }}</label>
|
||||
{{ Form::text('fill_weight', $product->fill_weight, ['placeholder' => __('leer = Menge = Gramm (1 g/ml)'), 'class' => 'form-control', 'id' => 'fill_weight']) }}
|
||||
<small class="form-text text-muted">{{ __('Basis für die Rezeptur-Mengenberechnung (Prozent × Füllgewicht).') }}</small>
|
||||
</div>
|
||||
<div class="form-group col-sm-2">
|
||||
<label class="form-label" for="contents">{{ __('Grundpreis Einheit') }}</label>
|
||||
{{ Form::select('unit', $product->unitTypes, $product->unit, ['data-live-search' => 'false', 'class' => 'selectpicker', 'id' => 'unit']) }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue