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

357 lines
15 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Sale;
use App\Models\Stock;
use App\Models\JournalEntry;
use App\Models\JournalDetail;
use App\Models\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SaleController extends Controller
{
public function index(Request $request)
{
$user = Auth::user();
$branches = \App\Models\Branch::all();
$query = Sale::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());
}
if ($user->role === 'admin') {
$products = \App\Models\Product::all();
$sales = $query->get();
// We no longer need salesByBranch as we use a single consolidated view
return view('laporan_penjualan', compact('sales', 'products', 'branches'));
} else {
$products = \App\Models\Product::all();
$sales = $query->where('branch', $user->branch)->get();
return view('laporan_penjualan', compact('sales', 'products', 'branches'));
}
}
public function create()
{
$user = Auth::user();
if ($user->role === 'admin') {
return redirect('/laporan/penjualan')->with('error', 'Fitur tambah penjualan hanya tersedia untuk karyawan.');
}
$branches = \App\Models\Branch::all();
$branch = $user->branch ?? 'Cabang 1';
$products = \App\Models\Product::where('branch', $branch)->get();
return view('transaksi.penjualan', compact('branches', 'products'));
}
public function exportPdf(Request $request)
{
$user = Auth::user();
if ($user->role !== 'admin') {
abort(403);
}
$query = Sale::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 admin submits the filter
// form with empty values intentionally, return the full dataset.
if (count($request->query()) === 0) {
$query->whereDate('date', \Carbon\Carbon::today());
}
$sales = $query->get();
$branches = \App\Models\Branch::all();
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('reports.sale_pdf', compact('sales', 'branches'));
return $pdf->download('laporan_penjualan.pdf');
}
public function store(Request $request)
{
$request->validate([
'date' => 'required|date',
'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1',
'total_price' => 'required|numeric|min:0',
]);
try {
\Illuminate\Support\Facades\DB::transaction(function () use ($request) {
$product = \App\Models\Product::findOrFail($request->product_id);
$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);
// First, adjust stocks: either raw materials (if product has recipes)
// or product stock per branch
$product = \App\Models\Product::with('recipes.rawMaterial')->findOrFail($request->product_id);
if ($product->recipes->count() > 0) {
// Check and deduct raw materials
foreach ($product->recipes as $recipe) {
$rm = $recipe->rawMaterial;
if (!$rm) continue;
$needed = $recipe->quantity_needed * $request->quantity;
if ($rm->stock_quantity < $needed) {
throw new \Exception('Bahan baku tidak cukup untuk membuat ' . $request->quantity . ' porsi.');
}
}
foreach ($product->recipes as $recipe) {
$rm = $recipe->rawMaterial;
if ($rm) {
$needed = $recipe->quantity_needed * $request->quantity;
$rm->stock_quantity -= $needed;
if ($rm->stock_quantity < 0) $rm->stock_quantity = 0;
$rm->save();
}
}
// Recalculate finished product stock once after all raw materials are updated
$this->recalculateProductsFromRawMaterials($product->id, $branch);
} else {
// Deduct from regular stock
$stock = \App\Models\Stock::where('product_id', $request->product_id)->where('branch', $branch)->first();
if (!$stock) {
throw new \Exception('Stok untuk produk ini tidak ditemukan pada cabang Anda.');
}
if ($stock->quantity < $request->quantity) {
throw new \Exception('Stok tidak cukup. Tersedia: ' . $stock->quantity);
}
$stock->quantity = $stock->quantity - $request->quantity;
if ($stock->quantity < 0) $stock->quantity = 0;
$stock->save();
}
// Now create sale record
$sale = Sale::create([
'date' => $request->date,
'branch' => $branch,
'product_id' => $request->product_id,
'item' => $product->name,
'quantity' => $request->quantity,
'total_price' => (int) $cleanPrice,
]);
// Create Journal Entry
$kasAccount = $this->ensureAccount('101', 'Kas', 'Asset');
$penjualanAccount = $this->ensureAccount('401', 'Penjualan', 'Revenue');
$hppAccount = $this->ensureAccount('501', 'Harga Pokok Penjualan', 'Expense');
$persediaanAccount = $this->ensureAccount('102', 'Persediaan Barang Dagang', 'Asset');
$journal = JournalEntry::create([
'date' => $sale->date,
'description' => 'Penjualan ' . $sale->item,
'reference_type' => 'Sale',
'reference_id' => $sale->id,
'branch' => $branch,
]);
// Debit Kas, Kredit Penjualan
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $kasAccount->id, 'debit' => $sale->total_price, 'credit' => 0]);
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $penjualanAccount->id, 'debit' => 0, 'credit' => $sale->total_price]);
// Debit HPP, Kredit Persediaan
$hppTotal = $sale->quantity * $product->purchase_price;
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $hppAccount->id, 'debit' => $hppTotal, 'credit' => 0]);
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $persediaanAccount->id, 'debit' => 0, 'credit' => $hppTotal]);
});
return back()->with('success', 'Transaksi penjualan berhasil ditambahkan!');
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Sale store error: ' . $e->getMessage());
return back()->withInput()->withErrors(['db_error' => 'Gagal menyimpan transaksi: ' . $e->getMessage()]);
}
}
private function ensureAccount($code, $name, $type)
{
return Account::updateOrCreate(
['code' => $code],
['name' => $name, 'type' => $type]
);
}
public function update(Request $request, $id)
{
$request->validate([
'date' => 'required|date',
'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1',
'total_price' => 'required|numeric|min:0',
]);
try {
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $id) {
$sale = Sale::findOrFail($id);
$product = \App\Models\Product::findOrFail($request->product_id);
// Bersihkan format angka (hapus titik pemisah ribuan jika ada)
$cleanPrice = str_replace('.', '', $request->total_price);
$data = $request->all();
$data['item'] = $product->name;
$data['total_price'] = (int) $cleanPrice;
if (Auth::user()->role === 'admin' && $request->has('branch')) {
$data['branch'] = $request->branch;
}
$sale->update($data);
// Recreate Journal Entry
JournalEntry::where('reference_type', 'Sale')->where('reference_id', $sale->id)->delete();
$kasAccount = Account::where('code', '101')->first();
$penjualanAccount = Account::where('code', '401')->first();
$hppAccount = Account::where('code', '501')->first();
$persediaanAccount = Account::where('code', '102')->first();
if ($kasAccount && $penjualanAccount && $hppAccount && $persediaanAccount) {
$journal = JournalEntry::create([
'date' => $sale->date,
'description' => 'Penjualan ' . $sale->item,
'reference_type' => 'Sale',
'reference_id' => $sale->id,
'branch' => $sale->branch,
]);
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $kasAccount->id, 'debit' => $sale->total_price, 'credit' => 0]);
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $penjualanAccount->id, 'debit' => 0, 'credit' => $sale->total_price]);
$hppTotal = $sale->quantity * $product->purchase_price;
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $hppAccount->id, 'debit' => $hppTotal, 'credit' => 0]);
JournalDetail::create(['journal_entry_id' => $journal->id, 'account_id' => $persediaanAccount->id, 'debit' => 0, 'credit' => $hppTotal]);
}
});
return back()->with('success', 'Transaksi penjualan berhasil diperbarui!');
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Sale update error: ' . $e->getMessage());
return back()->withInput()->withErrors(['db_error' => 'Gagal memperbarui transaksi: ' . $e->getMessage()]);
}
}
public function destroy($id)
{
$sale = Sale::findOrFail($id);
JournalEntry::where('reference_type', 'Sale')->where('reference_id', $sale->id)->delete();
$sale->delete();
return back()->with('success', 'Transaksi penjualan berhasil dihapus!');
}
private function recalculateProductsFromRawMaterials($productId, $branch)
{
$product = \App\Models\Product::with('recipes.rawMaterial')->find($productId);
if (!$product || $product->recipes->isEmpty()) {
return;
}
$minStock = null;
foreach ($product->recipes as $recipe) {
$rm = $recipe->rawMaterial;
if (!$rm || $recipe->quantity_needed <= 0) {
continue;
}
$possibleAmount = floor($rm->stock_quantity / $recipe->quantity_needed);
if ($minStock === null || $possibleAmount < $minStock) {
$minStock = $possibleAmount;
}
}
if ($minStock !== null && $minStock >= 0) {
$stock = \App\Models\Stock::firstOrCreate(
['product_id' => $productId, 'branch' => $branch],
['quantity' => 0, 'unit' => 'Pcs']
);
$stock->quantity = $minStock;
$stock->save();
}
}
private function recalculateProductsFromRawMaterial($rawMaterialId, $branch)
{
$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;
$rmStock = $rm->stock_quantity;
$needed = $prodRecipe->quantity_needed;
$possibleAmount = floor($rmStock / $needed);
if ($minStock === null || $possibleAmount < $minStock) {
$minStock = $possibleAmount;
}
}
if ($minStock !== null && $minStock >= 0) {
$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();
}
}
}
}
}