partial update stock opname feature

This commit is contained in:
root
2025-06-11 18:29:32 +07:00
parent 9b25a772a6
commit 647aa51187
4 changed files with 926 additions and 75 deletions

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Dealer;
use App\Models\Product;
use App\Models\Stock;
use App\Models\Transaction;
use App\Models\User;
use App\Models\Work;
@@ -26,7 +28,15 @@ class TransactionController extends Controller
->select('d.name as dealer_name', 'd.id as dealer_id', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
->where('users.id', Auth::user()->id)->first();
$now = Carbon::now()->translatedFormat('d F Y');
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic'));
// Get products with stock based on user role
$products = Product::with(['stocks' => function($query) {
$query->where('dealer_id', Auth::user()->dealer_id);
}, 'stocks.dealer'])
->where('active', true)
->get();
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products'));
}
public function workcategory($category_id)

View File

@@ -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);
}
}
}