1599 lines
67 KiB
PHP
1599 lines
67 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Modules\Business\App\Http\Controllers;
|
||
|
|
|
||
|
|
use Carbon\Carbon;
|
||
|
|
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 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
|
||
|
|
{
|
||
|
|
use HasUploader;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->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;
|
||
|
|
|
||
|
|
// Clears all cart items
|
||
|
|
Cart::destroy();
|
||
|
|
|
||
|
|
$customers = Party::where('type', '!=', 'supplier')
|
||
|
|
->where('business_id', $business_id)
|
||
|
|
->latest()
|
||
|
|
->get();
|
||
|
|
|
||
|
|
$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('is_displayed_in_pos', 1)
|
||
|
|
->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_id = 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;
|
||
|
|
|
||
|
|
$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 = ($subtotalWithVat * $discountAmount) / 100;
|
||
|
|
}
|
||
|
|
if ($discountAmount > $subtotalWithVat) {
|
||
|
|
return response()->json(['message' => __('Discount cannot be more than subtotal with VAT!')], 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
$shippingCharge = $request->shipping_charge ?? 0;
|
||
|
|
$actualTotalAmount = $subtotalWithVat - $discountAmount + $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,
|
||
|
|
'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,
|
||
|
|
]
|
||
|
|
]);
|
||
|
|
|
||
|
|
$avgDiscount = $discountAmount / max($carts->count(), 1);
|
||
|
|
$totalPurchaseAmount = 0;
|
||
|
|
$saleDetailsData = [];
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
$itemCartDiscount = ((float) $cartItem->options->discount ?? 0) * $qty;
|
||
|
|
|
||
|
|
if ($product->product_type == 'combo') {
|
||
|
|
$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})" : "";
|
||
|
|
return response()->json([
|
||
|
|
'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
|
||
|
|
$saleDetailsData[] = [
|
||
|
|
'sale_id' => $sale->id,
|
||
|
|
'stock_id' => null,
|
||
|
|
'product_id' => $cartItem->id,
|
||
|
|
'price' => $cartItem->price,
|
||
|
|
'discount' => $cartItem->options->discount ?? 0,
|
||
|
|
'lossProfit' => $lossProfit,
|
||
|
|
'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();
|
||
|
|
|
||
|
|
if ($stock->productStock < $qty) {
|
||
|
|
$batchText = $stock->batch_no ? " ($stock->batch_no)" : "";
|
||
|
|
return response()->json([
|
||
|
|
'message' => __($cartItem->name . $batchText . ' - stock not available. Available: ' . $stock->productStock)
|
||
|
|
], 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
$stock->decrement('productStock', $qty);
|
||
|
|
|
||
|
|
$lossProfit = (($cartItem->price - $stock->productPurchasePrice) * $qty) - $avgDiscount - $itemCartDiscount;
|
||
|
|
|
||
|
|
$saleDetailsData[] = [
|
||
|
|
'sale_id' => $sale->id,
|
||
|
|
'stock_id' => $cartItem->options->stock_id,
|
||
|
|
'product_id' => $cartItem->id,
|
||
|
|
'price' => $cartItem->price,
|
||
|
|
'discount' => $cartItem->options->discount ?? 0,
|
||
|
|
'lossProfit' => $lossProfit,
|
||
|
|
'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' => $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 ?? [],
|
||
|
|
$sale->id,
|
||
|
|
'sale',
|
||
|
|
$receiveAmount,
|
||
|
|
$request->party_id ?? null,
|
||
|
|
));
|
||
|
|
|
||
|
|
// 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' => route('business.sales.invoice', $sale->id),
|
||
|
|
]);
|
||
|
|
} 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;
|
||
|
|
|
||
|
|
$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')
|
||
|
|
->where('business_id', $business_id)
|
||
|
|
->latest()
|
||
|
|
->get();
|
||
|
|
|
||
|
|
$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,
|
||
|
|
'vat_percent' => $detail->product->vat->rate ?? 0,
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$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_id = 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;
|
||
|
|
|
||
|
|
$totalPurchaseAmount = 0;
|
||
|
|
$subtotal = 0;
|
||
|
|
$saleDetailsData = [];
|
||
|
|
|
||
|
|
$discountAmount = $request->discountAmount ?? 0;
|
||
|
|
$shippingCharge = $request->shipping_charge ?? 0;
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
$itemCartDiscount = ((float) $cartItem->options->discount ?? 0) * $newQty;
|
||
|
|
|
||
|
|
if ($product->product_type === 'combo') {
|
||
|
|
$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) {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
$stock->productStock -= $diffQty * $comboItem->quantity;
|
||
|
|
$stock->save();
|
||
|
|
|
||
|
|
$combo_total_purchase += $comboItem->purchase_price * $diffQty * $comboItem->quantity;
|
||
|
|
$totalPurchaseAmount += $comboItem->purchase_price * $diffQty * $comboItem->quantity;
|
||
|
|
}
|
||
|
|
|
||
|
|
$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' => $cartItem->options->discount ?? 0,
|
||
|
|
'lossProfit' => $lossProfit,
|
||
|
|
'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) {
|
||
|
|
return response()->json([
|
||
|
|
'message' => __($cartItem->name . ' - no stock found.')
|
||
|
|
], 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
$availableStock = $stock->productStock + $oldQty;
|
||
|
|
if ($availableStock < $newQty) {
|
||
|
|
return response()->json([
|
||
|
|
'message' => __($cartItem->name . ' - stock not available. Available: ' . $availableStock)
|
||
|
|
], 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
$stock->productStock -= $diffQty;
|
||
|
|
$stock->save();
|
||
|
|
|
||
|
|
$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' => $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 ? json_encode($product->warranty_guarantee_info) : null,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$subtotal += ($cartItem->price - ($cartItem->options->discount ?? 0)) * $newQty;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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 = ($subtotalWithVat * $discountAmount) / 100;
|
||
|
|
}
|
||
|
|
if ($discountAmount > $subtotalWithVat) {
|
||
|
|
return response()->json(['message' => __('Discount cannot be more than subtotal with VAT!')], 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
$actualTotalAmount = $subtotalWithVat - $discountAmount + $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,
|
||
|
|
'totalAmount' => $roundingTotalAmount,
|
||
|
|
'actual_total_amount' => $actualTotalAmount,
|
||
|
|
'rounding_amount' => $rounding_amount,
|
||
|
|
'lossProfit' => $subtotal - $totalPurchaseAmount - $discountAmount,
|
||
|
|
'paidAmount' => $paidAmount > $roundingTotalAmount ? $roundingTotalAmount : $paidAmount,
|
||
|
|
'change_amount' => $changeAmount,
|
||
|
|
'dueAmount' => $dueAmount,
|
||
|
|
'isPaid' => $dueAmount > 0 ? 0 : 1,
|
||
|
|
'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 (!$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);
|
||
|
|
|
||
|
|
DB::commit();
|
||
|
|
|
||
|
|
return response()->json([
|
||
|
|
'message' => __('Sales updated successfully.'),
|
||
|
|
'redirect' => route('business.sales.index'),
|
||
|
|
'secondary_redirect_url' => route('business.sales.invoice', $sale->id),
|
||
|
|
]);
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
DB::rollback();
|
||
|
|
return response()->json(['message' => __('Something went wrong!')], 404);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function destroy($id)
|
||
|
|
{
|
||
|
|
DB::beginTransaction();
|
||
|
|
try {
|
||
|
|
$sale = Sale::findOrFail($id);
|
||
|
|
|
||
|
|
foreach ($sale->details as $detail) {
|
||
|
|
$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::whereIn('id', $request->ids)->get();
|
||
|
|
|
||
|
|
foreach ($sales as $sale) {
|
||
|
|
// Restore stock
|
||
|
|
foreach ($sale->details as $detail) {
|
||
|
|
$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)
|
||
|
|
->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([
|
||
|
|
'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 . '%');
|
||
|
|
});
|
||
|
|
})
|
||
|
|
->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,
|
||
|
|
'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
|
||
|
|
|
||
|
|
$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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch stocks for cart list prices
|
||
|
|
$stockPrices = [];
|
||
|
|
if (!empty($cartStocks)) {
|
||
|
|
$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($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);
|
||
|
|
|
||
|
|
$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 generatePDF(Request $request, $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);
|
||
|
|
|
||
|
|
// 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 sendMail(Request $request, $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);
|
||
|
|
|
||
|
|
$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();
|
||
|
|
|
||
|
|
$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 ($pdf) {
|
||
|
|
$message->to(auth()->user()->email)
|
||
|
|
->subject('Sales Invoice')
|
||
|
|
->attachData($pdf->output(), 'sales-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();
|
||
|
|
|
||
|
|
$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()
|
||
|
|
->where('total_stock', '>', 0)
|
||
|
|
->values();
|
||
|
|
|
||
|
|
$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);
|
||
|
|
|
||
|
|
return view('business::sales.inventory', compact('customers', 'products', '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(),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|