Files
CKB/app/Models/Mutation.php

317 lines
8.9 KiB
PHP

<?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}";
}
}