partial update create mutations
This commit is contained in:
112
app/Console/Commands/ClearMutationsCommand.php
Normal file
112
app/Console/Commands/ClearMutationsCommand.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Mutation;
|
||||
use App\Models\MutationDetail;
|
||||
|
||||
class ClearMutationsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'mutations:clear {--force : Force the operation without confirmation}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clear all mutations and mutation details, then reset auto increment IDs';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Show warning
|
||||
$this->warn('⚠️ WARNING: This will permanently delete ALL mutations and mutation details!');
|
||||
$this->warn('⚠️ This action cannot be undone!');
|
||||
|
||||
// Check for force flag
|
||||
if (!$this->option('force')) {
|
||||
if (!$this->confirm('Are you sure you want to continue?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Show current counts
|
||||
$mutationCount = Mutation::count();
|
||||
$detailCount = MutationDetail::count();
|
||||
$trashedMutationCount = Mutation::onlyTrashed()->count();
|
||||
|
||||
$this->info("Current data:");
|
||||
$this->info("- Mutations: {$mutationCount}");
|
||||
$this->info("- Mutation Details: {$detailCount}");
|
||||
if ($trashedMutationCount > 0) {
|
||||
$this->info("- Soft Deleted Mutations: {$trashedMutationCount}");
|
||||
}
|
||||
|
||||
if ($mutationCount === 0 && $detailCount === 0 && $trashedMutationCount === 0) {
|
||||
$this->info('No data to clear.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('Starting cleanup process...');
|
||||
|
||||
try {
|
||||
// Delete data within transaction
|
||||
DB::beginTransaction();
|
||||
|
||||
// Delete mutation details first (foreign key constraint)
|
||||
$this->info('🗑️ Deleting mutation details...');
|
||||
MutationDetail::query()->delete();
|
||||
|
||||
// Delete mutations (including soft deleted ones)
|
||||
$this->info('🗑️ Deleting mutations...');
|
||||
Mutation::query()->delete();
|
||||
|
||||
// Force delete soft deleted mutations
|
||||
$this->info('🗑️ Force deleting soft deleted mutations...');
|
||||
Mutation::onlyTrashed()->forceDelete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Reset auto increment outside transaction (DDL operations auto-commit)
|
||||
$this->info('🔄 Resetting mutation_details auto increment...');
|
||||
DB::statement('ALTER TABLE mutation_details AUTO_INCREMENT = 1');
|
||||
|
||||
$this->info('🔄 Resetting mutations auto increment...');
|
||||
DB::statement('ALTER TABLE mutations AUTO_INCREMENT = 1');
|
||||
|
||||
$this->info('✅ Successfully cleared all mutations and reset auto increment!');
|
||||
$this->info('📊 Final counts:');
|
||||
$this->info('- Mutations: ' . Mutation::count());
|
||||
$this->info('- Mutation Details: ' . MutationDetail::count());
|
||||
|
||||
return 0;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if (DB::transactionLevel() > 0) {
|
||||
DB::rollback();
|
||||
}
|
||||
$this->error('❌ Failed to clear mutations: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/Enums/MutationStatus.php
Normal file
40
app/Enums/MutationStatus.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum MutationStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case SENT = 'sent';
|
||||
case RECEIVED = 'received';
|
||||
case APPROVED = 'approved';
|
||||
case REJECTED = 'rejected';
|
||||
case COMPLETED = 'completed';
|
||||
case CANCELLED = 'cancelled';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::PENDING => 'Menunggu Konfirmasi',
|
||||
self::SENT => 'Terkirim ke Dealer',
|
||||
self::RECEIVED => 'Diterima Dealer',
|
||||
self::APPROVED => 'Disetujui',
|
||||
self::REJECTED => 'Ditolak',
|
||||
self::COMPLETED => 'Selesai',
|
||||
self::CANCELLED => 'Dibatalkan',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::PENDING => 'warning',
|
||||
self::SENT => 'primary',
|
||||
self::RECEIVED => 'info',
|
||||
self::APPROVED => 'brand',
|
||||
self::REJECTED => 'danger',
|
||||
self::COMPLETED => 'success',
|
||||
self::CANCELLED => 'secondary',
|
||||
};
|
||||
}
|
||||
}
|
||||
278
app/Http/Controllers/WarehouseManagement/MutationsController.php
Normal file
278
app/Http/Controllers/WarehouseManagement/MutationsController.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Mutation;
|
||||
use App\Models\MutationDetail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Dealer;
|
||||
use App\Enums\MutationStatus;
|
||||
use App\Models\Menu;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Yajra\DataTables\DataTables;
|
||||
|
||||
class MutationsController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','mutations.index')->first();
|
||||
|
||||
if ($request->ajax()) {
|
||||
$data = Mutation::with(['fromDealer', 'toDealer', 'requestedBy', 'approvedBy', 'receivedBy'])
|
||||
->select('mutations.*');
|
||||
|
||||
// Filter berdasarkan dealer jika user bukan admin
|
||||
if (auth()->user()->dealer_id) {
|
||||
$data->where(function($query) {
|
||||
$query->where('from_dealer_id', auth()->user()->dealer_id)
|
||||
->orWhere('to_dealer_id', auth()->user()->dealer_id);
|
||||
});
|
||||
}
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('mutation_number', function($row) {
|
||||
return $row->mutation_number;
|
||||
})
|
||||
->addColumn('from_dealer', function($row) {
|
||||
return $row->fromDealer->name ?? '-';
|
||||
})
|
||||
->addColumn('to_dealer', function($row) {
|
||||
return $row->toDealer->name ?? '-';
|
||||
})
|
||||
->addColumn('requested_by', function($row) {
|
||||
return $row->requestedBy->name ?? '-';
|
||||
})
|
||||
->addColumn('status', function($row) {
|
||||
$statusColor = $row->status_color;
|
||||
$statusLabel = $row->status_label;
|
||||
return "<span class=\"kt-badge kt-badge--{$statusColor} kt-badge--dot\"></span> <span class=\"kt-font-bold kt-font-{$statusColor}\">{$statusLabel}</span>";
|
||||
})
|
||||
->addColumn('total_items', function($row) {
|
||||
return number_format($row->total_items, 0);
|
||||
})
|
||||
->addColumn('created_at', function($row) {
|
||||
return $row->created_at->format('d/m/Y H:i');
|
||||
})
|
||||
->addColumn('action', function($row) {
|
||||
return view('warehouse_management.mutations._action', compact('row'))->render();
|
||||
})
|
||||
->rawColumns(['status', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('warehouse_management.mutations.index', compact('menu'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$menu = Menu::where('link','mutations.create')->first();
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::with('stocks')->get();
|
||||
|
||||
return view('warehouse_management.mutations.create', compact('menu', 'dealers', 'products'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'from_dealer_id' => 'required|exists:dealers,id',
|
||||
'to_dealer_id' => 'required|exists:dealers,id|different:from_dealer_id',
|
||||
'notes' => 'nullable|string',
|
||||
'products' => 'required|array|min:1',
|
||||
'products.*.product_id' => 'required|exists:products,id',
|
||||
'products.*.quantity_requested' => 'required|numeric|min:0.01'
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Buat mutation record dengan status SENT (langsung terkirim ke dealer tujuan)
|
||||
$mutation = Mutation::create([
|
||||
'from_dealer_id' => $request->from_dealer_id,
|
||||
'to_dealer_id' => $request->to_dealer_id,
|
||||
'status' => 'sent',
|
||||
'requested_by' => auth()->id(),
|
||||
'notes' => $request->notes
|
||||
]);
|
||||
|
||||
// Buat mutation details
|
||||
foreach ($request->products as $productData) {
|
||||
MutationDetail::create([
|
||||
'mutation_id' => $mutation->id,
|
||||
'product_id' => $productData['product_id'],
|
||||
'quantity_requested' => $productData['quantity_requested'],
|
||||
'notes' => $productData['notes'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil dibuat dan terkirim ke dealer tujuan');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
return back()->withErrors(['error' => 'Gagal membuat mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(Mutation $mutation)
|
||||
{
|
||||
$mutation->load(['fromDealer', 'toDealer', 'requestedBy', 'approvedBy', 'receivedBy', 'mutationDetails.product']);
|
||||
|
||||
return view('warehouse_management.mutations.show', compact('mutation'));
|
||||
}
|
||||
|
||||
public function receive(Mutation $mutation)
|
||||
{
|
||||
if (!$mutation->canBeReceived()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat diterima dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->receive(auth()->id());
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil diterima dan menunggu persetujuan pengirim');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal menerima mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function approve(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'notes' => 'nullable|string',
|
||||
'details' => 'required|array',
|
||||
'details.*.quantity_approved' => 'required|numeric|min:0'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeApproved()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat disetujui dalam status saat ini']);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Update mutation details dengan quantity approved
|
||||
foreach ($request->details as $detailId => $detailData) {
|
||||
$mutationDetail = MutationDetail::findOrFail($detailId);
|
||||
$mutationDetail->update([
|
||||
'quantity_approved' => $detailData['quantity_approved']
|
||||
]);
|
||||
}
|
||||
|
||||
// Approve mutation
|
||||
$mutation->approve(auth()->id(), $request->notes);
|
||||
|
||||
DB::commit();
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil disetujui');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
return back()->withErrors(['error' => 'Gagal menyetujui mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function reject(Request $request, Mutation $mutation)
|
||||
{
|
||||
$request->validate([
|
||||
'rejection_reason' => 'required|string'
|
||||
]);
|
||||
|
||||
if (!$mutation->canBeApproved()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat ditolak dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->reject(auth()->id(), $request->rejection_reason);
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil ditolak');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal menolak mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function complete(Mutation $mutation)
|
||||
{
|
||||
if (!$mutation->canBeCompleted()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat diselesaikan dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->complete();
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil diselesaikan dan stock telah dipindahkan');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal menyelesaikan mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function cancel(Mutation $mutation)
|
||||
{
|
||||
if (!$mutation->canBeCancelled()) {
|
||||
return back()->withErrors(['error' => 'Mutasi tidak dapat dibatalkan dalam status saat ini']);
|
||||
}
|
||||
|
||||
try {
|
||||
$mutation->update(['status' => MutationStatus::CANCELLED]);
|
||||
|
||||
return redirect()->route('mutations.index')
|
||||
->with('success', 'Mutasi berhasil dibatalkan');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['error' => 'Gagal membatalkan mutasi: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// API untuk mendapatkan detail mutasi untuk approval
|
||||
public function getDetails(Mutation $mutation)
|
||||
{
|
||||
$mutation->load(['mutationDetails.product', 'fromDealer']);
|
||||
|
||||
$details = $mutation->mutationDetails->map(function($detail) use ($mutation) {
|
||||
$availableStock = $detail->product->getStockByDealer($mutation->from_dealer_id);
|
||||
|
||||
return [
|
||||
'id' => $detail->id,
|
||||
'product' => [
|
||||
'id' => $detail->product->id,
|
||||
'name' => $detail->product->name
|
||||
],
|
||||
'quantity_requested' => $detail->quantity_requested,
|
||||
'quantity_approved' => $detail->quantity_approved,
|
||||
'available_stock' => $availableStock
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'mutation' => [
|
||||
'id' => $mutation->id,
|
||||
'mutation_number' => $mutation->mutation_number,
|
||||
'from_dealer' => $mutation->fromDealer->name,
|
||||
'to_dealer' => $mutation->toDealer->name
|
||||
],
|
||||
'details' => $details
|
||||
]);
|
||||
}
|
||||
|
||||
// API untuk mendapatkan stock produk di dealer tertentu
|
||||
public function getProductStock(Request $request)
|
||||
{
|
||||
$dealerId = $request->dealer_id;
|
||||
$productId = $request->product_id;
|
||||
|
||||
$product = Product::findOrFail($productId);
|
||||
$stock = $product->getStockByDealer($dealerId);
|
||||
|
||||
return response()->json([
|
||||
'product_name' => $product->name,
|
||||
'current_stock' => $stock
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,11 @@ class ProductsController extends Controller
|
||||
|
||||
if (Gate::allows('update', $menu)) {
|
||||
$btn .= '<a href="' . route('products.edit', $row->id) . '" class="btn btn-warning btn-sm" style="margin-right: 8px;">Edit</a>';
|
||||
$btn .= '<button class="btn btn-sm btn-toggle-active '
|
||||
. ($row->active ? 'btn-danger' : 'btn-success') . '"
|
||||
data-url="' . route('products.toggleActive', $row->id) . '" data-active="'.$row->active.'" style="margin-right: 8px;">'
|
||||
. ($row->active ? 'Nonaktifkan' : 'Aktifkan') . '</button>';
|
||||
}
|
||||
|
||||
$btn .= '<button class="btn btn-sm btn-toggle-active '
|
||||
. ($row->active ? 'btn-danger' : 'btn-success') . '"
|
||||
data-url="' . route('products.toggleActive', $row->id) . '" data-active="'.$row->active.'" style="margin-right: 8px;">'
|
||||
. ($row->active ? 'Nonaktifkan' : 'Aktifkan') . '</button>';
|
||||
|
||||
$btn .= '<button class="btn btn-sm btn-secondary btn-product-stock-dealers"
|
||||
data-id="'.$row->id.'"
|
||||
data-url="'.route('products.dealers_stock').'"
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Product;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StocksController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$query = Stock::with(['dealer', 'product'])
|
||||
->when($request->dealer_id, function($q) use ($request) {
|
||||
return $q->where('dealer_id', $request->dealer_id);
|
||||
})
|
||||
->when($request->product_id, function($q) use ($request) {
|
||||
return $q->where('product_id', $request->product_id);
|
||||
});
|
||||
|
||||
return datatables()->of($query)
|
||||
->addColumn('dealer_name', function($stock) {
|
||||
return $stock->dealer->name;
|
||||
})
|
||||
->addColumn('product_name', function($stock) {
|
||||
return $stock->product->name;
|
||||
})
|
||||
->addColumn('action', function($stock) {
|
||||
return view('warehouse_management.stocks._action', compact('stock'));
|
||||
})
|
||||
->toJson();
|
||||
}
|
||||
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::where('active', true)->get();
|
||||
|
||||
return view('warehouse_management.stocks.index', compact('dealers', 'products'));
|
||||
}
|
||||
|
||||
public function adjust(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'stock_id' => 'required|exists:stocks,id',
|
||||
'type' => 'required|in:add,reduce',
|
||||
'quantity' => 'required|numeric|min:0.01',
|
||||
'note' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$stock = Stock::findOrFail($request->stock_id);
|
||||
$oldQuantity = $stock->quantity;
|
||||
|
||||
// Calculate new quantity
|
||||
$change = $request->type === 'add' ? $request->quantity : -$request->quantity;
|
||||
$newQuantity = $oldQuantity + $change;
|
||||
|
||||
// Update stock
|
||||
$stock->update(['quantity' => $newQuantity]);
|
||||
|
||||
// Log the change
|
||||
StockLog::create([
|
||||
'stock_id' => $stock->id,
|
||||
'user_id' => Auth::id(),
|
||||
'old_quantity' => $oldQuantity,
|
||||
'new_quantity' => $newQuantity,
|
||||
'change' => $change,
|
||||
'note' => $request->note,
|
||||
'reference_type' => 'manual_adjustment'
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Stok berhasil diadjust'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal mengadjust stok: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function history(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'stock_id' => 'required|exists:stocks,id'
|
||||
]);
|
||||
|
||||
$logs = StockLog::with('user')
|
||||
->where('stock_id', $request->stock_id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->map(function($log) {
|
||||
return [
|
||||
'date' => $log->created_at->format('d/m/Y H:i'),
|
||||
'user' => $log->user->name,
|
||||
'change' => $log->change > 0 ? '+' . $log->change : $log->change,
|
||||
'old_quantity' => $log->old_quantity,
|
||||
'new_quantity' => $log->new_quantity,
|
||||
'note' => $log->note
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json(['logs' => $logs]);
|
||||
}
|
||||
}
|
||||
@@ -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