akuntansi-sia/app/Http/Controllers/PurchaseController.php

474 lines
20 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Purchase;
use App\Models\JournalEntry;
use App\Models\JournalDetail;
use App\Models\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class PurchaseController extends Controller
{
public function index(Request $request)
{
$user = Auth::user();
$branches = \App\Models\Branch::all();
$branch = $user->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());
}
}
}