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 canBeReceived() { return $this->status === MutationStatus::SENT; } public function canBeApproved() { return $this->status === MutationStatus::RECEIVED; } public function canBeCancelled() { return $this->status === MutationStatus::SENT; } // 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 and move stock immediately public function approve($userId, $approvalNotes = null) { if (!$this->canBeApproved()) { throw new \Exception('Mutasi tidak dapat disetujui dalam status saat ini'); } DB::beginTransaction(); try { // Update status to approved first $this->update([ 'status' => MutationStatus::APPROVED, 'approved_by' => $userId, 'approved_at' => now(), 'approval_notes' => $approvalNotes ]); // Immediately move stock after approval foreach ($this->mutationDetails as $detail) { // Process all details that have quantity_requested > 0 // because goods have been sent from source dealer if ($detail->quantity_requested > 0) { $this->processStockMovement($detail); } } DB::commit(); } catch (\Exception $e) { DB::rollback(); throw $e; } 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 method removed - Stock moves automatically after approval 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}"; } }