feat: per-row and global discount with margin validation

Parts (wo_parts) already had discount_pct; now also added to wo_works.
Labor catalog gets a cost_price field as the hard minimum sell floor.

- WorkOrderWork: discount_pct + cost_price fields; total calculation
  now includes discount: hours × price_per_hour × (1 − disc/100)
- Labor model: cost_price field (used to pre-fill work rows and validate)
- WorksRelationManager: discount_pct field with live helper text showing
  effective price + margin %; validation blocks save if below buy cost
  or below company min_discount_margin_pct; "Reducere globală" header
  action applies % to all work rows (skips rows that would breach limits)
- PartsRelationManager: same validation on existing discount_pct field;
  helper text (🟢/🟠/🔴) shows effective price + margin in real time;
  "Reducere globală" header action for all parts
- Settings page: new "Reduceri & limite preț" section with
  min_discount_margin_pct — 0 = no limit, >0 = hard block below that %
- LaborResource form: cost_price field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 04:48:15 +00:00
parent c66b895948
commit ce9f082612
7 changed files with 281 additions and 10 deletions
+18
View File
@@ -69,6 +69,7 @@ class Settings extends Page
'telegram_bot_token' => data_get($settings, 'telegram.bot_token'),
'reminder_after_days' => data_get($settings, 'reminder.after_days', 365),
'reminder_cooldown_days' => data_get($settings, 'reminder.cooldown_days', 30),
'min_discount_margin_pct' => $settings['min_discount_margin_pct'] ?? 0,
'default_internal_margin_pct' => $settings['default_internal_margin_pct'] ?? null,
'show_internal_margin_details' => data_get($settings, 'show_internal_margin_details', true),
'shop_enabled' => data_get($settings, 'shop.enabled', false),
@@ -123,6 +124,21 @@ class Settings extends Page
->schema([
Forms\Components\TextInput::make('labor_rate')->label(__('Tarif normo-oră'))->numeric()->required(),
]),
Schemas\Components\Section::make(__('Reduceri & limite preț'))
->description(__('Setează marja minimă permisă la vânzare. Sub această limită formularul blochează salvarea.'))
->columns(2)
->schema([
Forms\Components\TextInput::make('min_discount_margin_pct')
->label(__('Marjă minimă vânzare (%)'))
->numeric()
->step(0.1)
->minValue(0)
->maxValue(99)
->suffix('%')
->placeholder('Ex: 10')
->default(0)
->helperText(__('0 = fără limită. Ex: 10 = marja minimă 10%, adică nu poți da reducere care aduce marja sub 10%.')),
]),
Schemas\Components\Section::make(__('Marjă internă (nu TVA)'))
->description(__('Procent implicit aplicat la manopere. Se scade din prețul de manoperă pentru a determina baza salariului mecanicului. Vizibilă doar rolurilor cu drept „finance.view_internal_margin".'))
->columns(2)
@@ -315,6 +331,8 @@ class Settings extends Page
'after_days' => (int) ($data['reminder_after_days'] ?? 365),
'cooldown_days' => (int) ($data['reminder_cooldown_days'] ?? 30),
],
'min_discount_margin_pct' => isset($data['min_discount_margin_pct']) && $data['min_discount_margin_pct'] !== ''
? (float) $data['min_discount_margin_pct'] : 0,
'default_internal_margin_pct' => $data['default_internal_margin_pct'] !== '' && $data['default_internal_margin_pct'] !== null
? (float) $data['default_internal_margin_pct'] : null,
'show_internal_margin_details' => (bool) ($data['show_internal_margin_details'] ?? true),