2026-03-15 17:08:23 +07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Business\App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Models\Vat;
|
|
|
|
|
use App\Models\Rack;
|
|
|
|
|
use App\Models\Unit;
|
|
|
|
|
use App\Models\Brand;
|
|
|
|
|
use App\Models\Shelf;
|
|
|
|
|
use App\Models\Stock;
|
|
|
|
|
use App\Models\Option;
|
|
|
|
|
use App\Models\Product;
|
|
|
|
|
use App\Models\Category;
|
|
|
|
|
use App\Models\Variation;
|
|
|
|
|
use App\Models\Warehouse;
|
|
|
|
|
use App\Helpers\HasUploader;
|
|
|
|
|
use App\Models\ComboProduct;
|
|
|
|
|
use App\Models\ProductModel;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Validation\Rule;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
use App\Services\PdfService;
|
|
|
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
use Modules\Business\App\Exports\ExportProduct;
|
|
|
|
|
|
|
|
|
|
class AcnooProductController extends Controller
|
|
|
|
|
{
|
|
|
|
|
use HasUploader;
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->middleware('check.permission:products.read')->only(['index', 'show', 'expiredProduct']);
|
|
|
|
|
$this->middleware('check.permission:products.create')->only(['create', 'store']);
|
|
|
|
|
$this->middleware('check.permission:products.update')->only(['edit', 'update', 'CreateStock']);
|
|
|
|
|
$this->middleware('check.permission:products.delete')->only(['destroy', 'deleteAll']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function index(Request $request)
|
|
|
|
|
{
|
2026-05-14 11:55:22 +07:00
|
|
|
$user = auth()->user();
|
2026-03-15 17:08:23 +07:00
|
|
|
$search = $request->input('search');
|
|
|
|
|
|
|
|
|
|
$products = Product::query()
|
|
|
|
|
->where('business_id', $user->business_id)
|
|
|
|
|
->with([
|
|
|
|
|
'stocks',
|
|
|
|
|
'unit:id,unitName',
|
|
|
|
|
'brand:id,brandName',
|
|
|
|
|
'category:id,categoryName',
|
|
|
|
|
'warehouse:id,name',
|
|
|
|
|
'rack:id,name',
|
|
|
|
|
'shelf:id,name',
|
|
|
|
|
'combo_products.stock.product:id,productName'
|
|
|
|
|
])
|
|
|
|
|
->withSum('stocks as total_stock', 'productStock')
|
|
|
|
|
->where(function ($query) {
|
|
|
|
|
$query->where('product_type', '!=', 'combo')
|
|
|
|
|
->orWhere(function ($q) {
|
|
|
|
|
$q->where('product_type', 'combo')
|
|
|
|
|
->whereHas('combo_products');
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->when($search, function ($q) use ($search) {
|
|
|
|
|
$q->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('productName', 'like', "%{$search}%")
|
2026-05-14 11:55:22 +07:00
|
|
|
->orWhere('productCode', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('hsn_code', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('productPurchasePrice', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('productSalePrice', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('product_type', 'like', "%{$search}%")
|
|
|
|
|
->orWhereHas('category', fn ($q) => $q->where('categoryName', 'like', "%{$search}%"))
|
|
|
|
|
->orWhereHas('brand', fn ($q) => $q->where('brandName', 'like', "%{$search}%"))
|
|
|
|
|
->orWhereHas('unit', fn ($q) => $q->where('unitName', 'like', "%{$search}%"));
|
2026-03-15 17:08:23 +07:00
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->latest()
|
|
|
|
|
->paginate($request->per_page ?? 20)
|
|
|
|
|
->appends($request->query());
|
|
|
|
|
|
|
|
|
|
$products->getCollection()->transform(function ($product) {
|
|
|
|
|
if ($product->product_type === 'combo') {
|
|
|
|
|
$product->total_stock = $product->combo_products->sum(
|
2026-05-14 11:55:22 +07:00
|
|
|
fn ($combo) => $combo->stock?->productStock ?? 0
|
2026-03-15 17:08:23 +07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$product->total_cost = $product->combo_products->sum(
|
2026-05-14 11:55:22 +07:00
|
|
|
fn ($combo) => ($combo->quantity ?? 0) * ($combo->purchase_price ?? 0)
|
2026-03-15 17:08:23 +07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$product->combo_items = $product->combo_products->map(function ($combo) {
|
|
|
|
|
return [
|
2026-05-14 11:55:22 +07:00
|
|
|
'name' => $combo->stock?->product?->productName ?? 'N/A',
|
|
|
|
|
'quantity' => $combo->quantity ?? 0,
|
2026-03-15 17:08:23 +07:00
|
|
|
'purchase_price' => currency_format(
|
|
|
|
|
($combo->purchase_price ?? 0) * ($combo->quantity ?? 0),
|
|
|
|
|
currency: business_currency()
|
|
|
|
|
),
|
2026-05-14 11:55:22 +07:00
|
|
|
'stock' => $combo->stock?->productStock ?? 0,
|
2026-03-15 17:08:23 +07:00
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $product;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if ($request->ajax()) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'data' => view('business::products.datas', compact('products'))->render()
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return view('business::products.index', compact('products'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function create()
|
|
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
$categories = Category::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$brands = Brand::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$units = Unit::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$product_id = (Product::where('business_id', $business_id)->count() ?? 0) + 1;
|
2026-05-14 11:55:22 +07:00
|
|
|
$vats = Vat::whereStatus(1)->where('business_id', $business_id)->latest()->get();
|
2026-03-15 17:08:23 +07:00
|
|
|
$code = str_pad($product_id, 4, '0', STR_PAD_LEFT);
|
|
|
|
|
$product_models = ProductModel::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$warehouses = Warehouse::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$variations = Variation::where('business_id', auth()->user()->business_id)->where('status', 1)->get();
|
|
|
|
|
$racks = Rack::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$shelves = Shelf::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$profit_option = Option::where('key', 'business-settings')
|
2026-05-14 11:55:22 +07:00
|
|
|
->where('value', 'LIKE', '%"business_id":' . $business_id . '%')
|
2026-03-15 17:08:23 +07:00
|
|
|
->get()
|
2026-05-14 11:55:22 +07:00
|
|
|
->firstWhere('value.business_id', $business_id)
|
|
|
|
|
->value['product_profit_option'] ?? '';
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
return view('business::products.create', compact('categories', 'brands', 'units', 'code', 'vats', 'product_models', 'warehouses', 'racks', 'shelves', 'profit_option', 'variations'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Store a newly created resource in storage.
|
|
|
|
|
*/
|
|
|
|
|
public function store(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
|
|
|
|
|
$request->validate([
|
|
|
|
|
'vat_id' => 'nullable|exists:vats,id',
|
|
|
|
|
'unit_id' => 'nullable|exists:units,id',
|
|
|
|
|
'brand_id' => 'nullable|exists:brands,id',
|
|
|
|
|
'category_id' => 'nullable|exists:categories,id',
|
|
|
|
|
'model_id' => 'nullable|exists:product_models,id',
|
|
|
|
|
'vat_type' => 'nullable|in:inclusive,exclusive',
|
|
|
|
|
'productName' => 'required|string|max:255',
|
|
|
|
|
'productPicture' => 'nullable|image|mimes:jpg,png,jpeg,svg',
|
|
|
|
|
'productCode' => [
|
|
|
|
|
'nullable',
|
|
|
|
|
Rule::unique('products')->where(function ($query) use ($business_id) {
|
|
|
|
|
return $query->where('business_id', $business_id);
|
|
|
|
|
}),
|
|
|
|
|
],
|
2026-05-14 11:55:22 +07:00
|
|
|
'hsn_code' => 'nullable|string|max:255',
|
2026-03-15 17:08:23 +07:00
|
|
|
'alert_qty' => 'nullable|numeric|min:0',
|
|
|
|
|
'size' => 'nullable|string|max:255',
|
|
|
|
|
'type' => 'nullable|string|max:255',
|
|
|
|
|
'color' => 'nullable|string|max:255',
|
|
|
|
|
'weight' => 'nullable|string|max:255',
|
|
|
|
|
'capacity' => 'nullable|string|max:255',
|
|
|
|
|
'productManufacturer' => 'nullable|string|max:255',
|
|
|
|
|
'product_type' => 'required|in:single,variant,combo',
|
|
|
|
|
'variation_ids' => 'nullable|array|exists:variations,id',
|
|
|
|
|
|
|
|
|
|
'stocks.*.warehouse_id' => 'nullable|exists:warehouses,id',
|
|
|
|
|
'stocks.*.productStock' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.exclusive_price' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.inclusive_price' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.profit_percent' => 'nullable|numeric|max:99999999.99',
|
|
|
|
|
'stocks.*.productSalePrice' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.productWholeSalePrice' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.productDealerPrice' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.mfg_date' => 'nullable|date',
|
|
|
|
|
'stocks.*.expire_date' => 'nullable|date|after_or_equal:stocks.*.mfg_date',
|
|
|
|
|
'stocks' => 'nullable|array',
|
|
|
|
|
'stocks.*.batch_no' => [
|
|
|
|
|
'nullable',
|
|
|
|
|
function ($attribute, $value, $fail) use ($request) {
|
|
|
|
|
$batchNos = collect($request->stocks)->pluck('batch_no')->filter()->toArray();
|
|
|
|
|
|
|
|
|
|
if (count($batchNos) !== count(array_unique($batchNos))) {
|
|
|
|
|
$fail('Duplicate batch number found in the request.');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Combo validation
|
|
|
|
|
'combo_products' => 'nullable|array',
|
|
|
|
|
'combo_products.*.stock_id' => [
|
|
|
|
|
'required_if:product_type,combo',
|
|
|
|
|
Rule::exists('stocks', 'id')->where('business_id', $business_id),
|
|
|
|
|
],
|
|
|
|
|
'combo_products.*.quantity' => 'required_if:product_type,combo|numeric|min:1',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
try {
|
|
|
|
|
// vat calculation
|
|
|
|
|
$vat = Vat::find($request->vat_id);
|
|
|
|
|
$vat_rate = $vat->rate ?? 0;
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$combo_tax_inclusive = $request->productSalePrice ?? 0;
|
|
|
|
|
$combo_tax_exclusive = $request->productSalePrice ?? 0;
|
|
|
|
|
|
|
|
|
|
if ($request->vat_id && $request->product_type === 'combo') {
|
|
|
|
|
$combo_tax_inclusive = number_format($combo_tax_exclusive * (1 + ($vat_rate / 100)), 3, '.', '');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
// Create the product
|
2026-05-14 11:55:22 +07:00
|
|
|
$product = Product::create($request->only('productName', 'unit_id', 'brand_id', 'vat_id', 'vat_type', 'category_id', 'productCode', 'hsn_code', 'product_type', 'rack_id', 'shelf_id', 'model_id', 'variation_ids', 'warranty_guarantee_info') + [
|
2026-03-15 17:08:23 +07:00
|
|
|
'business_id' => $business_id,
|
|
|
|
|
'alert_qty' => $request->alert_qty ?? 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'has_serial' => $request->product_type == 'combo' ? 0 : ($request->has_serial ?? 0),
|
2026-03-15 17:08:23 +07:00
|
|
|
'profit_percent' => $request->product_type == 'combo' ? $request->profit_percent ?? 0 : 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'productSalePrice' => $request->product_type == 'combo' ? $combo_tax_inclusive : 0, // inclusive sales price
|
|
|
|
|
'price_without_tax' => $request->product_type == 'combo' ? $combo_tax_exclusive : 0,
|
2026-03-15 17:08:23 +07:00
|
|
|
'productPicture' => $request->productPicture ? $this->upload($request, 'productPicture') : NULL,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Single or Variant Product
|
|
|
|
|
if (in_array($request->product_type, ['single', 'variant']) && !empty($request->stocks)) {
|
|
|
|
|
$stockData = [];
|
|
|
|
|
foreach ($request->stocks as $stock) {
|
|
|
|
|
|
|
|
|
|
$stockData[] = [
|
|
|
|
|
'business_id' => $business_id,
|
|
|
|
|
'product_id' => $product->id,
|
2026-05-14 11:55:22 +07:00
|
|
|
'exclusive_price' => $stock['exclusive_price'] ?? 0,
|
2026-03-15 17:08:23 +07:00
|
|
|
'batch_no' => $stock['batch_no'] ?? null,
|
|
|
|
|
'warehouse_id' => $stock['warehouse_id'] ?? null,
|
|
|
|
|
'productStock' => $stock['productStock'] ?? 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'productPurchasePrice' => $stock['inclusive_price'] ?? 0, // inclusive purchase price / net cost price
|
2026-03-15 17:08:23 +07:00
|
|
|
'profit_percent' => $stock['profit_percent'] ?? 0,
|
|
|
|
|
'productSalePrice' => $stock['productSalePrice'] ?? 0,
|
|
|
|
|
'productWholeSalePrice' => $stock['productWholeSalePrice'] ?? 0,
|
|
|
|
|
'productDealerPrice' => $stock['productDealerPrice'] ?? 0,
|
|
|
|
|
'mfg_date' => $stock['mfg_date'] ?? null,
|
|
|
|
|
'expire_date' => $stock['expire_date'] ?? null,
|
|
|
|
|
'variation_data' => $stock['variation_data'] ?? null,
|
|
|
|
|
'variant_name' => $stock['variant_name'] ?? null,
|
|
|
|
|
'serial_numbers' => $request->has_serial ? json_encode($stock['serial_numbers'] ?? []) : null,
|
|
|
|
|
'branch_id' => auth()->user()->branch_id ?? auth()->user()->active_branch_id,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Stock::insert($stockData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Combo Product
|
|
|
|
|
if ($request->product_type === 'combo' && !empty($request->combo_products)) {
|
|
|
|
|
foreach ($request->combo_products as $item) {
|
|
|
|
|
ComboProduct::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'stock_id' => $item['stock_id'],
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
'purchase_price' => $item['purchase_price'],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Product saved successfully.'),
|
|
|
|
|
'redirect' => route('business.products.index')
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollback();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
], 406);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function edit($id)
|
|
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
$categories = Category::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$brands = Brand::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$units = Unit::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
2026-05-14 11:55:22 +07:00
|
|
|
$vats = Vat::whereStatus(1)->where('business_id', $business_id)->latest()->get();
|
2026-03-15 17:08:23 +07:00
|
|
|
$product_models = ProductModel::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$warehouses = Warehouse::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$racks = Rack::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$shelves = Shelf::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$variations = Variation::where('business_id', auth()->user()->business_id)->where('status', 1)->get();
|
|
|
|
|
$profit_option = Option::where('key', 'business-settings')
|
2026-05-14 11:55:22 +07:00
|
|
|
->where('value', 'LIKE', '%"business_id":' . $business_id . '%')
|
2026-03-15 17:08:23 +07:00
|
|
|
->get()
|
2026-05-14 11:55:22 +07:00
|
|
|
->firstWhere('value.business_id', $business_id)
|
|
|
|
|
->value['product_profit_option'] ?? '';
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
$product = Product::with([
|
|
|
|
|
'stocks' => function ($query) {
|
|
|
|
|
$query->orderBy('variant_name', 'asc')
|
|
|
|
|
->orderBy('id', 'asc');
|
|
|
|
|
},
|
|
|
|
|
'combo_products.stock:id,batch_no,product_id',
|
|
|
|
|
'combo_products.stock.product:id,productName,productCode,unit_id',
|
|
|
|
|
'combo_products.stock.product.unit:id,unitName'
|
|
|
|
|
])
|
|
|
|
|
->where('business_id', $business_id)
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return view('business::products.edit', compact('categories', 'brands', 'units', 'product', 'vats', 'product_models', 'warehouses', 'racks', 'shelves', 'profit_option', 'variations'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function update(Request $request, $id)
|
|
|
|
|
{
|
|
|
|
|
$product = Product::findOrFail($id);
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
|
|
|
|
|
if ($product->product_type != $request->product_type) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Product type can not be changed.'),
|
|
|
|
|
], 406);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$request->validate([
|
|
|
|
|
'vat_id' => 'nullable|exists:vats,id',
|
|
|
|
|
'unit_id' => 'nullable|exists:units,id',
|
|
|
|
|
'brand_id' => 'nullable|exists:brands,id',
|
|
|
|
|
'category_id' => 'nullable|exists:categories,id',
|
|
|
|
|
'model_id' => 'nullable|exists:product_models,id',
|
|
|
|
|
'vat_type' => 'nullable|in:inclusive,exclusive',
|
|
|
|
|
'productName' => 'required|string|max:255',
|
|
|
|
|
'productPicture' => 'nullable|image|mimes:jpg,png,jpeg,svg',
|
|
|
|
|
'productCode' => [
|
|
|
|
|
'nullable',
|
|
|
|
|
Rule::unique('products', 'productCode')->ignore($product->id)->where(function ($query) use ($business_id) {
|
|
|
|
|
return $query->where('business_id', $business_id);
|
|
|
|
|
}),
|
|
|
|
|
],
|
2026-05-14 11:55:22 +07:00
|
|
|
'hsn_code' => 'nullable|string|max:255',
|
2026-03-15 17:08:23 +07:00
|
|
|
'alert_qty' => 'nullable|numeric|min:0',
|
|
|
|
|
'size' => 'nullable|string|max:255',
|
|
|
|
|
'type' => 'nullable|string|max:255',
|
|
|
|
|
'color' => 'nullable|string|max:255',
|
|
|
|
|
'weight' => 'nullable|string|max:255',
|
|
|
|
|
'capacity' => 'nullable|string|max:255',
|
|
|
|
|
'productManufacturer' => 'nullable|string|max:255',
|
|
|
|
|
'product_type' => 'required|in:single,variant,combo',
|
|
|
|
|
'variation_ids' => 'nullable|array|exists:variations,id',
|
|
|
|
|
|
|
|
|
|
'stocks.*.warehouse_id' => 'nullable|exists:warehouses,id',
|
|
|
|
|
'stocks.*.productStock' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.exclusive_price' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.inclusive_price' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.profit_percent' => 'nullable|numeric|max:99999999.99',
|
|
|
|
|
'stocks.*.productSalePrice' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.productWholeSalePrice' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.productDealerPrice' => 'nullable|numeric|min:0|max:99999999.99',
|
|
|
|
|
'stocks.*.mfg_date' => 'nullable|date',
|
|
|
|
|
'stocks.*.expire_date' => 'nullable|date|after_or_equal:stocks.*.mfg_date',
|
|
|
|
|
'stocks' => 'nullable|array',
|
|
|
|
|
'stocks.*.batch_no' => [
|
|
|
|
|
'nullable',
|
|
|
|
|
function ($attribute, $value, $fail) use ($request) {
|
|
|
|
|
$batchNos = collect($request->stocks)->pluck('batch_no')->filter()->toArray();
|
|
|
|
|
|
|
|
|
|
if (count($batchNos) !== count(array_unique($batchNos))) {
|
|
|
|
|
$fail('Duplicate batch number found in the request.');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Combo validation
|
|
|
|
|
'combo_products' => 'nullable|array',
|
|
|
|
|
'combo_products.*.stock_id' => [
|
|
|
|
|
'required_if:product_type,combo',
|
|
|
|
|
Rule::exists('stocks', 'id')->where('business_id', $business_id),
|
|
|
|
|
],
|
|
|
|
|
'combo_products.*.quantity' => 'required_if:product_type,combo|numeric|min:1',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
try {
|
|
|
|
|
// VAT calculation
|
|
|
|
|
$vat = Vat::find($request->vat_id);
|
|
|
|
|
$vat_rate = $vat->rate ?? 0;
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$combo_tax_exclusive = $request->productSalePrice ?? 0;
|
|
|
|
|
$combo_tax_inclusive = $request->productSalePrice ?? 0;
|
|
|
|
|
|
|
|
|
|
if ($request->vat_id && $request->product_type === 'combo') {
|
|
|
|
|
$combo_tax_inclusive = number_format($combo_tax_exclusive * (1 + ($vat_rate / 100)), 3, '.', '');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
// Update product
|
2026-05-14 11:55:22 +07:00
|
|
|
$product->update($request->except(['productPicture', 'productPurchasePrice', 'productDealerPrice', 'productWholeSalePrice', 'alert_qty', 'stocks', 'vat_amount', 'profit_percent', 'productSalePrice', 'price_without_tax']) + [
|
2026-03-15 17:08:23 +07:00
|
|
|
'business_id' => $business_id,
|
|
|
|
|
'alert_qty' => $request->alert_qty ?? 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'has_serial' => $request->product_type == 'combo' ? 0 : ($request->has_serial ?? 0),
|
|
|
|
|
'profit_percent' => $request->product_type == 'combo' ? $request->profit_percent ?? 0 : 0,
|
|
|
|
|
'productSalePrice' => $request->product_type == 'combo' ? $combo_tax_inclusive : 0, // inclusive sales price
|
|
|
|
|
'price_without_tax' => $request->product_type == 'combo' ? $combo_tax_exclusive : 0,
|
2026-03-15 17:08:23 +07:00
|
|
|
'productPicture' => $request->productPicture ? $this->upload($request, 'productPicture', $product->productPicture) : $product->productPicture,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Delete previous stocks and combos
|
|
|
|
|
if ($product->product_type === 'combo') {
|
|
|
|
|
ComboProduct::where('product_id', $product->id)->delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle Single/Variant Product Stocks
|
|
|
|
|
if (in_array($request->product_type, ['single', 'variant']) && !empty($request->stocks)) {
|
|
|
|
|
$existingStockIds = $product->stocks()->pluck('id')->toArray();
|
|
|
|
|
$incomingStockIds = collect($request->stocks)->pluck('stock_id')->filter()->toArray();
|
|
|
|
|
|
|
|
|
|
// Delete removed stocks
|
|
|
|
|
$stocksToDelete = array_diff($existingStockIds, $incomingStockIds);
|
|
|
|
|
Stock::whereIn('id', $stocksToDelete)->delete();
|
|
|
|
|
|
|
|
|
|
// Insert or Update
|
|
|
|
|
foreach ($request->stocks as $stock) {
|
|
|
|
|
$stockId = $stock['stock_id'] ?? null;
|
|
|
|
|
|
|
|
|
|
$payload = [
|
|
|
|
|
'business_id' => $business_id,
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'batch_no' => $stock['batch_no'] ?? null,
|
|
|
|
|
'warehouse_id' => $stock['warehouse_id'] ?? null,
|
|
|
|
|
'productStock' => $stock['productStock'] ?? 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'exclusive_price' => $stock['exclusive_price'] ?? 0,
|
|
|
|
|
'productPurchasePrice' =>$stock['inclusive_price'] ?? 0,
|
2026-03-15 17:08:23 +07:00
|
|
|
'profit_percent' => $stock['profit_percent'] ?? 0,
|
|
|
|
|
'productSalePrice' => $stock['productSalePrice'] ?? 0,
|
|
|
|
|
'productWholeSalePrice' => $stock['productWholeSalePrice'] ?? 0,
|
|
|
|
|
'productDealerPrice' => $stock['productDealerPrice'] ?? 0,
|
|
|
|
|
'mfg_date' => $stock['mfg_date'] ?? null,
|
|
|
|
|
'expire_date' => $stock['expire_date'] ?? null,
|
|
|
|
|
'variation_data' => $stock['variation_data'] ?? null,
|
|
|
|
|
'variant_name' => $stock['variant_name'] ?? null,
|
|
|
|
|
'branch_id' => auth()->user()->branch_id ?? auth()->user()->active_branch_id,
|
2026-05-14 11:55:22 +07:00
|
|
|
'serial_numbers' => $request->has_serial ? $stock['serial_numbers'] ?? null : null,
|
2026-03-15 17:08:23 +07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($stockId) {
|
|
|
|
|
Stock::where('id', $stockId)->update($payload);
|
|
|
|
|
} else {
|
|
|
|
|
Stock::create($payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle Combo Product
|
|
|
|
|
if ($request->product_type === 'combo' && !empty($request->combo_products)) {
|
|
|
|
|
foreach ($request->combo_products as $item) {
|
|
|
|
|
ComboProduct::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'stock_id' => $item['stock_id'],
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
'purchase_price' => $item['purchase_price'] ?? 0,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Product updated successfully.'),
|
|
|
|
|
'redirect' => route('business.products.index')
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollback();
|
|
|
|
|
return response()->json([
|
2026-05-14 11:55:22 +07:00
|
|
|
'message' => $e->getMessage(),
|
2026-03-15 17:08:23 +07:00
|
|
|
], 406);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
public function destroy(string $id)
|
2026-03-15 17:08:23 +07:00
|
|
|
{
|
|
|
|
|
$product = Product::findOrFail($id);
|
|
|
|
|
if (file_exists($product->productPicture)) {
|
|
|
|
|
Storage::delete($product->productPicture);
|
|
|
|
|
}
|
|
|
|
|
$product->delete();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Product deleted successfully'),
|
|
|
|
|
'redirect' => route('business.products.index')
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function deleteAll(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$products = Product::whereIn('id', $request->ids)->get();
|
|
|
|
|
|
|
|
|
|
foreach ($products as $product) {
|
|
|
|
|
if (file_exists($product->productPicture)) {
|
|
|
|
|
Storage::delete($product->productPicture);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Product::whereIn('id', $request->ids)->delete();
|
|
|
|
|
return response()->json([
|
2026-05-14 11:55:22 +07:00
|
|
|
'message' => __('Selected product deleted successfully'),
|
|
|
|
|
'redirect' => route('business.products.index')
|
2026-03-15 17:08:23 +07:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getAllProduct()
|
|
|
|
|
{
|
2026-05-14 11:55:22 +07:00
|
|
|
$isTransfer = request()->is_transfer == 1;
|
|
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
$products = Product::with([
|
2026-05-14 11:55:22 +07:00
|
|
|
'stocks' => function ($query) use ($isTransfer) {
|
2026-03-15 17:08:23 +07:00
|
|
|
$query->where('productStock', '>', 0);
|
2026-05-14 11:55:22 +07:00
|
|
|
if (!$isTransfer) {
|
|
|
|
|
$query->where(function ($q) {
|
|
|
|
|
$q->whereNull('serial_numbers')
|
|
|
|
|
->orWhereJsonLength('serial_numbers', 0);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-15 17:08:23 +07:00
|
|
|
},
|
|
|
|
|
'category:id,categoryName',
|
|
|
|
|
'unit:id,unitName',
|
2026-05-14 11:55:22 +07:00
|
|
|
'stocks.warehouse:id,name'
|
2026-03-15 17:08:23 +07:00
|
|
|
])
|
|
|
|
|
->where('business_id', auth()->user()->business_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->withSum(['stocks as total_stock' => function ($query) use ($isTransfer) {
|
|
|
|
|
$query->where('productStock', '>', 0);
|
|
|
|
|
if (!$isTransfer) {
|
|
|
|
|
$query->where(function ($q) {
|
|
|
|
|
$q->whereNull('serial_numbers')
|
|
|
|
|
->orWhereJsonLength('serial_numbers', 0);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}], 'productStock')
|
|
|
|
|
->having('total_stock', '>', 0)
|
2026-03-15 17:08:23 +07:00
|
|
|
->latest()
|
2026-05-14 11:55:22 +07:00
|
|
|
->get();
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
return response()->json($products);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
public function getByCategory(string $category_id)
|
2026-03-15 17:08:23 +07:00
|
|
|
{
|
|
|
|
|
$products = Product::where('business_id', auth()->user()->business_id)->where('category_id', $category_id)->get();
|
|
|
|
|
return response()->json($products);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function exportExcel()
|
|
|
|
|
{
|
|
|
|
|
return Excel::download(new ExportProduct, 'product.xlsx');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function exportCsv()
|
|
|
|
|
{
|
|
|
|
|
return Excel::download(new ExportProduct, 'product.csv');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
public function exportPdf(Request $request)
|
2026-03-15 17:08:23 +07:00
|
|
|
{
|
2026-05-14 11:55:22 +07:00
|
|
|
$user = auth()->user();
|
|
|
|
|
$search = $request->input('search');
|
|
|
|
|
|
|
|
|
|
$products = Product::query()
|
|
|
|
|
->where('business_id', $user->business_id)
|
|
|
|
|
->with([
|
|
|
|
|
'stocks',
|
|
|
|
|
'unit:id,unitName',
|
|
|
|
|
'brand:id,brandName',
|
|
|
|
|
'category:id,categoryName',
|
|
|
|
|
'warehouse:id,name',
|
|
|
|
|
'rack:id,name',
|
|
|
|
|
'shelf:id,name',
|
|
|
|
|
'combo_products.stock.product:id,productName'
|
|
|
|
|
])
|
2026-03-15 17:08:23 +07:00
|
|
|
->withSum('stocks as total_stock', 'productStock')
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($query) {
|
|
|
|
|
$query->where('product_type', '!=', 'combo')
|
|
|
|
|
->orWhere(function ($q) {
|
|
|
|
|
$q->where('product_type', 'combo')
|
|
|
|
|
->whereHas('combo_products');
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->when($search, function ($q) use ($search) {
|
|
|
|
|
$q->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('productName', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('productCode', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('hsn_code', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('productPurchasePrice', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('productSalePrice', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('product_type', 'like', "%{$search}%")
|
|
|
|
|
->orWhereHas('category', fn ($q) => $q->where('categoryName', 'like', "%{$search}%"))
|
|
|
|
|
->orWhereHas('brand', fn ($q) => $q->where('brandName', 'like', "%{$search}%"))
|
|
|
|
|
->orWhereHas('unit', fn ($q) => $q->where('unitName', 'like', "%{$search}%"));
|
|
|
|
|
});
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->latest()
|
2026-05-14 11:55:22 +07:00
|
|
|
->limit($request->per_page ?? 20)
|
2026-03-15 17:08:23 +07:00
|
|
|
->get();
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$products = $products->transform(function ($product) {
|
|
|
|
|
if ($product->product_type === 'combo') {
|
|
|
|
|
$product->total_stock = $product->combo_products->sum(
|
|
|
|
|
fn ($combo) => $combo->stock?->productStock ?? 0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$product->total_cost = $product->combo_products->sum(
|
|
|
|
|
fn ($combo) => ($combo->quantity ?? 0) * ($combo->purchase_price ?? 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $product;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return PdfService::render('business::products.pdf', compact('products'),'product-list.pdf');
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function expiredProduct(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$expired_products = Product::with('unit:id,unitName', 'brand:id,brandName', 'category:id,categoryName', 'stocks')
|
|
|
|
|
->where('business_id', auth()->user()->business_id)
|
|
|
|
|
->withSum('stocks as total_stock', 'productStock')
|
|
|
|
|
->whereHas('stocks', function ($query) {
|
|
|
|
|
$query->whereDate('expire_date', '<', today())
|
|
|
|
|
->where('productStock', '>', 0);
|
|
|
|
|
})
|
|
|
|
|
->when($request->search, function ($q) use ($request) {
|
|
|
|
|
$q->where(function ($q) use ($request) {
|
|
|
|
|
$q->where('type', 'like', '%' . $request->search . '%')
|
|
|
|
|
->orWhere('productName', 'like', '%' . $request->search . '%')
|
|
|
|
|
->orWhere('productCode', 'like', '%' . $request->search . '%')
|
2026-05-14 11:55:22 +07:00
|
|
|
->orWhere('hsn_code', 'like', '%' . $request->search . '%')
|
2026-03-15 17:08:23 +07:00
|
|
|
->orWhere('productSalePrice', 'like', '%' . $request->search . '%')
|
|
|
|
|
->orWhere('productPurchasePrice', 'like', '%' . $request->search . '%')
|
|
|
|
|
->orWhereHas('unit', function ($q) use ($request) {
|
|
|
|
|
$q->where('unitName', 'like', '%' . $request->search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('brand', function ($q) use ($request) {
|
|
|
|
|
$q->where('brandName', 'like', '%' . $request->search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('category', function ($q) use ($request) {
|
|
|
|
|
$q->where('categoryName', 'like', '%' . $request->search . '%');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->latest()
|
|
|
|
|
->paginate($request->per_page ?? 20)->appends($request->query());
|
|
|
|
|
|
|
|
|
|
if ($request->ajax()) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'data' => view('business::expired-products.datas', compact('expired_products'))->render()
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return view('business::expired-products.index', compact('expired_products'));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
public function show(string $id)
|
2026-03-15 17:08:23 +07:00
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
$product = Product::with('stocks')->where('business_id', $business_id)->findOrFail($id);
|
|
|
|
|
$categories = Category::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$brands = Brand::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
|
|
|
|
$units = Unit::where('business_id', $business_id)->whereStatus(1)->latest()->get();
|
2026-05-14 11:55:22 +07:00
|
|
|
$vats = Vat::whereStatus(1)->where('business_id', $business_id)->latest()->get();
|
2026-03-15 17:08:23 +07:00
|
|
|
$profit_option = Option::where('key', 'business-settings')
|
2026-05-14 11:55:22 +07:00
|
|
|
->where('value', 'LIKE', '%"business_id":' . $business_id . '%')
|
2026-03-15 17:08:23 +07:00
|
|
|
->get()
|
2026-05-14 11:55:22 +07:00
|
|
|
->firstWhere('value.business_id', $business_id)
|
|
|
|
|
->value['product_profit_option'] ?? '';
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
return view('business::products.create-stock', compact('categories', 'brands', 'units', 'product', 'vats', 'profit_option'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getShelf(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$rack = Rack::with('shelves')->find($request->rack_id);
|
|
|
|
|
return response()->json($rack ? $rack->shelves : []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getProductVariants($product_id)
|
|
|
|
|
{
|
|
|
|
|
$variant_stocks = Stock::select('id', 'variant_name', 'batch_no', 'expire_date')
|
|
|
|
|
->where('business_id', auth()->user()->business_id)
|
|
|
|
|
->where('product_id', $product_id)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return response()->json($variant_stocks);
|
|
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
|
|
|
|
|
public function checkSerial(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$request->validate([
|
|
|
|
|
'product_id' => 'required|exists:products,id',
|
|
|
|
|
'serial' => 'required|string'
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$exists = Stock::where('business_id', auth()->user()->business_id)
|
|
|
|
|
->where('product_id', $request->product_id)
|
|
|
|
|
->whereNotNull('serial_numbers')
|
|
|
|
|
->whereJsonContains('serial_numbers', $request->serial)
|
|
|
|
|
->exists();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'exists' => $exists
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getProductSerials(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$request->validate([
|
|
|
|
|
'product_id' => 'required|exists:products,id',
|
|
|
|
|
'warehouse_id' => 'nullable|exists:warehouses,id',
|
|
|
|
|
'batch_no' => 'nullable|string',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
|
|
|
|
|
// Get all serial_numbers from stocks
|
|
|
|
|
$serials = Stock::where('business_id', $business_id)
|
|
|
|
|
->where('product_id', $request->product_id)
|
|
|
|
|
->when($request->filled('batch_no'), function ($q) use ($request) {
|
|
|
|
|
$q->where('batch_no', $request->batch_no);
|
|
|
|
|
})
|
|
|
|
|
->when($request->filled('warehouse_id'), function ($q) use ($request) {
|
|
|
|
|
$q->where('warehouse_id', $request->warehouse_id);
|
|
|
|
|
})
|
|
|
|
|
->whereNotNull('serial_numbers')
|
|
|
|
|
->pluck('serial_numbers')
|
|
|
|
|
->flatten()
|
|
|
|
|
->values();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'serials' => $serials
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|