POS System
This commit is contained in:
parent
1f6021249e
commit
339031e370
|
@ -98,7 +98,8 @@ class BranchController extends Controller
|
||||||
auth()->user()->update(['active_branch_id' => $branch->id]);
|
auth()->user()->update(['active_branch_id' => $branch->id]);
|
||||||
|
|
||||||
// Store in session
|
// Store in session
|
||||||
session(['active_branch_id' => $branch->id]);
|
session(['branch_id' => $branch->id]);
|
||||||
|
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Branch switched successfully to ' . $branch->name);
|
return redirect()->back()->with('success', 'Branch switched successfully to ' . $branch->name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,8 @@ class ProductDataTable extends DataTable
|
||||||
})
|
})
|
||||||
->addColumn('product_image', function ($data) {
|
->addColumn('product_image', function ($data) {
|
||||||
$url = Product::find($data->id)->getFirstMediaUrl('images');
|
$url = Product::find($data->id)->getFirstMediaUrl('images');
|
||||||
return '<img src="'.$url.'" class="product-img-thumb" alt="">';
|
return '<img src="'.$url.'" class="img-thumbnail" style="width: 50px; height: 50px; object-fit: cover;" alt="Product Image">';
|
||||||
|
|
||||||
})
|
})
|
||||||
->addColumn('product_name', function ($data) {
|
->addColumn('product_name', function ($data) {
|
||||||
return '<a href="'.route('products.show', $data->id).'">'.$data->product_name.'</a>';
|
return '<a href="'.route('products.show', $data->id).'">'.$data->product_name.'</a>';
|
||||||
|
@ -33,28 +34,64 @@ class ProductDataTable extends DataTable
|
||||||
}
|
}
|
||||||
|
|
||||||
public function query(Product $model)
|
public function query(Product $model)
|
||||||
{
|
{
|
||||||
$products = DB::table('products')
|
// Subquery untuk total stock
|
||||||
->leftJoinSub(
|
$stockSubquery = DB::table('product_batches')
|
||||||
DB::table('product_batches')
|
->select('product_id', DB::raw('SUM(qty) as total_quantity'))
|
||||||
->select('product_id', DB::raw('SUM(qty) as total_quantity'))
|
->groupBy('product_id');
|
||||||
->groupBy('product_id'),
|
|
||||||
'pb',
|
|
||||||
'products.id',
|
|
||||||
'=',
|
|
||||||
'pb.product_id'
|
|
||||||
)
|
|
||||||
->select(
|
|
||||||
'products.id',
|
|
||||||
'products.product_name',
|
|
||||||
'products.product_code',
|
|
||||||
'products.product_unit',
|
|
||||||
'products.category_id',
|
|
||||||
DB::raw('COALESCE(pb.total_quantity, 0) as product_quantity')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $products;
|
// Subquery untuk batch pertama berdasarkan exp_date (FIFO)
|
||||||
}
|
$fifoSubquery = DB::table('product_batches as pb')
|
||||||
|
->select('pb.product_id', 'pb.unit_price', 'pb.exp_date')
|
||||||
|
->whereRaw('pb.id = (
|
||||||
|
SELECT id FROM product_batches
|
||||||
|
WHERE product_id = pb.product_id
|
||||||
|
ORDER BY exp_date ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
)');
|
||||||
|
|
||||||
|
// Join dengan subqueries
|
||||||
|
$products = DB::table('products')
|
||||||
|
->leftJoinSub($stockSubquery, 'stock', 'products.id', '=', 'stock.product_id')
|
||||||
|
->leftJoinSub($fifoSubquery, 'fifo', 'products.id', '=', 'fifo.product_id')
|
||||||
|
->select(
|
||||||
|
'products.id',
|
||||||
|
'products.product_name',
|
||||||
|
'products.product_code',
|
||||||
|
'products.product_unit',
|
||||||
|
'products.category_id',
|
||||||
|
DB::raw('COALESCE(stock.total_quantity, 0) as product_quantity'),
|
||||||
|
DB::raw('fifo.unit_price as fifo_price'),
|
||||||
|
DB::raw('fifo.exp_date as fifo_exp_date')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public function query(Product $model)
|
||||||
|
// {
|
||||||
|
// $products = DB::table('products')
|
||||||
|
// ->leftJoinSub(
|
||||||
|
// DB::table('product_batches')
|
||||||
|
// ->select('product_id', DB::raw('SUM(qty) as total_quantity'))
|
||||||
|
// ->groupBy('product_id'),
|
||||||
|
// 'pb',
|
||||||
|
// 'products.id',
|
||||||
|
// '=',
|
||||||
|
// 'pb.product_id'
|
||||||
|
// )
|
||||||
|
// ->select(
|
||||||
|
// 'products.id',
|
||||||
|
// 'products.product_name',
|
||||||
|
// 'products.product_code',
|
||||||
|
// 'products.product_unit',
|
||||||
|
// 'products.category_id',
|
||||||
|
// DB::raw('COALESCE(pb.total_quantity, 0) as product_quantity')
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return $products;
|
||||||
|
// }
|
||||||
|
|
||||||
public function html()
|
public function html()
|
||||||
{
|
{
|
||||||
|
@ -93,10 +130,6 @@ class ProductDataTable extends DataTable
|
||||||
->title('Code')
|
->title('Code')
|
||||||
->className('text-center align-middle'),
|
->className('text-center align-middle'),
|
||||||
|
|
||||||
// Column::make('category.category_name')
|
|
||||||
// ->title('Category')
|
|
||||||
// ->className('text-center align-middle'),
|
|
||||||
|
|
||||||
Column::make('product_unit')
|
Column::make('product_unit')
|
||||||
->title('Unit')
|
->title('Unit')
|
||||||
->className('text-center align-middle'),
|
->className('text-center align-middle'),
|
||||||
|
@ -105,6 +138,14 @@ class ProductDataTable extends DataTable
|
||||||
->title('Quantity')
|
->title('Quantity')
|
||||||
->className('text-center align-middle'),
|
->className('text-center align-middle'),
|
||||||
|
|
||||||
|
Column::make('fifo_price')
|
||||||
|
->title('Batch Price')
|
||||||
|
->className('text-center align-middle'),
|
||||||
|
|
||||||
|
Column::make('fifo_exp_date')
|
||||||
|
->title('Exp Date')
|
||||||
|
->className('text-center align-middle'),
|
||||||
|
|
||||||
Column::computed('action')
|
Column::computed('action')
|
||||||
->exportable(false)
|
->exportable(false)
|
||||||
->printable(false)
|
->printable(false)
|
||||||
|
@ -113,6 +154,7 @@ class ProductDataTable extends DataTable
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get filename for export.
|
* Get filename for export.
|
||||||
*
|
*
|
||||||
|
|
|
@ -69,6 +69,20 @@ class Product extends Model implements HasMedia
|
||||||
/**
|
/**
|
||||||
* Setter & Getter harga jual
|
* Setter & Getter harga jual
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
public function getFifoPriceAttribute(): ?float
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return \App\Models\ProductBatch::getFifoBatchPrice(
|
||||||
|
$this->id,
|
||||||
|
session('branch_id', 1), // pastikan session branch_id aktif
|
||||||
|
1 // ambil harga per unit
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function setProductPriceAttribute($value) {
|
public function setProductPriceAttribute($value) {
|
||||||
$this->attributes['product_price'] = $value !== null ? ($value * 100) : null;
|
$this->attributes['product_price'] = $value !== null ? ($value * 100) : null;
|
||||||
}
|
}
|
||||||
|
@ -80,18 +94,18 @@ class Product extends Model implements HasMedia
|
||||||
/**
|
/**
|
||||||
* Hitung harga grosir jika kuantitas mencukupi
|
* Hitung harga grosir jika kuantitas mencukupi
|
||||||
*/
|
*/
|
||||||
public function getWholesalePrice($quantity) {
|
// public function getWholesalePrice($quantity) {
|
||||||
if ($this->min_quantity_for_wholesale && $quantity >= $this->min_quantity_for_wholesale) {
|
// if ($this->min_quantity_for_wholesale && $quantity >= $this->min_quantity_for_wholesale) {
|
||||||
$discount = $this->wholesale_discount_percentage / 100;
|
// $discount = $this->wholesale_discount_percentage / 100;
|
||||||
return $this->product_price * (1 - $discount);
|
// return $this->product_price * (1 - $discount);
|
||||||
}
|
// }
|
||||||
return $this->product_price;
|
// return $this->product_price;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cek apakah kuantitas memenuhi harga grosir
|
* Cek apakah kuantitas memenuhi harga grosir
|
||||||
*/
|
*/
|
||||||
public function isWholesalePrice($quantity) {
|
// public function isWholesalePrice($quantity) {
|
||||||
return $this->min_quantity_for_wholesale && $quantity >= $this->min_quantity_for_wholesale;
|
// return $this->min_quantity_for_wholesale && $quantity >= $this->min_quantity_for_wholesale;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,11 +49,24 @@ class ProductController extends Controller
|
||||||
|
|
||||||
|
|
||||||
public function show(Product $product)
|
public function show(Product $product)
|
||||||
{
|
{
|
||||||
abort_if(Gate::denies('show_products'), 403);
|
abort_if(Gate::denies('show_products'), 403);
|
||||||
|
|
||||||
|
// Load relasi yang dibutuhkan
|
||||||
|
$product->load([
|
||||||
|
'category',
|
||||||
|
'batches' => function ($q) {
|
||||||
|
$q->where('qty', '>', 0)
|
||||||
|
->orderBy('exp_date', 'asc'); // FIFO
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('product::products.show', compact('product'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return view('product::products.show', compact('product'));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function edit(Product $product)
|
public function edit(Product $product)
|
||||||
|
|
|
@ -13,12 +13,16 @@
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-fluid mb-4">
|
<div class="container-fluid mb-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-9">
|
<!-- Informasi Produk -->
|
||||||
<div class="card h-100">
|
<div class="col-lg-8">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Product Information</h5>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-striped mb-0">
|
<table class="table table-bordered table-striped mb-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Product Code</th>
|
<th width="30%">Product Code</th>
|
||||||
<td>{{ $product->product_code }}</td>
|
<td>{{ $product->product_code }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -39,23 +43,63 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Note</th>
|
<th>Note</th>
|
||||||
<td>{{ $product->product_note }}</td>
|
<td>{{ $product->product_note ?? '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Details -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Batch Details (Qty > 0)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover table-sm mb-0 text-center">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Batch Code</th>
|
||||||
|
<th>Harga Beli</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Stock</th>
|
||||||
|
<th>Expiry Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($product->batches as $batch)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $batch->batch_code ?? '-' }}</td>
|
||||||
|
<td>Rp {{ number_format($batch->unit_price, 0, ',', '.') }}</td>
|
||||||
|
<td>Rp {{ number_format($batch->price, 0, ',', '.') }}</td>
|
||||||
|
<td>{{ $batch->qty }}</td>
|
||||||
|
<td>{{ $batch->exp_date ? \Carbon\Carbon::parse($batch->exp_date)->format('d M Y') : '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">No available batches.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-3">
|
<!-- Gambar Produk -->
|
||||||
|
<div class="col-lg-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Product Images</h5>
|
<h5 class="card-title">Product Images</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@foreach($product->getMedia('images') as $media)
|
@forelse($product->getMedia('images') as $media)
|
||||||
<div class="col-md-6 mb-2">
|
<div class="col-md-6 mb-2">
|
||||||
<img src="{{ $media->getUrl() }}" alt="{{ $product->product_name }}" class="img-fluid">
|
<img src="{{ $media->getUrl() }}" alt="{{ $product->product_name }}" class="img-fluid rounded border shadow-sm" style="max-height: 120px; object-fit: cover;">
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@empty
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-muted">No images uploaded.</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,6 +107,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Sale extends Model
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
'branch_id', // ← TAMBAHKAN INI
|
||||||
'date',
|
'date',
|
||||||
'reference',
|
'reference',
|
||||||
'customer_id',
|
'customer_id',
|
||||||
|
@ -24,6 +25,7 @@ class Sale extends Model
|
||||||
'note'
|
'note'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
public function saleDetails() {
|
public function saleDetails() {
|
||||||
return $this->hasMany(SaleDetails::class, 'sale_id', 'id');
|
return $this->hasMany(SaleDetails::class, 'sale_id', 'id');
|
||||||
}
|
}
|
||||||
|
@ -60,4 +62,6 @@ class Sale extends Model
|
||||||
public function getDiscountAmountAttribute($value) {
|
public function getDiscountAmountAttribute($value) {
|
||||||
return $value / 100;
|
return $value / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,11 @@ class SalePayment extends Model
|
||||||
|
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $fillable = [
|
||||||
|
'branch_id', 'sale_id', 'amount', 'date', 'reference', 'payment_method', 'note'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $with = ['sale'];
|
||||||
|
|
||||||
public function sale() {
|
public function sale() {
|
||||||
return $this->belongsTo(Sale::class, 'sale_id', 'id');
|
return $this->belongsTo(Sale::class, 'sale_id', 'id');
|
||||||
|
|
|
@ -3,13 +3,11 @@
|
||||||
namespace Modules\Sale\Http\Controllers;
|
namespace Modules\Sale\Http\Controllers;
|
||||||
|
|
||||||
use Gloudemans\Shoppingcart\Facades\Cart;
|
use Gloudemans\Shoppingcart\Facades\Cart;
|
||||||
use Illuminate\Contracts\Support\Renderable;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Controller;
|
use Illuminate\Routing\Controller;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Modules\People\Entities\Customer;
|
use Modules\People\Entities\Customer;
|
||||||
use Modules\Product\Entities\Category;
|
use Modules\Product\Entities\Category;
|
||||||
use Modules\Product\Entities\Product;
|
|
||||||
use Modules\Sale\Entities\Sale;
|
use Modules\Sale\Entities\Sale;
|
||||||
use Modules\Sale\Entities\SaleDetails;
|
use Modules\Sale\Entities\SaleDetails;
|
||||||
use Modules\Sale\Entities\SalePayment;
|
use Modules\Sale\Entities\SalePayment;
|
||||||
|
@ -17,8 +15,8 @@ use Modules\Sale\Http\Requests\StorePosSaleRequest;
|
||||||
|
|
||||||
class PosController extends Controller
|
class PosController extends Controller
|
||||||
{
|
{
|
||||||
|
public function index()
|
||||||
public function index() {
|
{
|
||||||
Cart::instance('sale')->destroy();
|
Cart::instance('sale')->destroy();
|
||||||
|
|
||||||
$customers = Customer::all();
|
$customers = Customer::all();
|
||||||
|
@ -27,27 +25,86 @@ class PosController extends Controller
|
||||||
return view('sale::pos.index', compact('product_categories', 'customers'));
|
return view('sale::pos.index', compact('product_categories', 'customers'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function allocateProductFromBatches($product_id, $required_qty)
|
||||||
|
{
|
||||||
|
$branch_id = session('branch_id');
|
||||||
|
|
||||||
public function store(StorePosSaleRequest $request) {
|
$batches = DB::table('product_batches')
|
||||||
|
->where('product_id', $product_id)
|
||||||
|
->where('branch_id', $branch_id)
|
||||||
|
->where('qty', '>', 0)
|
||||||
|
->orderBy('created_at') // FIFO
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$allocation = [];
|
||||||
|
$remaining = $required_qty;
|
||||||
|
$first_price = null;
|
||||||
|
$total_allocated_price = 0;
|
||||||
|
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
if ($remaining <= 0) break;
|
||||||
|
|
||||||
|
$used_qty = min($batch->qty, $remaining);
|
||||||
|
|
||||||
|
if ($first_price === null) {
|
||||||
|
$first_price = $batch->price;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allocation[] = [
|
||||||
|
'batch_id' => $batch->id,
|
||||||
|
'qty' => $used_qty,
|
||||||
|
'unit_price' => $batch->price
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::table('product_batches')
|
||||||
|
->where('id', $batch->id)
|
||||||
|
->decrement('qty', $used_qty);
|
||||||
|
|
||||||
|
$total_allocated_price += $used_qty * $batch->price;
|
||||||
|
$remaining -= $used_qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remaining > 0) {
|
||||||
|
throw new \Exception("Stok produk tidak cukup di cabang saat ini.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'allocation' => $allocation,
|
||||||
|
'unit_price' => $first_price,
|
||||||
|
'total_price' => $total_allocated_price
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StorePosSaleRequest $request)
|
||||||
|
{
|
||||||
DB::transaction(function () use ($request) {
|
DB::transaction(function () use ($request) {
|
||||||
|
$branch_id = session('branch_id');
|
||||||
|
|
||||||
|
if (!$branch_id) {
|
||||||
|
throw new \Exception("Cabang belum dipilih. Silakan pilih cabang terlebih dahulu.");
|
||||||
|
}
|
||||||
|
|
||||||
$due_amount = $request->total_amount - $request->paid_amount;
|
$due_amount = $request->total_amount - $request->paid_amount;
|
||||||
|
|
||||||
if ($due_amount == $request->total_amount) {
|
$payment_status = match (true) {
|
||||||
$payment_status = 'Unpaid';
|
$due_amount == $request->total_amount => 'Unpaid',
|
||||||
} elseif ($due_amount > 0) {
|
$due_amount > 0 => 'Partial',
|
||||||
$payment_status = 'Partial';
|
default => 'Paid',
|
||||||
} else {
|
};
|
||||||
$payment_status = 'Paid';
|
|
||||||
}
|
$discount_percentage = $request->discount_percentage ?? 0;
|
||||||
|
$tax_percentage = $request->tax_percentage ?? 0;
|
||||||
|
|
||||||
$sale = Sale::create([
|
$sale = Sale::create([
|
||||||
'date' => now()->format('Y-m-d'),
|
'date' => now()->format('Y-m-d'),
|
||||||
|
'branch_id' => $branch_id,
|
||||||
'reference' => 'PSL',
|
'reference' => 'PSL',
|
||||||
'customer_id' => $request->customer_id,
|
'customer_id' => $request->customer_id,
|
||||||
'customer_name' => $request->customer_id ? Customer::findOrFail($request->customer_id)->customer_name : 'Walk-in Customer',
|
'customer_name' => $request->customer_id
|
||||||
'tax_percentage' => $request->tax_percentage,
|
? Customer::findOrFail($request->customer_id)->customer_name
|
||||||
'discount_percentage' => $request->discount_percentage,
|
: 'Walk-in Customer',
|
||||||
'shipping_amount' => $request->shipping_amount * 100,
|
'tax_percentage' => $tax_percentage,
|
||||||
|
'discount_percentage' => $discount_percentage,
|
||||||
'paid_amount' => $request->paid_amount * 100,
|
'paid_amount' => $request->paid_amount * 100,
|
||||||
'total_amount' => $request->total_amount * 100,
|
'total_amount' => $request->total_amount * 100,
|
||||||
'due_amount' => $due_amount * 100,
|
'due_amount' => $due_amount * 100,
|
||||||
|
@ -56,27 +113,25 @@ class PosController extends Controller
|
||||||
'note' => $request->note,
|
'note' => $request->note,
|
||||||
'tax_amount' => Cart::instance('sale')->tax() * 100,
|
'tax_amount' => Cart::instance('sale')->tax() * 100,
|
||||||
'discount_amount' => Cart::instance('sale')->discount() * 100,
|
'discount_amount' => Cart::instance('sale')->discount() * 100,
|
||||||
|
'user_id' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach (Cart::instance('sale')->content() as $cart_item) {
|
foreach (Cart::instance('sale')->content() as $cart_item) {
|
||||||
|
$allocation = $this->allocateProductFromBatches($cart_item->id, $cart_item->qty);
|
||||||
|
|
||||||
SaleDetails::create([
|
SaleDetails::create([
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'product_id' => $cart_item->id,
|
'product_id' => $cart_item->id,
|
||||||
'product_name' => $cart_item->name,
|
'product_name' => $cart_item->name,
|
||||||
'product_code' => $cart_item->options->code,
|
'product_code' => $cart_item->options->code,
|
||||||
'quantity' => $cart_item->qty,
|
'quantity' => $cart_item->qty,
|
||||||
'price' => $cart_item->price * 100,
|
'price' => $allocation['total_price'] * 100,
|
||||||
'unit_price' => $cart_item->options->unit_price * 100,
|
'unit_price' => $allocation['unit_price'] * 100,
|
||||||
'sub_total' => $cart_item->options->sub_total * 100,
|
'sub_total' => $cart_item->options->sub_total * 100,
|
||||||
'product_discount_amount' => $cart_item->options->product_discount * 100,
|
'product_discount_amount' => $cart_item->options->product_discount * 100,
|
||||||
'product_discount_type' => $cart_item->options->product_discount_type,
|
'product_discount_type' => $cart_item->options->product_discount_type,
|
||||||
'product_tax_amount' => $cart_item->options->product_tax * 100,
|
'product_tax_amount' => $cart_item->options->product_tax * 100,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$product = Product::findOrFail($cart_item->id);
|
|
||||||
$product->update([
|
|
||||||
'product_quantity' => $product->product_quantity - $cart_item->qty
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart::instance('sale')->destroy();
|
Cart::instance('sale')->destroy();
|
||||||
|
@ -84,7 +139,7 @@ class PosController extends Controller
|
||||||
if ($sale->paid_amount > 0) {
|
if ($sale->paid_amount > 0) {
|
||||||
SalePayment::create([
|
SalePayment::create([
|
||||||
'date' => now()->format('Y-m-d'),
|
'date' => now()->format('Y-m-d'),
|
||||||
'reference' => 'INV/'.$sale->reference,
|
'reference' => 'INV/' . $sale->reference,
|
||||||
'amount' => $sale->paid_amount,
|
'amount' => $sale->paid_amount,
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'payment_method' => $request->payment_method
|
'payment_method' => $request->payment_method
|
||||||
|
@ -93,7 +148,6 @@ class PosController extends Controller
|
||||||
});
|
});
|
||||||
|
|
||||||
toast('POS Sale Created!', 'success');
|
toast('POS Sale Created!', 'success');
|
||||||
|
|
||||||
return redirect()->route('sales.index');
|
return redirect()->route('sales.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,73 +17,107 @@ use Modules\Sale\Http\Requests\UpdateSaleRequest;
|
||||||
|
|
||||||
class SaleController extends Controller
|
class SaleController extends Controller
|
||||||
{
|
{
|
||||||
|
public function index(SalesDataTable $dataTable)
|
||||||
public function index(SalesDataTable $dataTable) {
|
{
|
||||||
abort_if(Gate::denies('access_sales'), 403);
|
abort_if(Gate::denies('access_sales'), 403);
|
||||||
|
|
||||||
return $dataTable->render('sale::index');
|
return $dataTable->render('sale::index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
public function create() {
|
{
|
||||||
abort_if(Gate::denies('create_sales'), 403);
|
abort_if(Gate::denies('create_sales'), 403);
|
||||||
|
|
||||||
Cart::instance('sale')->destroy();
|
Cart::instance('sale')->destroy();
|
||||||
|
|
||||||
return view('sale::create');
|
return view('sale::create');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function allocateProductFromBatches($product_id, $required_qty)
|
||||||
|
{
|
||||||
|
$branch_id = session('branch_id');
|
||||||
|
|
||||||
public function store(StoreSaleRequest $request) {
|
$batches = DB::table('product_batches')
|
||||||
|
->where('product_id', $product_id)
|
||||||
|
->where('branch_id', $branch_id)
|
||||||
|
->where('quantity', '>', 0)
|
||||||
|
->orderBy('created_at') // FIFO
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$allocation = [];
|
||||||
|
$remaining = $required_qty;
|
||||||
|
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
if ($remaining <= 0) break;
|
||||||
|
|
||||||
|
$used_qty = min($batch->quantity, $remaining);
|
||||||
|
$allocation[] = [
|
||||||
|
'batch_id' => $batch->id,
|
||||||
|
'quantity' => $used_qty,
|
||||||
|
'unit_price' => $batch->unit_price
|
||||||
|
];
|
||||||
|
|
||||||
|
// Kurangi jumlah di batch
|
||||||
|
DB::table('product_batches')
|
||||||
|
->where('id', $batch->id)
|
||||||
|
->decrement('quantity', $used_qty);
|
||||||
|
|
||||||
|
$remaining -= $used_qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remaining > 0) {
|
||||||
|
throw new \Exception("Stok produk tidak cukup di cabang saat ini.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreSaleRequest $request)
|
||||||
|
{
|
||||||
DB::transaction(function () use ($request) {
|
DB::transaction(function () use ($request) {
|
||||||
$due_amount = $request->total_amount - $request->paid_amount;
|
$due_amount = $request->total_amount - $request->paid_amount;
|
||||||
|
$payment_status = $due_amount == $request->total_amount ? 'Unpaid' :
|
||||||
|
($due_amount > 0 ? 'Partial' : 'Paid');
|
||||||
|
|
||||||
if ($due_amount == $request->total_amount) {
|
$customer_id = $request->customer_id ?? 1;
|
||||||
$payment_status = 'Unpaid';
|
|
||||||
} elseif ($due_amount > 0) {
|
|
||||||
$payment_status = 'Partial';
|
|
||||||
} else {
|
|
||||||
$payment_status = 'Paid';
|
|
||||||
}
|
|
||||||
|
|
||||||
$sale = Sale::create([
|
$sale = Sale::create([
|
||||||
'date' => $request->date,
|
'branch_id' => session('branch_id'),
|
||||||
'reference' => $request->reference,
|
'date' => $request->date,
|
||||||
'customer_id' => $request->customer_id,
|
'reference' => $request->reference,
|
||||||
'customer_name' => Customer::findOrFail($request->customer_id)->customer_name,
|
'customer_id' => $request->customer_id,
|
||||||
'tax_percentage' => $request->tax_percentage,
|
'customer_name' => optional(Customer::find($request->customer_id))->customer_name,
|
||||||
|
'tax_percentage' => $request->tax_percentage,
|
||||||
'discount_percentage' => $request->discount_percentage,
|
'discount_percentage' => $request->discount_percentage,
|
||||||
'shipping_amount' => $request->shipping_amount * 100,
|
'shipping_amount' => $request->shipping_amount * 100,
|
||||||
'paid_amount' => $request->paid_amount * 100,
|
'paid_amount' => $request->paid_amount * 100,
|
||||||
'total_amount' => $request->total_amount * 100,
|
'total_amount' => $request->total_amount * 100,
|
||||||
'due_amount' => $due_amount * 100,
|
'due_amount' => $due_amount * 100,
|
||||||
'payment_status' => $payment_status,
|
'payment_status' => $payment_status,
|
||||||
'payment_method' => $request->payment_method,
|
'payment_method' => $request->payment_method,
|
||||||
'note' => $request->note,
|
'note' => $request->note,
|
||||||
'tax_amount' => Cart::instance('sale')->tax() * 100,
|
'tax_amount' => Cart::instance('sale')->tax() * 100,
|
||||||
'discount_amount' => Cart::instance('sale')->discount() * 100,
|
'discount_amount' => Cart::instance('sale')->discount() * 100,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach (Cart::instance('sale')->content() as $cart_item) {
|
foreach (Cart::instance('sale')->content() as $item) {
|
||||||
SaleDetails::create([
|
SaleDetails::create([
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'product_id' => $cart_item->id,
|
'product_id' => $item->id,
|
||||||
'product_name' => $cart_item->name,
|
'product_name' => $item->name,
|
||||||
'product_code' => $cart_item->options->code,
|
'product_code' => $item->options->code,
|
||||||
'quantity' => $cart_item->qty,
|
'quantity' => $item->qty,
|
||||||
'price' => $cart_item->price * 100,
|
'price' => $item->price * 100,
|
||||||
'unit_price' => $cart_item->options->unit_price * 100,
|
'unit_price' => $item->options->unit_price * 100,
|
||||||
'sub_total' => $cart_item->options->sub_total * 100,
|
'sub_total' => $item->options->sub_total * 100,
|
||||||
'product_discount_amount' => $cart_item->options->product_discount * 100,
|
'product_discount_amount' => $item->options->product_discount * 100,
|
||||||
'product_discount_type' => $cart_item->options->product_discount_type,
|
'product_discount_type' => $item->options->product_discount_type,
|
||||||
'product_tax_amount' => $cart_item->options->product_tax * 100,
|
'product_tax_amount' => $item->options->product_tax * 100,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->status == 'Shipped' || $request->status == 'Completed') {
|
if (in_array($request->status, ['Shipped', 'Completed'])) {
|
||||||
$product = Product::findOrFail($cart_item->id);
|
$product = Product::find($item->id);
|
||||||
$product->update([
|
if ($product) {
|
||||||
'product_quantity' => $product->product_quantity - $cart_item->qty
|
$product->decrement('product_quantity', $item->qty);
|
||||||
]);
|
$this->allocateProductFromBatches($item->id, $item->qty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,54 +125,48 @@ class SaleController extends Controller
|
||||||
|
|
||||||
if ($sale->paid_amount > 0) {
|
if ($sale->paid_amount > 0) {
|
||||||
SalePayment::create([
|
SalePayment::create([
|
||||||
'date' => $request->date,
|
'date' => $request->date,
|
||||||
'reference' => 'INV/'.$sale->reference,
|
'reference' => 'INV/' . $sale->reference,
|
||||||
'amount' => $sale->paid_amount,
|
'amount' => $sale->paid_amount,
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'payment_method' => $request->payment_method
|
'payment_method' => $request->payment_method
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
toast('Sale Created!', 'success');
|
toast('Sale Created!', 'success');
|
||||||
|
|
||||||
return redirect()->route('sales.index');
|
return redirect()->route('sales.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function show(Sale $sale)
|
||||||
public function show(Sale $sale) {
|
{
|
||||||
abort_if(Gate::denies('show_sales'), 403);
|
abort_if(Gate::denies('show_sales'), 403);
|
||||||
|
$customer = Customer::find($sale->customer_id);
|
||||||
$customer = Customer::findOrFail($sale->customer_id);
|
|
||||||
|
|
||||||
return view('sale::show', compact('sale', 'customer'));
|
return view('sale::show', compact('sale', 'customer'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function edit(Sale $sale)
|
||||||
public function edit(Sale $sale) {
|
{
|
||||||
abort_if(Gate::denies('edit_sales'), 403);
|
abort_if(Gate::denies('edit_sales'), 403);
|
||||||
|
|
||||||
$sale_details = $sale->saleDetails;
|
|
||||||
|
|
||||||
Cart::instance('sale')->destroy();
|
|
||||||
|
|
||||||
$cart = Cart::instance('sale');
|
$cart = Cart::instance('sale');
|
||||||
|
$cart->destroy();
|
||||||
|
|
||||||
foreach ($sale_details as $sale_detail) {
|
foreach ($sale->saleDetails as $detail) {
|
||||||
$cart->add([
|
$cart->add([
|
||||||
'id' => $sale_detail->product_id,
|
'id' => $detail->product_id,
|
||||||
'name' => $sale_detail->product_name,
|
'name' => $detail->product_name,
|
||||||
'qty' => $sale_detail->quantity,
|
'qty' => $detail->quantity,
|
||||||
'price' => $sale_detail->price,
|
'price' => $detail->price,
|
||||||
'weight' => 1,
|
'weight' => 1,
|
||||||
'options' => [
|
'options' => [
|
||||||
'product_discount' => $sale_detail->product_discount_amount,
|
'product_discount' => $detail->product_discount_amount,
|
||||||
'product_discount_type' => $sale_detail->product_discount_type,
|
'product_discount_type' => $detail->product_discount_type,
|
||||||
'sub_total' => $sale_detail->sub_total,
|
'sub_total' => $detail->sub_total,
|
||||||
'code' => $sale_detail->product_code,
|
'code' => $detail->product_code,
|
||||||
'stock' => Product::findOrFail($sale_detail->product_id)->product_quantity,
|
'stock' => optional(Product::find($detail->product_id))->product_quantity,
|
||||||
'product_tax' => $sale_detail->product_tax_amount,
|
'product_tax' => $detail->product_tax_amount,
|
||||||
'unit_price' => $sale_detail->unit_price
|
'unit_price' => $detail->unit_price
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -146,68 +174,62 @@ class SaleController extends Controller
|
||||||
return view('sale::edit', compact('sale'));
|
return view('sale::edit', compact('sale'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update(UpdateSaleRequest $request, Sale $sale)
|
||||||
public function update(UpdateSaleRequest $request, Sale $sale) {
|
{
|
||||||
DB::transaction(function () use ($request, $sale) {
|
DB::transaction(function () use ($request, $sale) {
|
||||||
|
|
||||||
$due_amount = $request->total_amount - $request->paid_amount;
|
$due_amount = $request->total_amount - $request->paid_amount;
|
||||||
|
$payment_status = $due_amount == $request->total_amount ? 'Unpaid' :
|
||||||
|
($due_amount > 0 ? 'Partial' : 'Paid');
|
||||||
|
|
||||||
if ($due_amount == $request->total_amount) {
|
foreach ($sale->saleDetails as $detail) {
|
||||||
$payment_status = 'Unpaid';
|
if (in_array($sale->status, ['Shipped', 'Completed'])) {
|
||||||
} elseif ($due_amount > 0) {
|
$product = Product::find($detail->product_id);
|
||||||
$payment_status = 'Partial';
|
if ($product) {
|
||||||
} else {
|
$product->increment('product_quantity', $detail->quantity);
|
||||||
$payment_status = 'Paid';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($sale->saleDetails as $sale_detail) {
|
|
||||||
if ($sale->status == 'Shipped' || $sale->status == 'Completed') {
|
|
||||||
$product = Product::findOrFail($sale_detail->product_id);
|
|
||||||
$product->update([
|
|
||||||
'product_quantity' => $product->product_quantity + $sale_detail->quantity
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
$sale_detail->delete();
|
$detail->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
$sale->update([
|
$sale->update([
|
||||||
'date' => $request->date,
|
'date' => $request->date,
|
||||||
'reference' => $request->reference,
|
'reference' => $request->reference,
|
||||||
'customer_id' => $request->customer_id,
|
'customer_id' => $request->customer_id,
|
||||||
'customer_name' => Customer::findOrFail($request->customer_id)->customer_name,
|
'customer_name' => optional(Customer::find($request->customer_id))->customer_name,
|
||||||
'tax_percentage' => $request->tax_percentage,
|
'tax_percentage' => $request->tax_percentage,
|
||||||
'discount_percentage' => $request->discount_percentage,
|
'discount_percentage' => $request->discount_percentage,
|
||||||
'shipping_amount' => $request->shipping_amount * 100,
|
'shipping_amount' => $request->shipping_amount * 100,
|
||||||
'paid_amount' => $request->paid_amount * 100,
|
'paid_amount' => $request->paid_amount * 100,
|
||||||
'total_amount' => $request->total_amount * 100,
|
'total_amount' => $request->total_amount * 100,
|
||||||
'due_amount' => $due_amount * 100,
|
'due_amount' => $due_amount * 100,
|
||||||
'payment_status' => $payment_status,
|
'payment_status' => $payment_status,
|
||||||
'payment_method' => $request->payment_method,
|
'payment_method' => $request->payment_method,
|
||||||
'note' => $request->note,
|
'note' => $request->note,
|
||||||
'tax_amount' => Cart::instance('sale')->tax() * 100,
|
'tax_amount' => Cart::instance('sale')->tax() * 100,
|
||||||
'discount_amount' => Cart::instance('sale')->discount() * 100,
|
'discount_amount' => Cart::instance('sale')->discount() * 100,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach (Cart::instance('sale')->content() as $cart_item) {
|
foreach (Cart::instance('sale')->content() as $item) {
|
||||||
SaleDetails::create([
|
SaleDetails::create([
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'product_id' => $cart_item->id,
|
'product_id' => $item->id,
|
||||||
'product_name' => $cart_item->name,
|
'product_name' => $item->name,
|
||||||
'product_code' => $cart_item->options->code,
|
'product_code' => $item->options->code,
|
||||||
'quantity' => $cart_item->qty,
|
'quantity' => $item->qty,
|
||||||
'price' => $cart_item->price * 100,
|
'price' => $item->price * 100,
|
||||||
'unit_price' => $cart_item->options->unit_price * 100,
|
'unit_price' => $item->options->unit_price * 100,
|
||||||
'sub_total' => $cart_item->options->sub_total * 100,
|
'sub_total' => $item->options->sub_total * 100,
|
||||||
'product_discount_amount' => $cart_item->options->product_discount * 100,
|
'product_discount_amount' => $item->options->product_discount * 100,
|
||||||
'product_discount_type' => $cart_item->options->product_discount_type,
|
'product_discount_type' => $item->options->product_discount_type,
|
||||||
'product_tax_amount' => $cart_item->options->product_tax * 100,
|
'product_tax_amount' => $item->options->product_tax * 100,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->status == 'Shipped' || $request->status == 'Completed') {
|
if (in_array($request->status, ['Shipped', 'Completed'])) {
|
||||||
$product = Product::findOrFail($cart_item->id);
|
$product = Product::find($item->id);
|
||||||
$product->update([
|
if ($product) {
|
||||||
'product_quantity' => $product->product_quantity - $cart_item->qty
|
$product->decrement('product_quantity', $item->qty);
|
||||||
]);
|
$this->allocateProductFromBatches($item->id, $item->qty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,18 +237,16 @@ class SaleController extends Controller
|
||||||
});
|
});
|
||||||
|
|
||||||
toast('Sale Updated!', 'info');
|
toast('Sale Updated!', 'info');
|
||||||
|
|
||||||
return redirect()->route('sales.index');
|
return redirect()->route('sales.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function destroy(Sale $sale)
|
||||||
public function destroy(Sale $sale) {
|
{
|
||||||
abort_if(Gate::denies('delete_sales'), 403);
|
abort_if(Gate::denies('delete_sales'), 403);
|
||||||
|
|
||||||
$sale->delete();
|
$sale->delete();
|
||||||
|
|
||||||
toast('Sale Deleted!', 'warning');
|
toast('Sale Deleted!', 'warning');
|
||||||
|
|
||||||
return redirect()->route('sales.index');
|
return redirect()->route('sales.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ class SalePaymentsController extends Controller
|
||||||
|
|
||||||
DB::transaction(function () use ($request) {
|
DB::transaction(function () use ($request) {
|
||||||
SalePayment::create([
|
SalePayment::create([
|
||||||
|
'branch_id' => session('branch_id'), // tambahkan ini
|
||||||
'date' => $request->date,
|
'date' => $request->date,
|
||||||
'reference' => $request->reference,
|
'reference' => $request->reference,
|
||||||
'amount' => $request->amount,
|
'amount' => $request->amount,
|
||||||
|
|
|
@ -16,9 +16,9 @@ class StorePosSaleRequest extends FormRequest
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'customer_id' => 'required|numeric',
|
'customer_id' => 'required|numeric',
|
||||||
'tax_percentage' => 'required|integer|min:0|max:100',
|
//'tax_percentage' => 'required|integer|min:0|max:100',
|
||||||
'discount_percentage' => 'required|integer|min:0|max:100',
|
//'discount_percentage' => 'required|integer|min:0|max:100',
|
||||||
'shipping_amount' => 'required|numeric',
|
//'shipping_amount' => 'required|numeric',
|
||||||
'total_amount' => 'required|numeric',
|
'total_amount' => 'required|numeric',
|
||||||
'paid_amount' => 'required|numeric',
|
'paid_amount' => 'required|numeric',
|
||||||
'note' => 'nullable|string|max:1000'
|
'note' => 'nullable|string|max:1000'
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Livewire\Pos;
|
||||||
|
|
||||||
use Gloudemans\Shoppingcart\Facades\Cart;
|
use Gloudemans\Shoppingcart\Facades\Cart;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
use App\Models\ProductBatch;
|
||||||
|
|
||||||
class Checkout extends Component
|
class Checkout extends Component
|
||||||
{
|
{
|
||||||
|
@ -69,25 +70,28 @@ class Checkout extends Component
|
||||||
|
|
||||||
if ($exists->isNotEmpty()) {
|
if ($exists->isNotEmpty()) {
|
||||||
session()->flash('message', 'Product exists in the cart!');
|
session()->flash('message', 'Product exists in the cart!');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$branchId = session('active_branch') ?? 1;
|
||||||
|
$qty = 1;
|
||||||
|
$fifoPrice = ProductBatch::getFifoBatchPrice($product['id'], $branchId, $qty);
|
||||||
|
|
||||||
$cart->add([
|
$cart->add([
|
||||||
'id' => $product['id'],
|
'id' => $product['id'],
|
||||||
'name' => $product['product_name'],
|
'name' => $product['product_name'],
|
||||||
'qty' => 1,
|
'qty' => $qty,
|
||||||
'price' => $this->calculate($product)['price'],
|
'price' => $fifoPrice,
|
||||||
'weight' => 1,
|
'weight' => 1,
|
||||||
'options' => [
|
'options' => [
|
||||||
'product_discount' => 0.00,
|
'product_discount' => 0.00,
|
||||||
'product_discount_type' => 'fixed',
|
'product_discount_type' => 'fixed',
|
||||||
'sub_total' => $this->calculate($product)['sub_total'],
|
'sub_total' => $fifoPrice,
|
||||||
'code' => $product['product_code'],
|
'code' => $product['product_code'],
|
||||||
'stock' => $product['product_quantity'],
|
'stock' => $product['product_quantity'],
|
||||||
'unit' => $product['product_unit'],
|
'unit' => $product['product_unit'],
|
||||||
'product_tax' => $this->calculate($product)['product_tax'],
|
'product_tax' => 0,
|
||||||
'unit_price' => $this->calculate($product)['unit_price']
|
'unit_price' => $fifoPrice
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use Gloudemans\Shoppingcart\Facades\Cart;
|
||||||
use Illuminate\Support\Facades\Request;
|
use Illuminate\Support\Facades\Request;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Modules\Product\Entities\Product;
|
use Modules\Product\Entities\Product;
|
||||||
|
use App\Models\ProductBatch;
|
||||||
|
|
||||||
class ProductCart extends Component
|
class ProductCart extends Component
|
||||||
{
|
{
|
||||||
|
@ -43,7 +44,7 @@ class ProductCart extends Component
|
||||||
foreach ($cart_items as $cart_item) {
|
foreach ($cart_items as $cart_item) {
|
||||||
$this->check_quantity[$cart_item->id] = [$cart_item->options->stock];
|
$this->check_quantity[$cart_item->id] = [$cart_item->options->stock];
|
||||||
$this->quantity[$cart_item->id] = $cart_item->qty;
|
$this->quantity[$cart_item->id] = $cart_item->qty;
|
||||||
$this->unit_price[$cart_item->id] = $cart_item->price;
|
$this->unit_price[$cart_item->id] = $cart_item->unit_price;
|
||||||
$this->discount_type[$cart_item->id] = $cart_item->options->product_discount_type;
|
$this->discount_type[$cart_item->id] = $cart_item->options->product_discount_type;
|
||||||
if ($cart_item->options->product_discount_type == 'fixed') {
|
if ($cart_item->options->product_discount_type == 'fixed') {
|
||||||
$this->item_discount[$cart_item->id] = $cart_item->options->product_discount;
|
$this->item_discount[$cart_item->id] = $cart_item->options->product_discount;
|
||||||
|
|
|
@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Modules\Product\Entities\Product;
|
use Modules\Product\Entities\Product;
|
||||||
|
use App\Models\Branch;
|
||||||
|
|
||||||
class ProductBatch extends Model
|
class ProductBatch extends Model
|
||||||
{
|
{
|
||||||
|
@ -15,6 +16,7 @@ class ProductBatch extends Model
|
||||||
'batch_code',
|
'batch_code',
|
||||||
'qty',
|
'qty',
|
||||||
'unit_price',
|
'unit_price',
|
||||||
|
'price',
|
||||||
'exp_date',
|
'exp_date',
|
||||||
'purchase_id',
|
'purchase_id',
|
||||||
'created_by',
|
'created_by',
|
||||||
|
@ -24,6 +26,7 @@ class ProductBatch extends Model
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'exp_date' => 'date',
|
'exp_date' => 'date',
|
||||||
'unit_price' => 'decimal:2',
|
'unit_price' => 'decimal:2',
|
||||||
|
'price' => 'decimal:2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function product(): BelongsTo
|
public function product(): BelongsTo
|
||||||
|
@ -77,7 +80,8 @@ class ProductBatch extends Model
|
||||||
'batch_id' => $batch->id,
|
'batch_id' => $batch->id,
|
||||||
'qty' => $deductAmount,
|
'qty' => $deductAmount,
|
||||||
'unit_price' => $batch->unit_price,
|
'unit_price' => $batch->unit_price,
|
||||||
'exp_date' => $batch->exp_date
|
'price' => $batch->price,
|
||||||
|
'exp_date' => $batch->exp_date,
|
||||||
];
|
];
|
||||||
|
|
||||||
$remainingQuantity -= $deductAmount;
|
$remainingQuantity -= $deductAmount;
|
||||||
|
@ -108,10 +112,40 @@ class ProductBatch extends Model
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate batch code if not provided
|
// Generate batch code if not provided
|
||||||
if (!isset($data['batch_code'])) {
|
if (empty($data['batch_code'])) {
|
||||||
$data['batch_code'] = self::generateBatchCode();
|
$data['batch_code'] = self::generateBatchCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::create($data);
|
return self::create($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get FIFO batch unit price (average from earliest batches)
|
||||||
|
*/
|
||||||
|
public static function getFifoBatchPrice(int $productId, int $branchId, int $qty = 1): float
|
||||||
|
{
|
||||||
|
$batches = self::where('product_id', $productId)
|
||||||
|
->where('branch_id', $branchId)
|
||||||
|
->where('qty', '>', 0)
|
||||||
|
->orderBy('exp_date', 'asc')
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$remaining = $qty;
|
||||||
|
$totalPrice = 0;
|
||||||
|
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
if ($remaining <= 0) break;
|
||||||
|
|
||||||
|
$take = min($remaining, $batch->qty);
|
||||||
|
$totalPrice += $take * $batch->unit_price;
|
||||||
|
$remaining -= $take;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remaining > 0) {
|
||||||
|
throw new \Exception('Stok tidak cukup untuk menghitung harga FIFO');
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($totalPrice / $qty, 2);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
|
@php
|
||||||
|
use Modules\Branch\Entities\Branch;
|
||||||
|
$branch = Branch::find(session('branch_id'));
|
||||||
|
@endphp
|
||||||
<button class="c-header-toggler c-class-toggler d-lg-none mfe-auto" type="button" data-target="#sidebar" data-class="c-sidebar-show">
|
<button class="c-header-toggler c-class-toggler d-lg-none mfe-auto" type="button" data-target="#sidebar" data-class="c-sidebar-show">
|
||||||
<i class="bi bi-list" style="font-size: 2rem;"></i>
|
<i class="bi bi-list" style="font-size: 2rem;"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -8,6 +12,102 @@
|
||||||
|
|
||||||
<ul class="c-header-nav ml-auto">
|
<ul class="c-header-nav ml-auto">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<ul class="c-header-nav ml-auto mr-4">
|
||||||
|
@can('access_branches')
|
||||||
|
<li class="c-header-nav-item mr-3">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-primary btn-pill dropdown-toggle" type="button" id="branchDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="bi bi-building mr-1"></i>
|
||||||
|
{{ $branch ? $branch->name : 'Pilih Cabang' }}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="branchDropdown">
|
||||||
|
@foreach(Branch::all() as $b)
|
||||||
|
<a class="dropdown-item {{ session('branch_id') == $b->id ? 'active' : '' }}"
|
||||||
|
href="{{ route('branch.switch', $b->id) }}">
|
||||||
|
{{ $b->name }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
@can('create_pos_sales')
|
||||||
|
<li class="c-header-nav-item mr-3">
|
||||||
|
<a class="btn btn-primary btn-pill {{ request()->routeIs('app.pos.index') ? 'disabled' : '' }}" href="{{ route('app.pos.index') }}">
|
||||||
|
<i class="bi bi-cart mr-1"></i> POS System
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
@can('show_notifications')
|
||||||
|
<li class="c-header-nav-item dropdown d-md-down-none mr-2">
|
||||||
|
<a class="c-header-nav-link" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="bi bi-bell" style="font-size: 20px;"></i>
|
||||||
|
<span class="badge badge-pill badge-danger">
|
||||||
|
@php
|
||||||
|
$low_quantity_products = \Modules\Product\Entities\Product::select('id', 'product_code', 'product_stock_alert')
|
||||||
|
->withSum('batches', 'qty', 'batches_sum_qty')
|
||||||
|
->having('batches_sum_qty', '<=', DB::raw('product_stock_alert'))
|
||||||
|
->get();
|
||||||
|
echo $low_quantity_products->count();
|
||||||
|
@endphp
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right dropdown-menu-lg pt-0">
|
||||||
|
<div class="dropdown-header bg-light">
|
||||||
|
<strong>{{ $low_quantity_products->count() }} Notifications</strong>
|
||||||
|
</div>
|
||||||
|
@forelse($low_quantity_products as $product)
|
||||||
|
<a class="dropdown-item" href="{{ route('products.show', $product->id) }}">
|
||||||
|
<i class="bi bi-hash mr-1 text-primary"></i> Product: "{{ $product->product_code }}" is low in quantity!
|
||||||
|
</a>
|
||||||
|
@empty
|
||||||
|
<a class="dropdown-item" href="#">
|
||||||
|
<i class="bi bi-app-indicator mr-2 text-danger"></i> No notifications available.
|
||||||
|
</a>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
<li class="c-header-nav-item dropdown">
|
||||||
|
<a class="c-header-nav-link" data-toggle="dropdown" href="#" role="button"
|
||||||
|
aria-haspopup="true" aria-expanded="false">
|
||||||
|
<div class="c-avatar mr-2">
|
||||||
|
<img class="c-avatar rounded-circle" src="{{ auth()->user()->getFirstMediaUrl('avatars') }}" alt="Profile Image">
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="font-weight-bold">{{ auth()->user()->name }}</span>
|
||||||
|
<span class="font-italic">Online <i class="bi bi-circle-fill text-success" style="font-size: 11px;"></i></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right pt-0">
|
||||||
|
<div class="dropdown-header bg-light py-2"><strong>Account</strong></div>
|
||||||
|
<a class="dropdown-item" href="{{ route('profile.edit') }}">
|
||||||
|
<i class="mfe-2 bi bi-person" style="font-size: 1.2rem;"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
|
||||||
|
<i class="mfe-2 bi bi-box-arrow-left" style="font-size: 1.2rem;"></i> Logout
|
||||||
|
</a>
|
||||||
|
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
|
||||||
|
@csrf
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{-- <button class="c-header-toggler c-class-toggler d-lg-none mfe-auto" type="button" data-target="#sidebar" data-class="c-sidebar-show">
|
||||||
|
<i class="bi bi-list" style="font-size: 2rem;"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="c-header-toggler c-class-toggler mfs-3 d-md-down-none" type="button" data-target="#sidebar" data-class="c-sidebar-lg-show" responsive="true">
|
||||||
|
<i class="bi bi-list" style="font-size: 2rem;"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="c-header-nav ml-auto">
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="c-header-nav ml-auto mr-4">
|
<ul class="c-header-nav ml-auto mr-4">
|
||||||
@can('access_branches')
|
@can('access_branches')
|
||||||
|
@ -91,4 +191,4 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul> --}}
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
{{ $product->product_code }}
|
{{ $product->product_code }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text font-weight-bold">{{ format_currency($product->product_price) }}</p>
|
<p class="card-text font-weight-bold">
|
||||||
|
{{ format_currency($product->fifo_price ?? 0) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue