create stock and stock logs
This commit is contained in:
93
app/Console/Commands/ClearOpnameData.php
Normal file
93
app/Console/Commands/ClearOpnameData.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
|
||||
class ClearOpnameData extends Command
|
||||
{
|
||||
protected $signature = 'opname:clear {--force : Force clear without confirmation}';
|
||||
protected $description = 'Clear all opname-related data including opnames, details, stocks, logs, and reset all IDs to 1';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->option('force')) {
|
||||
if (!$this->confirm('This will delete ALL opname data, stocks, stock logs, and reset ALL IDs to 1. This is irreversible! Are you sure?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Starting complete data cleanup...');
|
||||
|
||||
try {
|
||||
// Disable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
|
||||
// 1. Clear and reset stock logs
|
||||
if (Schema::hasTable('stock_logs')) {
|
||||
DB::table('stock_logs')->truncate();
|
||||
DB::statement('ALTER TABLE stock_logs AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset stock_logs table');
|
||||
}
|
||||
|
||||
// 2. Clear and reset stocks
|
||||
if (Schema::hasTable('stocks')) {
|
||||
DB::table('stocks')->truncate();
|
||||
DB::statement('ALTER TABLE stocks AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset stocks table');
|
||||
}
|
||||
|
||||
// 3. Clear and reset opname details
|
||||
if (Schema::hasTable('opname_details')) {
|
||||
DB::table('opname_details')->truncate();
|
||||
DB::statement('ALTER TABLE opname_details AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset opname_details table');
|
||||
}
|
||||
|
||||
// 4. Clear and reset opnames
|
||||
if (Schema::hasTable('opnames')) {
|
||||
DB::table('opnames')->truncate();
|
||||
DB::statement('ALTER TABLE opnames AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset opnames table');
|
||||
}
|
||||
|
||||
// Re-enable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$this->info('Successfully cleared all data and reset IDs to 1!');
|
||||
$this->info('Cleared tables:');
|
||||
$this->info('- stock_logs');
|
||||
$this->info('- stocks');
|
||||
$this->info('- opname_details');
|
||||
$this->info('- opnames');
|
||||
|
||||
Log::info('Complete data cleared and IDs reset by command', [
|
||||
'user' => auth()->user() ? auth()->user()->id : 'system',
|
||||
'timestamp' => now(),
|
||||
'tables_cleared' => ['stock_logs', 'stocks', 'opname_details', 'opnames']
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Re-enable foreign key checks if they were disabled
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$this->error('Error clearing data: ' . $e->getMessage());
|
||||
Log::error('Error in ClearOpnameData command: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return 1; // Return error code
|
||||
}
|
||||
|
||||
return 0; // Return success code
|
||||
}
|
||||
}
|
||||
@@ -28,5 +28,9 @@ class Kernel extends ConsoleKernel
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
|
||||
$this->commands = [
|
||||
Commands\ClearOpnameData::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
21
app/Enums/StockChangeType.php
Normal file
21
app/Enums/StockChangeType.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StockChangeType: string
|
||||
{
|
||||
case INCREASE = 'increase';
|
||||
case DECREASE = 'decrease';
|
||||
case ADJUSTMENT = 'adjustment'; // Untuk kasus dimana quantity sama tapi perlu dicatat
|
||||
case NO_CHANGE = 'no_change'; // Untuk kasus dimana quantity sama dan tidak perlu dicatat
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::INCREASE => 'Penambahan',
|
||||
self::DECREASE => 'Pengurangan',
|
||||
self::ADJUSTMENT => 'Penyesuaian',
|
||||
self::NO_CHANGE => 'Tidak Ada Perubahan'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ use App\Models\Menu;
|
||||
use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Stock;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
@@ -19,7 +20,7 @@ class OpnamesController extends Controller
|
||||
public function index(Request $request){
|
||||
$menu = Menu::where('link','opnames.index')->first();
|
||||
if($request->ajax()){
|
||||
$data = Opname::with('user','dealer');
|
||||
$data = Opname::with('user','dealer')->get();
|
||||
return DataTables::of($data)
|
||||
->addColumn('user_name', function ($row){
|
||||
return $row->user ? $row->user->name : '-';
|
||||
@@ -49,52 +50,167 @@ class OpnamesController extends Controller
|
||||
public function create(){
|
||||
try{
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::all();
|
||||
return view('warehouse_management.opnames.create', compact('dealers','products'));
|
||||
}catch(\Exception $ex){
|
||||
$products = Product::where('active', true)->get();
|
||||
|
||||
// Get initial stock data for the first dealer (if any)
|
||||
$initialDealerId = $dealers->first()?->id;
|
||||
$stocks = [];
|
||||
if ($initialDealerId) {
|
||||
$stocks = Stock::where('dealer_id', $initialDealerId)
|
||||
->whereIn('product_id', $products->pluck('id'))
|
||||
->get()
|
||||
->keyBy('product_id');
|
||||
}
|
||||
|
||||
return view('warehouse_management.opnames.create', compact('dealers', 'products', 'stocks'));
|
||||
} catch(\Exception $ex) {
|
||||
Log::error($ex->getMessage());
|
||||
return back()->with('error', 'Terjadi kesalahan saat memuat data');
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
$request->validate([
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// 1. Validasi input
|
||||
$validated = $request->validate([
|
||||
'dealer' => 'required|exists:dealers,id',
|
||||
'product' => 'required|array',
|
||||
'product.*' => 'nullable|exists:products,id',
|
||||
'product' => 'required|array|min:1',
|
||||
'product.*' => 'required|exists:products,id',
|
||||
'system_quantity' => 'required|array',
|
||||
'system_quantity.*' => 'required|numeric|min:0',
|
||||
'physical_quantity' => 'required|array',
|
||||
'physical_quantity.*' => 'required|numeric|min:0',
|
||||
'note' => 'nullable|string|max:1000', // note utama
|
||||
'item_notes' => 'nullable|array', // notes per item
|
||||
'item_notes.*' => 'required_if:physical_quantity.*,!=,system_quantity.*|nullable|string|max:255'
|
||||
]);
|
||||
|
||||
// 1. Create Opname master record
|
||||
$opname = Opname::create([
|
||||
'dealer_id' => $request->dealer,
|
||||
'opname_date' => now(), // or $request->opname_date if you provide it
|
||||
'user_id' => auth()->id(), // assuming the user is logged in
|
||||
'note' => null, // or $request->note if needed
|
||||
]);
|
||||
|
||||
// 2. Loop over products to create OpnameDetails
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue; // Skip empty rows
|
||||
|
||||
$system = $request->system_quantity[$index] ?? 0;
|
||||
$physical = $request->physical_quantity[$index] ?? 0;
|
||||
|
||||
OpnameDetail::create([
|
||||
'opname_id' => $opname->id,
|
||||
'product_id' => $productId,
|
||||
'system_stock' => $system,
|
||||
'physical_stock' => $physical,
|
||||
'difference' => $physical - $system,
|
||||
'note' => null, // or include from input
|
||||
]);
|
||||
|
||||
// 2. Validasi duplikasi produk
|
||||
$productCounts = array_count_values(array_filter($request->product));
|
||||
foreach ($productCounts as $productId => $count) {
|
||||
if ($count > 1) {
|
||||
throw new \Exception('Product tidak boleh duplikat.');
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('opnames.index')
|
||||
->with('success', 'Opname berhasil disimpan.');
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
|
||||
// 3. Validasi dealer
|
||||
$dealer = Dealer::findOrFail($request->dealer);
|
||||
|
||||
// 4. Validasi produk aktif
|
||||
$productIds = array_filter($request->product);
|
||||
$inactiveProducts = Product::whereIn('id', $productIds)
|
||||
->where('active', false)
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
||||
if (!empty($inactiveProducts)) {
|
||||
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
||||
}
|
||||
|
||||
// 5. Validasi stock dan note
|
||||
$stockDifferences = [];
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
||||
$itemNote = $request->input("item_notes.{$index}");
|
||||
|
||||
// Jika ada perbedaan stock dan note kosong
|
||||
if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) {
|
||||
$product = Product::find($productId);
|
||||
$stockDifferences[] = $product->name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($stockDifferences)) {
|
||||
throw new \Exception(
|
||||
'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' .
|
||||
implode(', ', $stockDifferences)
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Create Opname master record with approved status
|
||||
$opname = Opname::create([
|
||||
'dealer_id' => $request->dealer,
|
||||
'opname_date' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
'note' => $request->note,
|
||||
'status' => 'approved', // Set status langsung approved
|
||||
'approved_by' => auth()->id(), // Set current user sebagai approver
|
||||
'approved_at' => now() // Set waktu approval
|
||||
]);
|
||||
|
||||
// 7. Create OpnameDetails and update stock
|
||||
$details = [];
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
||||
$difference = $physicalStock - $systemStock;
|
||||
|
||||
// Create opname detail
|
||||
$details[] = [
|
||||
'opname_id' => $opname->id,
|
||||
'product_id' => $productId,
|
||||
'system_stock' => $systemStock,
|
||||
'physical_stock' => $physicalStock,
|
||||
'difference' => $difference,
|
||||
'note' => $request->input("item_notes.{$index}"),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
];
|
||||
|
||||
// Update stock langsung karena auto approve
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'dealer_id' => $request->dealer
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
|
||||
// Update stock dengan physical stock
|
||||
$stock->updateStock(
|
||||
$physicalStock,
|
||||
$opname,
|
||||
"Stock adjustment from auto-approved opname #{$opname->id}"
|
||||
);
|
||||
}
|
||||
|
||||
// Bulk insert untuk performa lebih baik
|
||||
OpnameDetail::insert($details);
|
||||
|
||||
// 8. Log aktivitas
|
||||
Log::info('Opname created and auto-approved', [
|
||||
'opname_id' => $opname->id,
|
||||
'dealer_id' => $opname->dealer_id,
|
||||
'user_id' => auth()->id(),
|
||||
'approver_id' => auth()->id(),
|
||||
'product_count' => count($details)
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->route('opnames.index')
|
||||
->with('success', 'Opname berhasil disimpan dan disetujui.');
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
DB::rollBack();
|
||||
return back()->withErrors($e->validator)->withInput();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
|
||||
return back()
|
||||
->with('error', $e->getMessage())
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +223,7 @@ class OpnamesController extends Controller
|
||||
return DataTables::of($opname->details)
|
||||
->addIndexColumn()
|
||||
->addColumn('opname_date', function () use ($opname) {
|
||||
return $opname->opname_date->format('d M Y');
|
||||
return Carbon::parse($opname->opname_date)->format('d M Y');
|
||||
})
|
||||
->addColumn('user_name', function () use ($opname) {
|
||||
return $opname->user ? $opname->user->name : '-';
|
||||
@@ -133,5 +249,29 @@ class OpnamesController extends Controller
|
||||
abort(500, 'Something went wrong');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add new method to get stock data via AJAX
|
||||
public function getStockData(Request $request)
|
||||
{
|
||||
try {
|
||||
$dealerId = $request->dealer_id;
|
||||
$productIds = $request->product_ids;
|
||||
|
||||
if (!$dealerId || !$productIds) {
|
||||
return response()->json(['error' => 'Dealer ID dan Product IDs diperlukan'], 400);
|
||||
}
|
||||
|
||||
$stocks = Stock::where('dealer_id', $dealerId)
|
||||
->whereIn('product_id', $productIds)
|
||||
->get()
|
||||
->mapWithKeys(function ($stock) {
|
||||
return [$stock->product_id => $stock->quantity];
|
||||
});
|
||||
|
||||
return response()->json(['stocks' => $stocks]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error getting stock data: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -25,19 +26,19 @@ class ProductsController extends Controller
|
||||
{
|
||||
$menu = Menu::where('link','products.index')->first();
|
||||
if($request->ajax()){
|
||||
$data = Product::with(['category','opnameDetails']);
|
||||
$data = Product::with(['category', 'stocks']);
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('category_name', function ($row) {
|
||||
return $row->category ? $row->category->name : '-';
|
||||
})
|
||||
->addColumn('total_stock', function ($row){
|
||||
return $row->opnameDetails->sum('system_stock');
|
||||
return number_format($row->current_total_stock, 2);
|
||||
})
|
||||
->addColumn('action', function ($row) use ($menu) {
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
if (Auth::user()->can('update', $menu)) {
|
||||
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>';
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ class ProductsController extends Controller
|
||||
|
||||
public function all_products(){
|
||||
try{
|
||||
$products = Product::select('id','name')->get();
|
||||
$products = Product::where('is_active', true)->select('id','name')->get();
|
||||
return response()->json($products);
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
@@ -206,17 +207,12 @@ class ProductsController extends Controller
|
||||
public function dealers_stock(Request $request){
|
||||
$productId = $request->get('product_id');
|
||||
|
||||
$product = Product::with(['opnameDetails.opname.dealer'])->findOrFail($productId);
|
||||
$product = Product::with(['stocks.dealer'])->findOrFail($productId);
|
||||
|
||||
$opnameDetails = $product->opnameDetails;
|
||||
|
||||
$data = $opnameDetails->map(function ($detail) {
|
||||
$data = $product->stocks->map(function ($stock) {
|
||||
return [
|
||||
'dealer_name' => $detail->opname->dealer->name ?? '-',
|
||||
'system_stock' => $detail->system_stock,
|
||||
'physical_stock' => $detail->physical_stock,
|
||||
'difference' => $detail->physical_stock - $detail->system_stock,
|
||||
'opname_date' => optional($detail->opname)->created_at->format('d M Y')
|
||||
'dealer_name' => $stock->dealer->name ?? '-',
|
||||
'quantity' => $stock->quantity
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
117
app/Http/Controllers/WarehouseManagement/StocksController.php
Normal file
117
app/Http/Controllers/WarehouseManagement/StocksController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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]);
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,108 @@ class Opname extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = ['dealer_id','opname_date','user_id','note'];
|
||||
protected $fillable = [
|
||||
'dealer_id',
|
||||
'opname_date',
|
||||
'user_id',
|
||||
'note',
|
||||
'status',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'rejection_note'
|
||||
];
|
||||
|
||||
public function dealer(){
|
||||
protected $casts = [
|
||||
'approved_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::updated(function ($opname) {
|
||||
// Jika status berubah menjadi approved
|
||||
if ($opname->isDirty('status') && $opname->status === 'approved') {
|
||||
// Update stock untuk setiap detail opname
|
||||
foreach ($opname->details as $detail) {
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $detail->product_id,
|
||||
'dealer_id' => $opname->dealer_id
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
|
||||
// Update stock dengan physical_stock dari opname
|
||||
$stock->updateStock(
|
||||
$detail->physical_stock,
|
||||
$opname,
|
||||
"Stock adjustment from approved opname #{$opname->id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
public function details(){
|
||||
public function details()
|
||||
{
|
||||
return $this->hasMany(OpnameDetail::class);
|
||||
}
|
||||
|
||||
public function user(){
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function approver()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
// Method untuk approve opname
|
||||
public function approve(User $approver)
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
throw new \Exception('Only pending opnames can be approved');
|
||||
}
|
||||
|
||||
$this->status = 'approved';
|
||||
$this->approved_by = $approver->id;
|
||||
$this->approved_at = now();
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Method untuk reject opname
|
||||
public function reject(User $rejector, string $note)
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
throw new \Exception('Only pending opnames can be rejected');
|
||||
}
|
||||
|
||||
$this->status = 'rejected';
|
||||
$this->approved_by = $rejector->id;
|
||||
$this->approved_at = now();
|
||||
$this->rejection_note = $note;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Method untuk submit opname untuk approval
|
||||
public function submit()
|
||||
{
|
||||
if ($this->status !== 'draft') {
|
||||
throw new \Exception('Only draft opnames can be submitted');
|
||||
}
|
||||
|
||||
$this->status = 'pending';
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,14 @@ class Product extends Model
|
||||
public function opnameDetails(){
|
||||
return $this->hasMany(OpnameDetail::class);
|
||||
}
|
||||
|
||||
public function stocks(){
|
||||
return $this->hasMany(Stock::class);
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan total stock saat ini
|
||||
public function getCurrentTotalStockAttribute()
|
||||
{
|
||||
return $this->stocks()->sum('quantity');
|
||||
}
|
||||
}
|
||||
|
||||
56
app/Models/Stock.php
Normal file
56
app/Models/Stock.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Stock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'product_id',
|
||||
'dealer_id',
|
||||
'quantity'
|
||||
];
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
public function stockLogs()
|
||||
{
|
||||
return $this->hasMany(StockLog::class);
|
||||
}
|
||||
|
||||
// Method untuk mengupdate stock
|
||||
public function updateStock($newQuantity, $source, $description = null)
|
||||
{
|
||||
$previousQuantity = $this->quantity;
|
||||
$quantityChange = $newQuantity - $previousQuantity;
|
||||
|
||||
$this->quantity = $newQuantity;
|
||||
$this->save();
|
||||
|
||||
// Buat log perubahan
|
||||
StockLog::create([
|
||||
'stock_id' => $this->id,
|
||||
'source_type' => get_class($source),
|
||||
'source_id' => $source->id,
|
||||
'previous_quantity' => $previousQuantity,
|
||||
'new_quantity' => $newQuantity,
|
||||
'quantity_change' => $quantityChange,
|
||||
'description' => $description,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
70
app/Models/StockLog.php
Normal file
70
app/Models/StockLog.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\StockChangeType;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StockLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'stock_id',
|
||||
'source_type',
|
||||
'source_id',
|
||||
'previous_quantity',
|
||||
'new_quantity',
|
||||
'quantity_change',
|
||||
'change_type',
|
||||
'description',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'change_type' => StockChangeType::class,
|
||||
'previous_quantity' => 'decimal:2',
|
||||
'new_quantity' => 'decimal:2',
|
||||
'quantity_change' => 'decimal:2'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($stockLog) {
|
||||
// Hitung quantity_change
|
||||
$stockLog->quantity_change = $stockLog->new_quantity - $stockLog->previous_quantity;
|
||||
|
||||
// Tentukan change_type berdasarkan quantity_change
|
||||
if ($stockLog->quantity_change == 0) {
|
||||
// Jika quantity sama persis (tanpa toleransi)
|
||||
$stockLog->change_type = StockChangeType::NO_CHANGE;
|
||||
} else if ($stockLog->quantity_change > 0) {
|
||||
$stockLog->change_type = StockChangeType::INCREASE;
|
||||
} else {
|
||||
$stockLog->change_type = StockChangeType::DECREASE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function stock()
|
||||
{
|
||||
return $this->belongsTo(Stock::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function source()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan label change_type
|
||||
public function getChangeTypeLabelAttribute()
|
||||
{
|
||||
return $this->change_type->label();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user