chore: update document approval workflow and bug fixes
This commit is contained in:
191
Modules/MaterialLogistics/app/Models/MaterialTransfer.php
Normal file
191
Modules/MaterialLogistics/app/Models/MaterialTransfer.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MaterialLogistics\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Modules\ApprovalWorkflow\Models\ApprovalChain;
|
||||
use Modules\ApprovalWorkflow\Traits\HasApprovable;
|
||||
use Modules\MaterialLogistics\Enums\MaterialTransferStatus;
|
||||
use Modules\ProjectManagement\Models\Project;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\MaterialLogistics\Models\WarehouseStock;
|
||||
use Modules\MaterialLogistics\Models\InventoryMovement;
|
||||
|
||||
class MaterialTransfer extends Model
|
||||
{
|
||||
use HasApprovable;
|
||||
|
||||
protected $fillable = [
|
||||
'transfer_number',
|
||||
'from_project_id',
|
||||
'to_project_id',
|
||||
'status',
|
||||
'requested_by',
|
||||
'approved_by',
|
||||
'dispatched_by',
|
||||
'received_by',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => MaterialTransferStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function generateTransferNumber(): string
|
||||
{
|
||||
$prefix = 'MT-' . date('Ym') . '-';
|
||||
$last = self::where('transfer_number', 'like', "{$prefix}%")
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if (! $last) {
|
||||
return $prefix . '0001';
|
||||
}
|
||||
|
||||
$lastNumber = (int) substr($last->transfer_number, -4);
|
||||
return $prefix . str_pad((string) ($lastNumber + 1), 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function onApprovalCompleted(ApprovalChain $chain): void
|
||||
{
|
||||
if ($chain->status->value === 'approved') {
|
||||
$lastStep = $chain->steps()->where('status', 'approved')->orderByDesc('acted_at')->first();
|
||||
$this->update([
|
||||
'status' => MaterialTransferStatus::Approved,
|
||||
'approved_by' => $lastStep ? $lastStep->approver_id : null,
|
||||
]);
|
||||
} elseif ($chain->status->value === 'rejected') {
|
||||
$this->update([
|
||||
'status' => MaterialTransferStatus::Draft,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function markAsReceived(int $receivedById): void
|
||||
{
|
||||
if ($this->status !== MaterialTransferStatus::Approved && $this->status !== MaterialTransferStatus::InTransit) {
|
||||
throw new \LogicException('Transfer must be Approved or In Transit to be received.');
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($receivedById) {
|
||||
$fromWarehouse = $this->fromProject->warehouse;
|
||||
$toWarehouse = $this->toProject->warehouse;
|
||||
|
||||
if (!$fromWarehouse || !$toWarehouse) {
|
||||
throw new \LogicException('Both projects must have an assigned warehouse.');
|
||||
}
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$qty = $item->received_quantity ?? $item->requested_quantity;
|
||||
|
||||
if ($qty <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Deduct from Source
|
||||
$sourceStock = WarehouseStock::where('warehouse_id', $fromWarehouse->id)
|
||||
->where('material_id', $item->material_id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$sourceStock || $sourceStock->quantity < $qty) {
|
||||
// Decide: abort or allow negative. We abort for safety.
|
||||
throw new \LogicException("Insufficient stock in source warehouse for material ID {$item->material_id}.");
|
||||
}
|
||||
|
||||
$sourceStock->decrement('quantity', $qty);
|
||||
|
||||
InventoryMovement::create([
|
||||
'ulid' => (string) Str::ulid(),
|
||||
'material_id' => $item->material_id,
|
||||
'warehouse_id' => $fromWarehouse->id,
|
||||
'project_id' => $this->from_project_id,
|
||||
'movement_type' => 'dispatch',
|
||||
'direction' => 'out',
|
||||
'quantity' => $qty,
|
||||
'balance_after' => $sourceStock->quantity - $qty,
|
||||
'to_location_type' => 'warehouse',
|
||||
'to_location_id' => $toWarehouse->id,
|
||||
'reference_type' => MaterialTransfer::class,
|
||||
'reference_id' => $this->id,
|
||||
'performed_by' => $receivedById,
|
||||
]);
|
||||
|
||||
// Add to Destination
|
||||
$destStock = WarehouseStock::firstOrCreate(
|
||||
['warehouse_id' => $toWarehouse->id, 'material_id' => $item->material_id],
|
||||
['ulid' => (string) Str::ulid(), 'quantity' => 0]
|
||||
);
|
||||
|
||||
// Re-fetch to lock
|
||||
$destStock = WarehouseStock::where('id', $destStock->id)->lockForUpdate()->first();
|
||||
$destStock->increment('quantity', $qty);
|
||||
|
||||
InventoryMovement::create([
|
||||
'ulid' => (string) Str::ulid(),
|
||||
'material_id' => $item->material_id,
|
||||
'warehouse_id' => $toWarehouse->id,
|
||||
'project_id' => $this->to_project_id,
|
||||
'movement_type' => 'site_receipt',
|
||||
'direction' => 'in',
|
||||
'quantity' => $qty,
|
||||
'balance_after' => $destStock->quantity + $qty,
|
||||
'from_location_type' => 'warehouse',
|
||||
'from_location_id' => $fromWarehouse->id,
|
||||
'reference_type' => MaterialTransfer::class,
|
||||
'reference_id' => $this->id,
|
||||
'performed_by' => $receivedById,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->update([
|
||||
'status' => MaterialTransferStatus::Received,
|
||||
'received_by' => $receivedById,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Relationships ---
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(MaterialTransferItem::class);
|
||||
}
|
||||
|
||||
public function fromProject(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class, 'from_project_id');
|
||||
}
|
||||
|
||||
public function toProject(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class, 'to_project_id');
|
||||
}
|
||||
|
||||
public function requester(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'requested_by');
|
||||
}
|
||||
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
public function dispatcher(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'dispatched_by');
|
||||
}
|
||||
|
||||
public function receiver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'received_by');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user