diff --git a/Modules/Business.zip b/Modules/Business.zip deleted file mode 100644 index 2261756e..00000000 Binary files a/Modules/Business.zip and /dev/null differ diff --git a/Modules/Business/App/Http/Controllers/AcnooProductLossProfitReportController.php b/Modules/Business/App/Http/Controllers/AcnooProductLossProfitReportController.php index 1ad4a45c..e50a0ac7 100644 --- a/Modules/Business/App/Http/Controllers/AcnooProductLossProfitReportController.php +++ b/Modules/Business/App/Http/Controllers/AcnooProductLossProfitReportController.php @@ -18,15 +18,18 @@ public function index(Request $request) $branchId = $user->branch_id ?? $user->active_branch_id; $baseQuery = SaleDetails::query() - ->whereHas('product', fn ($q) => $q->where('business_id', $user->business_id)) + ->whereHas('product', fn ($q) => + $q->where('business_id', $user->business_id) + ) ->when(moduleCheck('MultiBranchAddon') && $branchId, function ($q) use ($branchId) { - $q->whereHas('sale', fn ($q) => $q->where('branch_id', $branchId)); + $q->whereHas('sale', fn ($q) => + $q->where('branch_id', $branchId) + ); }) ->when($request->filled('search'), function ($q) use ($request) { $q->whereHas('product', function ($q) use ($request) { $q->where('productName', 'like', "%{$request->search}%") - ->orWhere('productCode', 'like', "%{$request->search}%") - ->orWhere('hsn_code', 'like', "%{$request->search}%"); + ->orWhere('productCode', 'like', "%{$request->search}%"); }); }); @@ -39,11 +42,11 @@ public function index(Request $request) ->sum('lossProfit'); $product_lossProfits = $baseQuery - ->with('product:id,productName,productCode,hsn_code') + ->with('product:id,productName,productCode') ->select( 'product_id', - DB::raw('SUM(CASE WHEN "lossProfit" > 0 THEN "lossProfit" ELSE 0 END) AS profit'), - DB::raw('SUM(CASE WHEN "lossProfit" < 0 THEN "lossProfit" ELSE 0 END) AS loss') + DB::raw('SUM(CASE WHEN lossProfit > 0 THEN lossProfit ELSE 0 END) AS profit'), + DB::raw('SUM(CASE WHEN lossProfit < 0 THEN lossProfit ELSE 0 END) AS loss') ) ->groupBy('product_id') ->paginate($request->per_page ?? 20) @@ -51,13 +54,17 @@ public function index(Request $request) if ($request->ajax()) { return response()->json([ - 'data' => view('business::reports.product-loss-profit.datas', compact('product_lossProfits'))->render(), - 'loss' => currency_format($loss, currency: business_currency()), - 'profit' => currency_format($profit, currency: business_currency()) + 'data' => view( + 'business::reports.product-loss-profit.datas', + compact('product_lossProfits') + )->render() ]); } - return view('business::reports.product-loss-profit.index', compact('product_lossProfits', 'profit', 'loss')); + return view( + 'business::reports.product-loss-profit.index', + compact('product_lossProfits', 'profit', 'loss') + ); } @@ -71,36 +78,26 @@ public function exportCsv() return Excel::download(new ExportProductLossProfit, 'product-lossProfit.csv'); } - public function exportPdf(Request $request) + public function exportPdf() { - $user = auth()->user(); - $branchId = $user->branch_id ?? $user->active_branch_id; + $branchId = moduleCheck('MultiBranchAddon') ? auth()->user()->branch_id ?? auth()->user()->active_branch_id : null; - $baseQuery = SaleDetails::query() - ->whereHas('product', fn ($q) => - $q->where('business_id', $user->business_id) - ) - ->when(moduleCheck('MultiBranchAddon') && $branchId, function ($q) use ($branchId) { - $q->whereHas('sale', fn ($q) => $q->where('branch_id', $branchId)); - }) - ->when($request->filled('search'), function ($q) use ($request) { - $q->whereHas('product', function ($q) use ($request) { - $q->where('productName', 'like', "%{$request->search}%") - ->orWhere('productCode', 'like', "%{$request->search}%") - ->orWhere('hsn_code', 'like', "%{$request->search}%"); - }); - }); - - $product_lossProfits = $baseQuery - ->with('product:id,productName,productCode,hsn_code') - ->select( - 'product_id', - DB::raw('SUM(CASE WHEN "lossProfit" > 0 THEN "lossProfit" ELSE 0 END) AS profit'), - DB::raw('SUM(CASE WHEN "lossProfit" < 0 THEN "lossProfit" ELSE 0 END) AS loss') - ) - ->groupBy('product_id') - ->limit($request->per_page ?? 20) - ->get(); + $product_lossProfits = SaleDetails::with('product:id,productName,productCode') + ->whereHas('product', function ($q) { + $q->where('business_id', auth()->user()->business_id); + }) + ->when($branchId, function ($q) use ($branchId) { + $q->whereHas('sale', function ($sale) use ($branchId) { + $sale->where('branch_id', $branchId); + }); + }) + ->select( + 'product_id', + DB::raw('SUM(CASE WHEN lossProfit > 0 THEN lossProfit ELSE 0 END) as profit'), + DB::raw('SUM(CASE WHEN lossProfit < 0 THEN lossProfit ELSE 0 END) as loss') + ) + ->groupBy('product_id') + ->get(); return PdfService::render('business::reports.product-loss-profit.pdf', compact('product_lossProfits'), 'product-loss-profit-report.pdf'); } diff --git a/Modules/Business/App/Http/Controllers/AcnooPurchaseController.php b/Modules/Business/App/Http/Controllers/AcnooPurchaseController.php index c8024758..e2dd118b 100644 --- a/Modules/Business/App/Http/Controllers/AcnooPurchaseController.php +++ b/Modules/Business/App/Http/Controllers/AcnooPurchaseController.php @@ -2,39 +2,34 @@ namespace Modules\Business\App\Http\Controllers; -use App\Events\MultiPaymentProcessed; -use App\Helpers\HasUploader; -use App\Http\Controllers\Controller; -use App\Imports\PurchaseProductImport; -use App\Models\Branch; +use Carbon\Carbon; +use App\Models\Vat; use App\Models\Brand; +use App\Models\Party; +use App\Models\Stock; +use App\Models\Branch; +use App\Models\Product; use App\Models\Business; use App\Models\Category; -use App\Models\Option; -use App\Models\Party; -use App\Models\PaymentType; -use App\Models\Product; use App\Models\Purchase; -use App\Models\PurchaseDetails; -use App\Models\PurchaseReturn; -use App\Models\PurchaseReturnDetail; -use App\Models\Stock; -use App\Models\Transaction; -use App\Models\Vat; -use App\Models\VatTransaction; use App\Models\Warehouse; -use App\Services\Invoice\PurchaseInvoiceService; -use App\Services\Invoice\PurchaseTaxInvoiceService; +use App\Events\PurchaseSms; +use App\Models\PaymentType; +use App\Models\Transaction; +use App\Helpers\HasUploader; use App\Services\PdfService; -use App\Services\PurchaseVatTransaction; -use Barryvdh\DomPDF\Facade\Pdf; -use Carbon\Carbon; -use Gloudemans\Shoppingcart\Facades\Cart; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Mail; +use App\Models\PurchaseReturn; +use App\Models\PurchaseDetails; +use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\DB; +use App\Http\Controllers\Controller; +use Illuminate\Support\Facades\Mail; use Maatwebsite\Excel\Facades\Excel; +use App\Events\MultiPaymentProcessed; +use App\Imports\PurchaseProductImport; +use Gloudemans\Shoppingcart\Facades\Cart; class AcnooPurchaseController extends Controller { @@ -94,16 +89,16 @@ public function index(Request $request) // Search Filter if ($request->filled('search')) { $purchasesQuery->where(function ($query) use ($request) { - $query->where('paymentType', 'like', '%'.$request->search.'%') - ->orWhere('invoiceNumber', 'like', '%'.$request->search.'%') + $query->where('paymentType', 'like', '%' . $request->search . '%') + ->orWhere('invoiceNumber', 'like', '%' . $request->search . '%') ->orWhereHas('party', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('payment_type', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('branch', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }); }); } @@ -111,6 +106,7 @@ public function index(Request $request) $perPage = $request->input('per_page', 20); $purchases = $purchasesQuery->latest()->paginate($perPage)->appends($request->query()); + if ($request->ajax()) { return response()->json([ 'data' => view('business::purchases.datas', compact('purchases', 'purchasesWithReturns'))->render(), @@ -126,7 +122,6 @@ public function index(Request $request) public function create() { $business_id = auth()->user()->business_id; - $activeBranchId = auth()->user()->active_branch?->id; // Clears all cart items Cart::destroy(); @@ -134,38 +129,27 @@ public function create() $suppliers = Party::where('type', 'Supplier') ->where('business_id', $business_id) ->latest() - ->get() - ->transform(function ($supplier) use ($activeBranchId) { - $supplier->due = $supplier->dueForBranch($activeBranchId); + ->get(); - return $supplier; - }); - - $products = Product::with(['category:id,categoryName', 'unit:id,unitName', 'brand:id,brandName']) + $products = Product::with(['category:id,categoryName', 'unit:id,unitName', 'brand:id,brandName',]) ->withSum('stocks', 'productStock') ->where('business_id', $business_id) ->whereNot('product_type', 'combo') ->latest() ->get(); - $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase'); + $cart_contents = Cart::content()->filter(fn($item) => $item->options->type == 'purchase'); $categories = Category::where('business_id', $business_id)->latest()->get(); $brands = Brand::where('business_id', $business_id)->latest()->get(); $vats = Vat::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $payment_types = PaymentType::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $warehouses = Warehouse::select('id', 'name')->where('business_id', $business_id)->latest()->get(); - $profit_option = Option::where('key', 'business-settings') - ->where('value', 'LIKE', '%"business_id":' . $business_id . '%') - ->get() - ->firstWhere('value.business_id', $business_id) - ->value['product_profit_option'] ?? ''; - // Generate a unique invoice number $purchase_id = (Purchase::max('id') ?? 0) + 1; - $invoice_no = 'P-'.str_pad($purchase_id, 5, '0', STR_PAD_LEFT); + $invoice_no = 'P-' . str_pad($purchase_id, 5, '0', STR_PAD_LEFT); - return view('business::purchases.create', compact('suppliers', 'products', 'cart_contents', 'invoice_no', 'categories', 'brands', 'vats', 'payment_types', 'warehouses', 'profit_option')); + return view('business::purchases.create', compact('suppliers', 'products', 'cart_contents', 'invoice_no', 'categories', 'brands', 'vats', 'payment_types', 'warehouses')); } public function store(Request $request) @@ -182,11 +166,10 @@ public function store(Request $request) ]); $user = auth()->user(); - $business = Business::findOrFail($user->business_id); $branch_id = $user->branch_id ?? $user->active_branch_id; // Check each cart item for batch duplication in Stock table - $carts = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase'); + $carts = Cart::content()->filter(fn($item) => $item->options->type == 'purchase'); if ($carts->count() < 1) { return response()->json(['message' => __('Cart is empty. Add items first!')], 400); @@ -195,27 +178,27 @@ public function store(Request $request) DB::beginTransaction(); try { // Subtotal - $subtotal = $carts->sum(fn ($cartItem) => (float) $cartItem->subtotal); + $subtotal = $carts->sum(fn($cartItem) => (float)$cartItem->subtotal); - // Discount (applied first on subtotal) + // VAT + $vat = Vat::find($request->vat_id); + $vatAmount = $vat ? ($subtotal * $vat->rate) / 100 : 0; + + // Subtotal with VAT + $subtotalWithVat = $subtotal + $vatAmount; + + // Discount $discountAmount = $request->discountAmount ?? 0; if ($request->discount_type === 'percent') { - $discountAmount = ($subtotal * $discountAmount) / 100; + $discountAmount = ($subtotalWithVat * $discountAmount) / 100; } - if ($discountAmount > $subtotal) { - return response()->json(['message' => __('Discount cannot be more than subtotal!')], 400); + if ($discountAmount > $subtotalWithVat) { + return response()->json(['message' => __('Discount cannot be more than subtotal with VAT!')], 400); } - // Subtotal after discount (before VAT) - $subtotalAfterDiscount = $subtotal - $discountAmount; - - // VAT (applied on subtotal after discount) - $vat = Vat::find($request->vat_id); - $vatAmount = $vat ? ($subtotalAfterDiscount * $vat->rate) / 100 : 0; - // Charges and totals $shippingCharge = $request->shipping_charge ?? 0; - $totalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge; + $totalAmount = $subtotalWithVat + $shippingCharge - $discountAmount; $receiveAmount = $request->receive_amount ?? 0; $changeAmount = max($receiveAmount - $totalAmount, 0); @@ -230,12 +213,12 @@ public function store(Request $request) $newTotalDue = $party->due + $dueAmount; if ($party->credit_limit > 0 && $newTotalDue > $party->credit_limit) { return response()->json([ - 'message' => __('Cannot create purchase. Party due will exceed credit limit!'), + 'message' => __('Cannot create purchase. Party due will exceed credit limit!') ], 400); } $party->update([ - 'due' => $newTotalDue, + 'due' => $newTotalDue ]); } @@ -244,103 +227,37 @@ public function store(Request $request) // Create Purchase $purchase = Purchase::create($request->except('discountAmount', 'discount_type', 'discount_percent', 'shipping_charge', 'payment_type_id') + [ - 'user_id' => $user->id, - 'vat_id' => $request->vat_id, - 'vat_amount' => $vatAmount, - 'business_id' => $user->business_id, - 'discountAmount' => $discountAmount, - 'discount_type' => $request->discount_type ?? 'flat', - 'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0, - 'totalAmount' => $totalAmount, - 'paidAmount' => $paidAmount, - 'change_amount' => $changeAmount, - 'dueAmount' => $dueAmount, - 'shipping_charge' => $shippingCharge, - 'purchaseDate' => $request->purchaseDate ? Carbon::parse($request->purchaseDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(), - 'isPaid' => $dueAmount > 0 ? 0 : 1, - ]); + 'user_id' => $user->id, + 'vat_id' => $request->vat_id, + 'vat_amount' => $vatAmount, + 'business_id' => $user->business_id, + 'discountAmount' => $discountAmount, + 'discount_type' => $request->discount_type ?? 'flat', + 'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0, + 'totalAmount' => $totalAmount, + 'paidAmount' => $paidAmount, + 'change_amount' => $changeAmount, + 'dueAmount' => $dueAmount, + 'shipping_charge' => $shippingCharge, + 'purchaseDate' => $request->purchaseDate ? Carbon::parse($request->purchaseDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(), + 'isPaid' => $dueAmount > 0 ? 0 : 1, + ]); + + $purchase_details = []; // Insert Purchase Details and Create Stocks foreach ($carts as $cartItem) { - $product = Product::findOrFail($cartItem->id); $batchNo = $cartItem->options['batch_no'] ?? null; $expireDate = $cartItem->options['expire_date'] ?? null; $variantName = $cartItem->options['variant_name'] ?? null; - $existingStock = Stock::where([ - 'business_id' => $user->business_id, - 'batch_no' => $batchNo, - 'product_id' => $cartItem->id, - 'branch_id' => $branch_id, - ]) - ->when($variantName, fn ($q) => $q->where('variant_name', $variantName)) + $existingStock = Stock::where(['batch_no' => $batchNo, 'product_id' => $cartItem->id]) + ->when($variantName, fn($q) => $q->where('variant_name', $variantName)) ->first(); $warehouseId = $cartItem->options['warehouse_id'] ?? ($existingStock->warehouse_id ?? null); - $newSerials = $cartItem->options['serial_numbers'] ?? []; - $existingSerials = $existingStock?->serial_numbers ?? []; - - // Only serials that don't already exist - $uniqueNewSerials = array_diff($newSerials, $existingSerials); - - if (! empty($newSerials) && count($uniqueNewSerials) === 0) { - return response()->json([ - 'success' => false, - 'message' => 'Serial already exists in stock', - 'duplicates' => array_values(array_intersect($newSerials, $existingSerials)), - ], 400); - } - - $mergedSerials = array_values( - array_unique(array_merge($existingSerials, $uniqueNewSerials)) - ); - - // qty depends on serial - $qtyToAdd = count($uniqueNewSerials) > 0 ? count($uniqueNewSerials) : $cartItem->qty; - - // price calculation - $vatRate = 0; - if ($product->vat_id) { - $vatData = Vat::find($product->vat_id); - $vatRate = $vatData?->rate ?? 0; - } - - $purchasePrice = $cartItem->price; - - // convert inclusive to exclusive - $exclusive_price = $vatRate > 0 - ? round($purchasePrice / (1 + ($vatRate / 100)), 4) - : $purchasePrice; - - $isSerialProduct = ! empty($newSerials); - - if ($isSerialProduct) { - // For serial products, use cart sales_price directly - $salesPrice = $cartItem->options['sales_price']; - $profitPercent = $cartItem->options['profit_percent']; - } else { - $profitPercent = ($cartItem->options['profit_percent'] ?? 0); - - $profitOption = Option::where('key', 'business-settings') - ->where('value', 'LIKE', '%"business_id":' . $business->id . '%') - ->get() - ->firstWhere('value.business_id', $business->id) - ->value['product_profit_option'] ?? 'markup'; - - if ($profitOption === 'margin') { - $salesPrice = $exclusive_price / (1 - ($profitPercent / 100)); - } else { - $salesPrice = $exclusive_price + ($exclusive_price * $profitPercent / 100); - } - - if ($product->vat_type === 'inclusive' && $vatRate > 0) { - $salesPrice = $salesPrice * (1 + ($vatRate / 100)); - } - $salesPrice = round($salesPrice, 2); - } - $stock = Stock::updateOrCreate( [ 'batch_no' => $batchNo, @@ -350,41 +267,33 @@ public function store(Request $request) 'business_id' => $user->business_id, ], [ - 'expire_date' => $expireDate, 'product_id' => $cartItem->id, - 'warehouse_id' => $warehouseId, - 'exclusive_price' => $exclusive_price, - 'productPurchasePrice' => $purchasePrice, - 'productSalePrice' => $salesPrice, - 'profit_percent' => $profitPercent, - 'productDealerPrice' => $cartItem->options['dealer_price'], - 'serial_numbers' => ! empty($newSerials) ? $mergedSerials : null, + 'expire_date' => $expireDate, + 'productPurchasePrice' => $cartItem->price, + 'productSalePrice' => $cartItem->options['sales_price'], 'productWholeSalePrice' => $cartItem->options['whole_sale_price'], + 'productDealerPrice' => $cartItem->options['dealer_price'], 'productStock' => $cartItem->qty + ($existingStock->productStock ?? 0), + 'warehouse_id' => $warehouseId, ] ); // purchase detail - $detail = PurchaseDetails::create([ + $purchase_details[] = [ 'stock_id' => $stock->id, 'purchase_id' => $purchase->id, 'product_id' => $cartItem->id, - 'quantities' => $qtyToAdd, - 'expire_date' => $expireDate, - 'productPurchasePrice' => $purchasePrice, - 'productSalePrice' => $salesPrice, - 'profit_percent' => $profitPercent, + 'quantities' => $cartItem->qty, + 'productPurchasePrice' => $cartItem->price, 'productDealerPrice' => $cartItem->options['dealer_price'], + 'expire_date' => $expireDate, + 'productSalePrice' => $cartItem->options['sales_price'], 'productWholeSalePrice' => $cartItem->options['whole_sale_price'], - 'serial_numbers' => ! empty($uniqueNewSerials) ? array_values($uniqueNewSerials) : null, - ]); - - $vatAmount = PurchaseVatTransaction::storeVatTransactions($business, $detail, $product, $party ?? null); - $detail->update([ - 'price_without_tax' => number_format($purchasePrice - ($vatAmount / $cartItem->qty), 3, '.', ''), - ]); + ]; } + PurchaseDetails::insert($purchase_details); + // MultiPaymentProcessed Event event(new MultiPaymentProcessed( $request->payments ?? [], @@ -399,26 +308,20 @@ public function store(Request $request) Cart::remove($cartItem->rowId); } + event(new PurchaseSms($purchase)); + sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('New Purchase created.'), $user->business_id); - if (moduleCheck('MarketingAddon')) { - $this->purchaseMessage($business, $party ?? null, $purchase); - } - - $tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($purchase->business_id) == 'standard_a4'; - $invoice_route = $tax_invoice ? route('business.purchases.tax.invoice', $purchase->id) : route('business.purchases.invoice', $purchase->id); - DB::commit(); return response()->json([ 'message' => __('Purchase created successfully.'), 'redirect' => route('business.purchases.index'), - 'secondary_redirect_url' => $invoice_route, + 'secondary_redirect_url' => route('business.purchases.invoice', $purchase->id), ]); } catch (\Exception $e) { DB::rollback(); - - return response()->json(['message' => $e->getMessage()], 500); + return response()->json(['message' => __('Something went wrong!')], 404); } } @@ -427,7 +330,6 @@ public function edit($id) // Clears all cart items Cart::destroy(); $business_id = auth()->user()->business_id; - $activeBranchId = auth()->user()->active_branch?->id; $purchase = Purchase::with('details', 'details.product', 'details.stock:id,batch_no', 'details.product.unit', 'payment_type:id,name', 'transactions') ->where('business_id', $business_id) @@ -436,14 +338,9 @@ public function edit($id) $suppliers = Party::where('type', 'Supplier') ->where('business_id', $business_id) ->latest() - ->get() - ->transform(function ($supplier) use ($activeBranchId) { - $supplier->due = $supplier->dueForBranch($activeBranchId); + ->get(); - return $supplier; - }); - - $products = Product::with(['category:id,categoryName', 'unit:id,unitName', 'brand:id,brandName']) + $products = Product::with(['category:id,categoryName', 'unit:id,unitName', 'brand:id,brandName',]) ->withSum('stocks', 'productStock') ->where('business_id', $business_id) ->whereNot('product_type', 'combo') @@ -456,12 +353,6 @@ public function edit($id) $payment_types = PaymentType::where('business_id', $business_id)->whereStatus(1)->latest()->get(); $warehouses = Warehouse::select('id', 'name')->where('business_id', $business_id)->latest()->get(); - $profit_option = Option::where('key', 'business-settings') - ->where('value', 'LIKE', '%"business_id":' . $business_id . '%') - ->get() - ->firstWhere('value.business_id', $business_id) - ->value['product_profit_option'] ?? ''; - // Add purchase details to the cart foreach ($purchase->details as $detail) { $stock = Stock::where('id', $detail->stock_id)->first(); @@ -478,23 +369,19 @@ public function edit($id) 'product_unit_name' => $detail->product->unit->unitName ?? '', 'product_image' => $detail->product->productPicture ?? '', 'sales_price' => $detail->productSalePrice, - 'profit_percent' => $detail->profit_percent, 'whole_sale_price' => $detail->productWholeSalePrice, 'dealer_price' => $detail->productDealerPrice, 'batch_no' => $detail->stock->batch_no ?? null, 'expire_date' => $detail->expire_date, 'variant_name' => $stock->variant_name ?? null, 'warehouse_id' => $stock->warehouse_id ?? null, - 'price_without_tax' => $detail->price_without_tax ?? 0, - 'serial_numbers' => $detail->serial_numbers ?? null, - 'has_serial' => $detail->product->has_serial ?? null, ], ]); } - $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase'); + $cart_contents = Cart::content()->filter(fn($item) => $item->options->type == 'purchase'); - return view('business::purchases.edit', compact('purchase', 'suppliers', 'products', 'cart_contents', 'categories', 'brands', 'vats', 'payment_types', 'warehouses', 'profit_option')); + return view('business::purchases.edit', compact('purchase', 'suppliers', 'products', 'cart_contents', 'categories', 'brands', 'vats', 'payment_types', 'warehouses')); } public function update(Request $request, $id) @@ -511,8 +398,9 @@ public function update(Request $request, $id) ]); $user = auth()->user(); - $business = Business::findOrFail($user->business_id); - $carts = Cart::content()->filter(fn ($item) => $item->options->type === 'purchase'); + $branch_id = $user->branch_id ?? $user->active_branch_id; + + $carts = Cart::content()->filter(fn($item) => $item->options->type === 'purchase'); if ($carts->count() < 1) { return response()->json(['message' => __('Cart is empty. Add items first!')], 400); @@ -523,34 +411,34 @@ public function update(Request $request, $id) $purchase = Purchase::with('details')->findOrFail($id); // Calculate amounts - $subtotal = $carts->sum(fn ($cartItem) => (float) $cartItem->subtotal); + $subtotal = $carts->sum(fn($cartItem) => (float)$cartItem->subtotal); - // Discount (applied first on subtotal) - $discountAmount = $request->discountAmount ?? 0; - if ($request->discount_type == 'percent') { - $discountAmount = ($subtotal * $discountAmount) / 100; - } - if ($discountAmount > $subtotal) { - return response()->json([ - 'message' => __('Discount cannot be more than subtotal!'), - ], 400); - } - - // Subtotal after discount (before VAT) - $subtotalAfterDiscount = $subtotal - $discountAmount; - - // VAT (applied on subtotal after discount) + // VAT $vat = Vat::find($request->vat_id); $vatAmount = 0; if ($vat) { - $vatAmount = ($subtotalAfterDiscount * $vat->rate) / 100; + $vatAmount = ($subtotal * $vat->rate) / 100; + } + + // Subtotal with VAT + $subtotalWithVat = $subtotal + $vatAmount; + + // Discount + $discountAmount = $request->discountAmount ?? 0; + if ($request->discount_type == 'percent') { + $discountAmount = ($subtotalWithVat * $discountAmount) / 100; + } + if ($discountAmount > $subtotalWithVat) { + return response()->json([ + 'message' => __('Discount cannot be more than subtotal with VAT!') + ], 400); } // Shipping Charge $shippingCharge = $request->shipping_charge ?? 0; // Total Amount - $totalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge; + $totalAmount = $subtotalWithVat - $discountAmount + $shippingCharge; // Receive, Change, Due Amount Calculation $receiveAmount = $request->receive_amount ?? 0; @@ -583,132 +471,48 @@ public function update(Request $request, $id) // Update purchase details $purchase->update($request->except('discountAmount', 'discount_type', 'discount_percent', 'shipping_charge') + [ - 'business_id' => $user->business_id, - 'user_id' => $user->id, - 'vat_id' => $request->vat_id, - 'vat_amount' => $vatAmount, - 'discountAmount' => $discountAmount, - 'totalAmount' => $totalAmount, - 'paidAmount' => $paidAmount, - 'change_amount' => $changeAmount, - 'dueAmount' => $dueAmount, - 'isPaid' => $dueAmount > 0 ? 0 : 1, - 'shipping_charge' => $shippingCharge, - 'payment_type_id' => $request->payment_type_id, - 'discount_type' => $request->discount_type ?? 'flat', - 'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0, - 'purchaseDate' => $request->purchaseDate ? Carbon::parse($request->purchaseDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(), - ]); + 'business_id' => $user->business_id, + 'user_id' => $user->id, + 'vat_id' => $request->vat_id, + 'vat_amount' => $vatAmount, + 'discountAmount' => $discountAmount, + 'discount_type' => $request->discount_type ?? 'flat', + 'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0, + 'totalAmount' => $totalAmount, + 'paidAmount' => $paidAmount, + 'change_amount' => $changeAmount, + 'dueAmount' => $dueAmount, + 'payment_type_id' => $request->payment_type_id, + 'shipping_charge' => $shippingCharge, + 'purchaseDate' => $request->purchaseDate ? Carbon::parse($request->purchaseDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(), + 'isPaid' => $dueAmount > 0 ? 0 : 1, + ]); // Revert previous stock changes foreach ($purchase->details as $detail) { - $stock = Stock::find($detail->stock_id); - if ($stock) { - if (! empty($detail->serial_numbers)) { - $stock->serial_numbers = array_values(array_diff($stock->serial_numbers, $detail->serial_numbers)); - $stock->productStock = count($stock->serial_numbers); - } else { - $stock->productStock -= $detail->quantities; - } - $stock->save(); - } + Stock::where('id', $detail->stock_id)->decrement('productStock', $detail->quantities); } - // Store old purchase details before deleting - $oldDetails = $purchase->details->keyBy('product_id'); - // Delete existing purchase details - $prev_details_ids = $oldDetails->pluck('id')->toArray(); - PurchaseDetails::where('purchase_id', $purchase->id)->delete(); - - // remove previous vat transactions - VatTransaction::where('vatable_id', $prev_details_ids) - ->where('vatable_type', PurchaseDetails::class) - ->delete(); + $purchase->details()->delete(); // Insert updated purchase details and adjust stock + $purchaseDetailsData = []; foreach ($carts as $cartItem) { - $product = Product::findOrFail($cartItem->id); - $batch_no = $cartItem->options['batch_no'] ?? null; - $expire_date = $cartItem->options['expire_date'] ?? null; + $batch_no = $cartItem->options['batch_no'] ?? NULL; + $expire_date = $cartItem->options['expire_date'] ?? NULL; + $dealer_price = $cartItem->options['dealer_price'] ?? 0.0; + $sales_price = $cartItem->options['sales_price'] ?? 0.0; + $whole_sale_price = $cartItem->options['whole_sale_price'] ?? 0.0; $variantName = $cartItem->options['variant_name'] ?? null; - $dealer_price = $cartItem->options['dealer_price'] ?? 0.0; - $whole_sale_price = $cartItem->options['whole_sale_price'] ?? 0.0; - $existingStock = Stock::where(['batch_no' => $batch_no, 'product_id' => $cartItem->id]) - ->when($variantName, fn ($q) => $q->where('variant_name', $variantName)) + ->when($variantName, fn($q) => $q->where('variant_name', $variantName)) ->first(); $warehouseId = $cartItem->options['warehouse_id'] ?? ($existingStock->warehouse_id ?? null); - // serial logic - $newSerials = $cartItem->options['serial_numbers'] ?? []; - $purchaseSerials = ! empty($newSerials) ? array_values($newSerials) : null; - $existingSerials = $existingStock?->serial_numbers ?? []; - - // get previous purchase serials - $oldPurchaseSerials = $oldDetails[$cartItem->id]->serial_numbers ?? []; - - $remainingSerials = array_diff($existingSerials, $oldPurchaseSerials); - - $mergedSerials = ! empty($newSerials) - ? array_values(array_unique(array_merge($remainingSerials, $newSerials))) - : array_values($remainingSerials); - - if (! empty($newSerials)) { - // Serial Product - $stockQty = count($mergedSerials); - $purchaseQty = count($newSerials); - - } else { - // Non Serial Product - $stockQty = ($existingStock->productStock ?? 0) + $cartItem->qty; - $purchaseQty = $cartItem->qty; - } - - // Price Calculation - - $purchasePrice = $cartItem->price; - // Exclusive Price - $exclusive_price = $purchasePrice; - $vatRate = 0; - if ($product->vat_id) { - $vat = Vat::find($product->vat_id); - $vatRate = $vat?->rate ?? 0; - if ($vatRate > 0) { - $exclusive_price = round($purchasePrice / (1 + ($vatRate / 100)), 3); - } - } - - $isSerialProduct = ! empty($newSerials); - - if ($isSerialProduct) { - // For serial products, use cart sales_price directly - $salesPrice = $cartItem->options['sales_price'] ?? $purchasePrice; - $profitPercent = 0; - } else { - $profitPercent = $cartItem->options['profit_percent'] ?? 0; - - $profitOption = Option::where('key', 'business-settings') - ->where('value', 'LIKE', '%"business_id":' . $business->id . '%') - ->get() - ->firstWhere('value.business_id', $business->id) - ->value['product_profit_option'] ?? 'markup'; - - if ($profitOption === 'margin') { - $salesPrice = $exclusive_price / (1 - ($profitPercent / 100)); - } else { - $salesPrice = $exclusive_price + ($exclusive_price * $profitPercent / 100); - } - - if ($product->vat_type === 'inclusive' && $vatRate > 0) { - $salesPrice = $salesPrice * (1 + ($vatRate / 100)); - } - $salesPrice = round($salesPrice, 3); - } - // update or create stock $stock = Stock::updateOrCreate( [ @@ -720,35 +524,37 @@ public function update(Request $request, $id) [ 'product_id' => $cartItem->id, 'expire_date' => $expire_date, + 'productSalePrice' => $sales_price, 'productDealerPrice' => $dealer_price, - 'productPurchasePrice' => $purchasePrice, - 'exclusive_price' => $exclusive_price, - 'profit_percent' => $profitPercent, - 'productSalePrice' => $salesPrice, + 'productPurchasePrice' => $cartItem->price, 'productWholeSalePrice' => $whole_sale_price, - 'productStock' => $stockQty, + 'productStock' => $cartItem->qty + ($existingStock->productStock ?? 0), 'warehouse_id' => $warehouseId, - 'serial_numbers' => $mergedSerials, ] ); - $detail = PurchaseDetails::create([ + $purchaseDetailsData[] = [ 'purchase_id' => $purchase->id, 'product_id' => $cartItem->id, - 'quantities' => $purchaseQty, + 'quantities' => $cartItem->qty, + 'productPurchasePrice' => $cartItem->price, + 'productDealerPrice' => $dealer_price, 'stock_id' => $stock->id, 'expire_date' => $expire_date, - 'productPurchasePrice' => $purchasePrice, - 'profit_percent' => $profitPercent, - 'productSalePrice' => $salesPrice, - 'serial_numbers' => $purchaseSerials, - 'productDealerPrice' => $dealer_price, + 'productSalePrice' => $sales_price, 'productWholeSalePrice' => $whole_sale_price, - ]); + ]; + } - $vatAmount = PurchaseVatTransaction::storeVatTransactions($business, $detail, $product, $party ?? null); - $detail->update([ - 'price_without_tax' => number_format($purchasePrice - ($vatAmount / $purchaseQty), 3, '.', ''), + PurchaseDetails::insert($purchaseDetailsData); + + foreach ($purchaseDetailsData as $item) { + $product = Product::findOrFail($item['product_id']); + $product->update([ + 'productSalePrice' => $item['productSalePrice'] ?? $product->productSalePrice, + 'productDealerPrice' => $item['productDealerPrice'] ?? $product->productDealerPrice, + 'productPurchasePrice' => $item['productPurchasePrice'] ?? $product->productPurchasePrice, + 'productWholeSalePrice' => $item['productWholeSalePrice'] ?? $product->productWholeSalePrice, ]); } @@ -803,20 +609,15 @@ public function update(Request $request, $id) sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('Purchase has been updated.'), $user->business_id); - $tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($purchase->business_id) == 'standard_a4'; - $invoice_route = $tax_invoice ? route('business.purchases.tax.invoice', $purchase->id) : route('business.purchases.invoice', $purchase->id); - DB::commit(); - return response()->json([ 'message' => __('Purchase updated successfully.'), 'redirect' => route('business.purchases.index'), - 'secondary_redirect_url' => $invoice_route, + 'secondary_redirect_url' => route('business.purchases.invoice', $purchase->id), ]); } catch (\Exception $e) { DB::rollback(); - - return response()->json(['message' => $e->getMessage()], 500); + return response()->json(['message' => __('Something went wrong!')], 500); } } @@ -827,17 +628,13 @@ public function destroy($id) $purchase = Purchase::with('details')->findOrFail($id); foreach ($purchase->details as $detail) { - VatTransaction::where('vatable_id', $detail->id) - ->where('vatable_type', get_class($detail)) - ->delete(); - Stock::where('id', $detail->stock_id)->decrement('productStock', $detail->quantities); } if ($purchase->party_id) { $party = Party::findOrFail($purchase->party_id); $party->update([ - 'due' => $party->due - $purchase->dueAmount, + 'due' => $party->due - $purchase->dueAmount ]); } @@ -854,12 +651,11 @@ public function destroy($id) return response()->json([ 'message' => __('Purchase deleted successfully.'), - 'redirect' => route('business.purchases.index'), + 'redirect' => route('business.purchases.index') ]); } catch (\Exception $e) { DB::rollback(); - - return response()->json(['message' => $e->getMessage()], 404); + return response()->json(['message' => __('Something went wrong!')], 404); } } @@ -868,25 +664,21 @@ public function deleteAll(Request $request) DB::beginTransaction(); try { - $purchases = Purchase::where('business_id', auth()->user()->business_id)->whereIn('id', $request->ids)->get(); + $purchases = Purchase::whereIn('id', $request->ids)->get(); foreach ($purchases as $purchase) { foreach ($purchase->details as $detail) { - VatTransaction::where('vatable_id', $detail->id) - ->where('vatable_type', get_class($detail)) - ->delete(); - Stock::where('id', $detail->stock_id)->decrement('productStock', $detail->quantities); } if ($purchase->party_id) { $party = Party::findOrFail($purchase->party_id); $party->update([ - 'due' => $party->due - $purchase->dueAmount, + 'due' => $party->due - $purchase->dueAmount ]); } - updateBalance($purchase->paidAmount, 'increment'); + updateBalance($purchases->paidAmount, 'decrement'); sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('Purchases has been deleted.'), $purchase->business_id); @@ -900,12 +692,11 @@ public function deleteAll(Request $request) return response()->json([ 'message' => __('Selected purchases deleted successfully.'), - 'redirect' => route('business.purchases.index'), + 'redirect' => route('business.purchases.index') ]); } catch (\Exception $e) { DB::rollback(); - - return response()->json(['message' => $e->getMessage()], 404); + return response()->json(['message' => __('Something went wrong!')], 404); } } @@ -955,16 +746,16 @@ public function acnooFilter(Request $request) // Search Filter if ($request->filled('search')) { $purchasesQuery->where(function ($query) use ($request) { - $query->where('paymentType', 'like', '%'.$request->search.'%') - ->orWhere('invoiceNumber', 'like', '%'.$request->search.'%') + $query->where('paymentType', 'like', '%' . $request->search . '%') + ->orWhere('invoiceNumber', 'like', '%' . $request->search . '%') ->orWhereHas('party', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('payment_type', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('branch', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }); }); } @@ -974,7 +765,7 @@ public function acnooFilter(Request $request) if ($request->ajax()) { return response()->json([ - 'data' => view('business::purchases.datas', compact('purchases', 'purchasesWithReturns'))->render(), + 'data' => view('business::purchases.datas', compact('purchases', 'purchasesWithReturns'))->render() ]); } @@ -987,8 +778,8 @@ public function productFilter(Request $request) $products = Product::where('business_id', $business_id) ->when($request->search, function ($query) use ($request) { $query->where(function ($q) use ($request) { - $q->where('productName', 'like', '%'.$request->search.'%') - ->orWhere('productCode', 'like', '%'.$request->search.'%'); + $q->where('productName', 'like', '%' . $request->search . '%') + ->orWhere('productCode', 'like', '%' . $request->search . '%'); }); }) ->when($request->category_id, function ($query) use ($request) { @@ -1004,14 +795,14 @@ public function productFilter(Request $request) // Query categories for search options $categories = Category::where('business_id', $business_id) ->when($request->search, function ($query) use ($request) { - $query->where('categoryName', 'like', '%'.$request->search.'%'); + $query->where('categoryName', 'like', '%' . $request->search . '%'); }) ->get(); // Query brands for search options $brands = Brand::where('business_id', $business_id) ->when($request->search, function ($query) use ($request) { - $query->where('brandName', 'like', '%'.$request->search.'%'); + $query->where('brandName', 'like', '%' . $request->search . '%'); }) ->get(); @@ -1036,7 +827,7 @@ public function categoryFilter(Request $request) $search = $request->search; $categories = Category::where('business_id', auth()->user()->business_id) ->when($search, function ($query) use ($search) { - $query->where('categoryName', 'like', '%'.$search.'%'); + $query->where('categoryName', 'like', '%' . $search . '%'); }) ->get(); @@ -1051,7 +842,7 @@ public function brandFilter(Request $request) $search = $request->search; $brands = Brand::where('business_id', auth()->user()->business_id) ->when($search, function ($query) use ($search) { - $query->where('brandName', 'like', '%'.$search.'%'); + $query->where('brandName', 'like', '%' . $search . '%'); }) ->get(); @@ -1060,30 +851,90 @@ public function brandFilter(Request $request) ]); } - public function getInvoice($purchase_id, PurchaseInvoiceService $service) + public function getInvoice($purchase_id) { - $data = $service->getInvoiceData($purchase_id); + $purchase = Purchase::with('user:id,name,role', 'party:id,name,phone,address', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,productPurchasePrice,quantities,product_id,purchase_id', 'details.stock:id,batch_no', 'details.product:id,productName', 'payment_type:id,name') + ->findOrFail($purchase_id); - return view('business::purchases.invoice', $data); - } + $transactionTypes = $purchase->transactions + ->map(function ($transaction) { + if ($transaction->transaction_type === 'bank_payment' && !empty($transaction->paymentType?->name)) { + return $transaction->paymentType->name; + } + return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : ''; + }) + ->unique() + ->implode(', '); - public function getTaxInvoice($purchase_id, PurchaseTaxInvoiceService $service) - { - $data = $service->getInvoiceData($purchase_id); + $purchase_returns = PurchaseReturn::with('purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'purchase.party:id,name', 'details', 'details.purchaseDetail.product:id,productName', 'details.purchaseDetail.stock:id,batch_no') + ->where('business_id', auth()->user()->business_id) + ->where('purchase_id', $purchase_id) + ->latest() + ->get(); - return view('taxinvoiceaddon::purchases.tax', $data); + $returnTransaction = $purchase_returns->first()?->transactions->first(); + $returnTransactionType = ''; + + if ($returnTransaction) { + $returnTransactionType = $returnTransaction->transaction_type === 'bank_payment' + ? ($returnTransaction->paymentType?->name) + : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); + } + + // sum of return_qty + $purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) { + $return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) { + return $return->details->where('purchaseDetail.id', $detail->id)->pluck('return_qty'); + })->sum(); + + $detail->quantities = $detail->quantities + $return_qty_sum; + + return $detail; + }); + + // Calculate total discount based on return quantities and amounts + $total_discount = 0; + $product_discounts = []; + + foreach ($purchase_returns as $return) { + foreach ($return->details as $detail) { + // Initialize discount tracking for the first occurrence of each purchase_detail_id + if (!isset($product_discounts[$detail->purchase_detail_id])) { + $product_discounts[$detail->purchase_detail_id] = [ + 'return_qty' => 0, + 'return_amount' => 0, + 'price' => $detail->purchaseDetail->productPurchasePrice, + ]; + } + + // Accumulate return quantities and return amounts + $product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty; + $product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount; + } + } + + // Calculate the total discount for each returned product + foreach ($product_discounts as $data) { + $product_price = $data['price'] * $data['return_qty']; + $discount = $product_price - $data['return_amount']; + + $total_discount += $discount; + } + + $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); + + return view('business::purchases.invoice', compact('purchase', 'purchase_returns', 'total_discount', 'transactionTypes', 'bank_detail', 'returnTransactionType')); } public function showPurchaseCart() { - $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase'); - + $cart_contents = Cart::content()->filter(fn($item) => $item->options->type == 'purchase'); return view('business::purchases.cart-list', compact('cart_contents')); } public function getCartData() { - $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase'); + $cart_contents = Cart::content()->filter(fn($item) => $item->options->type == 'purchase'); $data['sub_total'] = 0; foreach ($cart_contents as $cart) { @@ -1119,13 +970,13 @@ public function generatePDF($purchase_id) * Purchase returns */ $purchase_returns = PurchaseReturn::with( - 'purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', - 'purchase.party:id,name', - 'details', - 'details.purchaseDetail:id,productPurchasePrice,product_id,stock_id', - 'details.purchaseDetail.product:id,productName', - 'details.purchaseDetail.stock:id,batch_no' - ) + 'purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', + 'purchase.party:id,name', + 'details', + 'details.purchaseDetail:id,productPurchasePrice,product_id,stock_id', + 'details.purchaseDetail.product:id,productName', + 'details.purchaseDetail.stock:id,batch_no' + ) ->where('business_id', auth()->user()->business_id) ->where('purchase_id', $purchase_id) ->latest() @@ -1155,7 +1006,6 @@ public function generatePDF($purchase_id) })->sum(); $detail->quantities += $return_qty_sum; - return $detail; }); @@ -1167,7 +1017,7 @@ public function generatePDF($purchase_id) foreach ($purchase_returns as $return) { foreach ($return->details as $detail) { - if (! isset($product_discounts[$detail->purchase_detail_id])) { + if (!isset($product_discounts[$detail->purchase_detail_id])) { $product_discounts[$detail->purchase_detail_id] = [ 'return_qty' => 0, 'return_amount' => 0, @@ -1209,150 +1059,6 @@ public function generatePDF($purchase_id) ); } - public function generateTaxPDF($purchase_id) - { - $purchase = Purchase::where('business_id', auth()->user()->business_id) - ->with( - 'user:id,name,role', 'party:id,name,phone,address,email', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price_without_tax,productPurchasePrice,quantities,product_id,purchase_id,serial_numbers', 'details.stock:id,batch_no', 'details.product:id,vat_id,productName,has_serial', 'payment_type:id,name' - ) - ->findOrFail($purchase_id); - - $business_id = auth()->check() ? auth()->user()->business_id : null; - - /** - * Transaction types (same helper used in Sale) - */ - $detail_ids = $purchase->details->pluck('id'); - $tax_items = VatTransaction::selectRaw('vat_id, vat_rate, SUM(vat_amount) as vat_amount') - ->with('vat:id,name') - ->whereIn('vatable_id', $detail_ids) - ->where('vatable_type', PurchaseDetails::class) - ->groupBy('vat_id', 'vat_rate') - ->get(); - - $vatRowMap = VatTransaction::whereIn('vatable_id', $detail_ids) - ->where('vatable_type', PurchaseDetails::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - $transactionTypes = transaction_types($purchase->transactions); - - /** - * Purchase returns - */ - $purchase_returns = PurchaseReturn::with( - 'purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'purchase.party:id,name,email', 'details', 'details.purchaseDetail.product:id,productName,has_serial', 'details.purchaseDetail.stock:id,batch_no' - ) - ->where('business_id', auth()->user()->business_id) - ->where('purchase_id', $purchase_id) - ->latest() - ->get(); - - // collect return detail ids - $return_detail_ids = $purchase_returns->flatMap(function ($return) { - return $return->details->pluck('id'); - }); - - // get VAT for purchase return details - $returnVatRowMap = VatTransaction::whereIn('vatable_id', $return_detail_ids) - ->where('vatable_type', PurchaseReturnDetail::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - /** - * Return transaction type (same logic as Sale) - */ - $returnTransaction = $purchase_returns->first()?->transactions->first(); - $returnTransactionType = ''; - - if ($returnTransaction) { - $returnTransactionType = - $returnTransaction->transaction_type === 'bank_payment' - ? ($returnTransaction->paymentType?->name) - : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); - } - - /** - * Merge return qty into purchase details (same as Sale) - */ - $purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) { - $return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) { - return $return->details - ->where('purchaseDetail.id', $detail->id) - ->pluck('return_qty'); - })->sum(); - - $detail->quantities += $return_qty_sum; - - return $detail; - }); - - /** - * Discount calculation from returned items (same pattern as Sale) - */ - $total_discount = 0; - $product_discounts = []; - - foreach ($purchase_returns as $return) { - foreach ($return->details as $detail) { - if (! isset($product_discounts[$detail->purchase_detail_id])) { - $product_discounts[$detail->purchase_detail_id] = [ - 'return_qty' => 0, - 'return_amount' => 0, - 'price' => $detail->purchaseDetail->productPurchasePrice, - ]; - } - - $product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty; - $product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount; - } - } - - foreach ($product_discounts as $data) { - $product_price = $data['price'] * $data['return_qty']; - $total_discount += ($product_price - $data['return_amount']); - } - - /** - * Bank / Payment info - */ - $bank_detail = PaymentType::where('business_id', auth()->user()->business_id) - ->latest() - ->first(); - - /** - * CENTRALIZED PDF RENDER (same as Sale) - */ - return PdfService::render( - 'taxinvoiceaddon::purchases.pdf', - compact( - 'purchase', - 'purchase_returns', - 'total_discount', - 'transactionTypes', - 'returnTransactionType', - 'bank_detail', - 'tax_items', - 'business_id', - 'vatRowMap', - 'returnVatRowMap' - ), - 'purchase-tax-invoice.pdf' - ); - } - public function sendMail($purchase_id) { $purchase = Purchase::with('user:id,name,role', 'party:id,name,phone,address', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,productPurchasePrice,quantities,product_id,purchase_id', 'details.stock:id,batch_no', 'details.product:id,productName', 'payment_type:id,name') @@ -1360,10 +1066,9 @@ public function sendMail($purchase_id) $transactionTypes = $purchase->transactions ->map(function ($transaction) { - if ($transaction->transaction_type === 'bank_payment' && ! empty($transaction->paymentType?->name)) { + if ($transaction->transaction_type === 'bank_payment' && !empty($transaction->paymentType?->name)) { return $transaction->paymentType->name; } - return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : ''; }) ->unique() @@ -1402,7 +1107,7 @@ public function sendMail($purchase_id) foreach ($purchase_returns as $return) { foreach ($return->details as $detail) { // Initialize discount tracking for the first occurrence of each purchase_detail_id - if (! isset($product_discounts[$detail->purchase_detail_id])) { + if (!isset($product_discounts[$detail->purchase_detail_id])) { $product_discounts[$detail->purchase_detail_id] = [ 'return_qty' => 0, 'return_amount' => 0, @@ -1443,137 +1148,10 @@ public function sendMail($purchase_id) ]); } - public function sendTaxMail($purchase_id) - { - $purchase = Purchase::with('user:id,name,role', 'party:id,name,phone,address,email', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price_without_tax,productPurchasePrice,quantities,product_id,purchase_id,serial_numbers', 'details.stock:id,batch_no', 'details.product:id,vat_id,productName,has_serial', 'payment_type:id,name') - ->findOrFail($purchase_id); - - $business_id = auth()->check() ? auth()->user()->business_id : null; - - $detail_ids = $purchase->details->pluck('id'); - $tax_items = VatTransaction::selectRaw('vat_id, vat_rate, SUM(vat_amount) as vat_amount') - ->with('vat:id,name') - ->whereIn('vatable_id', $detail_ids) - ->where('vatable_type', PurchaseDetails::class) - ->groupBy('vat_id', 'vat_rate') - ->get(); - - $vatRowMap = VatTransaction::whereIn('vatable_id', $detail_ids) - ->where('vatable_type', PurchaseDetails::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - $transactionTypes = $purchase->transactions - ->map(function ($transaction) { - if ($transaction->transaction_type === 'bank_payment' && ! empty($transaction->paymentType?->name)) { - return $transaction->paymentType->name; - } - - return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : ''; - }) - ->unique() - ->implode(', '); - - $purchase_returns = PurchaseReturn::with('purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'purchase.party:id,name,email', 'details', 'details.purchaseDetail.product:id,productName,has_serial', 'details.purchaseDetail.stock:id,batch_no') - ->where('business_id', auth()->user()->business_id) - ->where('purchase_id', $purchase_id) - ->latest() - ->get(); - - $return_detail_ids = $purchase_returns->flatMap(function ($return) { - return $return->details->pluck('id'); - }); - - // get VAT for purchase return details - $returnVatRowMap = VatTransaction::whereIn('vatable_id', $return_detail_ids) - ->where('vatable_type', PurchaseReturnDetail::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - $returnTransaction = $purchase_returns->first()?->transactions->first(); - $returnTransactionType = ''; - - if ($returnTransaction) { - $returnTransactionType = $returnTransaction->transaction_type === 'bank_payment' - ? ($returnTransaction->paymentType?->name) - : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); - } - - // sum of return_qty - $purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) { - $return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) { - return $return->details->where('purchaseDetail.id', $detail->id)->pluck('return_qty'); - })->sum(); - - $detail->quantities = $detail->quantities + $return_qty_sum; - - return $detail; - }); - - // Calculate total discount based on return quantities and amounts - $total_discount = 0; - $product_discounts = []; - - foreach ($purchase_returns as $return) { - foreach ($return->details as $detail) { - // Initialize discount tracking for the first occurrence of each purchase_detail_id - if (! isset($product_discounts[$detail->purchase_detail_id])) { - $product_discounts[$detail->purchase_detail_id] = [ - 'return_qty' => 0, - 'return_amount' => 0, - 'price' => $detail->purchaseDetail->productPurchasePrice, - ]; - } - - // Accumulate return quantities and return amounts - $product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty; - $product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount; - } - } - - // Calculate the total discount for each returned product - foreach ($product_discounts as $data) { - $product_price = $data['price'] * $data['return_qty']; - $discount = $product_price - $data['return_amount']; - - $total_discount += $discount; - } - - $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); - - $pdf = Pdf::loadView('taxinvoiceaddon::purchases.pdf', compact('purchase', 'purchase_returns', 'total_discount', 'transactionTypes', 'bank_detail', 'returnTransactionType', 'tax_items', 'business_id', 'vatRowMap', 'returnVatRowMap')); - - // Send email with PDF attachment - Mail::raw('Please find attached your Purchase invoice.', function ($message) use ($pdf) { - $message->to(auth()->user()->email) - ->subject('Purchase Invoice') - ->attachData($pdf->output(), 'purchase-tax-invoice.pdf', [ - 'mime' => 'application/pdf', - ]); - }); - - return response()->json([ - 'message' => __('Email Sent Successfully.'), - 'redirect' => route('business.purchases.index'), - ]); - } - public function createSupplier(Request $request) { $request->validate([ - 'phone' => 'nullable|max:20|'.Rule::unique('parties')->where('business_id', auth()->user()->business_id), + 'phone' => 'nullable|max:20|' . Rule::unique('parties')->where('business_id', auth()->user()->business_id), 'name' => 'required|string|max:255', 'type' => 'required|string|in:Retailer,Dealer,Wholesaler,Supplier', 'email' => 'nullable|email', @@ -1583,14 +1161,14 @@ public function createSupplier(Request $request) ]); Party::create($request->except('image', 'due') + [ - 'due' => $request->due ?? 0, - 'image' => $request->image ? $this->upload($request, 'image') : null, - 'business_id' => auth()->user()->business_id, - ]); + 'due' => $request->due ?? 0, + 'image' => $request->image ? $this->upload($request, 'image') : NULL, + 'business_id' => auth()->user()->business_id + ]); return response()->json([ 'message' => __('Supplier created successfully'), - 'redirect' => route('business.purchases.create'), + 'redirect' => route('business.purchases.create') ]); } @@ -1602,7 +1180,7 @@ public function bulkIndex() public function bulkCartStore(Request $request) { $request->validate([ - 'file' => 'required|file|mimes:xlsx,xls,csv', + 'file' => 'required|file|mimes:xlsx,xls,csv' ]); $businessId = auth()->user()->business_id; @@ -1620,7 +1198,7 @@ public function bulkCartStore(Request $request) return response()->json([ 'success' => true, 'message' => __('Bulk upload successfully.'), - 'redirect' => route('business.purchases.create'), + 'redirect' => route('business.purchases.create') ]); } diff --git a/Modules/Business/App/Http/Controllers/AcnooSaleController.php b/Modules/Business/App/Http/Controllers/AcnooSaleController.php index 0c61354b..8de01143 100644 --- a/Modules/Business/App/Http/Controllers/AcnooSaleController.php +++ b/Modules/Business/App/Http/Controllers/AcnooSaleController.php @@ -2,40 +2,35 @@ namespace Modules\Business\App\Http\Controllers; -use App\Events\MultiPaymentProcessed; -use App\Helpers\HasUploader; -use App\Http\Controllers\Controller; -use App\Models\Branch; -use App\Models\Brand; -use App\Models\Business; -use App\Models\Category; -use App\Models\DueCollect; -use App\Models\Expense; -use App\Models\Income; -use App\Models\Party; -use App\Models\PaymentType; -use App\Models\Product; -use App\Models\Purchase; -use App\Models\Sale; -use App\Models\SaleDetails; -use App\Models\SaleReturn; -use App\Models\SaleReturnDetails; -use App\Models\Stock; -use App\Models\Transaction; -use App\Models\Vat; -use App\Models\VatTransaction; -use App\Models\Warehouse; -use App\Services\Invoice\SaleInvoiceService; -use App\Services\Invoice\SaleTaxInvoiceService; -use App\Services\PdfService; -use App\Services\VatTransactionService; -use Barryvdh\DomPDF\Facade\Pdf; use Carbon\Carbon; -use Gloudemans\Shoppingcart\Facades\Cart; +use App\Models\Vat; +use App\Models\Sale; +use App\Models\Brand; +use App\Models\Party; +use App\Models\Stock; +use App\Models\Branch; +use App\Models\Income; +use App\Events\SaleSms; +use App\Models\Expense; +use App\Models\Product; +use App\Models\Category; +use App\Models\Purchase; +use App\Models\Warehouse; +use App\Models\DueCollect; +use App\Models\SaleReturn; +use App\Models\PaymentType; +use App\Models\SaleDetails; +use App\Models\Transaction; +use App\Helpers\HasUploader; +use App\Services\PdfService; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Mail; +use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\DB; +use App\Http\Controllers\Controller; +use Illuminate\Support\Facades\Mail; +use App\Events\MultiPaymentProcessed; +use Gloudemans\Shoppingcart\Facades\Cart; class AcnooSaleController extends Controller { @@ -94,16 +89,16 @@ public function index(Request $request) // Search Filter if ($request->filled('search')) { $salesQuery->where(function ($query) use ($request) { - $query->where('paymentType', 'like', '%'.$request->search.'%') - ->orWhere('invoiceNumber', 'like', '%'.$request->search.'%') + $query->where('paymentType', 'like', '%' . $request->search . '%') + ->orWhere('invoiceNumber', 'like', '%' . $request->search . '%') ->orWhereHas('party', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('payment_type', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('branch', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }); }); } @@ -113,7 +108,7 @@ public function index(Request $request) if ($request->ajax()) { return response()->json([ - 'data' => view('business::sales.datas', compact('sales', 'salesWithReturns'))->render(), + 'data' => view('business::sales.datas', compact('sales', 'salesWithReturns'))->render() ]); } @@ -126,20 +121,14 @@ public function index(Request $request) public function create() { $business_id = auth()->user()->business_id; - $activeBranchId = auth()->user()->active_branch?->id; // Clears all cart items Cart::destroy(); - $customers = Party::where('type', '!=', 'Supplier') + $customers = Party::where('type', '!=', 'supplier') ->where('business_id', $business_id) ->latest() - ->get() - ->transform(function ($customer) use ($activeBranchId) { - $customer->due = $customer->dueForBranch($activeBranchId); - - return $customer; - }); + ->get(); $products = Product::with([ 'unit:id,unitName', @@ -150,12 +139,13 @@ public function create() 'stocks', 'combo_products.stock.product:id,productName,productCode', 'rack:id,name', - 'shelf:id,name', + 'shelf:id,name' ]) ->withSum('saleDetails', 'quantities') ->withSum('purchaseDetails', 'quantities') ->withSum('stocks', 'productStock') ->where('business_id', $business_id) + ->where('is_displayed_in_pos', 1) ->where(function ($query) { $query->where(function ($q) { $q->where('product_type', '!=', 'combo') @@ -179,7 +169,7 @@ public function create() // Generate a unique invoice number $sale_id = (Sale::max('id') ?? 0) + 1; - $invoice_no = 'S-'.str_pad($sale_id, 5, '0', STR_PAD_LEFT); + $invoice_no = 'S-' . str_pad($sale_id, 5, '0', STR_PAD_LEFT); return view('business::sales.create', compact('customers', 'products', 'invoice_no', 'categories', 'brands', 'vats', 'payment_types', 'warehouses')); } @@ -198,10 +188,10 @@ public function store(Request $request) 'saleDate' => 'nullable|date', ]); - $business = Business::findOrFail(auth()->user()->business_id); + $business_id = auth()->user()->business_id; // Get only 'sale' type items from cart - $carts = Cart::content()->filter(fn ($item) => $item->options->type == 'sale'); + $carts = Cart::content()->filter(fn($item) => $item->options->type == 'sale'); if ($carts->count() < 1) { return response()->json(['message' => __('Cart is empty. Add items first!')], 400); @@ -210,36 +200,33 @@ public function store(Request $request) DB::beginTransaction(); try { // Calculation: subtotal, vat, discount, shipping, rounding - $rawSubtotal = $carts->sum(fn ($item) => (float) $item->subtotal); + $rawSubtotal = $carts->sum(fn($item) => (float) $item->subtotal); // Minus each cart discount from subtotal $cartWiseDiscountTotal = $carts->sum(function ($item) { $dis = (float) ($item->options->discount ?? 0); - return $dis * $item->qty; }); $subtotal = $rawSubtotal - $cartWiseDiscountTotal; - // overall discount (applied first on subtotal) + $vatAmount = $carts->sum(function ($item) { + return ($item->price * $item->qty) * ($item->options->vat_percent ?? 0) / 100; + }); + + // overall discount $discountAmount = $request->discountAmount ?? 0; + $subtotalWithVat = $subtotal + $vatAmount; if ($request->discount_type == 'percent') { - $discountAmount = ($subtotal * $discountAmount) / 100; + $discountAmount = ($subtotalWithVat * $discountAmount) / 100; } - if ($discountAmount > $subtotal) { - return response()->json(['message' => __('Discount cannot be more than subtotal!')], 400); + if ($discountAmount > $subtotalWithVat) { + return response()->json(['message' => __('Discount cannot be more than subtotal with VAT!')], 400); } - // Subtotal after discount (before VAT) - $subtotalAfterDiscount = $subtotal - $discountAmount; - - // VAT (applied on subtotal after discount) - $vat = Vat::find($request->vat_id); - $vatAmount = $vat ? ($subtotalAfterDiscount * $vat->rate) / 100 : 0; - $shippingCharge = $request->shipping_charge ?? 0; - $actualTotalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge; + $actualTotalAmount = $subtotalWithVat - $discountAmount + $shippingCharge; $roundingTotalAmount = sale_rounding($actualTotalAmount); $rounding_amount = $roundingTotalAmount - $actualTotalAmount; $rounding_option = sale_rounding(); @@ -253,8 +240,8 @@ public function store(Request $request) } $receiveAmount = collect($payments) - ->reject(fn ($p) => strtolower($p['type'] ?? '') === 'cheque') - ->sum(fn ($p) => $p['amount'] ?? 0); + ->reject(fn($p) => strtolower($p['type'] ?? '') === 'cheque') + ->sum(fn($p) => $p['amount'] ?? 0); $changeAmount = max($receiveAmount - $roundingTotalAmount, 0); $dueAmount = max($roundingTotalAmount - $receiveAmount, 0); @@ -266,7 +253,7 @@ public function store(Request $request) // Create sale record $sale = Sale::create([ 'user_id' => auth()->id(), - 'business_id' => $business->id, + 'business_id' => $business_id, 'branch_id' => auth()->user()->branch_id ?? session('branch_id'), 'type' => $request->type == 'inventory' ? 'inventory' : 'sale', 'party_id' => $request->party_id, @@ -276,7 +263,7 @@ public function store(Request $request) 'vat_amount' => $vatAmount, 'discountAmount' => $discountAmount, 'discount_type' => $request->discount_type ?? 'flat', - 'discount_percent' => $request->discount_type == 'percent' ? ($request->discountAmount ?? 0) : 0, + 'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0, 'totalAmount' => $roundingTotalAmount, 'actual_total_amount' => $actualTotalAmount, 'rounding_amount' => $rounding_amount, @@ -289,35 +276,12 @@ public function store(Request $request) 'meta' => [ 'customer_phone' => $request->customer_phone, 'note' => $request->note, - ], + ] ]); - // Handle due tracking for customers - if ($dueAmount > 0) { - $allowDueForGuest = product_setting()?->modules['allow_due_sale'] ?? false; - // No party - if (! $request->party_id) { - if (! $allowDueForGuest) { - return response()->json([ - 'message' => __('You have not allowed guest sales, so you cannot sell on due for a walking customer.'), - ], 400); - } - } else { - // Party exist - $party = Party::findOrFail($request->party_id); - - if ($party->credit_limit > 0 && ($party->due + $dueAmount) > $party->credit_limit) { - return response()->json([ - 'message' => __('Sale cannot be created. Party due will exceed credit limit!'), - ], 400); - } - - $party->update(['due' => $party->due + $dueAmount]); - } - } - - $totalLossProfit = 0; $avgDiscount = $discountAmount / max($carts->count(), 1); + $totalPurchaseAmount = 0; + $saleDetailsData = []; foreach ($carts as $cartItem) { $qty = $cartItem->qty; @@ -326,120 +290,116 @@ public function store(Request $request) // Find the product $product = Product::find($cartItem->id); - if (! $product) { + if (!$product) { return response()->json([ - 'message' => __("Product not found: {$cartItem->name}"), + 'message' => __("Product not found: {$cartItem->name}") ], 400); } - $discount = $cartItem->options->discount ?? 0; - $actualPrice = $cartItem->price - $discount; + $itemCartDiscount = ((float) $cartItem->options->discount ?? 0) * $qty; - // Combo Products if ($product->product_type == 'combo') { - - $combo_purchase_price = $product->combo_products->sum('purchase_price'); + $combo_total_purchase = 0; + $combo_total_sale = $cartItem->price * $qty; foreach ($product->combo_products as $comboItem) { $stock = Stock::where('id', $comboItem->stock_id)->first(); $required_qty = $comboItem->quantity * $qty; // multiply by cart quantity if ($stock->productStock < $required_qty) { - $batchText = $stock->batch_no ? " ({$stock->batch_no})" : ''; - + $batchText = $stock->batch_no ? " ({$stock->batch_no})" : ""; return response()->json([ - 'message' => __("Combo item '{$stock->product->productName}'{$batchText} - stock not available. Available: {$stock->productStock}"), + 'message' => __("Combo item '{$stock->product->productName}'{$batchText} - stock not available. Available: {$stock->productStock}") ], 400); } // Decrement stock $stock->decrement('productStock', $required_qty); + + // Add combo item purchase total + $combo_total_purchase += $comboItem->purchase_price * $required_qty; + + // total purchase amount for sale + $totalPurchaseAmount += $comboItem->purchase_price * $required_qty; } + $lossProfit = $combo_total_sale - $combo_total_purchase - $avgDiscount - $itemCartDiscount; + // Store sale details for combo product itself - $detail = SaleDetails::create([ + $saleDetailsData[] = [ 'sale_id' => $sale->id, 'stock_id' => null, 'product_id' => $cartItem->id, 'price' => $cartItem->price, - 'quantities' => $qty, - 'expire_date' => null, - 'discount' => $discount, - 'productPurchasePrice' => $combo_purchase_price, - 'warranty_guarantee_info' => $product->warranty_guarantee_info ?? null, - ]); - - $vatAmount = VatTransactionService::storeVatTransactions($business, $detail, $product, $actualPrice, $party ?? null); - $price_without_tax = number_format($actualPrice - ($vatAmount / $qty), 3, '.', ''); - - $lossProfit = ($price_without_tax * $qty) - ($combo_purchase_price * $qty) - $avgDiscount; - $totalLossProfit += $lossProfit; - - $detail->update([ + 'discount' => $cartItem->options->discount ?? 0, 'lossProfit' => $lossProfit, - 'price_without_tax' => $price_without_tax, - ]); - } - // Single / Variant Products - else { + 'quantities' => $qty, + 'productPurchasePrice' => $combo_total_purchase, + 'expire_date' => null, + 'warranty_guarantee_info' => $product->warranty_guarantee_info ? json_encode($product->warranty_guarantee_info) : null, + ]; + } else { $stock = Stock::where('id', $cartItem->options->stock_id)->first(); - $newSerials = $cartItem->options->serial_numbers ?? []; - $existingSerials = $stock->serial_numbers ?? []; - - $finalQty = ! empty($newSerials) ? count($newSerials) : $qty; - - if ($stock->productStock < $finalQty) { - $batchText = $stock->batch_no ? " ($stock->batch_no)" : ''; - + if ($stock->productStock < $qty) { + $batchText = $stock->batch_no ? " ($stock->batch_no)" : ""; return response()->json([ - 'message' => __($cartItem->name.$batchText.' - stock not available. Available: '.$stock->productStock), + 'message' => __($cartItem->name . $batchText . ' - stock not available. Available: ' . $stock->productStock) ], 400); } - if (! empty($newSerials)) { + $stock->decrement('productStock', $qty); - // Remove sold serials from stock - $remainingSerials = array_values(array_diff($existingSerials, $newSerials)); + $lossProfit = (($cartItem->price - $stock->productPurchasePrice) * $qty) - $avgDiscount - $itemCartDiscount; - // Update stock serial list - $stock->update([ - 'serial_numbers' => $remainingSerials, - ]); - } - - $stock->decrement('productStock', $finalQty); - - $detail = SaleDetails::create([ + $saleDetailsData[] = [ 'sale_id' => $sale->id, + 'stock_id' => $cartItem->options->stock_id, 'product_id' => $cartItem->id, 'price' => $cartItem->price, - 'quantities' => $finalQty, - 'discount' => $discount, - 'productPurchasePrice' => $purchase_price, - 'stock_id' => $cartItem->options->stock_id, - 'expire_date' => $cartItem->options->expire_date ?? null, - 'serial_numbers' => ! empty($newSerials) ? $newSerials : null, - 'warranty_guarantee_info' => $product->warranty_guarantee_info ?? null, - ]); - - $vatAmount = VatTransactionService::storeVatTransactions($business, $detail, $product, $actualPrice, $party ?? null); - $price_without_tax = number_format($actualPrice - ($vatAmount / $finalQty), 3, '.', ''); - - $lossProfit = ($price_without_tax * $finalQty) - ($stock->exclusive_price * $finalQty) - $avgDiscount; - $totalLossProfit += $lossProfit; - - $detail->update([ + 'discount' => $cartItem->options->discount ?? 0, 'lossProfit' => $lossProfit, - 'price_without_tax' => $price_without_tax, - ]); + 'quantities' => $cartItem->qty, + 'productPurchasePrice' => $purchase_price, + 'expire_date' => $cartItem->options->expire_date ?? null, + 'warranty_guarantee_info' => $product->warranty_guarantee_info ? json_encode($product->warranty_guarantee_info) : null, + ]; + + $totalPurchaseAmount += $purchase_price * $qty; } } + // Insert all sale details + SaleDetails::insert($saleDetailsData); + $sale->update([ - 'lossProfit' => $totalLossProfit, + 'lossProfit' => $subtotal - $totalPurchaseAmount - $discountAmount, ]); + // Handle due tracking for customers + if ($dueAmount > 0) { + $allowDueForGuest = product_setting()?->modules['allow_due_sale'] ?? false; + // No party + if (!$request->party_id) { + if (!$allowDueForGuest) { + return response()->json([ + 'message' => __('You have not allowed guest sales, so you cannot sell on due for a walking customer.') + ], 400); + } + } else { + // Party exist + $party = Party::findOrFail($request->party_id); + + if ($party->credit_limit > 0 && ($party->due + $dueAmount) > $party->credit_limit) { + return response()->json([ + 'message' => __('Sale cannot be created. Party due will exceed credit limit!') + ], 400); + } + + $party->update(['due' => $party->due + $dueAmount]); + } + } + // MultiPaymentProcessed Event event(new MultiPaymentProcessed( $payments ?? [], @@ -449,31 +409,26 @@ public function store(Request $request) $request->party_id ?? null, )); - // Notify user - sendNotifyToUser($sale->id, route('business.sales.index', ['id' => $sale->id]), __('New sale created.'), $business->id); - - if (moduleCheck('MarketingAddon')) { - $this->saleMessage($business, $party ?? null, $sale); - } - - $tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($sale->business_id) == 'standard_a4'; - $invoice_route = $tax_invoice ? route('business.sales.tax.invoice', $sale->id) : route('business.sales.invoice', $sale->id); - // Clear all items from cart foreach ($carts as $cartItem) { Cart::remove($cartItem->rowId); } + // Notify user + sendNotifyToUser($sale->id, route('business.sales.index', ['id' => $sale->id]), __('New sale created.'), $business_id); + + // Send SMS + event(new SaleSms($sale)); + DB::commit(); return response()->json([ 'message' => __('Sales created successfully.'), 'redirect' => route('business.sales.index'), - 'secondary_redirect_url' => $invoice_route, + 'secondary_redirect_url' => route('business.sales.invoice', $sale->id), ]); } catch (\Exception $e) { DB::rollback(); - return response()->json(['message' => $e->getMessage()], 400); } } @@ -483,21 +438,15 @@ public function edit($id) // Clears all cart items Cart::destroy(); $business_id = auth()->user()->business_id; - $activeBranchId = auth()->user()->active_branch?->id; - $sale = Sale::with('user:id,name', 'party:id,name,email,phone,type', 'details', 'details.stock', 'details.product:id,productName,category_id,unit_id,productCode,productSalePrice,productPicture,has_serial', 'details.product.category:id,categoryName', 'details.product.unit:id,unitName', 'payment_type:id,name', 'transactions') + $sale = Sale::with('user:id,name', 'party:id,name,email,phone,type', 'details', 'details.stock', 'details.product:id,productName,category_id,unit_id,productCode,productSalePrice,productPicture,vat_id', 'details.product.category:id,categoryName', 'details.product.unit:id,unitName', 'details.product.vat:id,rate', 'payment_type:id,name', 'transactions') ->where('business_id', $business_id) ->findOrFail($id); - $customers = Party::where('type', '!=', 'Supplier') + $customers = Party::where('type', '!=', 'supplier') ->where('business_id', $business_id) ->latest() - ->get() - ->transform(function ($customer) use ($activeBranchId) { - $customer->due = $customer->dueForBranch($activeBranchId); - - return $customer; - }); + ->get(); $products = Product::with('category:id,categoryName', 'unit:id,unitName', 'stocks', 'stocks.warehouse') ->where('business_id', $business_id) @@ -536,16 +485,14 @@ public function edit($id) 'expire_date' => $detail->expire_date ?? '', 'purchase_price' => $detail->productPurchasePrice ?? 0, 'discount' => $detail->discount ?? 0, - 'serial_numbers' => $detail->serial_numbers ?? null, - 'has_serial' => $detail->product->has_serial ?? null, + 'vat_percent' => $detail->product->vat->rate ?? 0, ], ]); } - $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'sale'); + $cart_contents = Cart::content()->filter(fn($item) => $item->options->type == 'sale'); $view = $sale->type == 'inventory' ? 'business::sales.edit-inventory' : 'business::sales.edit'; - return view($view, compact('sale', 'customers', 'products', 'cart_contents', 'categories', 'brands', 'vats', 'payment_types', 'warehouses')); } @@ -563,9 +510,9 @@ public function update(Request $request, $id) 'shipping_charge' => 'nullable|numeric', ]); - $business = Business::findOrFail(auth()->user()->business_id); + $business_id = auth()->user()->business_id; - $carts = Cart::content()->filter(fn ($item) => $item->options->type == 'sale'); + $carts = Cart::content()->filter(fn($item) => $item->options->type == 'sale'); if ($carts->count() < 1) { return response()->json(['message' => __('Cart is empty. Add items first!')], 400); @@ -577,47 +524,13 @@ public function update(Request $request, $id) $sale_prev_due = $sale->dueAmount; $prevDetails = $sale->details; + $totalPurchaseAmount = 0; $subtotal = 0; + $saleDetailsData = []; $discountAmount = $request->discountAmount ?? 0; $shippingCharge = $request->shipping_charge ?? 0; - $vat = Vat::find($request->vat_id); - - // remove previous sale details - $prev_details_ids = SaleDetails::where('sale_id', $sale->id)->pluck('id')->toArray(); - SaleDetails::where('sale_id', $sale->id)->delete(); - - // remove previous vat transactions - VatTransaction::whereIn('vatable_id', $prev_details_ids) - ->where('vatable_type', SaleDetails::class) - ->delete(); - - // Restore previous stock quantities and serial numbers - foreach ($prevDetails as $prevItem) { - if ($prevItem->stock_id) { - $stock = Stock::find($prevItem->stock_id); - if ($stock) { - // Restore stock quantity - $stock->increment('productStock', $prevItem->quantities); - - // Restore serials if exists - if (! empty($prevItem->serial_numbers)) { - $prevSerials = $prevItem->serial_numbers; - - $existingSerials = $stock->serial_numbers ?? []; - - $mergedSerials = array_values(array_unique(array_merge($existingSerials, $prevSerials))); - - $stock->update(['serial_numbers' => $mergedSerials]); - } - } - } - } - - $totalLossProfit = 0; - $avgDiscount = $discountAmount / max($carts->count(), 1); - foreach ($carts as $cartItem) { $prevProduct = $prevDetails->firstWhere('product_id', $cartItem->id); $oldQty = $prevProduct ? $prevProduct->quantities : 0; @@ -625,143 +538,107 @@ public function update(Request $request, $id) $diffQty = $newQty - $oldQty; $product = Product::with('combo_products.product')->find($cartItem->id); - if (! $product) { + if (!$product) { return response()->json(['message' => __("Product not found: {$cartItem->name}")], 400); } - $discount = $cartItem->options->discount ?? 0; - $actualPrice = $cartItem->price - $discount; + $itemCartDiscount = ((float) $cartItem->options->discount ?? 0) * $newQty; if ($product->product_type === 'combo') { - - $combo_purchase_price = $product->combo_products->sum('purchase_price'); + $combo_total_purchase = 0; + $combo_total_sale = $cartItem->price * $newQty; foreach ($product->combo_products as $comboItem) { $stock = Stock::where('id', $comboItem->stock_id)->first(); - if (! $stock) { + if (!$stock) { return response()->json([ - 'message' => __("Stock not found for combo item '{$comboItem->product->productName}'"), + 'message' => __("Stock not found for combo item '{$comboItem->product->productName}'") ], 400); } $requiredQty = $comboItem->quantity * $newQty; if ($stock->productStock + ($prevProduct && $prevProduct->stock_id == $stock->id ? $oldQty * $comboItem->quantity : 0) < $requiredQty) { - $batchText = $stock->batch_no ? " ({$stock->batch_no})" : ''; - + $batchText = $stock->batch_no ? " ({$stock->batch_no})" : ""; return response()->json([ - 'message' => __("Combo item '{$stock->product->productName}'{$batchText} - stock not available. Available: {$stock->productStock}"), + 'message' => __("Combo item '{$stock->product->productName}'{$batchText} - stock not available. Available: {$stock->productStock}") ], 400); } - if ($diffQty != 0) { - $stock->productStock -= $diffQty * $comboItem->quantity; - $stock->save(); - } + $stock->productStock -= $diffQty * $comboItem->quantity; + $stock->save(); + + $combo_total_purchase += $comboItem->purchase_price * $diffQty * $comboItem->quantity; + $totalPurchaseAmount += $comboItem->purchase_price * $diffQty * $comboItem->quantity; } - $detail = SaleDetails::create([ + $lossProfit = $combo_total_sale - $combo_total_purchase - ($discountAmount / max($carts->count(), 1)) - $itemCartDiscount; + + $saleDetailsData[] = [ 'sale_id' => $sale->id, 'stock_id' => null, 'product_id' => $cartItem->id, 'price' => $cartItem->price, - 'discount' => $discount, - 'quantities' => $newQty, - 'expire_date' => null, - 'productPurchasePrice' => $combo_purchase_price, - 'warranty_guarantee_info' => $product->warranty_guarantee_info ?? null, - ]); - - $vatAmount = VatTransactionService::storeVatTransactions($business, $detail, $product, $actualPrice, $party ?? null); - $price_without_tax = number_format($actualPrice - ($vatAmount / $newQty), 3, '.', ''); - - $lossProfit = ($price_without_tax * $newQty) - ($combo_purchase_price * $newQty) - $avgDiscount; - $totalLossProfit += $lossProfit; - - $detail->update([ + 'discount' => $cartItem->options->discount ?? 0, 'lossProfit' => $lossProfit, - 'price_without_tax' => $price_without_tax, - ]); - + 'quantities' => $newQty, + 'productPurchasePrice' => $combo_total_purchase, + 'expire_date' => null, + 'warranty_guarantee_info' => $product->warranty_guarantee_info ? json_encode($product->warranty_guarantee_info) : null, + ]; } else { $stock = Stock::where('id', $cartItem->options->stock_id ?? null) ->first() ?? Stock::where('product_id', $cartItem->id)->orderBy('id', 'asc')->first(); - if (! $stock) { + if (!$stock) { return response()->json([ - 'message' => __($cartItem->name.' - no stock found.'), + 'message' => __($cartItem->name . ' - no stock found.') ], 400); } - $newSerials = $cartItem->options->serial_numbers ?? []; - $existingSerials = $stock->serial_numbers ?? []; - - $finalQty = ! empty($newSerials) ? count($newSerials) : $newQty; - - if ($stock->productStock < $finalQty) { - return response()->json([ - 'message' => __($cartItem->name.' - stock not available. Available: '.$stock->productStock), - ], 400); - } - - // Remove new serials from stock - if (! empty($newSerials)) { - $remainingSerials = array_values(array_diff($existingSerials, $newSerials)); - $stock->update(['serial_numbers' => $remainingSerials]); - } - $availableStock = $stock->productStock + $oldQty; if ($availableStock < $newQty) { return response()->json([ - 'message' => __($cartItem->name.' - stock not available. Available: '.$availableStock), + 'message' => __($cartItem->name . ' - stock not available. Available: ' . $availableStock) ], 400); } - $stock->decrement('productStock', $finalQty); + $stock->productStock -= $diffQty; + $stock->save(); - $detail = SaleDetails::create([ + $lossProfit = (($cartItem->price - $cartItem->options->purchase_price) * $newQty) - ($discountAmount / max($carts->count(), 1)) - $itemCartDiscount; + $totalPurchaseAmount += $cartItem->options->purchase_price * $newQty; + + $saleDetailsData[] = [ 'sale_id' => $sale->id, 'stock_id' => $stock->id, 'product_id' => $cartItem->id, 'price' => $cartItem->price, - 'discount' => $discount, - 'quantities' => $finalQty, + 'discount' => $cartItem->options->discount ?? 0, + 'lossProfit' => $lossProfit, + 'quantities' => $newQty, 'productPurchasePrice' => $cartItem->options->purchase_price ?? 0, 'expire_date' => $cartItem->options->expire_date ?? null, - 'warranty_guarantee_info' => $product->warranty_guarantee_info ?? null, - 'serial_numbers' => ! empty($newSerials) ? $newSerials : null, - ]); - - $vatAmount = VatTransactionService::storeVatTransactions($business, $detail, $product, $actualPrice, $party ?? null); - $price_without_tax = number_format($actualPrice - ($vatAmount / $finalQty), 3, '.', ''); - - $lossProfit = ($price_without_tax * $finalQty) - ($stock->exclusive_price * $finalQty) - $avgDiscount; - $totalLossProfit += $lossProfit; - - $detail->update([ - 'lossProfit' => $lossProfit, - 'price_without_tax' => $price_without_tax, - ]); + 'warranty_guarantee_info' => $product->warranty_guarantee_info ? json_encode($product->warranty_guarantee_info) : null, + ]; } $subtotal += ($cartItem->price - ($cartItem->options->discount ?? 0)) * $newQty; } - // Discount (applied first on subtotal) - $discountAmount = $request->discountAmount ?? 0; + // VAT & total calculations + $vatAmount = $carts->sum(function ($item) { + return ($item->price * $item->qty) * ($item->options->vat_percent ?? 0) / 100; + }); + $subtotalWithVat = $subtotal + $vatAmount; + if ($request->discount_type == 'percent') { - $discountAmount = ($subtotal * $discountAmount) / 100; + $discountAmount = ($subtotalWithVat * $discountAmount) / 100; } - if ($discountAmount > $subtotal) { - return response()->json(['message' => __('Discount cannot be more than subtotal!')], 400); + if ($discountAmount > $subtotalWithVat) { + return response()->json(['message' => __('Discount cannot be more than subtotal with VAT!')], 400); } - // Subtotal after discount (before VAT) - $subtotalAfterDiscount = $subtotal - $discountAmount; - - // VAT & total calculations (applied on subtotal after discount) - $vatAmount = $vat ? ($subtotalAfterDiscount * $vat->rate) / 100 : 0; - - $actualTotalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge; + $actualTotalAmount = $subtotalWithVat - $discountAmount + $shippingCharge; $roundingTotalAmount = sale_rounding($actualTotalAmount, $sale->rounding_option); $rounding_amount = $roundingTotalAmount - $actualTotalAmount; @@ -775,7 +652,7 @@ public function update(Request $request, $id) } // Check if any old transaction type = deposit - $hasDeposit = Transaction::where('business_id', $business->id) + $hasDeposit = Transaction::where('business_id', $business_id) ->where('reference_id', $sale->id) ->where('platform', 'sale') ->where('type', 'deposit') @@ -785,9 +662,9 @@ public function update(Request $request, $id) $receiveAmount = collect($payments) ->reject(function ($p) use ($hasDeposit) { // exclude cheque only if not deposit - return ! $hasDeposit && strtolower($p['type'] ?? '') === 'cheque'; + return !$hasDeposit && strtolower($p['type'] ?? '') === 'cheque'; }) - ->sum(fn ($p) => $p['amount'] ?? 0); + ->sum(fn($p) => $p['amount'] ?? 0); $changeAmount = $receiveAmount > $roundingTotalAmount ? $receiveAmount - $roundingTotalAmount : 0; $dueAmount = max($roundingTotalAmount - $receiveAmount, 0); @@ -803,12 +680,11 @@ public function update(Request $request, $id) 'vat_amount' => $vatAmount, 'discountAmount' => $discountAmount, 'discount_type' => $request->discount_type ?? 'flat', - 'discount_percent' => $request->discount_type == 'percent' ? ($request->discountAmount ?? 0) : 0, + 'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0, 'totalAmount' => $roundingTotalAmount, 'actual_total_amount' => $actualTotalAmount, 'rounding_amount' => $rounding_amount, - 'lossProfit' => $totalLossProfit, - 'shipping_charge' => $shippingCharge, + 'lossProfit' => $subtotal - $totalPurchaseAmount - $discountAmount, 'paidAmount' => $paidAmount > $roundingTotalAmount ? $roundingTotalAmount : $paidAmount, 'change_amount' => $changeAmount, 'dueAmount' => $dueAmount, @@ -816,18 +692,22 @@ public function update(Request $request, $id) 'meta' => [ 'customer_phone' => $request->customer_phone, 'note' => $request->note, - ], + ] ]); + // Clear old sale details & insert new + SaleDetails::where('sale_id', $sale->id)->delete(); + SaleDetails::insert($saleDetailsData); + if ($dueAmount > 0) { $allowDueForGuest = product_setting()?->modules['allow_due_sale'] ?? false; // If guest or no party - if (! $request->party_id) { + if (!$request->party_id) { - if (! $allowDueForGuest) { + if (!$allowDueForGuest) { return response()->json([ - 'message' => __('You have not allowed guest sales. So you cannot sell on due for a walking customer.'), + 'message' => __('You have not allowed guest sales. So you cannot sell on due for a walking customer.') ], 400); } } else { @@ -840,7 +720,7 @@ public function update(Request $request, $id) // Check credit limit if ($party->credit_limit > 0 && $newTotalDue > $party->credit_limit) { return response()->json([ - 'message' => __('Cannot update sale. Party due will exceed credit limit!'), + 'message' => __('Cannot update sale. Party due will exceed credit limit!') ], 400); } @@ -856,7 +736,7 @@ public function update(Request $request, $id) } // Multiple Payment Process - $oldTransactions = Transaction::where('business_id', $business->id) + $oldTransactions = Transaction::where('business_id', $business_id) ->where('reference_id', $sale->id) ->where('platform', 'sale') ->get(); @@ -888,7 +768,7 @@ public function update(Request $request, $id) } // Delete old transactions - Transaction::where('business_id', $business->id) + Transaction::where('business_id', $business_id) ->where('reference_id', $sale->id) ->where('platform', 'sale') ->delete(); @@ -906,22 +786,18 @@ public function update(Request $request, $id) Cart::remove($cartItem->rowId); } - sendNotifyToUser($sale->id, route('business.sales.index', ['id' => $sale->id]), __('Sale has been updated.'), $business->id); - - $tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($sale->business_id) == 'standard_a4'; - $invoice_route = $tax_invoice ? route('business.sales.tax.invoice', $sale->id) : route('business.sales.invoice', $sale->id); + sendNotifyToUser($sale->id, route('business.sales.index', ['id' => $sale->id]), __('Sale has been updated.'), $business_id); DB::commit(); return response()->json([ 'message' => __('Sales updated successfully.'), 'redirect' => route('business.sales.index'), - 'secondary_redirect_url' => $invoice_route, + 'secondary_redirect_url' => route('business.sales.invoice', $sale->id), ]); } catch (\Exception $e) { DB::rollback(); - - return response()->json(['message' => $e->getMessage()], 404); + return response()->json(['message' => __('Something went wrong!')], 404); } } @@ -929,16 +805,12 @@ public function destroy($id) { DB::beginTransaction(); try { - $sale = Sale::where('business_id', auth()->user()->business_id)->findOrFail($id); + $sale = Sale::findOrFail($id); foreach ($sale->details as $detail) { - VatTransaction::where('vatable_id', $detail->id) - ->where('vatable_type', get_class($detail)) - ->delete(); - $stock = Stock::find($detail->stock_id); - if (! $stock) { + if (!$stock) { $stock = Stock::where('product_id', $detail->product_id)->orderBy('id', 'asc')->first(); } @@ -965,11 +837,10 @@ public function destroy($id) return response()->json([ 'message' => __('Sale deleted successfully.'), - 'redirect' => route('business.sales.index'), + 'redirect' => route('business.sales.index') ]); } catch (\Exception $e) { DB::rollback(); - return response()->json(['message' => __('Something went wrong!')], 404); } } @@ -979,18 +850,14 @@ public function deleteAll(Request $request) DB::beginTransaction(); try { - $sales = Sale::where('business_id', auth()->user()->business_id)->whereIn('id', $request->ids)->get(); + $sales = Sale::whereIn('id', $request->ids)->get(); foreach ($sales as $sale) { // Restore stock foreach ($sale->details as $detail) { - VatTransaction::where('vatable_id', $detail->id) - ->where('vatable_type', get_class($detail)) - ->delete(); - $stock = Stock::find($detail->stock_id); - if (! $stock) { + if (!$stock) { $stock = Stock::where('product_id', $detail->product_id)->orderBy('id', 'asc')->first(); } @@ -1019,11 +886,10 @@ public function deleteAll(Request $request) return response()->json([ 'message' => __('Selected sales deleted successfully.'), - 'redirect' => route('business.sales.index'), + 'redirect' => route('business.sales.index') ]); } catch (\Exception $e) { DB::rollback(); - return response()->json(['message' => __('Something went wrong!')], 404); } } @@ -1071,16 +937,16 @@ public function acnooFilter(Request $request) // Search Filter if ($request->filled('search')) { $salesQuery->where(function ($query) use ($request) { - $query->where('paymentType', 'like', '%'.$request->search.'%') - ->orWhere('invoiceNumber', 'like', '%'.$request->search.'%') + $query->where('paymentType', 'like', '%' . $request->search . '%') + ->orWhere('invoiceNumber', 'like', '%' . $request->search . '%') ->orWhereHas('party', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('payment_type', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }) ->orWhereHas('branch', function ($q) use ($request) { - $q->where('name', 'like', '%'.$request->search.'%'); + $q->where('name', 'like', '%' . $request->search . '%'); }); }); } @@ -1090,7 +956,7 @@ public function acnooFilter(Request $request) if ($request->ajax()) { return response()->json([ - 'data' => view('business::sales.datas', compact('sales', 'salesWithReturns'))->render(), + 'data' => view('business::sales.datas', compact('sales', 'salesWithReturns'))->render() ]); } @@ -1106,22 +972,26 @@ public function productFilter(Request $request) ->count(); $products = Product::where('business_id', auth()->user()->business_id) + ->where('is_displayed_in_pos', 1) ->whereHas('stocks', function ($s) use ($request) { $s->when($request->warehouse_id, function ($sw) use ($request) { $sw->where('warehouse_id', $request->warehouse_id); }) ->where('productStock', '>', 0); }) - ->with(['stocks' => function ($s) use ($request) { - $s->when($request->warehouse_id, function ($sw) use ($request) { - $sw->where('warehouse_id', $request->warehouse_id); - }) - ->where('productStock', '>', 0); - }]) + ->with([ + 'vat:id,rate', + 'stocks' => function ($s) use ($request) { + $s->when($request->warehouse_id, function ($sw) use ($request) { + $sw->where('warehouse_id', $request->warehouse_id); + }) + ->where('productStock', '>', 0); + } + ]) ->when($request->search, function ($query) use ($request) { $query->where(function ($q) use ($request) { - $q->where('productName', 'like', '%'.$request->search.'%') - ->orWhere('productCode', 'like', '%'.$request->search.'%'); + $q->where('productName', 'like', '%' . $request->search . '%') + ->orWhere('productCode', 'like', '%' . $request->search . '%'); }); }) ->when($request->category_id, function ($query) use ($request) { @@ -1136,14 +1006,14 @@ public function productFilter(Request $request) // Query categories for search options $categories = Category::where('business_id', auth()->user()->business_id) ->when($request->search, function ($query) use ($request) { - $query->where('categoryName', 'like', '%'.$request->search.'%'); + $query->where('categoryName', 'like', '%' . $request->search . '%'); }) ->get(); // Query brands for search options $brands = Brand::where('business_id', auth()->user()->business_id) ->when($request->search, function ($query) use ($request) { - $query->where('brandName', 'like', '%'.$request->search.'%'); + $query->where('brandName', 'like', '%' . $request->search . '%'); }) ->get(); @@ -1154,7 +1024,6 @@ public function productFilter(Request $request) 'total_products' => $total_products, 'total_products_count' => $total_products_count, 'product_id' => $total_products == 1 ? $products->first()->id : null, - 'has_serial' => $total_products == 1 ? $products->first()->has_serial : null, 'data' => view('business::sales.product-list', compact('products'))->render(), 'categories' => view('business::sales.category-list', compact('categories'))->render(), 'brands' => view('business::sales.brand-list', compact('brands'))->render(), @@ -1170,7 +1039,7 @@ public function categoryFilter(Request $request) $search = $request->search; $categories = Category::where('business_id', auth()->user()->business_id) ->when($search, function ($query) use ($search) { - $query->where('categoryName', 'like', '%'.$search.'%'); + $query->where('categoryName', 'like', '%' . $search . '%'); }) ->get(); @@ -1185,7 +1054,7 @@ public function brandFilter(Request $request) $search = $request->search; $brands = Brand::where('business_id', auth()->user()->business_id) ->when($search, function ($query) use ($search) { - $query->where('brandName', 'like', '%'.$search.'%'); + $query->where('brandName', 'like', '%' . $search . '%'); }) ->get(); @@ -1212,7 +1081,6 @@ public function getProductPrices(Request $request) $prices[$product->id] = currency_format($product->productSalePrice, currency: business_currency()); } } - return response()->json($prices); } @@ -1222,7 +1090,6 @@ public function getStockPrices(Request $request) $businessId = auth()->user()->business_id; $customerType = $request->input('type'); $cartStocks = $request->input('stocks', []); // optional, only cart rows - $forInventory = $request->input('inventory', false); // if true, return all stocks $currency = business_currency(); @@ -1244,26 +1111,9 @@ public function getStockPrices(Request $request) $productPrices[$stock->product_id] = currency_format($price, currency: $currency); } - // For inventory sale, return all stock prices; otherwise only cart stocks + // Fetch stocks for cart list prices $stockPrices = []; - - if ($forInventory) { - // Return all stocks for inventory sale - foreach ($allStocks as $stock) { - $batchNo = $stock->batch_no ?? 'default'; - - if ($customerType == 'Dealer') { - $price = $stock->productDealerPrice; - } elseif ($customerType == 'Wholesaler') { - $price = $stock->productWholeSalePrice; - } else { - $price = $stock->productSalePrice; - } - - $stockPrices[$stock->id][$batchNo] = currency_format($price, currency: $currency); - } - } elseif (! empty($cartStocks)) { - // Return only cart stocks for normal sale/transfer + if (!empty($cartStocks)) { $cartStockIds = collect($cartStocks)->pluck('stock_id')->toArray(); $cartStockQuery = Stock::where('business_id', $businessId) @@ -1296,7 +1146,7 @@ public function getStockPrices(Request $request) /** Get cart info */ public function getCartData() { - $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'sale'); + $cart_contents = Cart::content()->filter(fn($item) => $item->options->type == 'sale'); $data['sub_total'] = 0; @@ -1308,21 +1158,100 @@ public function getCartData() return response()->json($data); } - public function getInvoice(string $sale_id, SaleInvoiceService $service) + public function getInvoice($sale_id) { - $data = $service->getInvoiceData($sale_id); + $sale = Sale::where('business_id', auth()->user()->business_id)->with('user:id,name,role', 'party:id,name,phone,address', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price,discount,quantities,product_id,sale_id,stock_id,warranty_guarantee_info', 'details.stock:id,batch_no', 'details.product:id,productName,vat_id', 'details.product.vat:id,rate', 'payment_type:id,name', 'transactions:id,reference_id,transaction_type,payment_type_id', 'transactions.paymentType:id,name')->findOrFail($sale_id); - return view('business::sales.invoice', $data); + $transactionTypes = $sale->transactions + ->map(function ($transaction) { + if ($transaction->transaction_type === 'bank_payment' && !empty($transaction->paymentType?->name)) { + return $transaction->paymentType->name; + } + return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : ''; + }) + ->unique() + ->implode(', '); + + $sale_returns = SaleReturn::with('sale:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'sale.party:id,name', 'details', 'details.saleDetail.product:id,productName') + ->where('business_id', auth()->user()->business_id) + ->where('sale_id', $sale_id) + ->latest() + ->get(); + + $returnTransaction = $sale_returns->first()?->transactions->first(); + $returnTransactionType = ''; + + if ($returnTransaction) { + $returnTransactionType = $returnTransaction->transaction_type === 'bank_payment' + ? ($returnTransaction->paymentType?->name) + : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); + } + + // sum of return_qty + $sale->details = $sale->details->map(function ($detail) use ($sale_returns) { + $return_qty_sum = $sale_returns->flatMap(function ($return) use ($detail) { + return $return->details->where('saleDetail.id', $detail->id)->pluck('return_qty'); + })->sum(); + + $detail->quantities = $detail->quantities + $return_qty_sum; + return $detail; + }); + + // Calculate the initial discount for each product during sale returns + $total_discount = 0; + $product_discounts = []; + + foreach ($sale_returns as $return) { + foreach ($return->details as $detail) { + // Add the return quantities and return amounts for each sale_detail_id + if (!isset($product_discounts[$detail->sale_detail_id])) { + // Initialize the first occurrence + $product_discounts[$detail->sale_detail_id] = [ + 'return_qty' => 0, + 'return_amount' => 0, + 'price' => $detail->saleDetail->price, + ]; + } + + // Accumulate quantities and return amounts for the same sale_detail_id + $product_discounts[$detail->sale_detail_id]['return_qty'] += $detail->return_qty; + $product_discounts[$detail->sale_detail_id]['return_amount'] += $detail->return_amount; + } + } + + // Calculate the total discount based on accumulated quantities and return amounts + foreach ($product_discounts as $data) { + $product_price = $data['price'] * $data['return_qty']; + $discount = $product_price - $data['return_amount']; + + $total_discount += $discount; + } + + $hasWarranty = false; + $hasGuarantee = false; + + foreach ($sale->details as $detail) { + $info = $detail->warranty_guarantee_info ?? []; + + if (!empty($info['warranty_duration'])) { + $hasWarranty = true; + } + + if (!empty($info['guarantee_duration'])) { + $hasGuarantee = true; + } + + if ($hasWarranty && $hasGuarantee) + break; + } + ; + + $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); + + return view('business::sales.invoice', compact('sale', 'sale_returns', 'total_discount', 'hasWarranty', 'hasGuarantee', 'transactionTypes', 'bank_detail', 'returnTransactionType')); } - public function getTaxInvoice($sale_id, SaleTaxInvoiceService $service) - { - $data = $service->getInvoiceData($sale_id); - - return view('taxinvoiceaddon::sales.tax', $data); - } - - public function generatePDF($sale_id) + public function generatePDF(Request $request, $sale_id) { $sale = Sale::where('business_id', auth()->user()->business_id) ->with( @@ -1331,7 +1260,8 @@ public function generatePDF($sale_id) 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price,discount,quantities,product_id,sale_id,stock_id,warranty_guarantee_info', 'details.stock:id,batch_no', - 'details.product:id,productName', + 'details.product:id,productName,vat_id', + 'details.product.vat:id,rate', 'payment_type:id,name', 'transactions:id,reference_id,transaction_type,payment_type_id', 'transactions.paymentType:id,name' @@ -1360,8 +1290,8 @@ public function generatePDF($sale_id) if ($returnTransaction) { $returnTransactionType = $returnTransaction->transaction_type === 'bank_payment' - ? ($returnTransaction->paymentType?->name) - : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); + ? ($returnTransaction->paymentType?->name) + : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); } // Return qty merge (unchanged) @@ -1373,7 +1303,6 @@ public function generatePDF($sale_id) })->sum(); $detail->quantities += $return_qty_sum; - return $detail; }); @@ -1383,7 +1312,7 @@ public function generatePDF($sale_id) foreach ($sale_returns as $return) { foreach ($return->details as $detail) { - if (! isset($product_discounts[$detail->sale_detail_id])) { + if (!isset($product_discounts[$detail->sale_detail_id])) { $product_discounts[$detail->sale_detail_id] = [ 'return_qty' => 0, 'return_amount' => 0, @@ -1408,15 +1337,12 @@ public function generatePDF($sale_id) foreach ($sale->details as $detail) { $info = $detail->warranty_guarantee_info ?? []; - if (! empty($info['warranty_duration'])) { + if (!empty($info['warranty_duration'])) $hasWarranty = true; - } - if (! empty($info['guarantee_duration'])) { + if (!empty($info['guarantee_duration'])) $hasGuarantee = true; - } - if ($hasWarranty && $hasGuarantee) { + if ($hasWarranty && $hasGuarantee) break; - } } $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); @@ -1438,185 +1364,21 @@ public function generatePDF($sale_id) ); } - public function generateTaxPDF(Request $request, $sale_id) + public function sendMail(Request $request, $sale_id) { - $sale = Sale::where('business_id', auth()->user()->business_id) - ->with( - 'user:id,name,role', - 'party:id,name,phone,address,email', - 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', - 'details:id,price,price_without_tax,discount,quantities,product_id,sale_id,stock_id,warranty_guarantee_info,serial_numbers', - 'details.stock:id,batch_no', - 'details.product:id,productName,has_serial', - 'payment_type:id,name', - 'transactions:id,reference_id,transaction_type,payment_type_id', - 'transactions.paymentType:id,name' - ) - ->findOrFail($sale_id); - $business_id = auth()->check() ? auth()->user()->business_id : null; - - $detail_ids = $sale->details->pluck('id'); - $tax_items = VatTransaction::selectRaw('vat_id, vat_rate, SUM(vat_amount) as vat_amount') - ->with('vat:id,name') - ->whereIn('vatable_id', $detail_ids) - ->where('vatable_type', SaleDetails::class) - ->groupBy('vat_id', 'vat_rate') - ->get(); - - $vatRowMap = VatTransaction::whereIn('vatable_id', $detail_ids) - ->where('vatable_type', SaleDetails::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - // Transaction types (helper) - $transactionTypes = transaction_types($sale->transactions); - - // Sale returns - $sale_returns = SaleReturn::with( - 'sale:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', - 'sale.party:id,name', - 'details', - 'details.saleDetail.product:id,productName,has_serial' - ) - ->where('business_id', auth()->user()->business_id) - ->where('sale_id', $sale_id) - ->latest() - ->get(); - - // collect return detail ids - $return_detail_ids = $sale_returns->flatMap(function ($return) { - return $return->details->pluck('id'); - }); - - // get VAT for sale return details - $returnVatRowMap = VatTransaction::whereIn('vatable_id', $return_detail_ids) - ->where('vatable_type', SaleReturnDetails::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - // Return transaction type (unchanged) - $returnTransaction = $sale_returns->first()?->transactions->first(); - $returnTransactionType = ''; - - if ($returnTransaction) { - $returnTransactionType = - $returnTransaction->transaction_type === 'bank_payment' - ? ($returnTransaction->paymentType?->name) - : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); - } - - // Return qty merge (unchanged) - $sale->details = $sale->details->map(function ($detail) use ($sale_returns) { - $return_qty_sum = $sale_returns->flatMap(function ($return) use ($detail) { - return $return->details - ->where('saleDetail.id', $detail->id) - ->pluck('return_qty'); - })->sum(); - - $detail->quantities += $return_qty_sum; - - return $detail; - }); - - // Discount calculation (unchanged) - $total_discount = 0; - $product_discounts = []; - - foreach ($sale_returns as $return) { - foreach ($return->details as $detail) { - if (! isset($product_discounts[$detail->sale_detail_id])) { - $product_discounts[$detail->sale_detail_id] = [ - 'return_qty' => 0, - 'return_amount' => 0, - 'price' => $detail->saleDetail->price, - ]; - } - - $product_discounts[$detail->sale_detail_id]['return_qty'] += $detail->return_qty; - $product_discounts[$detail->sale_detail_id]['return_amount'] += $detail->return_amount; - } - } - - foreach ($product_discounts as $data) { - $product_price = $data['price'] * $data['return_qty']; - $total_discount += ($product_price - $data['return_amount']); - } - - // Warranty / Guarantee detection - $hasWarranty = false; - $hasGuarantee = false; - - foreach ($sale->details as $detail) { - $info = $detail->warranty_guarantee_info ?? []; - - if (! empty($info['warranty_duration'])) { - $hasWarranty = true; - } - if (! empty($info['guarantee_duration'])) { - $hasGuarantee = true; - } - if ($hasWarranty && $hasGuarantee) { - break; - } - } - - $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); - - // ONLY PDF rendering is centralized - return PdfService::render( - 'taxinvoiceaddon::sales.pdf', - compact( - 'sale', - 'tax_items', - 'sale_returns', - 'total_discount', - 'transactionTypes', - 'returnTransactionType', - 'bank_detail', - 'hasWarranty', - 'hasGuarantee', - 'business_id', - 'vatRowMap', - 'returnVatRowMap' - ), - 'sales-tax-invoice.pdf' - ); - } - - public function sendMail(string $sale_id) - { - $sale = Sale::where('business_id', auth()->user()->business_id)->with('user:id,name,role', 'party:id,name,phone,address,email', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price,discount,quantities,product_id,sale_id,stock_id,warranty_guarantee_info', 'details.stock:id,batch_no', 'details.product:id,productName,has_serial', 'payment_type:id,name', 'transactions:id,reference_id,transaction_type,payment_type_id', 'transactions.paymentType:id,name')->findOrFail($sale_id); - - if (!$sale->party?->email) { - return response()->json([ - 'message' => __('Customer email not found. Please update customer information with an email address to send the invoice.'), - ], 404); - } + $sale = Sale::where('business_id', auth()->user()->business_id)->with('user:id,name,role', 'party:id,name,phone,address', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price,discount,quantities,product_id,sale_id,stock_id,warranty_guarantee_info', 'details.stock:id,batch_no', 'details.product:id,productName', 'payment_type:id,name', 'transactions:id,reference_id,transaction_type,payment_type_id', 'transactions.paymentType:id,name')->findOrFail($sale_id); $transactionTypes = $sale->transactions ->map(function ($transaction) { - if ($transaction->transaction_type === 'bank_payment' && ! empty($transaction->paymentType?->name)) { + if ($transaction->transaction_type === 'bank_payment' && !empty($transaction->paymentType?->name)) { return $transaction->paymentType->name; } - return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : ''; }) ->unique() ->implode(', '); - $sale_returns = SaleReturn::with('sale:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'sale.party:id,name', 'details', 'details.saleDetail.product:id,productName,has_serial') + $sale_returns = SaleReturn::with('sale:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'sale.party:id,name', 'details', 'details.saleDetail.product:id,productName') ->where('business_id', auth()->user()->business_id) ->where('sale_id', $sale_id) ->latest() @@ -1638,7 +1400,6 @@ public function sendMail(string $sale_id) })->sum(); $detail->quantities = $detail->quantities + $return_qty_sum; - return $detail; }); @@ -1649,7 +1410,7 @@ public function sendMail(string $sale_id) foreach ($sale_returns as $return) { foreach ($return->details as $detail) { // Add the return quantities and return amounts for each sale_detail_id - if (! isset($product_discounts[$detail->sale_detail_id])) { + if (!isset($product_discounts[$detail->sale_detail_id])) { // Initialize the first occurrence $product_discounts[$detail->sale_detail_id] = [ 'return_qty' => 0, @@ -1678,220 +1439,28 @@ public function sendMail(string $sale_id) foreach ($sale->details as $detail) { $info = $detail->warranty_guarantee_info ?? []; - if (! empty($info['warranty_duration'])) { + if (!empty($info['warranty_duration'])) { $hasWarranty = true; } - if (! empty($info['guarantee_duration'])) { + if (!empty($info['guarantee_duration'])) { $hasGuarantee = true; } - if ($hasWarranty && $hasGuarantee) { + if ($hasWarranty && $hasGuarantee) break; - } } + ; $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); - $pdf = PdfService::render( - 'business::sales.pdf', - compact( - 'sale', - 'sale_returns', - 'total_discount', - 'transactionTypes', - 'returnTransactionType', - 'bank_detail', - 'hasWarranty', - 'hasGuarantee' - ), - 'sales-invoice.pdf' - ); - - $pdfContent = $pdf->getContent(); + $pdf = Pdf::loadView('business::sales.pdf', compact('sale', 'sale_returns', 'total_discount', 'transactionTypes', 'returnTransactionType', 'bank_detail', 'hasWarranty', 'hasGuarantee')); // Send email with PDF attachment - Mail::raw('Please find attached your sales invoice.', function ($message) use ($sale, $pdfContent) { - $message->to($sale->party->email) + Mail::raw('Please find attached your sales invoice.', function ($message) use ($pdf) { + $message->to(auth()->user()->email) ->subject('Sales Invoice') - ->attachData($pdfContent, 'sales-invoice.pdf', [ - 'mime' => 'application/pdf', - ]); - }); - - return response()->json([ - 'message' => __('Email Sent Successfully.'), - 'redirect' => route('business.sales.index'), - ]); - } - - public function sendTaxMail(string $sale_id) - { - $sale = Sale::where('business_id', auth()->user()->business_id)->with('user:id,name,role', - 'party:id,name,phone,address,email', - 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', - 'details:id,price,price_without_tax,discount,quantities,product_id,sale_id,stock_id,warranty_guarantee_info,serial_numbers', - 'details.stock:id,batch_no', - 'details.product:id,productName,has_serial', - 'payment_type:id,name', - 'transactions:id,reference_id,transaction_type,payment_type_id', - 'transactions.paymentType:id,name')->findOrFail($sale_id); - - if (!$sale->party?->email) { - return response()->json([ - 'message' => __('Customer email not found. Please update customer information with an email address to send the invoice.'), - ], 404); - } - - $business_id = auth()->check() ? auth()->user()->business_id : null; - $detail_ids = $sale->details->pluck('id'); - $tax_items = VatTransaction::selectRaw('vat_id, vat_rate, SUM(vat_amount) as vat_amount') - ->with('vat:id,name') - ->whereIn('vatable_id', $detail_ids) - ->where('vatable_type', SaleDetails::class) - ->groupBy('vat_id', 'vat_rate') - ->get(); - - $vatRowMap = VatTransaction::whereIn('vatable_id', $detail_ids) - ->where('vatable_type', SaleDetails::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - $transactionTypes = $sale->transactions - ->map(function ($transaction) { - if ($transaction->transaction_type === 'bank_payment' && ! empty($transaction->paymentType?->name)) { - return $transaction->paymentType->name; - } - - return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : ''; - }) - ->unique() - ->implode(', '); - - $sale_returns = SaleReturn::with('sale:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'sale.party:id,name', 'details', 'details.saleDetail.product:id,productName,has_serial') - ->where('business_id', auth()->user()->business_id) - ->where('sale_id', $sale_id) - ->latest() - ->get(); - - $return_detail_ids = $sale_returns->flatMap(function ($return) { - return $return->details->pluck('id'); - }); - - $returnVatRowMap = VatTransaction::whereIn('vatable_id', $return_detail_ids) - ->where('vatable_type', SaleReturnDetails::class) - ->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total') - ->groupBy('vatable_id', 'vat_id') - ->get() - ->groupBy('vatable_id') - ->map(function ($items) { - return $items->pluck('total', 'vat_id')->toArray(); - }) - ->toArray(); - - $returnTransaction = $sale_returns->first()?->transactions->first(); - $returnTransactionType = ''; - - if ($returnTransaction) { - $returnTransactionType = $returnTransaction->transaction_type === 'bank_payment' - ? ($returnTransaction->paymentType?->name) - : ucfirst(explode('_', $returnTransaction->transaction_type)[0]); - } - - // sum of return_qty - $sale->details = $sale->details->map(function ($detail) use ($sale_returns) { - $return_qty_sum = $sale_returns->flatMap(function ($return) use ($detail) { - return $return->details->where('saleDetail.id', $detail->id)->pluck('return_qty'); - })->sum(); - - $detail->quantities = $detail->quantities + $return_qty_sum; - - return $detail; - }); - - // Calculate the initial discount for each product during sale returns - $total_discount = 0; - $product_discounts = []; - - foreach ($sale_returns as $return) { - foreach ($return->details as $detail) { - // Add the return quantities and return amounts for each sale_detail_id - if (! isset($product_discounts[$detail->sale_detail_id])) { - // Initialize the first occurrence - $product_discounts[$detail->sale_detail_id] = [ - 'return_qty' => 0, - 'return_amount' => 0, - 'price' => $detail->saleDetail->price, - ]; - } - - // Accumulate quantities and return amounts for the same sale_detail_id - $product_discounts[$detail->sale_detail_id]['return_qty'] += $detail->return_qty; - $product_discounts[$detail->sale_detail_id]['return_amount'] += $detail->return_amount; - } - } - - // Calculate the total discount based on accumulated quantities and return amounts - foreach ($product_discounts as $data) { - $product_price = $data['price'] * $data['return_qty']; - $discount = $product_price - $data['return_amount']; - - $total_discount += $discount; - } - - $hasWarranty = false; - $hasGuarantee = false; - - foreach ($sale->details as $detail) { - $info = $detail->warranty_guarantee_info ?? []; - - if (! empty($info['warranty_duration'])) { - $hasWarranty = true; - } - - if (! empty($info['guarantee_duration'])) { - $hasGuarantee = true; - } - - if ($hasWarranty && $hasGuarantee) { - break; - } - } - - $bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first(); - - $pdf = PdfService::render( - 'taxinvoiceaddon::sales.pdf', - compact( - 'sale', - 'sale_returns', - 'total_discount', - 'transactionTypes', - 'returnTransactionType', - 'bank_detail', - 'hasWarranty', - 'hasGuarantee', - 'tax_items', - 'business_id', - 'vatRowMap', - 'returnVatRowMap' - ), - 'sales-tax-invoice.pdf' - ); - - $pdfContent = $pdf->getContent(); - - // Send email with PDF attachment - Mail::raw('Please find attached your sales invoice.', function ($message) use ($sale, $pdfContent) { - $message->to($sale->party->email) - ->subject('Sales Tax Invoice') - ->attachData($pdfContent, 'sales-tax-invoice.pdf', [ + ->attachData($pdf->output(), 'sales-invoice.pdf', [ 'mime' => 'application/pdf', ]); }); @@ -1905,7 +1474,7 @@ public function sendTaxMail(string $sale_id) public function createCustomer(Request $request) { $request->validate([ - 'phone' => 'nullable|max:20|'.Rule::unique('parties')->where('business_id', auth()->user()->business_id), + 'phone' => 'nullable|max:20|' . Rule::unique('parties')->where('business_id', auth()->user()->business_id), 'name' => 'required|string|max:255', 'type' => 'required|string|in:Retailer,Dealer,Wholesaler,Supplier', 'email' => 'nullable|email', @@ -1916,13 +1485,13 @@ public function createCustomer(Request $request) Party::create($request->except('image', 'due') + [ 'due' => $request->due ?? 0, - 'image' => $request->image ? $this->upload($request, 'image') : null, - 'business_id' => auth()->user()->business_id, + 'image' => $request->image ? $this->upload($request, 'image') : NULL, + 'business_id' => auth()->user()->business_id ]); return response()->json([ 'message' => __('Customer created successfully'), - 'redirect' => route('business.sales.create'), + 'redirect' => route('business.sales.create') ]); } @@ -1930,28 +1499,36 @@ public function createInventory() { // Clears all cart items Cart::destroy(); - $business_id = auth()->user()->business_id; - $activeBranchId = auth()->user()->active_branch?->id; - $customers = Party::where('type', '!=', 'Supplier') - ->where('business_id', $business_id) + $customers = Party::where('type', '!=', 'supplier') + ->where('business_id', auth()->user()->business_id) + ->latest() + ->get(); + + $products = Product::with([ + 'stocks' => function ($query) { + $query->where('productStock', '>', 0); + }, + 'category:id,categoryName', + 'unit:id,unitName', + 'stocks.warehouse' + ]) + ->where('business_id', auth()->user()->business_id) + ->withSum('stocks as total_stock', 'productStock') ->latest() ->get() - ->transform(function ($customer) use ($activeBranchId) { - $customer->due = $customer->dueForBranch($activeBranchId); + ->where('total_stock', '>', 0) + ->values(); - return $customer; - }); - - $categories = Category::where('business_id', $business_id)->latest()->get(); - $vats = Vat::where('business_id', $business_id)->whereStatus(1)->latest()->get(); - $payment_types = PaymentType::where('business_id', $business_id)->whereStatus(1)->latest()->get(); + $categories = Category::where('business_id', auth()->user()->business_id)->latest()->get(); + $vats = Vat::where('business_id', auth()->user()->business_id)->whereStatus(1)->latest()->get(); + $payment_types = PaymentType::where('business_id', auth()->user()->business_id)->whereStatus(1)->latest()->get(); // Generate a unique invoice number $sale_id = (Sale::max('id') ?? 0) + 1; - $invoice_no = 'S-'.str_pad($sale_id, 5, '0', STR_PAD_LEFT); + $invoice_no = 'S-' . str_pad($sale_id, 5, '0', STR_PAD_LEFT); - return view('business::sales.inventory', compact('customers', 'invoice_no', 'categories', 'vats', 'payment_types')); + return view('business::sales.inventory', compact('customers', 'products', 'invoice_no', 'categories', 'vats', 'payment_types')); } public function viewPayment(string $type, string $id) @@ -1964,7 +1541,7 @@ public function viewPayment(string $type, string $id) 'due' => DueCollect::class, ]; - if (! isset($modelMap[$type])) { + if (!isset($modelMap[$type])) { return response()->json(['message' => 'Invalid type'], 400); } @@ -2007,7 +1584,7 @@ public function deletePayment(string $type, string $id) 'due' => DueCollect::class, ]; - if (! isset($modelMap[$type])) { + if (!isset($modelMap[$type])) { return response()->json(['message' => 'Invalid type'], 400); } diff --git a/Modules/Business/App/Http/Controllers/CartController.php b/Modules/Business/App/Http/Controllers/CartController.php index a8050c7f..e117eabd 100644 --- a/Modules/Business/App/Http/Controllers/CartController.php +++ b/Modules/Business/App/Http/Controllers/CartController.php @@ -40,212 +40,65 @@ public function store(Request $request) 'warehouse_id' => 'nullable|exists:warehouses,id', 'type' => 'nullable|string|in:sale,purchase', 'id' => 'required|integer', - 'name' => 'nullable|string', + 'name' => 'required|string', 'quantity' => 'required|numeric', 'price' => 'required|numeric', 'product_code' => 'nullable|string', 'product_unit_id' => 'nullable|integer', 'product_unit_name' => 'nullable|string', 'product_image' => 'nullable|string', - 'profit_percent' => 'nullable|numeric', 'sales_price' => 'nullable|numeric', 'whole_sale_price' => 'nullable|numeric', 'dealer_price' => 'nullable|numeric', 'expire_date' => 'nullable|date', 'product_type' => 'nullable|in:single,variant,combo', 'variant_name' => 'nullable|string', - 'serial_numbers' => 'nullable|array', - 'serial_numbers.*' => 'string', - 'has_serial' => 'nullable|boolean', - 'price_without_tax' => 'required_without:serial_numbers|numeric|nullable', - 'customer_type' => 'nullable|string' + 'vat_percent' => 'nullable|numeric', ]); - $incomingSerials = $request->serial_numbers ?? []; - - // Serial Mode - if (!empty($incomingSerials)) { - // Sale - if ($request->type === 'sale') { - - $stocks = Stock::where('business_id', auth()->user()->business_id) - ->where('product_id', $request->id) - ->where(function ($query) use ($incomingSerials) { - foreach ($incomingSerials as $serial) { - $query->orWhereJsonContains('serial_numbers', $serial); - } - })->get(); - - // Map serial → stock - $serialStockMap = []; - - foreach ($stocks as $stock) { - foreach ($incomingSerials as $serial) { - if (in_array($serial, $stock->serial_numbers ?? [])) { - $serialStockMap[$stock->id][] = $serial; - } - } - } - - // Add or merge cart per stock - foreach ($serialStockMap as $stockId => $serialsForStock) { - - $resolvedStock = $stocks->firstWhere('id', $stockId); - $qty = count($serialsForStock); - - $customerType = $request->customer_type ?? 'Retailer'; - - // Determine price based on customer type - $price = round($resolvedStock->productSalePrice ?? 0, 2); - - if ($customerType === 'Dealer') { - $price = round($resolvedStock->productDealerPrice ?? 0, 2); - } elseif ($customerType === 'Wholesaler') { - $price = round($resolvedStock->productWholeSalePrice ?? 0, 2); - } - - $existingCartItem = Cart::search(function ($item) use ($request, $stockId) { - return $item->id == $request->id && - $item->options->type === 'sale' && - $item->options->stock_id == $stockId; - })->first(); - - if ($existingCartItem) { - - $existingSerials = $existingCartItem->options->serial_numbers ?? []; - - // Remove already existing serials from incoming - $newSerials = array_diff($serialsForStock, $existingSerials); - - $duplicates = array_intersect($existingSerials, $newSerials); - - if (!empty($duplicates)) { - return response()->json([ - 'success' => false, - 'message' => 'Serial already exists in cart', - 'duplicates' => array_values($duplicates), - ], 422); - } - - $mergedSerials = array_values(array_unique( - array_merge($existingSerials, $serialsForStock) - )); - - Cart::update($existingCartItem->rowId, [ - 'qty' => count($mergedSerials), - 'options' => $existingCartItem->options->merge([ - 'serial_numbers' => $mergedSerials, - ]), - ]); - - } else { - - Cart::add([ - 'id' => $request->id, - 'name' => $request->name, - 'qty' => $qty, - 'price' => $price, - 'options' => [ - 'type' => 'sale', - 'product_code' => $request->product_code, - 'product_unit_id' => $request->product_unit_id, - 'product_unit_name' => $request->product_unit_name, - 'product_image' => $request->product_image, - 'product_type' => $request->product_type, - 'variant_name' => $request->variant_name, - 'stock_id' => $resolvedStock->id, - 'warehouse_id' => $resolvedStock->warehouse_id, - 'sales_price' => $resolvedStock->productSalePrice, - 'whole_sale_price' => $resolvedStock->productWholeSalePrice, - 'dealer_price' => $resolvedStock->productDealerPrice, - 'serial_numbers' => $serialsForStock, - 'has_serial' => $request->has_serial ?? 1, - 'price_without_tax' => $resolvedStock->price_without_tax, - ] - ]); - } - } + // Check for existing item in cart by type + $existingCartItem = Cart::search( + fn($item) => $item->id == $request->id && + $item->options->type == $request->type && + match ($request->type) { + 'purchase' => $item->options->batch_no == $request->batch_no, + 'sale' => $item->options->stock_id == $request->stock_id, + default => false, } - // Purchase - else { - Cart::add([ - 'id' => $request->id, - 'name' => $request->name, - 'qty' => count($incomingSerials), - 'price' => round($request->price, 2), - 'options' => [ - 'type' => 'purchase', - 'product_code' => $request->product_code, - 'product_unit_id' => $request->product_unit_id, - 'product_unit_name' => $request->product_unit_name, - 'product_image' => $request->product_image, - 'product_type' => $request->product_type, - 'variant_name' => $request->variant_name, - 'stock_id' => null, - 'warehouse_id' => $request->warehouse_id, - 'serial_numbers' => $incomingSerials, - 'batch_no' => $request->batch_no, - 'expire_date' => $request->expire_date, - 'purchase_price' => $request->purchase_price, - 'profit_percent' => $request->profit_percent, - 'sales_price' => $request->sales_price, - 'whole_sale_price' => $request->whole_sale_price, - 'dealer_price' => $request->dealer_price, - 'has_serial' => $request->has_serial ?? 1, - 'price_without_tax' => $request->price_without_tax, - ] - ]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Serial product added successfully.' - ]); - } - - // Normal product ( without serial) - $existingCartItem = Cart::search(function ($item) use ($request) { - return $item->id == $request->id && - $item->options->type == $request->type; - })->first(); + )->first(); if ($existingCartItem) { - - Cart::update($existingCartItem->rowId, [ - 'qty' => $existingCartItem->qty + $request->quantity - ]); - + $newQty = $existingCartItem->qty + $request->quantity; + Cart::update($existingCartItem->rowId, ['qty' => $newQty]); } else { - - Cart::add([ + // Add new item to cart + $mainItemData = [ 'id' => $request->id, 'name' => $request->name, 'qty' => $request->quantity, - 'price' => round($request->price, 2), + 'price' => $request->price, 'options' => [ 'type' => $request->type, 'product_code' => $request->product_code, 'product_unit_id' => $request->product_unit_id, 'product_unit_name' => $request->product_unit_name, - 'product_image' => $request->product_image, - 'product_type' => $request->product_type, - 'variant_name' => $request->variant_name, 'stock_id' => $request->stock_id, 'batch_no' => $request->batch_no, + 'product_image' => $request->product_image, 'expire_date' => $request->expire_date, 'purchase_price' => $request->purchase_price, - 'profit_percent' => $request->profit_percent, 'sales_price' => $request->sales_price, 'whole_sale_price' => $request->whole_sale_price, 'dealer_price' => $request->dealer_price, + 'product_type' => $request->product_type, 'warehouse_id' => $request->warehouse_id, - 'has_serial' => $request->has_serial ?? 0, - 'price_without_tax' => $request->price_without_tax, + 'variant_name' => $request->variant_name, + 'vat_percent' => $request->vat_percent, ] - ]); + ]; + Cart::add($mainItemData); } - return response()->json([ 'success' => true, 'message' => 'Item added to cart successfully.' @@ -255,45 +108,20 @@ public function store(Request $request) public function update(Request $request, $id) { $cart = Cart::get($id); + if (!$cart) { - return response()->json([ - 'success' => false, - 'message' => __('Item not found in cart') - ]); + return response()->json(['success' => false, 'message' => __('Item not found in cart')]); } $qty = $request->qty ?? $cart->qty; if ($qty < 0) { - return response()->json([ - 'success' => false, - 'message' => __('Enter a valid quantity') - ]); - } - - // incoming serial - $incomingSerials = $request->serial_numbers ?? null; - - if (is_array($incomingSerials)) { - $existingSerials = $incomingSerials; - } else { - $existingSerials = $cart->options->serial_numbers ?? []; - } - - // normalize to array - if (!is_array($existingSerials)) { - $existingSerials = !empty($existingSerials) ? [$existingSerials] : []; - } - - $hasSerial = $cart->options->has_serial ?? 0; - - if ($hasSerial) { - $qty = count($existingSerials); + return response()->json(['success' => false, 'message' => __('Enter a valid quantity')]); } Cart::update($id, [ 'qty' => $qty, - 'price' => round($request->price ?? $cart->price, 2), + 'price' => $request->price ?? $cart->price, 'options' => [ 'type' => $cart->options->type, 'expire_date' => $request->expire_date ?? $cart->options->expire_date, @@ -303,7 +131,6 @@ public function update(Request $request, $id) 'product_unit_id' => $cart->options->product_unit_id, 'product_unit_name' => $cart->options->product_unit_name, 'product_image' => $cart->options->product_image, - 'profit_percent' => $cart->options->profit_percent, 'sales_price' => $cart->options->sales_price, 'discount' => $request->discount ?? $cart->options->discount, 'whole_sale_price' => $cart->options->whole_sale_price, @@ -312,9 +139,7 @@ public function update(Request $request, $id) 'product_type' => $cart->options->product_type, 'warehouse_id' => $cart->options->warehouse_id, 'variant_name' => $cart->options->variant_name, - 'price_without_tax' => $cart->options->price_without_tax, - 'has_serial' => $hasSerial, - 'serial_numbers' => $existingSerials, + 'vat_percent' => $cart->options->vat_percent, ] ]); diff --git a/Modules/Business/App/Http/Controllers/SaleReturnController.php b/Modules/Business/App/Http/Controllers/SaleReturnController.php index 60b6b512..9a4027ca 100644 --- a/Modules/Business/App/Http/Controllers/SaleReturnController.php +++ b/Modules/Business/App/Http/Controllers/SaleReturnController.php @@ -3,17 +3,16 @@ namespace Modules\Business\App\Http\Controllers; use App\Events\MultiPaymentProcessed; -use App\Http\Controllers\Controller; -use App\Models\Branch; -use App\Models\Party; use App\Models\PaymentType; use App\Models\Sale; -use App\Models\SaleReturn; -use App\Models\SaleReturnDetails; +use App\Models\Party; use App\Models\Stock; -use App\Services\VatTransactionService; +use App\Models\Branch; +use App\Models\SaleReturn; use Illuminate\Http\Request; +use App\Models\SaleReturnDetails; use Illuminate\Support\Facades\DB; +use App\Http\Controllers\Controller; class SaleReturnController extends Controller { @@ -85,67 +84,47 @@ public function create(Request $request) return view('business::sale-returns.create', compact('sale', 'discount_per_unit_factor', 'avg_rounding_amount', 'payment_types')); } + public function store(Request $request) { - // split-array structure from front-end request - $products = []; - for ($index = 0; $index < count($request->products); $index += 3) { - $products[] = [ - 'detail_id' => $request->products[$index]['detail_id'] ?? null, - 'return_qty' => $request->products[$index + 1]['return_qty'] ?? 0, - 'serial_numbers' => $request->products[$index + 2]['serial_numbers'] ?? '[]', - ]; - } - $request->merge(['products' => $products]); - $request->validate([ 'sale_id' => 'required|exists:sales,id', - 'products' => 'required|array|min:1', - 'products.*.detail_id' => 'required|exists:sale_details,id', - 'products.*.return_qty' => 'required|integer', - 'products.*.serial_numbers' => 'nullable|string', + 'return_qty' => 'required|array', ]); $business_id = auth()->user()->business_id; DB::beginTransaction(); try { - // Load sale with details and product info - $sale = Sale::with( - 'details:id,sale_id,product_id,price,discount,lossProfit,quantities,productPurchasePrice,stock_id,expire_date,serial_numbers', - 'details.product:id,product_type,has_serial', - 'details.product.combo_products' - ) - ->where('business_id', $business_id) - ->findOrFail($request->sale_id); + $sale = Sale::with('details:id,sale_id,product_id,price,discount,lossProfit,quantities,productPurchasePrice,stock_id,expire_date', 'details.product:id,product_type', 'details.product.combo_products') + ->where('business_id', $business_id) + ->findOrFail($request->sale_id); - $old_sale_due = $sale->dueAmount; - - // Calculate discount factors + // Calculate total discount factor with itemwise discount $total_discount = $sale->discountAmount; $total_sale_amount = $sale->details->sum(fn($detail) => $detail->price * $detail->quantities); $discount_per_unit_factor = $total_sale_amount > 0 ? $total_discount / $total_sale_amount : 0; $rounding_amount_per_unit = $sale->details->sum('quantities') > 0 ? $sale->rounding_amount / $sale->details->sum('quantities') : 0; - // Create sale return record $sale_return = SaleReturn::create([ - 'return_date' => now(), 'business_id' => $business_id, 'sale_id' => $request->sale_id, 'invoice_no' => $sale->invoiceNumber, + 'return_date' => now(), ]); + $sale_return_detail_data = []; $total_return_amount = 0; $total_return_discount = 0; + $total_return_vat = 0; $total_loss_profit_adjustment = 0; - foreach ($request->products as $indextem) { + foreach ($sale->details as $key => $detail) { + $requested_qty = $request->return_qty[$key]; - $detail = $sale->details->where('id', $indextem['detail_id'])->first(); - if (!$detail) continue; // skip if detail not found - - $requested_qty = (int) ($indextem['return_qty'] ?? 0); - if ($requested_qty <= 0) continue; + if ($requested_qty <= 0) { + continue; + } if ($requested_qty > $detail->quantities) { return response()->json([ @@ -153,78 +132,63 @@ public function store(Request $request) ], 400); } - $return_serials = !empty($indextem['serial_numbers']) ? json_decode($indextem['serial_numbers'], true) : []; + $product = $detail->product; // Include SaleDetails discount in return calculation $unit_discount = $detail->price * $discount_per_unit_factor; - $indextem_cart_discount = $detail->discount ?? 0; - $total_discount_per_unit = $unit_discount + $indextem_cart_discount; + $item_cart_discount = $detail->discount ?? 0; + $total_discount_per_unit = $unit_discount + $item_cart_discount; + + // Calculate VAT for the item + $item_vat_percent = $detail->product->vat->rate ?? 0; + $item_vat_amount = ($detail->price * $item_vat_percent) / 100; $return_discount = $total_discount_per_unit * $requested_qty; - $return_amount = ($detail->price - $total_discount_per_unit + $rounding_amount_per_unit) * $requested_qty; + $return_amount = ($detail->price - $total_discount_per_unit + $rounding_amount_per_unit + $item_vat_amount) * $requested_qty; $total_return_amount += $return_amount; $total_return_discount += $return_discount; + $total_return_vat += ($item_vat_amount * $requested_qty); - $product = $detail->product; - - // Combo products if ($product && $product->product_type === 'combo') { + $combo_total_purchase = 0; + $combo_total_sale = $detail->price * $requested_qty; + foreach ($product->combo_products as $comboItem) { $stock = Stock::find($comboItem->stock_id); + if (!$stock) { return response()->json([ 'message' => __("Stock not found for combo item '{$comboItem->product->productName}'"), ], 400); } + // increase stock by combo component quantity * returned qty $restore_qty = $comboItem->quantity * $requested_qty; $stock->increment('productStock', $restore_qty); + + $combo_total_purchase += $comboItem->purchase_price * $restore_qty; } + + // loss/profit adjustment for combo + $loss_profit_adjustment = ($combo_total_sale - $combo_total_purchase) - $return_discount; + $total_loss_profit_adjustment += $loss_profit_adjustment; } - // Single / variant products + else { $stock = Stock::where('id', $detail->stock_id)->first(); - if (!$stock) return response()->json(['error' => 'Stock not found.'], 404); - - if ($product->has_serial) { - - // Validate serial count - if (count($return_serials) !== $requested_qty) { - return response()->json([ - 'message' => 'Serial count must match return quantity' - ], 400); - } - - $sold_serials = is_array($detail->serial_numbers) ? $detail->serial_numbers : json_decode($detail->serial_numbers ?? '[]', true); - $indexnvalid = array_diff($return_serials, $sold_serials); - if (!empty($indexnvalid)) { - return response()->json(['message' => 'Invalid serial detected'], 400); - } - - // Restore stock - $stock->increment('productStock', $requested_qty); - - // Restore serials to stock - $stock_serials = $stock->serial_numbers ?? []; - $stock->update([ - 'serial_numbers' => array_values(array_unique(array_merge($stock_serials, $return_serials))) - ]); - - // Remove serials from sale detail - $detail->update([ - 'serial_numbers' => array_values(array_diff($sold_serials, $return_serials)) - ]); - } else { - $stock->increment('productStock', $requested_qty); + if (!$stock) { + return response()->json(['error' => 'Stock not found.'], 404); } + + $stock->increment('productStock', $requested_qty); + + $loss_profit_adjustment = (($detail->price - $stock->productPurchasePrice) * $requested_qty) - $return_discount; + $total_loss_profit_adjustment += $loss_profit_adjustment; } - $loss_profit_adjustment = $detail->lossProfit * ($requested_qty / $detail->quantities); - $total_loss_profit_adjustment += $loss_profit_adjustment; - - // Update sale detail + // Update sale details $detail->quantities -= $requested_qty; $detail->lossProfit -= $loss_profit_adjustment; $detail->timestamps = false; @@ -236,21 +200,22 @@ public function store(Request $request) 'sale_return_id' => $sale_return->id, 'return_qty' => $requested_qty, 'return_amount' => $return_amount, - 'serial_numbers' => $return_serials ?? null, ]); - $previous_returned_qty = SaleReturnDetails::where('sale_detail_id', $detail->id)->sum('return_qty'); - $totalQty = $detail->quantities + $previous_returned_qty; - - VatTransactionService::productReturnVatCalculation($detail, $requested_qty, $returnDetail, $totalQty); + // Record VAT transaction for the return + if (class_exists('App\Services\VatTransactionService')) { + $totalQty = $detail->quantities + $requested_qty; // Original quantity before this return decrement + \App\Services\VatTransactionService::productReturnVatCalculation($detail, $requested_qty, $returnDetail, $totalQty); + } } if ($total_return_amount <= 0) { return response()->json("You cannot return an empty product.", 400); } - // Remaining refund / party logic $remaining_refund = $total_return_amount; + + // Adjust Due $new_due = $sale->dueAmount; if ($remaining_refund > 0) { if ($remaining_refund >= $sale->dueAmount) { @@ -262,6 +227,7 @@ public function store(Request $request) } } + // Adjust Paid (refund part) $new_paid = $sale->paidAmount; if ($remaining_refund > 0) { if ($remaining_refund >= $sale->paidAmount) { @@ -273,49 +239,53 @@ public function store(Request $request) } } + // total sale amount $new_total_amount = max(0, $sale->totalAmount - $total_return_amount); + $sale->update([ 'change_amount' => 0, 'dueAmount' => $new_due, 'paidAmount' => $new_paid, 'totalAmount' => $new_total_amount, 'actual_total_amount' => $new_total_amount, + 'vat_amount' => max(0, $sale->vat_amount - $total_return_vat), 'discountAmount' => max(0, $sale->discountAmount - $total_return_discount), 'lossProfit' => $sale->lossProfit - $total_loss_profit_adjustment, ]); - // Party refund logic - $due_reduction = $old_sale_due - $new_due; + // Party Refund Logic + $party = Party::find($sale->party_id); - if ($due_reduction > 0) { - Party::where('business_id', $business_id) - ->where('id', $sale->party_id) - ->decrement('due', $due_reduction); - } + if ($party) { - $party = Party::where('business_id', $business_id)->find($sale->party_id); - - if ($party && $remaining_refund > 0) { + // use leftover refund after due/paid adjustments + $refund_amount = $remaining_refund; + // Reduce party due if ($party->due > 0) { - - if ($party->due >= $remaining_refund) { - $party->decrement('due', $remaining_refund); - $remaining_refund = 0; + if ($party->due >= $refund_amount) { + $party->decrement('due', $refund_amount); + $refund_amount = 0; } else { - $remaining_refund -= $party->due; + $refund_amount -= $party->due; $party->update(['due' => 0]); } - } - if ($remaining_refund > 0) { - $party->increment('wallet', $remaining_refund); + // Add leftover refund to wallet + if ($refund_amount > 0) { + $party->increment('wallet', $refund_amount); } } - // Payment event - $payments = $request->payments ?? []; + $payments = $request->payments; + + if (isset($payments['main'])) { + $mainPayment = $payments['main']; + $mainPayment['amount'] = $request->receive_amount ?? 0; + $payments = [$mainPayment]; + } + $payments = collect($payments)->map(function ($payment) use ($total_return_amount) { $payment['amount'] = $total_return_amount; return $payment; @@ -325,22 +295,20 @@ public function store(Request $request) $payments, $sale_return->id, 'sale_return', - $total_return_amount, + $total_return_amount ?? 0, )); - $tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($sale->business_id) == 'standard_a4'; - $invoice_route = $tax_invoice ? route('business.sales.tax.invoice', $sale->id) : route('business.sales.invoice', $sale->id); - DB::commit(); return response()->json([ 'message' => __('Sale returned successfully.'), 'redirect' => route('business.sale-returns.index'), - 'secondary_redirect_url' => $invoice_route, + 'secondary_redirect_url' => route('business.sales.invoice', $sale->id), ]); } catch (\Exception $e) { DB::rollback(); - return response()->json(['message' => __('Something went wrong!') . ' ' . $e->getMessage()], 500); + return response()->json(['message' => __('Something went wrong!')], 500); } } + } diff --git a/Modules/Business/resources/views/purchases/bulk-upload/index.blade.php b/Modules/Business/resources/views/purchases/bulk-upload/index.blade.php index 56be726c..dcfec8f9 100644 --- a/Modules/Business/resources/views/purchases/bulk-upload/index.blade.php +++ b/Modules/Business/resources/views/purchases/bulk-upload/index.blade.php @@ -20,23 +20,7 @@ class="bulk_cart_upload_form">
-
-
-
-
-
- - - - {{ __('Add File') }} -
- -

- -
-
-
-
+
@@ -48,9 +32,9 @@ class="bulk_cart_upload_form">
{{__('Instructions') }}
-
{{__('Note') }}: {{__('Please follow the instructions below to upload your file.')}}
+
{{__('Note') }}: {{__(' Please follow the instructions below to upload your file.')}}
    -
  • {{__('1.')}}{{__('Download the sample file first and add all your purchases data to it.')}}
  • +
  • {{__('1.')}}{{__(' Download the sample file first and add all your purchases data to it.')}}
  • {{__('2.')}} * {{__('Indicates a required field. If you do not provide the required fields, the system will ignore except product information.')}}
  • {{__('3.')}} {{__('After adding all your purchases, please save the file and then upload the updated version.')}}
diff --git a/Modules/Business/resources/views/purchases/bulk-upload/purchase-import.blade.php b/Modules/Business/resources/views/purchases/bulk-upload/purchase-import.blade.php index 49fff51f..a4eaa6e2 100644 --- a/Modules/Business/resources/views/purchases/bulk-upload/purchase-import.blade.php +++ b/Modules/Business/resources/views/purchases/bulk-upload/purchase-import.blade.php @@ -19,23 +19,7 @@
-
-
-
-
-
- - - - {{ __('Add File') }} -
- -

- -
-
-
-
+
diff --git a/Modules/Business/resources/views/purchases/cart-list.blade.php b/Modules/Business/resources/views/purchases/cart-list.blade.php index 9c07391c..6ff2ad2a 100644 --- a/Modules/Business/resources/views/purchases/cart-list.blade.php +++ b/Modules/Business/resources/views/purchases/cart-list.blade.php @@ -2,14 +2,8 @@ @php $modules = product_setting()->modules ?? []; @endphp - - + + {{ $cart->name }} @@ -23,9 +17,9 @@ @if (is_module_enabled($modules, 'show_product_expire_date')) @if (isset($modules['expire_date_type']) && ($modules['expire_date_type'] == 'dmy' || is_null($modules['expire_date_type']))) - - @else + @else + @endif @endif @@ -34,27 +28,13 @@ @endusercan - @if (moduleCheck('SerialCodeAddon')) - - - - @endif
- - options->has_serial ?? 0) == 1) disabled readonly @endif> -
diff --git a/Modules/Business/resources/views/purchases/create.blade.php b/Modules/Business/resources/views/purchases/create.blade.php index f4bff30a..91675b7a 100644 --- a/Modules/Business/resources/views/purchases/create.blade.php +++ b/Modules/Business/resources/views/purchases/create.blade.php @@ -97,9 +97,6 @@ @usercan('purchases.price') {{ __('Purchase Price') }} @endusercan - @if (moduleCheck('SerialCodeAddon')) - {{ __('Serial') }} - @endif {{ __('Qty') }} {{ __('Sub Total') }} {{ __('Action') }} @@ -150,23 +147,6 @@
{{ currency_format(0, currency: business_currency()) }}
-
-
{{ __('Discount') }}
-
-
- -
- -
-
{{ get_business_option('business-settings')['vat_name'] ?? 'Vat'}}
@@ -186,6 +166,23 @@ class="form-control right-start-input" placeholder="{{ __('0') }}" readonly>
+
+
{{ __('Discount') }}
+
+
+ +
+ +
+
{{ __('Shipping Charge') }}
@@ -246,7 +243,7 @@ class="btn btn-category w-100">{{ __('Category') }}
-
+
@include('business::purchases.product-list')
@@ -255,30 +252,25 @@ class="btn btn-category w-100">{{ __('Category') }}
+ @include('business::purchases.product-modal') + - - - @endsection @push('modal') - @if (moduleCheck('SerialCodeAddon')) - @include('serialcodeaddon::serial-code-modal') - @endif @include('business::purchases.calculator') @include('business::purchases.category-search') @include('business::purchases.brand-search') @include('business::purchases.supplier-create') @include('business::purchases.bulk-upload.index') - @include('business::purchases.product-modal') @endpush @push('js') - + diff --git a/Modules/Business/resources/views/purchases/datas.blade.php b/Modules/Business/resources/views/purchases/datas.blade.php index b4ae3c05..4885803d 100644 --- a/Modules/Business/resources/views/purchases/datas.blade.php +++ b/Modules/Business/resources/views/purchases/datas.blade.php @@ -65,57 +65,19 @@
-
-
{{ __('Discount') }}
-
-
- -
- -
-
-
{{ get_business_option('business-settings')['vat_name'] ?? 'Vat' }}
@@ -189,6 +169,23 @@ class="form-control receive_amount"
+
+
{{ __('Discount') }}
+
+
+ +
+ +
+
+
{{ __('Shipping Charge') }}
@@ -256,30 +253,22 @@ class="btn btn-category w-100">{{ __('Category') }}
+ @include('business::purchases.product-modal') + - - - - @endsection @push('modal') - @if (moduleCheck('SerialCodeAddon')) - @include('serialcodeaddon::serial-code-modal') - @endif @include('business::purchases.calculator') @include('business::purchases.category-search') @include('business::purchases.brand-search') - @include('business::purchases.product-modal') - @include('business::purchases.supplier-create') - @include('business::purchases.bulk-upload.index') @endpush @push('js') - + diff --git a/Modules/Business/resources/views/purchases/index.blade.php b/Modules/Business/resources/views/purchases/index.blade.php index 154a9841..0fd0c86c 100644 --- a/Modules/Business/resources/views/purchases/index.blade.php +++ b/Modules/Business/resources/views/purchases/index.blade.php @@ -4,10 +4,6 @@ {{ __('Purchase List') }} @endsection -@php - $modules = product_setting()->modules ?? []; -@endphp - @section('main_content')
diff --git a/Modules/Business/resources/views/purchases/invoice.blade.php b/Modules/Business/resources/views/purchases/invoice.blade.php index fc23eae0..1a926604 100644 --- a/Modules/Business/resources/views/purchases/invoice.blade.php +++ b/Modules/Business/resources/views/purchases/invoice.blade.php @@ -6,7 +6,7 @@ @section('main_content') -@if (invoice_setting($purchase->business_id) == '3_inch_80mm' && moduleCheck('ThermalPrinterAddon')) +@if (invoice_setting() == '3_inch_80mm' && moduleCheck('ThermalPrinterAddon')) @include('thermalprinteraddon::purchases.3_inch_80mm') @else @include('business::purchases.invoices.a4-size') diff --git a/Modules/Business/resources/views/purchases/invoices/a4-size.blade.php b/Modules/Business/resources/views/purchases/invoices/a4-size.blade.php index a944c0b3..35315127 100644 --- a/Modules/Business/resources/views/purchases/invoices/a4-size.blade.php +++ b/Modules/Business/resources/views/purchases/invoices/a4-size.blade.php @@ -1,46 +1,33 @@ -@php - $isBusinessRoute = request()->routeIs('business.purchases.*'); - $business_id = $purchase->business_id; - $vat_amount = $purchase->vat_amount; -@endphp