partial update create mutations
This commit is contained in:
@@ -26,4 +26,19 @@ class Dealer extends Model
|
||||
public function opnames(){
|
||||
return $this->hasMany(Opname::class);
|
||||
}
|
||||
|
||||
public function outgoingMutations()
|
||||
{
|
||||
return $this->hasMany(Mutation::class, 'from_dealer_id');
|
||||
}
|
||||
|
||||
public function incomingMutations()
|
||||
{
|
||||
return $this->hasMany(Mutation::class, 'to_dealer_id');
|
||||
}
|
||||
|
||||
public function stocks()
|
||||
{
|
||||
return $this->hasMany(Stock::class);
|
||||
}
|
||||
}
|
||||
|
||||
262
app/Models/Mutation.php
Normal file
262
app/Models/Mutation.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\MutationStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Mutation extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'mutation_number',
|
||||
'from_dealer_id',
|
||||
'to_dealer_id',
|
||||
'status',
|
||||
'requested_by',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'received_by',
|
||||
'received_at',
|
||||
'notes',
|
||||
'rejection_reason'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => MutationStatus::class,
|
||||
'approved_at' => 'datetime',
|
||||
'received_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 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)
|
||||
{
|
||||
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()
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Approve mutation
|
||||
public function approve($userId, $notes = 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(),
|
||||
'notes' => $notes
|
||||
]);
|
||||
|
||||
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,
|
||||
'approved_by' => $userId,
|
||||
'approved_at' => now(),
|
||||
'rejection_reason' => $rejectionReason
|
||||
]);
|
||||
|
||||
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) {
|
||||
if ($detail->quantity_approved > 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
|
||||
$fromStock = Stock::firstOrCreate([
|
||||
'product_id' => $detail->product_id,
|
||||
'dealer_id' => $this->from_dealer_id
|
||||
], ['quantity' => 0]);
|
||||
|
||||
if ($fromStock->quantity < $detail->quantity_approved) {
|
||||
throw new \Exception("Stock tidak mencukupi untuk produk {$detail->product->name} di {$this->fromDealer->name}");
|
||||
}
|
||||
|
||||
$fromStock->updateStock(
|
||||
$fromStock->quantity - $detail->quantity_approved,
|
||||
$this,
|
||||
"Mutasi keluar ke {$this->toDealer->name} - {$this->mutation_number}"
|
||||
);
|
||||
|
||||
// Tambah stock ke dealer tujuan
|
||||
$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}"
|
||||
);
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
98
app/Models/MutationDetail.php
Normal file
98
app/Models/MutationDetail.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MutationDetail extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'mutation_id',
|
||||
'product_id',
|
||||
'quantity_requested',
|
||||
'quantity_approved',
|
||||
'notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity_requested' => 'decimal:2',
|
||||
'quantity_approved' => 'decimal:2'
|
||||
];
|
||||
|
||||
public function mutation()
|
||||
{
|
||||
return $this->belongsTo(Mutation::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
public function getQuantityDifferenceAttribute()
|
||||
{
|
||||
return $this->quantity_approved - $this->quantity_requested;
|
||||
}
|
||||
|
||||
public function isFullyApproved()
|
||||
{
|
||||
return $this->quantity_approved == $this->quantity_requested;
|
||||
}
|
||||
|
||||
public function isPartiallyApproved()
|
||||
{
|
||||
return $this->quantity_approved > 0 && $this->quantity_approved < $this->quantity_requested;
|
||||
}
|
||||
|
||||
public function isRejected()
|
||||
{
|
||||
return $this->quantity_approved == 0;
|
||||
}
|
||||
|
||||
public function getApprovalStatusAttribute()
|
||||
{
|
||||
if ($this->isFullyApproved()) {
|
||||
return 'Disetujui Penuh';
|
||||
} elseif ($this->isPartiallyApproved()) {
|
||||
return 'Disetujui Sebagian';
|
||||
} elseif ($this->isRejected()) {
|
||||
return 'Ditolak';
|
||||
} else {
|
||||
return 'Menunggu';
|
||||
}
|
||||
}
|
||||
|
||||
public function getApprovalStatusColorAttribute()
|
||||
{
|
||||
if ($this->isFullyApproved()) {
|
||||
return 'success';
|
||||
} elseif ($this->isPartiallyApproved()) {
|
||||
return 'warning';
|
||||
} elseif ($this->isRejected()) {
|
||||
return 'danger';
|
||||
} else {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
|
||||
// Scope untuk filter berdasarkan status approval
|
||||
public function scopeFullyApproved($query)
|
||||
{
|
||||
return $query->whereColumn('quantity_approved', '=', 'quantity_requested');
|
||||
}
|
||||
|
||||
public function scopePartiallyApproved($query)
|
||||
{
|
||||
return $query->where('quantity_approved', '>', 0)
|
||||
->whereColumn('quantity_approved', '<', 'quantity_requested');
|
||||
}
|
||||
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('quantity_approved', '=', 0);
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,20 @@ class Product extends Model
|
||||
return $this->hasMany(Stock::class);
|
||||
}
|
||||
|
||||
public function mutationDetails()
|
||||
{
|
||||
return $this->hasMany(MutationDetail::class);
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan total stock saat ini
|
||||
public function getCurrentTotalStockAttribute()
|
||||
{
|
||||
return $this->stocks()->sum('quantity');
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan stock di dealer tertentu
|
||||
public function getStockByDealer($dealerId)
|
||||
{
|
||||
return $this->stocks()->where('dealer_id', $dealerId)->first()?->quantity ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user