474 lines
20 KiB
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());
|
|
}
|
|
}
|
|
}
|