357 lines
15 KiB
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|