middleware('check.permission:sales.create')->only(['create', 'store']); $this->middleware('check.permission:sales.read')->only(['index']); $this->middleware('check.permission:sales.update')->only(['edit', 'update']); $this->middleware('check.permission:sales.delete')->only(['destroy', 'deleteAll']); $this->middleware('check.permission:inventory.create')->only(['createInventory']); } public function index(Request $request) { $business_id = auth()->user()->business_id; $salesWithReturns = SaleReturn::where('business_id', $business_id) ->pluck('sale_id') ->toArray(); $salesQuery = Sale::with(['user:id,name', 'branch:id,name', 'party:id,name,email,phone,type', 'details', 'details.product:id,productName,category_id', 'details.product.category:id,categoryName', 'payment_type:id,name'])->where('business_id', $business_id); $salesQuery->when($request->branch_id, function ($q) use ($request) { $q->where('branch_id', $request->branch_id); }); // Default to today $startDate = Carbon::today()->format('Y-m-d'); $endDate = Carbon::today()->format('Y-m-d'); if ($request->custom_days === 'yesterday') { $startDate = Carbon::yesterday()->format('Y-m-d'); $endDate = Carbon::yesterday()->format('Y-m-d'); } elseif ($request->custom_days === 'last_seven_days') { $startDate = Carbon::today()->subDays(6)->format('Y-m-d'); } elseif ($request->custom_days === 'last_thirty_days') { $startDate = Carbon::today()->subDays(29)->format('Y-m-d'); } elseif ($request->custom_days === 'current_month') { $startDate = Carbon::now()->startOfMonth()->format('Y-m-d'); $endDate = Carbon::now()->endOfMonth()->format('Y-m-d'); } elseif ($request->custom_days === 'last_month') { $startDate = Carbon::now()->subMonth()->startOfMonth()->format('Y-m-d'); $endDate = Carbon::now()->subMonth()->endOfMonth()->format('Y-m-d'); } elseif ($request->custom_days === 'current_year') { $startDate = Carbon::now()->startOfYear()->format('Y-m-d'); $endDate = Carbon::now()->endOfYear()->format('Y-m-d'); } elseif ($request->custom_days === 'custom_date' && $request->from_date && $request->to_date) { $startDate = Carbon::parse($request->from_date)->format('Y-m-d'); $endDate = Carbon::parse($request->to_date)->format('Y-m-d'); } $salesQuery->whereDate('saleDate', '>=', $startDate) ->whereDate('saleDate', '<=', $endDate); // Search Filter if ($request->filled('search')) { $salesQuery->where(function ($query) use ($request) { $query->where('paymentType', 'like', '%'.$request->search.'%') ->orWhere('invoiceNumber', 'like', '%'.$request->search.'%') ->orWhereHas('party', function ($q) use ($request) { $q->where('name', 'like', '%'.$request->search.'%'); }) ->orWhereHas('payment_type', function ($q) use ($request) { $q->where('name', 'like', '%'.$request->search.'%'); }) ->orWhereHas('branch', function ($q) use ($request) { $q->where('name', 'like', '%'.$request->search.'%'); }); }); } $perPage = $request->input('per_page', 20); $sales = $salesQuery->latest()->paginate($perPage)->appends($request->query()); if ($request->ajax()) { return response()->json([ 'data' => view('business::sales.datas', compact('sales', 'salesWithReturns'))->render(), ]); } $branches = Branch::withTrashed()->where('business_id', $business_id)->latest()->get(); $payment_types = PaymentType::where('business_id', $business_id)->whereStatus(1)->latest()->get(); return view('business::sales.index', compact('sales', 'salesWithReturns', 'branches', 'payment_types')); } 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') ->where('business_id', $business_id) ->latest() ->get() ->transform(function ($customer) use ($activeBranchId) { $customer->due = $customer->dueForBranch($activeBranchId); return $customer; }); $products = Product::with([ 'unit:id,unitName', 'vat:id,rate', 'brand:id,brandName', 'category:id,categoryName', 'product_model:id,name', 'stocks', 'combo_products.stock.product:id,productName,productCode', 'rack:id,name', 'shelf:id,name', ]) ->withSum('saleDetails', 'quantities') ->withSum('purchaseDetails', 'quantities') ->withSum('stocks', 'productStock') ->where('business_id', $business_id) ->where(function ($query) { $query->where(function ($q) { $q->where('product_type', '!=', 'combo') ->whereHas('stocks', function ($s) { $s->where('productStock', '>', 0); }); }) ->orWhere(function ($q) { $q->where('product_type', 'combo') ->whereHas('combo_products'); }); }) ->latest() ->get(); $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(); // Generate a unique invoice number $sale_id = (Sale::max('id') ?? 0) + 1; $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')); } public function store(Request $request) { $request->validate([ 'party_id' => 'nullable|exists:parties,id', 'vat_id' => 'nullable|exists:vats,id', 'invoiceNumber' => 'required|string', 'customer_phone' => 'nullable|string', 'receive_amount' => 'nullable|numeric', 'discountAmount' => 'nullable|numeric', 'discount_type' => 'nullable|in:flat,percent', 'shipping_charge' => 'nullable|numeric', 'saleDate' => 'nullable|date', ]); $business = Business::findOrFail(auth()->user()->business_id); // Get only 'sale' type items from cart $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); } DB::beginTransaction(); try { // Calculation: subtotal, vat, discount, shipping, rounding $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) $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::find($request->vat_id); $vatAmount = $vat ? ($subtotalAfterDiscount * $vat->rate) / 100 : 0; $shippingCharge = $request->shipping_charge ?? 0; $actualTotalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge; $roundingTotalAmount = sale_rounding($actualTotalAmount); $rounding_amount = $roundingTotalAmount - $actualTotalAmount; $rounding_option = sale_rounding(); $payments = $request->payments; if (isset($payments['main'])) { $mainPayment = $payments['main']; $mainPayment['amount'] = $request->receive_amount ?? 0; $payments = [$mainPayment]; } $receiveAmount = collect($payments) ->reject(fn ($p) => strtolower($p['type'] ?? '') === 'cheque') ->sum(fn ($p) => $p['amount'] ?? 0); $changeAmount = max($receiveAmount - $roundingTotalAmount, 0); $dueAmount = max($roundingTotalAmount - $receiveAmount, 0); $paidAmount = $receiveAmount - $changeAmount; // Update business/branch balance updateBalance($paidAmount, 'increment'); // Create sale record $sale = Sale::create([ 'user_id' => auth()->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, 'invoiceNumber' => $request->invoiceNumber, 'saleDate' => $request->saleDate ? Carbon::parse($request->saleDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(), '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) : 0, 'totalAmount' => $roundingTotalAmount, 'actual_total_amount' => $actualTotalAmount, 'rounding_amount' => $rounding_amount, 'rounding_option' => $rounding_option, 'paidAmount' => min($paidAmount, $roundingTotalAmount), 'change_amount' => $changeAmount, 'dueAmount' => $dueAmount, 'shipping_charge' => $shippingCharge, 'isPaid' => $dueAmount > 0 ? 0 : 1, '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); foreach ($carts as $cartItem) { $qty = $cartItem->qty; $purchase_price = $cartItem->options->purchase_price ?? 0; // Find the product $product = Product::find($cartItem->id); if (! $product) { return response()->json([ 'message' => __("Product not found: {$cartItem->name}"), ], 400); } $discount = $cartItem->options->discount ?? 0; $actualPrice = $cartItem->price - $discount; // Combo Products if ($product->product_type == 'combo') { $combo_purchase_price = $product->combo_products->sum('purchase_price'); 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})" : ''; return response()->json([ 'message' => __("Combo item '{$stock->product->productName}'{$batchText} - stock not available. Available: {$stock->productStock}"), ], 400); } // Decrement stock $stock->decrement('productStock', $required_qty); } // Store sale details for combo product itself $detail = SaleDetails::create([ '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([ 'lossProfit' => $lossProfit, 'price_without_tax' => $price_without_tax, ]); } // Single / Variant Products 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)" : ''; return response()->json([ 'message' => __($cartItem->name.$batchText.' - stock not available. Available: '.$stock->productStock), ], 400); } if (! empty($newSerials)) { // Remove sold serials from stock $remainingSerials = array_values(array_diff($existingSerials, $newSerials)); // Update stock serial list $stock->update([ 'serial_numbers' => $remainingSerials, ]); } $stock->decrement('productStock', $finalQty); $detail = SaleDetails::create([ 'sale_id' => $sale->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([ 'lossProfit' => $lossProfit, 'price_without_tax' => $price_without_tax, ]); } } $sale->update([ 'lossProfit' => $totalLossProfit, ]); // MultiPaymentProcessed Event event(new MultiPaymentProcessed( $payments ?? [], $sale->id, 'sale', $receiveAmount, $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); } DB::commit(); return response()->json([ 'message' => __('Sales created successfully.'), 'redirect' => route('business.sales.index'), 'secondary_redirect_url' => $invoice_route, ]); } catch (\Exception $e) { DB::rollback(); return response()->json(['message' => $e->getMessage()], 400); } } 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') ->where('business_id', $business_id) ->findOrFail($id); $customers = Party::where('type', '!=', 'Supplier') ->where('business_id', $business_id) ->latest() ->get() ->transform(function ($customer) use ($activeBranchId) { $customer->due = $customer->dueForBranch($activeBranchId); return $customer; }); $products = Product::with('category:id,categoryName', 'unit:id,unitName', 'stocks', 'stocks.warehouse') ->where('business_id', $business_id) ->withSum('stocks as total_stock', 'productStock') ->where(function ($query) { $query->where('product_type', 'combo') ->orWhereHas('stocks', function ($q) { $q->where('productStock', '>', 0); }); }) ->latest() ->get(); $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(); // Add sale details to the cart foreach ($sale->details as $detail) { // Add to cart Cart::add([ 'id' => $detail->product_id, 'name' => $detail->product->productName ?? '', 'qty' => $detail->quantities, 'price' => $detail->price ?? 0, 'options' => [ 'type' => 'sale', 'product_code' => $detail->product->productCode ?? '', 'product_unit_id' => $detail->product->unit_id ?? null, 'product_unit_name' => $detail->product->unit->unitName ?? '', 'product_image' => $detail->product->productPicture ?? '', 'stock_id' => $detail->stock_id ?? null, 'batch_no' => $detail->stock->batch_no ?? '', '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, ], ]); } $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')); } public function update(Request $request, $id) { $request->validate([ 'party_id' => 'nullable|exists:parties,id', 'vat_id' => 'nullable|exists:vats,id', 'invoiceNumber' => 'required|string', 'customer_phone' => 'nullable|string', 'receive_amount' => 'nullable|numeric', 'discountAmount' => 'nullable|numeric', 'discount_type' => 'nullable|in:flat,percent', 'saleDate' => 'nullable|date', 'shipping_charge' => 'nullable|numeric', ]); $business = Business::findOrFail(auth()->user()->business_id); $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); } DB::beginTransaction(); try { $sale = Sale::findOrFail($id); $sale_prev_due = $sale->dueAmount; $prevDetails = $sale->details; $subtotal = 0; $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; $newQty = $cartItem->qty; $diffQty = $newQty - $oldQty; $product = Product::with('combo_products.product')->find($cartItem->id); if (! $product) { return response()->json(['message' => __("Product not found: {$cartItem->name}")], 400); } $discount = $cartItem->options->discount ?? 0; $actualPrice = $cartItem->price - $discount; if ($product->product_type === 'combo') { $combo_purchase_price = $product->combo_products->sum('purchase_price'); foreach ($product->combo_products as $comboItem) { $stock = Stock::where('id', $comboItem->stock_id)->first(); if (! $stock) { return response()->json([ '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})" : ''; return response()->json([ 'message' => __("Combo item '{$stock->product->productName}'{$batchText} - stock not available. Available: {$stock->productStock}"), ], 400); } if ($diffQty != 0) { $stock->productStock -= $diffQty * $comboItem->quantity; $stock->save(); } } $detail = SaleDetails::create([ '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([ 'lossProfit' => $lossProfit, 'price_without_tax' => $price_without_tax, ]); } else { $stock = Stock::where('id', $cartItem->options->stock_id ?? null) ->first() ?? Stock::where('product_id', $cartItem->id)->orderBy('id', 'asc')->first(); if (! $stock) { return response()->json([ '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), ], 400); } $stock->decrement('productStock', $finalQty); $detail = SaleDetails::create([ 'sale_id' => $sale->id, 'stock_id' => $stock->id, 'product_id' => $cartItem->id, 'price' => $cartItem->price, 'discount' => $discount, 'quantities' => $finalQty, '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, ]); } $subtotal += ($cartItem->price - ($cartItem->options->discount ?? 0)) * $newQty; } // 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 & total calculations (applied on subtotal after discount) $vatAmount = $vat ? ($subtotalAfterDiscount * $vat->rate) / 100 : 0; $actualTotalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge; $roundingTotalAmount = sale_rounding($actualTotalAmount, $sale->rounding_option); $rounding_amount = $roundingTotalAmount - $actualTotalAmount; // Normalize payments $payments = $request->payments ?? []; if (isset($payments['main'])) { $mainPayment = $payments['main']; $mainPayment['amount'] = $request->receive_amount ?? 0; $payments = [$mainPayment]; } // Check if any old transaction type = deposit $hasDeposit = Transaction::where('business_id', $business->id) ->where('reference_id', $sale->id) ->where('platform', 'sale') ->where('type', 'deposit') ->exists(); // Calculate total amount $receiveAmount = collect($payments) ->reject(function ($p) use ($hasDeposit) { // exclude cheque only if not deposit return ! $hasDeposit && strtolower($p['type'] ?? '') === 'cheque'; }) ->sum(fn ($p) => $p['amount'] ?? 0); $changeAmount = $receiveAmount > $roundingTotalAmount ? $receiveAmount - $roundingTotalAmount : 0; $dueAmount = max($roundingTotalAmount - $receiveAmount, 0); $paidAmount = $receiveAmount - $changeAmount; updateBalance($paidAmount, 'increment'); $sale->update([ 'invoiceNumber' => $request->invoiceNumber, 'saleDate' => $request->saleDate ? Carbon::parse($request->saleDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(), 'vat_id' => $request->vat_id, 'party_id' => $request->party_id, 'vat_amount' => $vatAmount, 'discountAmount' => $discountAmount, 'discount_type' => $request->discount_type ?? 'flat', 'discount_percent' => $request->discount_type == 'percent' ? ($request->discountAmount ?? 0) : 0, 'totalAmount' => $roundingTotalAmount, 'actual_total_amount' => $actualTotalAmount, 'rounding_amount' => $rounding_amount, 'lossProfit' => $totalLossProfit, 'shipping_charge' => $shippingCharge, 'paidAmount' => $paidAmount > $roundingTotalAmount ? $roundingTotalAmount : $paidAmount, 'change_amount' => $changeAmount, 'dueAmount' => $dueAmount, 'isPaid' => $dueAmount > 0 ? 0 : 1, 'meta' => [ 'customer_phone' => $request->customer_phone, 'note' => $request->note, ], ]); if ($dueAmount > 0) { $allowDueForGuest = product_setting()?->modules['allow_due_sale'] ?? false; // If guest or 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 { // Regular party $party = Party::findOrFail($request->party_id); // Calculate total due $newTotalDue = $request->party_id == $sale->party_id ? $party->due + $dueAmount - $sale_prev_due : $party->due + $dueAmount; // 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!'), ], 400); } // Update party dues if ($request->party_id == $sale->party_id) { $party->update(['due' => $party->due + $dueAmount - $sale_prev_due]); } else { $party->update(['due' => $party->due + $dueAmount]); $prevParty = Party::findOrFail($sale->party_id); $prevParty->update(['due' => $prevParty->due - $sale_prev_due]); } } } // Multiple Payment Process $oldTransactions = Transaction::where('business_id', $business->id) ->where('reference_id', $sale->id) ->where('platform', 'sale') ->get(); // Revert old transactions foreach ($oldTransactions as $old) { switch ($old->transaction_type) { case 'bank_payment': $paymentType = PaymentType::find($old->payment_type_id); if ($paymentType) { $paymentType->decrement('balance', $old->amount); } break; case 'wallet_payment': if ($request->party_id) { $party = Party::find($request->party_id); if ($party) { $party->increment('wallet', $old->amount); } } break; case 'cash_payment': case 'cheque_payment': // nothing to revert for cash/cheque break; } } // Delete old transactions Transaction::where('business_id', $business->id) ->where('reference_id', $sale->id) ->where('platform', 'sale') ->delete(); // Process new payments event(new MultiPaymentProcessed( $payments ?? [], $sale->id, 'sale', $receiveAmount, $request->party_id ?? null, )); foreach ($carts as $cartItem) { 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); DB::commit(); return response()->json([ 'message' => __('Sales updated successfully.'), 'redirect' => route('business.sales.index'), 'secondary_redirect_url' => $invoice_route, ]); } catch (\Exception $e) { DB::rollback(); return response()->json(['message' => $e->getMessage()], 404); } } public function destroy($id) { DB::beginTransaction(); try { $sale = Sale::where('business_id', auth()->user()->business_id)->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) { $stock = Stock::where('product_id', $detail->product_id)->orderBy('id', 'asc')->first(); } if ($stock) { $stock->increment('productStock', $detail->quantities); } } if ($sale->party_id) { $party = Party::findOrFail($sale->party_id); $party->update(['due' => $party->due - $sale->dueAmount]); } updateBalance($sale->paidAmount, 'decrement'); sendNotifyToUser($sale->id, route('business.sales.index', ['id' => $sale->id]), __('Sale has been deleted.'), $sale->business_id); $sale->delete(); // Clears all cart items Cart::destroy(); DB::commit(); return response()->json([ 'message' => __('Sale deleted successfully.'), 'redirect' => route('business.sales.index'), ]); } catch (\Exception $e) { DB::rollback(); return response()->json(['message' => __('Something went wrong!')], 404); } } public function deleteAll(Request $request) { DB::beginTransaction(); try { $sales = Sale::where('business_id', auth()->user()->business_id)->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) { $stock = Stock::where('product_id', $detail->product_id)->orderBy('id', 'asc')->first(); } if ($stock) { $stock->increment('productStock', $detail->quantities); } } // Adjust party due if ($sale->party_id) { $party = Party::findOrFail($sale->party_id); $party->update(['due' => $party->due - $sale->dueAmount]); } // Adjust business balance updateBalance($sale->paidAmount, 'decrement'); sendNotifyToUser($sale->id, route('business.sales.index', ['id' => $sale->id]), __('Sale has been deleted.'), $sale->business_id); $sale->delete(); } Cart::destroy(); DB::commit(); return response()->json([ 'message' => __('Selected sales deleted successfully.'), 'redirect' => route('business.sales.index'), ]); } catch (\Exception $e) { DB::rollback(); return response()->json(['message' => __('Something went wrong!')], 404); } } public function acnooFilter(Request $request) { $salesWithReturns = SaleReturn::where('business_id', auth()->user()->business_id) ->pluck('sale_id') ->toArray(); $salesQuery = Sale::with('user:id,name', 'branch:id,name', 'party:id,name,email,phone,type', 'details', 'details.product:id,productName,category_id', 'details.product.category:id,categoryName', 'payment_type:id,name') ->where('business_id', auth()->user()->business_id); $salesQuery->when($request->branch_id, function ($q) use ($request) { $q->where('branch_id', $request->branch_id); }); // Default to today $startDate = Carbon::today()->format('Y-m-d'); $endDate = Carbon::today()->format('Y-m-d'); if ($request->custom_days == 'yesterday') { $startDate = Carbon::yesterday()->format('Y-m-d'); $endDate = Carbon::yesterday()->format('Y-m-d'); } elseif ($request->custom_days == 'last_seven_days') { $startDate = Carbon::today()->subDays(6)->format('Y-m-d'); } elseif ($request->custom_days == 'last_thirty_days') { $startDate = Carbon::today()->subDays(29)->format('Y-m-d'); } elseif ($request->custom_days == 'current_month') { $startDate = Carbon::now()->startOfMonth()->format('Y-m-d'); $endDate = Carbon::now()->endOfMonth()->format('Y-m-d'); } elseif ($request->custom_days == 'last_month') { $startDate = Carbon::now()->subMonth()->startOfMonth()->format('Y-m-d'); $endDate = Carbon::now()->subMonth()->endOfMonth()->format('Y-m-d'); } elseif ($request->custom_days == 'current_year') { $startDate = Carbon::now()->startOfYear()->format('Y-m-d'); $endDate = Carbon::now()->endOfYear()->format('Y-m-d'); } elseif ($request->custom_days == 'custom_date' && $request->from_date && $request->to_date) { $startDate = Carbon::parse($request->from_date)->format('Y-m-d'); $endDate = Carbon::parse($request->to_date)->format('Y-m-d'); } $salesQuery->whereDate('saleDate', '>=', $startDate) ->whereDate('saleDate', '<=', $endDate); // Search Filter if ($request->filled('search')) { $salesQuery->where(function ($query) use ($request) { $query->where('paymentType', 'like', '%'.$request->search.'%') ->orWhere('invoiceNumber', 'like', '%'.$request->search.'%') ->orWhereHas('party', function ($q) use ($request) { $q->where('name', 'like', '%'.$request->search.'%'); }) ->orWhereHas('payment_type', function ($q) use ($request) { $q->where('name', 'like', '%'.$request->search.'%'); }) ->orWhereHas('branch', function ($q) use ($request) { $q->where('name', 'like', '%'.$request->search.'%'); }); }); } $perPage = $request->input('per_page', 10); $sales = $salesQuery->latest()->paginate($perPage); if ($request->ajax()) { return response()->json([ 'data' => view('business::sales.datas', compact('sales', 'salesWithReturns'))->render(), ]); } return redirect(url()->previous()); } public function productFilter(Request $request) { $total_products_count = Product::where('business_id', auth()->user()->business_id) ->whereHas('stocks', function ($s) { $s->where('productStock', '>', 0); }) ->count(); $products = Product::where('business_id', auth()->user()->business_id) ->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); }]) ->when($request->search, function ($query) use ($request) { $query->where(function ($q) use ($request) { $q->where('productName', 'like', '%'.$request->search.'%') ->orWhere('productCode', 'like', '%'.$request->search.'%'); }); }) ->when($request->category_id, function ($query) use ($request) { $query->where('category_id', $request->category_id); }) ->when($request->brand_id, function ($query) use ($request) { $query->where('brand_id', $request->brand_id); }) ->latest() ->get(); // 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.'%'); }) ->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.'%'); }) ->get(); $total_products = $products->count(); if ($request->ajax()) { return response()->json([ '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(), ]); } return redirect(url()->previous()); } // Category search Filter 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.'%'); }) ->get(); return response()->json([ 'categories' => view('business::sales.category-list', compact('categories'))->render(), ]); } // Brand search Filter 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.'%'); }) ->get(); return response()->json([ 'brands' => view('business::sales.brand-list', compact('brands'))->render(), ]); } /** Get customer wise prices */ public function getProductPrices(Request $request) { $type = $request->type; $products = Product::where('business_id', auth()->user()->business_id)->get(); $prices = []; foreach ($products as $product) { if ($type == 'Dealer') { $prices[$product->id] = currency_format($product->productDealerPrice, currency: business_currency()); } elseif ($type == 'Wholesaler') { $prices[$product->id] = currency_format($product->productWholeSalePrice, currency: business_currency()); } else { // For Retailer or any other type $prices[$product->id] = currency_format($product->productSalePrice, currency: business_currency()); } } return response()->json($prices); } /** Get batch wise prices */ 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(); // Fetch all stocks for product list prices $allStocks = Stock::where('business_id', $businessId) ->where('productStock', '>', 0) ->get(); $productPrices = []; // Product-single prices foreach ($allStocks as $stock) { // Determine price based on customer type if ($customerType == 'Dealer') { $price = $stock->productDealerPrice; } elseif ($customerType == 'Wholesaler') { $price = $stock->productWholeSalePrice; } else { $price = $stock->productSalePrice; } $productPrices[$stock->product_id] = currency_format($price, currency: $currency); } // For inventory sale, return all stock prices; otherwise only cart stocks $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 $cartStockIds = collect($cartStocks)->pluck('stock_id')->toArray(); $cartStockQuery = Stock::where('business_id', $businessId) ->whereIn('id', $cartStockIds) ->where('productStock', '>', 0) ->get(); foreach ($cartStockQuery 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); } } // Return both product and cart prices return response()->json([ 'products' => $productPrices, 'stocks' => $stockPrices, ]); } /** Get cart info */ public function getCartData() { $cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'sale'); $data['sub_total'] = 0; foreach ($cart_contents as $cart) { $data['sub_total'] += $cart->price; } $data['sub_total'] = currency_format($data['sub_total'], currency: business_currency()); return response()->json($data); } public function getInvoice(string $sale_id, SaleInvoiceService $service) { $data = $service->getInvoiceData($sale_id); return view('business::sales.invoice', $data); } public function getTaxInvoice($sale_id, SaleTaxInvoiceService $service) { $data = $service->getInvoiceData($sale_id); return view('taxinvoiceaddon::sales.tax', $data); } public function generatePDF($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', 'payment_type:id,name', 'transactions:id,reference_id,transaction_type,payment_type_id', 'transactions.paymentType:id,name' ) ->findOrFail($sale_id); // 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' ) ->where('business_id', auth()->user()->business_id) ->where('sale_id', $sale_id) ->latest() ->get(); // 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( 'business::sales.pdf', compact( 'sale', 'sale_returns', 'total_discount', 'transactionTypes', 'returnTransactionType', 'bank_detail', 'hasWarranty', 'hasGuarantee' ), 'sales-invoice.pdf' ); } public function generateTaxPDF(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); } $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(); $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( 'business::sales.pdf', compact( 'sale', 'sale_returns', 'total_discount', 'transactionTypes', 'returnTransactionType', 'bank_detail', 'hasWarranty', 'hasGuarantee' ), 'sales-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 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', [ 'mime' => 'application/pdf', ]); }); return response()->json([ 'message' => __('Email Sent Successfully.'), 'redirect' => route('business.sales.index'), ]); } public function createCustomer(Request $request) { $request->validate([ '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', 'image' => 'nullable|image', 'address' => 'nullable|string|max:255', 'due' => 'nullable|numeric|min:0', ]); Party::create($request->except('image', 'due') + [ 'due' => $request->due ?? 0, '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'), ]); } 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) ->latest() ->get() ->transform(function ($customer) use ($activeBranchId) { $customer->due = $customer->dueForBranch($activeBranchId); 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(); // Generate a unique invoice number $sale_id = (Sale::max('id') ?? 0) + 1; $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')); } public function viewPayment(string $type, string $id) { $modelMap = [ 'sale' => Sale::class, 'purchase' => Purchase::class, 'income' => Income::class, 'expense' => Expense::class, 'due' => DueCollect::class, ]; if (! isset($modelMap[$type])) { return response()->json(['message' => 'Invalid type'], 400); } $modelClass = $modelMap[$type]; $record = $modelClass::with(['transactions.paymentType:id,name']) ->where('business_id', auth()->user()->business_id) ->findOrFail($id); $transactions = $record->transactions->map(function ($transaction) { return [ 'id' => $transaction->id, 'date' => formatted_date($transaction->date), 'raw_date' => $transaction->date, 'receipt_no' => $transaction->invoice_no ?? '-', 'amount' => currency_format($transaction->amount, currency: business_currency()), 'raw_amount' => $transaction->amount, 'payment_type' => $transaction->transaction_type === 'bank_payment' ? ($transaction->paymentType->name ?? '') : ucfirst(explode('_', $transaction->transaction_type)[0] ?? ''), 'note' => $transaction->note ?? '', 'cheque_number' => $transaction->meta['cheque_number'] ?? '', ]; }); // Return additional data including total amount return response()->json([ 'data' => $transactions, 'total_amount' => $record->actual_total_amount ?? 0, ]); } public function deletePayment(string $type, string $id) { $modelMap = [ 'sale' => Sale::class, 'purchase' => Purchase::class, 'income' => Income::class, 'expense' => Expense::class, 'due' => DueCollect::class, ]; if (! isset($modelMap[$type])) { return response()->json(['message' => 'Invalid type'], 400); } Transaction::where('id', $id)->delete(); return response()->json([ 'message' => __('Transaction deleted successfully.'), 'redirect' => url()->previous(), ]); } }