partial update stock opname feature
This commit is contained in:
@@ -9,10 +9,12 @@ use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Stock;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
class OpnamesController extends Controller
|
||||
@@ -89,34 +91,117 @@ class OpnamesController extends Controller
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// 1. Validasi input
|
||||
$validated = $request->validate([
|
||||
'dealer' => 'required|exists:dealers,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'
|
||||
]);
|
||||
// Check if this is from transaction form or regular opname form
|
||||
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
||||
|
||||
if ($isTransactionForm) {
|
||||
// Custom validation for transaction form
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'opname_date' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'date_format:Y-m-d',
|
||||
'before_or_equal:today'
|
||||
],
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'product_id' => 'required|array|min:1',
|
||||
'product_id.*' => 'required|exists:products,id',
|
||||
'system_stock' => 'required|array',
|
||||
'system_stock.*' => 'required|numeric|min:0',
|
||||
'physical_stock' => 'required|array',
|
||||
'physical_stock.*' => 'required|numeric|min:0'
|
||||
]);
|
||||
|
||||
// Process transaction form data with proper date parsing
|
||||
$dealerId = $request->dealer_id;
|
||||
$userId = $request->user_id;
|
||||
|
||||
// Parse opname date (YYYY-MM-DD format) or use today if empty
|
||||
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
||||
Log::info('Parsing opname date', ['input' => $request->opname_date, 'using' => $inputDate]);
|
||||
$opnameDate = Carbon::createFromFormat('Y-m-d', $inputDate);
|
||||
Log::info('Successfully parsed opname date', ['parsed' => $opnameDate->format('Y-m-d')]);
|
||||
|
||||
$note = $request->description;
|
||||
$productIds = $request->product_id;
|
||||
$systemStocks = $request->system_stock;
|
||||
$physicalStocks = $request->physical_stock;
|
||||
|
||||
// Log input data untuk debugging
|
||||
Log::info('Transaction form input data', [
|
||||
'product_ids' => $productIds,
|
||||
'system_stocks' => $systemStocks,
|
||||
'physical_stocks' => $physicalStocks,
|
||||
'dealer_id' => $dealerId,
|
||||
'user_id' => $userId
|
||||
]);
|
||||
|
||||
} else {
|
||||
// Original validation for regular opname form
|
||||
$request->validate([
|
||||
'dealer' => 'required|exists:dealers,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',
|
||||
'item_notes' => 'nullable|array',
|
||||
'item_notes.*' => 'nullable|string|max:255',
|
||||
'opname_date' => 'nullable|date' // Add opname_date validation for regular form
|
||||
]);
|
||||
|
||||
// Process regular form data
|
||||
$dealerId = $request->dealer;
|
||||
$userId = auth()->id();
|
||||
|
||||
// Use provided date or current date
|
||||
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
||||
$opnameDate = $request->opname_date ?
|
||||
Carbon::createFromFormat('Y-m-d', $inputDate) :
|
||||
now();
|
||||
|
||||
$note = $request->note;
|
||||
$productIds = $request->product;
|
||||
$systemStocks = $request->system_quantity;
|
||||
$physicalStocks = $request->physical_quantity;
|
||||
}
|
||||
|
||||
// 2. Validasi duplikasi produk
|
||||
$productCounts = array_count_values(array_filter($request->product));
|
||||
// 2. Validasi minimal ada produk yang diisi (termasuk nilai 0)
|
||||
$validProductIds = array_filter($productIds);
|
||||
$validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; });
|
||||
$validPhysicalStocks = array_filter($physicalStocks, function($value) {
|
||||
return $value !== null && $value !== '' && is_numeric($value);
|
||||
});
|
||||
|
||||
if (empty($validProductIds) || count($validProductIds) === 0) {
|
||||
throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.');
|
||||
}
|
||||
|
||||
if (count($validPhysicalStocks) === 0) {
|
||||
throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).');
|
||||
}
|
||||
|
||||
// 3. Validasi duplikasi produk
|
||||
$productCounts = array_count_values($validProductIds);
|
||||
foreach ($productCounts as $productId => $count) {
|
||||
if ($count > 1) {
|
||||
throw new \Exception('Product tidak boleh duplikat.');
|
||||
throw new \Exception('Produk tidak boleh duplikat dalam satu opname.');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Validasi dealer
|
||||
$dealer = Dealer::findOrFail($request->dealer);
|
||||
// 4. Validasi dealer
|
||||
$dealer = Dealer::findOrFail($dealerId);
|
||||
|
||||
// 4. Validasi produk aktif
|
||||
$productIds = array_filter($request->product);
|
||||
$inactiveProducts = Product::whereIn('id', $productIds)
|
||||
// 5. Validasi user exists
|
||||
$user = User::findOrFail($userId);
|
||||
|
||||
// 6. Validasi produk aktif
|
||||
$filteredProductIds = array_filter($productIds);
|
||||
$inactiveProducts = Product::whereIn('id', $filteredProductIds)
|
||||
->where('active', false)
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
@@ -125,48 +210,72 @@ class OpnamesController extends Controller
|
||||
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
||||
}
|
||||
|
||||
// 5. Validasi stock dan note
|
||||
// 7. Validasi stock difference (for transaction form, we'll allow any difference without note requirement)
|
||||
$stockDifferences = [];
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
if (!$isTransactionForm) {
|
||||
// Only validate notes for regular opname form
|
||||
foreach ($productIds 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}");
|
||||
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||
$physicalStock = floatval($physicalStocks[$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;
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 8. Create Opname master record with approved status
|
||||
$opname = Opname::create([
|
||||
'dealer_id' => $request->dealer,
|
||||
'opname_date' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
'note' => $request->note,
|
||||
'dealer_id' => $dealerId,
|
||||
'opname_date' => $opnameDate,
|
||||
'user_id' => $userId,
|
||||
'note' => $note,
|
||||
'status' => 'approved', // Set status langsung approved
|
||||
'approved_by' => auth()->id(), // Set current user sebagai approver
|
||||
'approved_by' => $userId, // Set current user sebagai approver
|
||||
'approved_at' => now() // Set waktu approval
|
||||
]);
|
||||
|
||||
// 7. Create OpnameDetails and update stock
|
||||
// 9. Create OpnameDetails and update stock - only for valid entries
|
||||
$details = [];
|
||||
foreach ($request->product as $index => $productId) {
|
||||
$processedCount = 0;
|
||||
|
||||
foreach ($productIds as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
// Skip only if physical stock is truly not provided (empty string or null)
|
||||
// Accept 0 as valid input
|
||||
if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate that physical stock is numeric (including 0)
|
||||
if (!is_numeric($physicalStocks[$index])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
||||
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||
$physicalStock = floatval($physicalStocks[$index]);
|
||||
$difference = $physicalStock - $systemStock;
|
||||
|
||||
$processedCount++;
|
||||
|
||||
// Get item note (only for regular opname form)
|
||||
$itemNote = null;
|
||||
if (!$isTransactionForm) {
|
||||
$itemNote = $request->input("item_notes.{$index}");
|
||||
}
|
||||
|
||||
// Create opname detail
|
||||
$details[] = [
|
||||
@@ -175,7 +284,7 @@ class OpnamesController extends Controller
|
||||
'system_stock' => $systemStock,
|
||||
'physical_stock' => $physicalStock,
|
||||
'difference' => $difference,
|
||||
'note' => $request->input("item_notes.{$index}"),
|
||||
'note' => $itemNote,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
];
|
||||
@@ -184,7 +293,7 @@ class OpnamesController extends Controller
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'dealer_id' => $request->dealer
|
||||
'dealer_id' => $dealerId
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
@@ -197,35 +306,84 @@ class OpnamesController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
// Validate we have at least one detail to insert
|
||||
if (empty($details)) {
|
||||
throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.');
|
||||
}
|
||||
|
||||
// Bulk insert untuk performa lebih baik
|
||||
OpnameDetail::insert($details);
|
||||
|
||||
// 8. Log aktivitas
|
||||
// 10. Log aktivitas dengan detail produk yang diproses
|
||||
$processedProducts = collect($details)->map(function($detail) {
|
||||
return [
|
||||
'product_id' => $detail['product_id'],
|
||||
'system_stock' => $detail['system_stock'],
|
||||
'physical_stock' => $detail['physical_stock'],
|
||||
'difference' => $detail['difference']
|
||||
];
|
||||
});
|
||||
|
||||
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)
|
||||
'user_id' => $userId,
|
||||
'approver_id' => $userId,
|
||||
'product_count' => count($details),
|
||||
'processed_count' => $processedCount,
|
||||
'form_type' => $isTransactionForm ? 'transaction' : 'regular',
|
||||
'opname_date' => $opnameDate->format('Y-m-d'),
|
||||
'processed_products' => $processedProducts->toArray()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->route('opnames.index')
|
||||
->with('success', 'Opname berhasil disimpan dan disetujui.');
|
||||
if ($isTransactionForm) {
|
||||
// Redirect back to transaction page with success message
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
||||
} else {
|
||||
// Redirect to opname index for regular form
|
||||
return redirect()
|
||||
->route('opnames.index')
|
||||
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
||||
}
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
DB::rollBack();
|
||||
return back()->withErrors($e->validator)->withInput();
|
||||
Log::error('Validation error in OpnamesController@store', [
|
||||
'errors' => $e->errors(),
|
||||
'input' => $request->all()
|
||||
]);
|
||||
|
||||
if ($isTransactionForm) {
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->withErrors($e->validator)
|
||||
->withInput()
|
||||
->with('error', 'Terjadi kesalahan validasi. Periksa kembali data yang dimasukkan.');
|
||||
} else {
|
||||
return back()->withErrors($e->validator)->withInput();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
Log::error('Request data:', $request->all());
|
||||
|
||||
return back()
|
||||
->with('error', $e->getMessage())
|
||||
->withInput();
|
||||
$errorMessage = $e->getMessage();
|
||||
|
||||
if ($isTransactionForm) {
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->with('error', $errorMessage)
|
||||
->withInput();
|
||||
} else {
|
||||
return back()
|
||||
->with('error', $errorMessage)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,4 +447,6 @@ class OpnamesController extends Controller
|
||||
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user