Warenwirtschaft: AP-09 bis AP-13 (Produktbestand, Set-Produkte, Ausschuss, Konzepte)
- AP-09 Produktbestand inkl. Bewegungshistorie (product_stock_movements, ProductStockService) - AP-10 Rohstoffbestand-Ansicht je Lager (RawMaterialStockController) - AP-11 Bestandsschwellen / Out-of-Stock-Handling fuer Produkte und Shop - AP-12 Ausgang/Ausschuss (stock_disposals, StockDisposalController, InventoryService) - Set-Produkte (product_set_items) inkl. Aufloesung - Produktentwicklung & Hinweise-Verwaltung (Notices) - AP-13 Entwicklungskonzept Shop-Bestandsabzug im Plan dokumentiert - Feature-Tests fuer neue Module + aktualisierter Entwicklungsplan Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
78679e0c55
commit
3ee2d756e9
63 changed files with 5968 additions and 901 deletions
|
|
@ -6,13 +6,16 @@
|
|||
id="product-form-section-nav" aria-label="{{ __('Sprungmarken Produktformular') }}">
|
||||
<span class="navbar-text small font-weight-bold text-muted mr-2 mb-1">{{ __('Bereiche') }}:</span>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-produkt">{{ __('Produkt') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-verfuegbarkeit">{{ __('Verfügbarkeit') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-set">{{ __('Set') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-preise">{{ __('Preise in EUR') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-laenderpreise">{{ __('Landesspezifische Preise') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-whitelabel">{{ __('White-Label') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-details">{{ __('Details') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-rezeptur">{{ __('Inhaltsstoffe') }} / {{ __('Rezeptur') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-herstellerrezeptur">{{ __('Hersteller Rezeptur') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-warenwirtschaft">{{ __('Warenwirtschaft') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-rezeptur">{{ __('Inhaltsstoffe') }} / {{ __('Rezeptur') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-herstellerrezeptur">{{ __('Hersteller Rezeptur') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-verpackung">{{ __('Verpackung') }}</a>
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1 js-nav-recipe" href="#product-section-warenwirtschaft">{{ __('Warenwirtschaft') }}</a>
|
||||
@if (Auth::user()->isSySAdmin())
|
||||
<a class="btn btn-sm btn-outline-secondary mb-1 mr-1" href="#product-section-sysadmin">{{ __('SySAdmin Einstellungen') }}</a>
|
||||
@endif
|
||||
|
|
@ -97,6 +100,171 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2" id="product-section-verfuegbarkeit">
|
||||
<h5 class="card-header">{{ __('Verfügbarkeit') }}</h5>
|
||||
<div class="card-body">
|
||||
@php($outOfStockDays = $product->outOfStockRemainingDays())
|
||||
<div class="form-group">
|
||||
<label class="custom-control custom-checkbox">
|
||||
{!! Form::checkbox('out_of_stock_active', 1, old('out_of_stock_active', $outOfStockDays !== null), ['class' => 'custom-control-input', 'id' => 'out_of_stock_active']) !!}
|
||||
<span class="custom-control-label">{{ __('Vorübergehend nicht vorrätig (mit Zeitangabe)') }}</span>
|
||||
</label>
|
||||
<p class="text-muted small mb-0">{{ __('Zeigt im Shop den Hinweis „In ca. X Tagen wieder da!". Der Kauf bleibt weiterhin möglich.') }}</p>
|
||||
</div>
|
||||
<div class="form-row js-out-of-stock-days" style="display:none;">
|
||||
<div class="form-group col-sm-4">
|
||||
<label class="form-label" for="out_of_stock_days">{{ __('Wieder verfügbar in (Tagen)') }}</label>
|
||||
{{ Form::number('out_of_stock_days', old('out_of_stock_days', $outOfStockDays), ['placeholder' => __('z. B. 14'), 'class' => 'form-control', 'id' => 'out_of_stock_days', 'min' => 0, 'step' => 1]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="custom-control custom-checkbox">
|
||||
{!! Form::checkbox('out_of_stock_indefinite', 1, old('out_of_stock_indefinite', $product->out_of_stock_indefinite), ['class' => 'custom-control-input', 'id' => 'out_of_stock_indefinite']) !!}
|
||||
<span class="custom-control-label">{{ __('Auf unbestimmte Zeit nicht vorrätig') }}</span>
|
||||
</label>
|
||||
<p class="text-muted small mb-0">{{ __('Daueranzeige ohne Zeitangabe (hat Vorrang vor der Tagesangabe). Der Kauf bleibt weiterhin möglich.') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<hr>
|
||||
<button type="submit" class=" float-right btn btn-sm btn-submit">{{ __('save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2" id="product-section-set">
|
||||
<h5 class="card-header">{{ __('Set / Produktart') }}</h5>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="custom-control custom-checkbox">
|
||||
{!! Form::checkbox('is_set', 1, old('is_set', $product->is_set), ['class' => 'custom-control-input', 'id' => 'is_set']) !!}
|
||||
<span class="custom-control-label">{{ __('Dieses Produkt ist ein Set (Bündel mehrerer Einzelprodukte)') }}</span>
|
||||
</label>
|
||||
<p class="text-muted small mb-0">{{ __('Bei aktivem Set werden Rezeptur, Verpackung und Warenwirtschaft ausgeblendet. Ein Set wird nicht produziert; beim Verkauf werden später die enthaltenen Einzelprodukte abgebucht.') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="js-set-fields" style="display:none;">
|
||||
<p class="text-muted small mb-2">{{ __('Set-Bestandteile (nur Einzelprodukte). Reihenfolge per Drag & Drop ändern, danach speichern.') }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2rem"></th>
|
||||
<th>{{ __('Name') }}</th>
|
||||
<th>{{ __('Artikelnr.') }}</th>
|
||||
<th class="text-right" style="width:8rem">{{ __('Gewicht') }}</th>
|
||||
<th class="text-right" style="width:10rem">{{ __('Preis VK in EUR (Brutto)') }}</th>
|
||||
<th style="width:8rem">{{ __('Menge') }}</th>
|
||||
<th style="width:3rem"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="set-sortable-rows">
|
||||
@foreach ($product->setItems as $component)
|
||||
<tr data-set-product-id="{{ $component->id }}">
|
||||
<td class="text-muted align-middle set-drag-handle" style="cursor:grab"
|
||||
title="{{ __('Verschieben') }}">☰</td>
|
||||
<td class="align-middle">{{ $component->name }}</td>
|
||||
<td class="align-middle small text-muted">{{ $component->number ?? '—' }}</td>
|
||||
<td class="align-middle text-right small text-muted">{{ $component->weight ? $component->weight . ' g' : '—' }}</td>
|
||||
<td class="align-middle text-right small text-muted">{{ $component->getFormattedPrice() !== '' ? $component->getFormattedPrice() . ' €' : '—' }}</td>
|
||||
<td>
|
||||
<input type="hidden" name="set_component_id[]" value="{{ $component->id }}">
|
||||
<input type="number" min="1" step="1" name="set_quantity[]"
|
||||
class="form-control form-control-sm set-quantity"
|
||||
value="{{ (int) ($component->pivot->quantity ?? 1) }}" autocomplete="off">
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<a class="text-danger set-row-remove" href="#"
|
||||
title="{{ __('Entfernen') }}"><i class="far fa-trash-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-default" data-toggle="modal"
|
||||
data-target="#modal-set-pick">{{ __('Einzelprodukte hinzufügen') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modal-set-pick" tabindex="-1" role="dialog"
|
||||
aria-labelledby="modal-set-pick-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-set-pick-title">{{ __('Einzelprodukte auswählen') }}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="{{ __('Schließen') }}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group mb-2">
|
||||
<input type="search" id="set-modal-search" class="form-control" autocomplete="off"
|
||||
placeholder="{{ __('Name oder Artikelnr. filtern …') }}"
|
||||
aria-label="{{ __('Name oder Artikelnr. filtern …') }}">
|
||||
</div>
|
||||
<p class="small text-muted mb-2 d-none" id="set-modal-no-results">{{ __('Keine Treffer.') }}
|
||||
</p>
|
||||
<div class="table-responsive border rounded"
|
||||
style="max-height: min(60vh, 480px); overflow-y: auto;">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="thead-light sticky-top">
|
||||
<tr style="background-color: #f8f9fa;">
|
||||
<th style="width:2.5rem"></th>
|
||||
<th>{{ __('Name') }}</th>
|
||||
<th>{{ __('Artikelnr.') }}</th>
|
||||
<th class="text-right">{{ __('Gewicht') }}</th>
|
||||
<th class="text-right">{{ __('Preis VK in EUR (Brutto)') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="set-modal-tbody">
|
||||
@foreach ($set_product_catalog as $sp)
|
||||
<tr class="set-modal-row" data-set-product-id="{{ $sp->id }}"
|
||||
data-set-search="{{ e(mb_strtolower(trim(($sp->name ?? '') . ' ' . ($sp->number ?? '')), 'UTF-8')) }}">
|
||||
<td class="align-middle">
|
||||
<label class="custom-control custom-checkbox mb-0">
|
||||
<input type="checkbox" class="custom-control-input js-set-modal-cb"
|
||||
value="{{ $sp->id }}"
|
||||
@if ($product->setItems->contains('id', $sp->id)) disabled @endif>
|
||||
<span class="custom-control-label"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="align-middle js-set-modal-name">{{ $sp->name }}</td>
|
||||
<td class="align-middle small text-muted js-set-modal-number">{{ $sp->number ?? '—' }}</td>
|
||||
<td class="align-middle small text-muted text-right">{{ $sp->weight ? $sp->weight . ' g' : '—' }}</td>
|
||||
<td class="align-middle small text-muted text-right">{{ $sp->getFormattedPrice() !== '' ? $sp->getFormattedPrice() . ' €' : '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default"
|
||||
data-dismiss="modal">{{ __('Abbrechen') }}</button>
|
||||
<button type="button" class="btn btn-primary"
|
||||
id="btn-set-modal-add">{{ __('Hinzufügen') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<hr>
|
||||
<button type="submit" class=" float-right btn btn-sm btn-submit">{{ __('save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2" id="product-section-preise">
|
||||
<h5 class="card-header">
|
||||
{{ __('Preise in EUR') }}
|
||||
|
|
@ -419,6 +587,16 @@
|
|||
</h5>
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="product_inci_sync_sent" value="1">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="custom-control custom-checkbox">
|
||||
{!! Form::checkbox('no_recipe_required', 1, old('no_recipe_required', $product->no_recipe_required), ['class' => 'custom-control-input', 'id' => 'no_recipe_required']) !!}
|
||||
<span class="custom-control-label">{{ __('Dieses Produkt benötigt keine Rezeptur (Eigenprodukt, z. B. Broschüre, Etikett)') }}</span>
|
||||
</label>
|
||||
<p class="text-muted small mb-0">{{ __('Bei aktiver Option werden die Rezeptur-Felder ausgeblendet und in der Produktion wird keine Rezeptur abgefragt.') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="js-recipe-fields">
|
||||
<p class="text-muted small mb-2">{{ __('Reihenfolge per Drag & Drop ändern, danach speichern.') }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered mb-3" id="recipe-table-product">
|
||||
|
|
@ -553,6 +731,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{{-- /.js-recipe-fields --}}
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<hr>
|
||||
|
|
@ -568,6 +747,7 @@
|
|||
</h5>
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="manufacturer_inci_sync_sent" value="1">
|
||||
<div class="js-recipe-fields">
|
||||
<p class="text-muted small mb-2">{{ __('Eigene Hersteller-Rezeptur (separate INCI-Liste). Reihenfolge per Drag & Drop ändern, danach speichern.') }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered mb-3" id="recipe-table-manufacturer">
|
||||
|
|
@ -683,6 +863,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{{-- /.js-recipe-fields --}}
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<hr>
|
||||
|
|
@ -846,6 +1027,40 @@
|
|||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<p class="text-muted small mb-2">{{ __('Produktbestand-Schwellwerte (Stück). Bei Erreichen wird das Produkt im Produktbestand farblich markiert.') }}</p>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-6">
|
||||
<label class="form-label" for="min_product_stock">{{ __('Meldebestand (gelb)') }}</label>
|
||||
{{ Form::number('min_product_stock', old('min_product_stock', $product->min_product_stock), ['placeholder' => __('z. B. 20'), 'class' => 'form-control', 'id' => 'min_product_stock', 'min' => 0, 'step' => 1]) }}
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label class="form-label" for="critical_product_stock">{{ __('Kritischer Bestand (rot)') }}</label>
|
||||
{{ Form::number('critical_product_stock', old('critical_product_stock', $product->critical_product_stock), ['placeholder' => __('z. B. 10'), 'class' => 'form-control', 'id' => 'critical_product_stock', 'min' => 0, 'step' => 1]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<p class="text-muted small mb-2">{{ __('Optional: dieses Einzelprodukt einem Hauptprodukt zuordnen (z. B. „50 × 15 ml"). Der Produktbestand führt nur Haupt-/Einzelprodukte.') }}</p>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-8">
|
||||
<label class="form-label" for="main_product_id">{{ __('Hauptprodukt') }}</label>
|
||||
<select name="main_product_id" id="main_product_id" class="form-control">
|
||||
<option value="">{{ __('— kein Hauptprodukt —') }}</option>
|
||||
@foreach ($set_product_catalog as $mp)
|
||||
<option value="{{ $mp->id }}" @selected((int) old('main_product_id', $product->main_product_id) === (int) $mp->id)>
|
||||
{{ $mp->name }}@if ($mp->number) ({{ $mp->number }})@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-sm-4">
|
||||
<label class="form-label" for="main_product_quantity">{{ __('Menge im Hauptprodukt') }}</label>
|
||||
{{ Form::number('main_product_quantity', old('main_product_quantity', $product->main_product_quantity), ['placeholder' => __('z. B. 50'), 'class' => 'form-control', 'id' => 'main_product_quantity', 'min' => 1, 'step' => 1]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<hr>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue