branch ?? 'Cabang 1'; $query = Purchase::with('product')->orderBy('date', 'desc'); if ($request->filled('year')) { $query->whereYear('date', $request->year); } if ($request->filled('month')) { $query->whereMonth('date', $request->month); } if ($request->filled('day')) { $query->whereDay('date', $request->day); } if ($request->filled('search')) { $query->where('item', 'like', '%' . $request->search . '%'); } // Apply "today" filter by default only when the page is first loaded // (i.e. there are no query parameters). If the user submitted the filter // form with "Semua Tahun" (empty values), we treat that as an explicit // request and do not force today's data. if (count($request->query()) === 0) { $query->whereDate('date', \Carbon\Carbon::today()); } // Filter raw materials by branch for karyawan, show all for admin if ($user->role === 'admin') { $rawMaterials = \App\Models\RawMaterial::all(); $purchases = $query->get(); return view('laporan_pembelian', compact('purchases', 'branches', 'rawMaterials')); } else { $rawMaterials = \App\Models\RawMaterial::where('branch', $branch)->get(); $purchases = $query->where('branch', $user->branch)->get(); return view('laporan_pembelian', compact('purchases', 'branches', 'rawMaterials')); } } public function create() { $user = Auth::user(); if ($user->role === 'admin') { return redirect('/laporan/pembelian')->with('error', 'Fitur tambah pembelian hanya tersedia untuk karyawan.'); } $branches = \App\Models\Branch::all(); $branch = $user->branch ?? 'Cabang 1'; $rawMaterials = \App\Models\RawMaterial::where('branch', $branch)->get(); return view('transaksi.pembelian', compact('branches', 'rawMaterials')); } public function exportPdf(Request $request) { $user = Auth::user(); if ($user->role !== 'admin') { abort(403); } $query = Purchase::with('product')->orderBy('date', 'desc'); if ($request->filled('year')) { $query->whereYear('date', $request->year); } if ($request->filled('month')) { $query->whereMonth('date', $request->month); } if ($request->filled('day')) { $query->whereDay('date', $request->day); } if ($request->filled('search')) { $query->where('item', 'like', '%' . $request->search . '%'); } // Apply default today filter for export only when there are no query params if (count($request->query()) === 0) { $query->whereDate('date', \Carbon\Carbon::today()); } $purchases = $query->get(); $branches = \App\Models\Branch::all(); $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('reports.purchase_pdf', compact('purchases', 'branches')); return $pdf->download('laporan_pembelian.pdf'); } public function store(Request $request) { $rules = [ 'date' => 'required|date', 'item' => 'required|string|max:255', 'quantity' => 'required|string|max:255', 'total_price' => 'required|numeric|min:0', 'type' => 'nullable|in:product,raw_material', 'raw_material_id' => 'required_if:type,raw_material|not_in:0', ]; // Hanya require field bahan baku baru jika raw_material_id = 'new' if ($request->input('type') === 'raw_material' && $request->input('raw_material_id') === 'new') { $rules['new_raw_material_name'] = 'required|string|max:255'; $rules['new_raw_material_unit'] = 'required|string|max:50'; } $request->validate($rules); // Debug log \Illuminate\Support\Facades\Log::info('Purchase store - Request data:', [ 'type' => $request->type, 'raw_material_id' => $request->raw_material_id, 'new_raw_material_name' => $request->new_raw_material_name, 'new_raw_material_unit' => $request->new_raw_material_unit, 'item' => $request->item, 'quantity' => $request->quantity ]); try { \Illuminate\Support\Facades\DB::transaction(function () use ($request) { $branch = Auth::user()->branch ?? 'Cabang 1'; if (Auth::user()->role === 'admin' && $request->has('branch')) { $branch = $request->branch; } // Bersihkan format angka (hapus titik pemisah ribuan jika ada) $cleanPrice = str_replace('.', '', $request->total_price); // Handle raw material creation if new raw material is provided $rawMaterialId = $request->raw_material_id; if ($request->type === 'raw_material' && $request->raw_material_id === 'new') { if ($request->filled('new_raw_material_name') && $request->filled('new_raw_material_unit')) { // Create new raw material $newRm = \App\Models\RawMaterial::create([ 'name' => $request->new_raw_material_name, 'unit' => $request->new_raw_material_unit, 'stock_quantity' => 0, 'branch' => $branch ]); $rawMaterialId = $newRm->id; } else { throw new \Exception('Nama dan satuan bahan baku baru harus diisi.'); } } $purchase = Purchase::create([ 'date' => $request->date, 'branch' => $branch, 'item' => $request->item, 'quantity' => $request->quantity, 'total_price' => (int) $cleanPrice, 'product_id' => null, 'raw_material_id' => $request->type === 'raw_material' ? $rawMaterialId : null, 'type' => $request->type ?? 'product', ]); if ($purchase->type === 'raw_material' && $purchase->raw_material_id) { $rm = \App\Models\RawMaterial::find($purchase->raw_material_id); if ($rm) { // Only update stock if raw material belongs to user's branch if ($rm->branch !== $branch) { throw new \Exception('Bahan baku ini tidak terdaftar di cabang Anda.'); } // Parse quantity string, e.g. "10 Kilogram" → value=10, unit=kilogram $parsed = $this->parseQuantity($purchase->quantity); $qtyValue = $parsed['value']; $qtyUnit = $parsed['unit'] ?: strtolower($rm->unit); // Konversi ke satuan dasar bahan baku $qtyInBase = $this->convertToBaseUnit($qtyValue, $qtyUnit); $rm->stock_quantity += $qtyInBase; $rm->save(); $this->recalculateProductsFromRawMaterial($rm->id, $branch); } } // Create Journal Entry $kasAccount = Account::where('code', '101')->first(); $persediaanAccount = Account::where('code', '102')->first(); if ($kasAccount && $persediaanAccount) { $journal = JournalEntry::create([ 'date' => $purchase->date, 'description' => 'Pembelian ' . $purchase->item, 'reference_type' => 'Purchase', 'reference_id' => $purchase->id, 'branch' => $branch, ]); // Debit Persediaan, Kredit Kas JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $persediaanAccount->id, 'debit' => $purchase->total_price, 'credit' => 0]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $kasAccount->id, 'debit' => 0, 'credit' => $purchase->total_price]); } }); return back()->with('success', 'Transaksi pembelian berhasil ditambahkan!'); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Purchase store error: ' . $e->getMessage()); return back()->withInput()->withErrors(['db_error' => 'Gagal menyimpan transaksi: ' . $e->getMessage()]); } } /** * Konversi nilai ke satuan stok yang dipakai aplikasi. * Untuk bisnis sederhana, pembelian liter dan unit lain disimpan apa adanya * agar stok tetap sesuai dengan satuan yang dimasukkan. */ private function convertToBaseUnit($value, $unit) { $unit = strtolower(trim($unit)); $conversions = [ 'kilogram' => 1000, 'kg' => 1000, 'gram' => 1, 'gr' => 1, 'g' => 1, 'liter' => 1, 'l' => 1, 'mililiter'=> 1, 'ml' => 1, 'pcs' => 1, 'dus' => 1, 'pack' => 1, 'karung' => 1, 'ikat' => 1, 'botol' => 1, 'kaleng' => 1, 'lembar' => 1, 'sendok makan' => 1, 'sendok teh' => 1, ]; $multiplier = $conversions[$unit] ?? 1; return $value * $multiplier; } /** * Parse quantity string seperti "10 Kilogram" → returns ['value' => 10, 'unit' => 'kilogram'] */ private function parseQuantity($quantityStr) { $quantityStr = trim($quantityStr); // Coba pisahkan angka dan satuan if (preg_match('/^([\d.,]+)\s*(.*)$/', $quantityStr, $matches)) { $value = floatval(str_replace(',', '.', $matches[1])); $unit = strtolower(trim($matches[2])); return ['value' => $value, 'unit' => $unit]; } return ['value' => floatval($quantityStr), 'unit' => '']; } private function recalculateProductsFromRawMaterial($rawMaterialId, $branch) { // Ambil semua resep yang menggunakan bahan baku ini, eager load product & semua resep produk tsb $recipes = \App\Models\ProductRecipe::where('raw_material_id', $rawMaterialId) ->with(['product.recipes.rawMaterial']) ->get(); foreach ($recipes as $recipe) { $product = $recipe->product; if (!$product) continue; $minStock = null; foreach ($product->recipes as $prodRecipe) { $rm = $prodRecipe->rawMaterial; if (!$rm || $prodRecipe->quantity_needed <= 0) continue; // Stok bahan baku sudah dalam satuan dasar (disimpan apa adanya) $rmStock = $rm->stock_quantity; // quantity_needed di resep dalam satuan apa? Anggap sama dengan satuan bahan baku $needed = $prodRecipe->quantity_needed; $possibleAmount = floor($rmStock / $needed); if ($minStock === null || $possibleAmount < $minStock) { $minStock = $possibleAmount; } } if ($minStock !== null && $minStock >= 0) { // Update semua cabang (atau hanya cabang ini) $allBranches = \App\Models\Branch::all(); foreach ($allBranches as $b) { $stock = \App\Models\Stock::firstOrCreate( ['product_id' => $product->id, 'branch' => $b->name], ['quantity' => 0, 'unit' => 'Pcs'] ); $stock->quantity = $minStock; $stock->save(); } } } } public function update(Request $request, $id) { $rules = [ 'date' => 'required|date', 'item' => 'required|string|max:255', 'quantity' => 'required|string|max:255', 'total_price' => 'required|numeric|min:0', 'type' => 'nullable|in:product,raw_material', 'raw_material_id' => 'required_if:type,raw_material|not_in:0', ]; // Hanya require field bahan baku baru jika raw_material_id = 'new' if ($request->input('type') === 'raw_material' && $request->input('raw_material_id') === 'new') { $rules['new_raw_material_name'] = 'required|string|max:255'; $rules['new_raw_material_unit'] = 'required|string|max:50'; } $request->validate($rules); try { \Illuminate\Support\Facades\DB::transaction(function () use ($request, $id) { $purchase = Purchase::findOrFail($id); $oldType = $purchase->type; $oldRawMaterialId = $purchase->raw_material_id; $branch = $purchase->branch; // Jika pembelian lama adalah bahan baku, hapus stok lama dulu if ($oldType === 'raw_material' && $oldRawMaterialId) { $oldRm = \App\Models\RawMaterial::find($oldRawMaterialId); if ($oldRm) { $oldParsed = $this->parseQuantity($purchase->quantity); $oldQty = $this->convertToBaseUnit($oldParsed['value'], $oldParsed['unit'] ?: strtolower($oldRm->unit)); $oldRm->stock_quantity -= $oldQty; if ($oldRm->stock_quantity < 0) { $oldRm->stock_quantity = 0; } $oldRm->save(); } } $newRawMaterialId = null; if ($request->type === 'raw_material') { $newRawMaterialId = $request->raw_material_id; if ($newRawMaterialId === 'new') { $newRm = \App\Models\RawMaterial::create([ 'name' => $request->new_raw_material_name, 'unit' => $request->new_raw_material_unit, 'stock_quantity' => 0, 'branch' => Auth::user()->branch ?? 'Cabang 1', ]); $newRawMaterialId = $newRm->id; } $rm = \App\Models\RawMaterial::find($newRawMaterialId); if ($rm) { $parsed = $this->parseQuantity($request->quantity); $qty = $this->convertToBaseUnit($parsed['value'], $parsed['unit'] ?: strtolower($rm->unit)); $rm->stock_quantity += $qty; $rm->save(); } } // Perbarui data pembelian $cleanPrice = str_replace('.', '', $request->total_price); $data = $request->all(); $data['total_price'] = (int) $cleanPrice; $data['product_id'] = null; if (Auth::user()->role === 'admin' && $request->has('branch')) { $data['branch'] = $request->branch; } $purchase->update($data); // Recalculate product stock from raw materials if needed if ($oldType === 'raw_material' && $oldRawMaterialId) { $this->recalculateProductsFromRawMaterial($oldRawMaterialId, $branch); } if ($request->type === 'raw_material' && $newRawMaterialId) { $this->recalculateProductsFromRawMaterial($newRawMaterialId, $branch); } // Recreate Journal Entry JournalEntry::where('reference_type', 'Purchase')->where('reference_id', $purchase->id)->delete(); $kasAccount = Account::where('code', '101')->first(); $persediaanAccount = Account::where('code', '102')->first(); if ($kasAccount && $persediaanAccount) { $journal = JournalEntry::create([ 'date' => $purchase->date, 'description' => 'Pembelian ' . $purchase->item, 'reference_type' => 'Purchase', 'reference_id' => $purchase->id, 'branch' => $purchase->branch, ]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $persediaanAccount->id, 'debit' => $purchase->total_price, 'credit' => 0]); JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $kasAccount->id, 'debit' => 0, 'credit' => $purchase->total_price]); } }); return back()->with('success', 'Transaksi pembelian berhasil diperbarui!'); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Purchase update error: ' . $e->getMessage()); return back()->withInput()->withErrors(['db_error' => 'Gagal memperbarui transaksi: ' . $e->getMessage()]); } } public function destroy($id) { try { \Illuminate\Support\Facades\DB::transaction(function () use ($id) { $purchase = Purchase::findOrFail($id); // If this is a raw material purchase, update stock and potentially delete raw material if ($purchase->type === 'raw_material' && $purchase->raw_material_id) { $rm = \App\Models\RawMaterial::find($purchase->raw_material_id); if ($rm) { // Parse quantity to subtract from stock $parsed = $this->parseQuantity($purchase->quantity); $qtyValue = $parsed['value']; $qtyUnit = $parsed['unit'] ?: strtolower($rm->unit); $qtyInBase = $this->convertToBaseUnit($qtyValue, $qtyUnit); // Subtract from stock $rm->stock_quantity -= $qtyInBase; if ($rm->stock_quantity < 0) $rm->stock_quantity = 0; $rm->save(); // Recalculate products $this->recalculateProductsFromRawMaterial($rm->id, $purchase->branch); // Check if this is the only purchase for this raw material $otherPurchases = Purchase::where('raw_material_id', $rm->id) ->where('id', '!=', $purchase->id) ->count(); if ($otherPurchases === 0) { // No other purchases, delete the raw material $rm->delete(); } } } // Delete journal entries JournalEntry::where('reference_type', 'Purchase')->where('reference_id', $purchase->id)->delete(); // Delete purchase $purchase->delete(); }); return back()->with('success', 'Transaksi pembelian berhasil dihapus!'); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Purchase destroy error: ' . $e->getMessage()); return back()->with('error', 'Gagal menghapus transaksi: ' . $e->getMessage()); } } }