305 lines
11 KiB
PHP
305 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Product;
|
|
use App\Models\StockAdjustment;
|
|
use App\Models\ProductSale;
|
|
use App\Models\ProductSaleItem;
|
|
use App\Models\Account;
|
|
use App\Models\Branch;
|
|
use App\Models\ProductCategory;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class InventoryController extends Controller
|
|
{
|
|
public function getProducts(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if (!$user) return response()->json(['message' => 'Unauthenticated'], 401);
|
|
|
|
$branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
|
$status = $request->query('status'); // In Stock, Low Stock, Out of Stock
|
|
|
|
$query = Product::with(['branch', 'category']);
|
|
|
|
if ($branchId) {
|
|
$query->where('branch_id', $branchId);
|
|
}
|
|
|
|
if ($status) {
|
|
$query->where('status', $status);
|
|
}
|
|
|
|
return response()->json($query->orderBy('name')->get());
|
|
}
|
|
|
|
public function storeProduct(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'sku' => 'nullable|string|unique:products,sku',
|
|
'product_category_id' => 'required|exists:product_categories,id',
|
|
'branch_id' => 'required|exists:branches,id',
|
|
'cost_price' => 'required|numeric|min:0',
|
|
'selling_price' => 'required|numeric|min:0',
|
|
'current_stock' => 'required|integer|min:0',
|
|
'reorder_level' => 'required|integer|min:0',
|
|
]);
|
|
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $validated['branch_id'] != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized branch assignment'], 403);
|
|
}
|
|
|
|
$status = 'In Stock';
|
|
if ($validated['current_stock'] <= 0) $status = 'Out of Stock';
|
|
else if ($validated['current_stock'] <= $validated['reorder_level']) $status = 'Low Stock';
|
|
|
|
$product = Product::create(array_merge($validated, ['status' => $status]));
|
|
|
|
// Record Initial Stock Adjustment
|
|
StockAdjustment::create([
|
|
'product_id' => $product->id,
|
|
'adjustment_qty' => $product->current_stock,
|
|
'new_stock' => $product->current_stock,
|
|
'adjustment_date' => Carbon::now()->toDateString(),
|
|
'reason' => 'Initial',
|
|
'remarks' => 'System initialization'
|
|
]);
|
|
|
|
return response()->json($product->load(['branch', 'category']), 201);
|
|
}
|
|
|
|
public function adjustStock(Request $request, $id)
|
|
{
|
|
$product = Product::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $product->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'adjustment_qty' => 'required|integer', // Positive for add, negative for remove
|
|
'adjustment_date' => 'required|date',
|
|
'reason' => 'required|string',
|
|
'remarks' => 'nullable|string'
|
|
]);
|
|
|
|
DB::transaction(function () use ($product, $validated) {
|
|
$newStock = $product->current_stock + $validated['adjustment_qty'];
|
|
|
|
$status = 'In Stock';
|
|
if ($newStock <= 0) $status = 'Out of Stock';
|
|
else if ($newStock <= $product->reorder_level) $status = 'Low Stock';
|
|
|
|
$product->update([
|
|
'current_stock' => $newStock,
|
|
'status' => $status
|
|
]);
|
|
|
|
StockAdjustment::create(array_merge($validated, [
|
|
'product_id' => $product->id,
|
|
'new_stock' => $newStock
|
|
]));
|
|
});
|
|
|
|
return response()->json($product->fresh(['branch', 'category']));
|
|
}
|
|
|
|
public function getStockHistory($id)
|
|
{
|
|
$product = Product::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $product->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$history = StockAdjustment::where('product_id', $id)
|
|
->orderBy('adjustment_date', 'desc')
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
return response()->json($history);
|
|
}
|
|
|
|
public function getSales(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
$branchId = $user && $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
|
$startDate = $request->query('start_date');
|
|
$endDate = $request->query('end_date');
|
|
|
|
$query = ProductSale::with(['branch', 'items.product']);
|
|
|
|
if ($branchId) {
|
|
$query->where('branch_id', $branchId);
|
|
}
|
|
if ($startDate) {
|
|
$query->where('date', '>=', $startDate);
|
|
}
|
|
if ($endDate) {
|
|
$query->where('date', '<=', $endDate);
|
|
}
|
|
|
|
return response()->json($query->orderBy('date', 'desc')->get());
|
|
}
|
|
|
|
public function storeSale(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if (!$user) return response()->json(['message' => 'Unauthenticated'], 401);
|
|
|
|
$data = $request->all();
|
|
|
|
if ($user->isReceptionist()) {
|
|
$data['branch_id'] = $user->branch_id;
|
|
}
|
|
|
|
$validated = \Illuminate\Support\Facades\Validator::make($data, [
|
|
'branch_id' => 'required|exists:branches,id',
|
|
'payment_method' => 'required|string',
|
|
'items' => 'required|array|min:1',
|
|
'items.*.product_id' => 'required|exists:products,id',
|
|
'items.*.quantity' => 'required|integer|min:1',
|
|
'items.*.unit_price' => 'nullable|numeric|min:0',
|
|
'items.*.price' => 'nullable|numeric|min:0',
|
|
'total_amount' => 'nullable|numeric|min:0',
|
|
'remarks' => 'nullable|string'
|
|
])->validate();
|
|
|
|
// Security check for branch_id if receptionist
|
|
if ($user->isReceptionist() && $validated['branch_id'] != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized branch assignment'], 403);
|
|
}
|
|
|
|
return DB::transaction(function () use ($validated) {
|
|
$subtotal = 0;
|
|
$items = $validated['items'];
|
|
foreach ($items as &$item) {
|
|
$price = $item['unit_price'] ?? $item['price'] ?? 0;
|
|
$item['final_price'] = $price;
|
|
$subtotal += $item['quantity'] * $price;
|
|
}
|
|
|
|
$vatPercentage = 0.05;
|
|
$vatAmount = round($subtotal * $vatPercentage, 2);
|
|
$totalWithVat = $subtotal + $vatAmount;
|
|
|
|
// If a manual total_amount was provided, we use it, otherwise use calculated total
|
|
$totalAmount = $validated['total_amount'] ?? $totalWithVat;
|
|
|
|
$count = ProductSale::count() + 1;
|
|
$transactionId = "COL-" . (1000 + $count);
|
|
|
|
$sale = ProductSale::create([
|
|
'transaction_id' => $transactionId,
|
|
'branch_id' => $validated['branch_id'],
|
|
'subtotal_amount' => $subtotal,
|
|
'vat_amount' => $vatAmount,
|
|
'total_amount' => $totalAmount,
|
|
'payment_method' => $validated['payment_method'],
|
|
'date' => Carbon::now()->toDateString(),
|
|
'remarks' => $validated['remarks'] ?? null
|
|
]);
|
|
|
|
foreach ($items as $item) {
|
|
ProductSaleItem::create([
|
|
'product_sale_id' => $sale->id,
|
|
'product_id' => $item['product_id'],
|
|
'quantity' => $item['quantity'],
|
|
'unit_price' => $item['final_price'],
|
|
'subtotal' => $item['quantity'] * $item['final_price']
|
|
]);
|
|
|
|
// Update Stock
|
|
$product = Product::find($item['product_id']);
|
|
$newStock = $product->current_stock - $item['quantity'];
|
|
|
|
$status = 'In Stock';
|
|
if ($newStock <= 0) $status = 'Out of Stock';
|
|
else if ($newStock <= $product->reorder_level) $status = 'Low Stock';
|
|
|
|
$product->update([
|
|
'current_stock' => $newStock,
|
|
'status' => $status
|
|
]);
|
|
|
|
// Record Adjustment
|
|
StockAdjustment::create([
|
|
'product_id' => $product->id,
|
|
'adjustment_qty' => -$item['quantity'],
|
|
'new_stock' => $newStock,
|
|
'adjustment_date' => Carbon::now()->toDateString(),
|
|
'reason' => 'Sale',
|
|
'remarks' => "Sale #{$transactionId}"
|
|
]);
|
|
}
|
|
|
|
// Record to Accounts (Credit)
|
|
Account::create([
|
|
'date' => Carbon::now()->toDateString(),
|
|
'time' => Carbon::now()->toTimeString(),
|
|
'credit' => $totalAmount,
|
|
'debit' => 0,
|
|
'type' => 'sale',
|
|
'branch_id' => $validated['branch_id'],
|
|
'accountable_id' => $sale->id,
|
|
'accountable_type' => ProductSale::class,
|
|
'description' => "Product Sale #{$transactionId}"
|
|
]);
|
|
|
|
return response()->json($sale->load('items.product'), 201);
|
|
});
|
|
}
|
|
|
|
public function getCategories()
|
|
{
|
|
return response()->json(ProductCategory::where('status', 'Active')->get());
|
|
}
|
|
|
|
public function getAllMovements(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
$query = StockAdjustment::with(['product.branch']);
|
|
|
|
$branchId = $user && $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
|
$startDate = $request->query('start_date');
|
|
$endDate = $request->query('end_date');
|
|
|
|
if ($branchId) {
|
|
$query->whereHas('product', function($q) use ($branchId) {
|
|
$q->where('branch_id', $branchId);
|
|
});
|
|
}
|
|
if ($startDate) {
|
|
$query->where('adjustment_date', '>=', $startDate);
|
|
}
|
|
if ($endDate) {
|
|
$query->where('adjustment_date', '<=', $endDate);
|
|
}
|
|
|
|
$movements = $query->orderBy('adjustment_date', 'desc')
|
|
->orderBy('created_at', 'desc')
|
|
->get()
|
|
->map(function ($adj) {
|
|
return [
|
|
'id' => $adj->id,
|
|
'date' => $adj->adjustment_date,
|
|
'product_name' => $adj->product->name,
|
|
'sku' => $adj->product->sku,
|
|
'branch' => $adj->product->branch->name,
|
|
'reason' => $adj->reason,
|
|
'change' => $adj->adjustment_qty,
|
|
'new_stock' => $adj->new_stock,
|
|
'remarks' => $adj->remarks
|
|
];
|
|
});
|
|
|
|
return response()->json($movements);
|
|
}
|
|
}
|