Files
autocrm/app/Models/Tenant/SubcontractJob.php
T
Vasyka e8078f157a Stage 9 — Subcontractor System: outsourced work with cost+markup
Schema:
- subcontractors (specialty, rating, contact)
- subcontract_jobs (work_order link, cost, markup_pct, client_price, status
  workflow, sent_at/eta/returned_at, paid_to_sub)

Models:
- SubcontractJob: auto number (SC-YY-NNNN), client_price = cost×(1+markup/100)
  when markup>0 (else manual), margin() helper, recalcs parent WO on save/delete
- WorkOrder.recalcTotal now includes non-cancelled subcontract job client_price

Filament (new "Subcontractare" nav group):
- SubcontractorResource (specialty/rating CRUD)
- SubcontractJobResource board with cost/client/margin columns + status filters,
  nav badge = open jobs
- SubcontractJobsRelationManager on WorkOrder

Tests (7 new):
- client_price from markup; manual price without markup; auto number;
  WO total includes jobs; cancelled excluded; delete recalcs; tenant isolation

Closes roadmap to 16/18 stages (only Stage 10 Bodyshop remains).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 06:43:15 +00:00

86 lines
2.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Models\Tenant;
use App\Models\Concerns\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SubcontractJob extends Model
{
use BelongsToTenant, SoftDeletes;
public const STATUSES = [
'sent' => 'Trimis',
'in_progress' => 'În lucru',
'done' => 'Gata',
'returned' => 'Returnat',
'cancelled' => 'Anulat',
];
protected $fillable = [
'company_id', 'work_order_id', 'subcontractor_id',
'number', 'category', 'description',
'cost', 'markup_pct', 'client_price',
'status', 'sent_at', 'eta', 'returned_at', 'paid_to_sub', 'notes',
];
protected $casts = [
'cost' => 'decimal:2',
'markup_pct' => 'decimal:2',
'client_price' => 'decimal:2',
'sent_at' => 'date',
'eta' => 'date',
'returned_at' => 'date',
'paid_to_sub' => 'boolean',
];
public function workOrder(): BelongsTo
{
return $this->belongsTo(WorkOrder::class);
}
public function subcontractor(): BelongsTo
{
return $this->belongsTo(Subcontractor::class);
}
/** Our margin = what we bill the client what the sub charges us. */
public function margin(): float
{
return round((float) $this->client_price - (float) $this->cost, 2);
}
public static function generateNumber(int $companyId): string
{
$year = date('y');
$count = static::withoutGlobalScopes()
->where('company_id', $companyId)
->whereYear('created_at', date('Y'))
->count();
return sprintf('SC-%s-%04d', $year, $count + 1);
}
protected static function booted(): void
{
static::creating(function (self $job) {
if (empty($job->number)) {
$job->number = static::generateNumber(
$job->company_id ?: app(\App\Tenancy\TenantManager::class)->currentId()
);
}
});
static::saving(function (self $job) {
// markup drives client_price unless markup is zero (then keep manual price).
if ((float) $job->markup_pct > 0) {
$job->client_price = round((float) $job->cost * (1 + (float) $job->markup_pct / 100), 2);
}
});
static::saved(fn (self $job) => $job->workOrder?->recalcTotal());
static::deleted(fn (self $job) => $job->workOrder?->recalcTotal());
}
}