Warenwirtschaft: Audit-Niedrigbefunde N-2/3/4/5/7/8/9/15
- N-2: Letzter Netto-Preis je Lieferant nur aus tatsächlichen Eingängen (status received). - N-3: Verbrauchsfenster exakt (kein startOfDay-Off-by-one). - N-4: Kritisch-Zählung vereinheitlicht — Controller liefert die Kachel-Zählweise (inkl. critical_ordered), tote View-Überschreibung entfernt; Sidenav-Badge bleibt bewusst ohne bestellte Kritische. - N-5: Ausschuss-Liste zeigt das 500er-Limit an + "Alle anzeigen" (?all=1), analog OPT-3. - N-7: parseQuantity parst Strings konsequent im deutschen Format — Server und UI-Ampel interpretieren "1.500" jetzt identisch. - N-8: Produktions-Kopie übernimmt keine alten Chargen-Nummern mehr (Auto-Vorschlag Präfix+ttmmjj greift). - N-9: recordProductionStock lädt Produkte withTrashed — Korrektur- Gegenbuchungen soft-gelöschter Output-Produkte werden nicht mehr übersprungen. - N-15: Hinweise/Leitfaden-Markdown mit html_input=strip + allow_unsafe_links=false gerendert. Bewusst offen (Kundenentscheid/Design): N-1 (Chargen-Gruppierung ohne supplier_id), N-6 (reFormatNumber-Konvention app-weit), N-10 (manuelle Bewegung darf ins Minus, Inventur-Korrekturen), N-13 (mfg_row_phase- Fallback auf phasenlos). Suite: 231 grün, 823 Assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9ed47c02f3
commit
d34f0a77f4
10 changed files with 35 additions and 8 deletions
|
|
@ -37,7 +37,8 @@ class NoticeController extends Controller
|
|||
return view('admin.inventory.notices.index', [
|
||||
'title' => $title,
|
||||
'subtitle' => $subtitle,
|
||||
'content' => Str::markdown($markdown),
|
||||
// Audit N-15: Roh-HTML im Markdown strippen (Defense-in-Depth).
|
||||
'content' => Str::markdown($markdown, ['html_input' => 'strip', 'allow_unsafe_links' => false]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,11 @@ class ProductionController extends Controller
|
|||
'outputs',
|
||||
]);
|
||||
|
||||
// Audit N-8: Chargen-Nummern nicht in die Kopie übernehmen — das neue
|
||||
// Produktionsdatum erzeugt sonst Duplikat-Chargen mit falschem Datum;
|
||||
// leer ⇒ JS-Vorschlag (Präfix + ttmmjj) greift.
|
||||
$production->outputs->each(fn ($output) => $output->setAttribute('batch_number', null));
|
||||
|
||||
$defaultLocationId = $production->location_id;
|
||||
|
||||
return view('admin.inventory.productions.create', [
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ class RawMaterialStockController extends Controller
|
|||
|
||||
return view('admin.inventory.raw-material-stock.index', [
|
||||
'rows' => $rows,
|
||||
'criticalCount' => $rows->where('status', 'critical')->count(),
|
||||
// Audit N-4: gleiche Zählweise wie die Kacheln (kritisch inkl. bereits bestellt).
|
||||
'criticalCount' => $rows->whereIn('status', ['critical', 'critical_ordered'])->count(),
|
||||
'horizonOptions' => $this->horizonOptions(),
|
||||
'defaultHorizon' => 90,
|
||||
'scope' => $scope,
|
||||
|
|
|
|||
|
|
@ -32,9 +32,13 @@ class StockDisposalController extends Controller
|
|||
$query->where('disposal_type', $request->query('type'));
|
||||
}
|
||||
|
||||
// Audit N-5: Limit sichtbar machen statt still abzuschneiden (analog OPT-3).
|
||||
$limit = $request->boolean('all') ? null : 500;
|
||||
|
||||
return view('admin.inventory.stock-disposals.index', [
|
||||
'values' => $query->limit(500)->get(),
|
||||
'values' => $limit !== null ? $query->limit($limit)->get() : $query->get(),
|
||||
'typeFilter' => $request->query('type'),
|
||||
'listLimit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,9 @@ class StockEntryRepository
|
|||
$ranked = StockEntry::query()
|
||||
->selectRaw('supplier_id, price_per_kg, ROW_NUMBER() OVER (PARTITION BY supplier_id ORDER BY ordered_at DESC, id DESC) as rn')
|
||||
->where('entry_type', 'ingredient')
|
||||
// Audit N-2: nur tatsächliche Eingänge — offene, evtl. nie gelieferte
|
||||
// Bestellungen bestimmen den letzten Einkaufspreis nicht mehr.
|
||||
->where('status', 'received')
|
||||
->where('ingredient_id', $ingredientId)
|
||||
->whereNotNull('supplier_id')
|
||||
->whereNotNull('price_per_kg');
|
||||
|
|
|
|||
|
|
@ -241,8 +241,10 @@ class InventoryService
|
|||
*/
|
||||
public function dailyConsumptionByIngredient(?array $ingredientIds = null, int $windowDays = self::CONSUMPTION_WINDOW_DAYS): array
|
||||
{
|
||||
// Audit N-3: exaktes Fenster — startOfDay() verlängerte den Zeitraum um
|
||||
// den angebrochenen heutigen Tag (Tagesverbrauch bis ~1 % überschätzt).
|
||||
$windowDays = max(1, $windowDays);
|
||||
$since = Carbon::now()->subDays($windowDays)->startOfDay();
|
||||
$since = Carbon::now()->subDays($windowDays);
|
||||
|
||||
$rows = ProductionIngredient::query()
|
||||
->join('productions', 'productions.id', '=', 'production_ingredients.production_id')
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ class ProductStockService
|
|||
return;
|
||||
}
|
||||
|
||||
$products = Product::query()->whereIn('id', $productIds)->get()->keyBy('id');
|
||||
// Audit N-9: withTrashed — Gegenbuchungen entfernter Output-Produkte
|
||||
// dürfen nicht still übersprungen werden, wenn das Produkt inzwischen
|
||||
// soft-gelöscht wurde.
|
||||
$products = Product::withTrashed()->whereIn('id', $productIds)->get()->keyBy('id');
|
||||
|
||||
foreach ($productIds as $productId) {
|
||||
$alreadyBooked = (int) ($booked[$productId] ?? 0);
|
||||
|
|
|
|||
|
|
@ -629,12 +629,15 @@ class ProductionService
|
|||
if ($value === null || $value === '') {
|
||||
return 0.0;
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
// Audit N-7: Zahlen (interne Aufrufer) direkt; Strings konsequent im
|
||||
// deutschen Format parsen — vorher wich „1.500" (Server: 1,5) von der
|
||||
// UI-Ampel (1500) ab.
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
$n = reFormatNumber((string) $value);
|
||||
|
||||
return $n !== null && $n !== '' ? (float) $n : 0.0;
|
||||
return $n !== null && $n !== '' && is_numeric($n) ? (float) $n : 0.0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
@php
|
||||
$total = $rows->count();
|
||||
$criticalCount = $rows->whereIn('status', ['critical', 'critical_ordered'])->count();
|
||||
$warningCount = $rows->where('status', 'warning')->count();
|
||||
$okCount = $total - $criticalCount - $warningCount;
|
||||
@endphp
|
||||
|
|
|
|||
|
|
@ -95,6 +95,12 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if(($listLimit ?? null) !== null && $values->count() >= $listLimit)
|
||||
<p class="text-muted small px-3 pb-2 mb-0">
|
||||
{{ __('Es werden die neuesten :n Abgänge angezeigt.', ['n' => $listLimit]) }}
|
||||
<a href="{{ route('admin.inventory.stock-disposals.index', array_filter(['all' => 1, 'type' => $typeFilter])) }}">{{ __('Alle anzeigen') }}</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue