317 lines
8.9 KiB
PHP
Executable File
317 lines
8.9 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Enums\MutationStatus;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class Mutation extends Model
|
|
{
|
|
use HasFactory, SoftDeletes;
|
|
|
|
protected $fillable = [
|
|
'mutation_number',
|
|
'from_dealer_id',
|
|
'to_dealer_id',
|
|
'status',
|
|
'requested_by',
|
|
'approved_by',
|
|
'approved_at',
|
|
'approval_notes',
|
|
'received_by',
|
|
'received_at',
|
|
'reception_notes',
|
|
'shipping_notes',
|
|
'rejection_reason',
|
|
'rejected_by',
|
|
'rejected_at',
|
|
'cancelled_by',
|
|
'cancelled_at',
|
|
'cancellation_reason'
|
|
];
|
|
|
|
protected $casts = [
|
|
'status' => MutationStatus::class,
|
|
'approved_at' => 'datetime',
|
|
'received_at' => 'datetime',
|
|
'rejected_at' => 'datetime',
|
|
'cancelled_at' => 'datetime'
|
|
];
|
|
|
|
protected static function booted()
|
|
{
|
|
static::creating(function ($mutation) {
|
|
if (empty($mutation->mutation_number)) {
|
|
$mutation->mutation_number = $mutation->generateMutationNumber();
|
|
}
|
|
});
|
|
}
|
|
|
|
public function fromDealer()
|
|
{
|
|
return $this->belongsTo(Dealer::class, 'from_dealer_id');
|
|
}
|
|
|
|
public function toDealer()
|
|
{
|
|
return $this->belongsTo(Dealer::class, 'to_dealer_id');
|
|
}
|
|
|
|
public function requestedBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'requested_by');
|
|
}
|
|
|
|
public function approvedBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'approved_by');
|
|
}
|
|
|
|
public function receivedBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'received_by');
|
|
}
|
|
|
|
public function rejectedBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'rejected_by');
|
|
}
|
|
|
|
public function cancelledBy()
|
|
{
|
|
return $this->belongsTo(User::class, 'cancelled_by');
|
|
}
|
|
|
|
public function mutationDetails()
|
|
{
|
|
return $this->hasMany(MutationDetail::class);
|
|
}
|
|
|
|
public function stockLogs()
|
|
{
|
|
return $this->morphMany(StockLog::class, 'source');
|
|
}
|
|
|
|
// Helper methods
|
|
public function getStatusLabelAttribute()
|
|
{
|
|
return $this->status->label();
|
|
}
|
|
|
|
public function getStatusColorAttribute()
|
|
{
|
|
return $this->status->color();
|
|
}
|
|
|
|
public function getTotalItemsAttribute()
|
|
{
|
|
return $this->mutationDetails()->sum('quantity_requested');
|
|
}
|
|
|
|
public function getTotalApprovedItemsAttribute()
|
|
{
|
|
return $this->mutationDetails()->sum('quantity_approved');
|
|
}
|
|
|
|
public function canBeSent()
|
|
{
|
|
return $this->status === MutationStatus::PENDING;
|
|
}
|
|
|
|
public function canBeReceived()
|
|
{
|
|
return $this->status === MutationStatus::SENT;
|
|
}
|
|
|
|
public function canBeApproved()
|
|
{
|
|
return $this->status === MutationStatus::RECEIVED;
|
|
}
|
|
|
|
public function canBeCompleted()
|
|
{
|
|
return $this->status === MutationStatus::APPROVED;
|
|
}
|
|
|
|
public function canBeCancelled()
|
|
{
|
|
return in_array($this->status, [MutationStatus::PENDING, MutationStatus::SENT]);
|
|
}
|
|
|
|
// Send mutation to destination dealer
|
|
public function send($userId)
|
|
{
|
|
if (!$this->canBeSent()) {
|
|
throw new \Exception('Mutasi tidak dapat dikirim dalam status saat ini');
|
|
}
|
|
|
|
$this->update([
|
|
'status' => MutationStatus::SENT
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Receive mutation by destination dealer
|
|
public function receive($userId, $receptionNotes = null)
|
|
{
|
|
if (!$this->canBeReceived()) {
|
|
throw new \Exception('Mutasi tidak dapat diterima dalam status saat ini');
|
|
}
|
|
|
|
$this->update([
|
|
'status' => MutationStatus::RECEIVED,
|
|
'received_by' => $userId,
|
|
'received_at' => now(),
|
|
'reception_notes' => $receptionNotes
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Approve mutation
|
|
public function approve($userId, $approvalNotes = null)
|
|
{
|
|
if (!$this->canBeApproved()) {
|
|
throw new \Exception('Mutasi tidak dapat disetujui dalam status saat ini');
|
|
}
|
|
|
|
$this->update([
|
|
'status' => MutationStatus::APPROVED,
|
|
'approved_by' => $userId,
|
|
'approved_at' => now(),
|
|
'approval_notes' => $approvalNotes
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Reject mutation
|
|
public function reject($userId, $rejectionReason)
|
|
{
|
|
if (!$this->canBeApproved()) {
|
|
throw new \Exception('Mutasi tidak dapat ditolak dalam status saat ini');
|
|
}
|
|
|
|
$this->update([
|
|
'status' => MutationStatus::REJECTED,
|
|
'rejected_by' => $userId,
|
|
'rejected_at' => now(),
|
|
'rejection_reason' => $rejectionReason
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Cancel mutation
|
|
public function cancel($userId, $cancellationReason = null)
|
|
{
|
|
if (!$this->canBeCancelled()) {
|
|
throw new \Exception('Mutasi tidak dapat dibatalkan dalam status saat ini');
|
|
}
|
|
|
|
$this->update([
|
|
'status' => MutationStatus::CANCELLED,
|
|
'cancelled_by' => $userId,
|
|
'cancelled_at' => now(),
|
|
'cancellation_reason' => $cancellationReason
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Complete mutation (actually move the stock)
|
|
public function complete()
|
|
{
|
|
if (!$this->canBeCompleted()) {
|
|
throw new \Exception('Mutasi tidak dapat diselesaikan dalam status saat ini');
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
foreach ($this->mutationDetails as $detail) {
|
|
// Proses semua detail yang memiliki quantity_requested > 0
|
|
// karena barang sudah dikirim dari dealer asal
|
|
if ($detail->quantity_requested > 0) {
|
|
$this->processStockMovement($detail);
|
|
}
|
|
}
|
|
|
|
$this->update(['status' => MutationStatus::COMPLETED]);
|
|
DB::commit();
|
|
} catch (\Exception $e) {
|
|
DB::rollback();
|
|
throw $e;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
private function processStockMovement(MutationDetail $detail)
|
|
{
|
|
// Kurangi stock dari dealer asal berdasarkan quantity_requested (barang yang dikirim)
|
|
$fromStock = Stock::firstOrCreate([
|
|
'product_id' => $detail->product_id,
|
|
'dealer_id' => $this->from_dealer_id
|
|
], ['quantity' => 0]);
|
|
|
|
if ($fromStock->quantity < $detail->quantity_requested) {
|
|
throw new \Exception("Stock tidak mencukupi untuk produk {$detail->product->name} di {$this->fromDealer->name}");
|
|
}
|
|
|
|
$fromStock->updateStock(
|
|
$fromStock->quantity - $detail->quantity_requested,
|
|
$this,
|
|
"Mutasi keluar ke {$this->toDealer->name} - {$this->mutation_number} (Dikirim: {$detail->quantity_requested}, Diterima: {$detail->quantity_approved})"
|
|
);
|
|
|
|
// Tambah stock ke dealer tujuan berdasarkan quantity_approved (barang yang diterima)
|
|
$toStock = Stock::firstOrCreate([
|
|
'product_id' => $detail->product_id,
|
|
'dealer_id' => $this->to_dealer_id
|
|
], ['quantity' => 0]);
|
|
|
|
$toStock->updateStock(
|
|
$toStock->quantity + $detail->quantity_approved,
|
|
$this,
|
|
"Mutasi masuk dari {$this->fromDealer->name} - {$this->mutation_number} (Dikirim: {$detail->quantity_requested}, Diterima: {$detail->quantity_approved})"
|
|
);
|
|
|
|
// Jika ada selisih (kehilangan), catat sebagai stock log terpisah untuk audit
|
|
$lostQuantity = $detail->quantity_requested - $detail->quantity_approved;
|
|
if ($lostQuantity > 0) {
|
|
// Buat stock log untuk barang yang hilang/rusak
|
|
StockLog::create([
|
|
'stock_id' => $fromStock->id,
|
|
'previous_quantity' => $fromStock->quantity + $detail->quantity_requested, // Stock sebelum pengurangan
|
|
'new_quantity' => $fromStock->quantity, // Stock setelah pengurangan
|
|
'source_type' => get_class($this),
|
|
'source_id' => $this->id,
|
|
'description' => "Kehilangan/kerusakan saat mutasi ke {$this->toDealer->name} - {$this->mutation_number} (Hilang: {$lostQuantity})",
|
|
'user_id' => auth()->id()
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function generateMutationNumber()
|
|
{
|
|
$prefix = 'MUT';
|
|
$date = now()->format('Ymd');
|
|
$lastNumber = static::whereDate('created_at', today())
|
|
->whereNotNull('mutation_number')
|
|
->latest('id')
|
|
->first();
|
|
|
|
if ($lastNumber) {
|
|
$lastSequence = (int) substr($lastNumber->mutation_number, -4);
|
|
$sequence = str_pad($lastSequence + 1, 4, '0', STR_PAD_LEFT);
|
|
} else {
|
|
$sequence = '0001';
|
|
}
|
|
|
|
return "{$prefix}{$date}{$sequence}";
|
|
}
|
|
}
|