Files
kulakpos_web/Modules/Business/App/Http/Controllers/AcnooPurchaseController.php

1645 lines
69 KiB
PHP
Raw Normal View History

2026-03-15 17:08:23 +07:00
<?php
namespace Modules\Business\App\Http\Controllers;
2026-05-14 11:55:22 +07:00
use App\Events\MultiPaymentProcessed;
use App\Helpers\HasUploader;
use App\Http\Controllers\Controller;
use App\Imports\PurchaseProductImport;
2026-03-15 17:08:23 +07:00
use App\Models\Branch;
2026-05-14 11:55:22 +07:00
use App\Models\Brand;
2026-03-15 17:08:23 +07:00
use App\Models\Business;
use App\Models\Category;
2026-05-14 11:55:22 +07:00
use App\Models\Option;
use App\Models\Party;
2026-03-15 17:08:23 +07:00
use App\Models\PaymentType;
2026-05-14 11:55:22 +07:00
use App\Models\Product;
use App\Models\Purchase;
use App\Models\PurchaseDetails;
use App\Models\PurchaseReturn;
use App\Models\PurchaseReturnDetail;
use App\Models\Stock;
2026-03-15 17:08:23 +07:00
use App\Models\Transaction;
2026-05-14 11:55:22 +07:00
use App\Models\Vat;
use App\Models\VatTransaction;
use App\Models\Warehouse;
use App\Services\Invoice\PurchaseInvoiceService;
use App\Services\Invoice\PurchaseTaxInvoiceService;
2026-03-15 17:08:23 +07:00
use App\Services\PdfService;
2026-05-14 11:55:22 +07:00
use App\Services\PurchaseVatTransaction;
2026-03-15 17:08:23 +07:00
use Barryvdh\DomPDF\Facade\Pdf;
2026-05-14 11:55:22 +07:00
use Carbon\Carbon;
use Gloudemans\Shoppingcart\Facades\Cart;
use Illuminate\Http\Request;
2026-03-15 17:08:23 +07:00
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
2026-05-14 11:55:22 +07:00
use Illuminate\Validation\Rule;
2026-03-15 17:08:23 +07:00
use Maatwebsite\Excel\Facades\Excel;
class AcnooPurchaseController extends Controller
{
use HasUploader;
public function __construct()
{
$this->middleware('check.permission:purchases.read')->only(['index']);
$this->middleware('check.permission:purchases.create')->only(['create', 'store']);
$this->middleware('check.permission:purchases.update')->only(['edit', 'update']);
$this->middleware('check.permission:purchases.delete')->only(['destroy', 'deleteAll']);
}
public function index(Request $request)
{
$business_id = auth()->user()->business_id;
$purchasesWithReturns = PurchaseReturn::where('business_id', $business_id)
->pluck('purchase_id')
->toArray();
$purchasesQuery = Purchase::with('user:id,name', 'party:id,name,email,phone,type', 'payment_type:id,name', 'branch:id,name')
->where('business_id', $business_id);
$purchasesQuery->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');
}
$purchasesQuery->whereDate('purchaseDate', '>=', $startDate)
->whereDate('purchaseDate', '<=', $endDate);
// Search Filter
if ($request->filled('search')) {
$purchasesQuery->where(function ($query) use ($request) {
2026-05-14 11:55:22 +07:00
$query->where('paymentType', 'like', '%'.$request->search.'%')
->orWhere('invoiceNumber', 'like', '%'.$request->search.'%')
2026-03-15 17:08:23 +07:00
->orWhereHas('party', function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('name', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
})
->orWhereHas('payment_type', function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('name', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
})
->orWhereHas('branch', function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('name', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
});
});
}
$perPage = $request->input('per_page', 20);
$purchases = $purchasesQuery->latest()->paginate($perPage)->appends($request->query());
if ($request->ajax()) {
return response()->json([
'data' => view('business::purchases.datas', compact('purchases', 'purchasesWithReturns'))->render(),
]);
}
$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::purchases.index', compact('purchases', 'purchasesWithReturns', 'branches', 'payment_types'));
}
public function create()
{
$business_id = auth()->user()->business_id;
2026-05-14 11:55:22 +07:00
$activeBranchId = auth()->user()->active_branch?->id;
2026-03-15 17:08:23 +07:00
// Clears all cart items
Cart::destroy();
$suppliers = Party::where('type', 'Supplier')
->where('business_id', $business_id)
->latest()
2026-05-14 11:55:22 +07:00
->get()
->transform(function ($supplier) use ($activeBranchId) {
$supplier->due = $supplier->dueForBranch($activeBranchId);
return $supplier;
});
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
$products = Product::with(['category:id,categoryName', 'unit:id,unitName', 'brand:id,brandName'])
2026-03-15 17:08:23 +07:00
->withSum('stocks', 'productStock')
->where('business_id', $business_id)
->whereNot('product_type', 'combo')
->latest()
->get();
2026-05-14 11:55:22 +07:00
$cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase');
2026-03-15 17:08:23 +07:00
$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();
2026-05-14 11:55:22 +07:00
$profit_option = Option::where('key', 'business-settings')
->where('value', 'LIKE', '%"business_id":' . $business_id . '%')
->get()
->firstWhere('value.business_id', $business_id)
->value['product_profit_option'] ?? '';
2026-03-15 17:08:23 +07:00
// Generate a unique invoice number
$purchase_id = (Purchase::max('id') ?? 0) + 1;
2026-05-14 11:55:22 +07:00
$invoice_no = 'P-'.str_pad($purchase_id, 5, '0', STR_PAD_LEFT);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
return view('business::purchases.create', compact('suppliers', 'products', 'cart_contents', 'invoice_no', 'categories', 'brands', 'vats', 'payment_types', 'warehouses', 'profit_option'));
2026-03-15 17:08:23 +07:00
}
public function store(Request $request)
{
$request->validate([
'party_id' => 'required|exists:parties,id',
'invoiceNumber' => 'required|string',
'receive_amount' => 'nullable|numeric',
'vat_id' => 'nullable|exists:vats,id',
'discountAmount' => 'nullable|numeric',
'discount_type' => 'nullable|in:flat,percent',
'shipping_charge' => 'nullable|numeric',
'purchaseDate' => 'nullable|date',
]);
$user = auth()->user();
2026-05-14 11:55:22 +07:00
$business = Business::findOrFail($user->business_id);
2026-03-15 17:08:23 +07:00
$branch_id = $user->branch_id ?? $user->active_branch_id;
// Check each cart item for batch duplication in Stock table
2026-05-14 11:55:22 +07:00
$carts = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase');
2026-03-15 17:08:23 +07:00
if ($carts->count() < 1) {
return response()->json(['message' => __('Cart is empty. Add items first!')], 400);
}
DB::beginTransaction();
try {
// Subtotal
2026-05-14 11:55:22 +07:00
$subtotal = $carts->sum(fn ($cartItem) => (float) $cartItem->subtotal);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
// Discount (applied first on subtotal)
2026-03-15 17:08:23 +07:00
$discountAmount = $request->discountAmount ?? 0;
if ($request->discount_type === 'percent') {
2026-05-14 11:55:22 +07:00
$discountAmount = ($subtotal * $discountAmount) / 100;
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
if ($discountAmount > $subtotal) {
return response()->json(['message' => __('Discount cannot be more than subtotal!')], 400);
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
// 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;
2026-03-15 17:08:23 +07:00
// Charges and totals
$shippingCharge = $request->shipping_charge ?? 0;
2026-05-14 11:55:22 +07:00
$totalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge;
2026-03-15 17:08:23 +07:00
$receiveAmount = $request->receive_amount ?? 0;
$changeAmount = max($receiveAmount - $totalAmount, 0);
$dueAmount = max($totalAmount - $receiveAmount, 0);
$paidAmount = $receiveAmount - $changeAmount;
// Party due update
if ($dueAmount > 0) {
$party = Party::findOrFail($request->party_id);
// Check party credit limit
$newTotalDue = $party->due + $dueAmount;
if ($party->credit_limit > 0 && $newTotalDue > $party->credit_limit) {
return response()->json([
2026-05-14 11:55:22 +07:00
'message' => __('Cannot create purchase. Party due will exceed credit limit!'),
2026-03-15 17:08:23 +07:00
], 400);
}
$party->update([
2026-05-14 11:55:22 +07:00
'due' => $newTotalDue,
2026-03-15 17:08:23 +07:00
]);
}
// Business balance update
updateBalance($paidAmount, 'decrement');
// Create Purchase
$purchase = Purchase::create($request->except('discountAmount', 'discount_type', 'discount_percent', 'shipping_charge', 'payment_type_id') + [
2026-05-14 11:55:22 +07:00
'user_id' => $user->id,
'vat_id' => $request->vat_id,
'vat_amount' => $vatAmount,
'business_id' => $user->business_id,
'discountAmount' => $discountAmount,
'discount_type' => $request->discount_type ?? 'flat',
'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0,
'totalAmount' => $totalAmount,
'paidAmount' => $paidAmount,
'change_amount' => $changeAmount,
'dueAmount' => $dueAmount,
'shipping_charge' => $shippingCharge,
'purchaseDate' => $request->purchaseDate ? Carbon::parse($request->purchaseDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(),
'isPaid' => $dueAmount > 0 ? 0 : 1,
]);
2026-03-15 17:08:23 +07:00
// Insert Purchase Details and Create Stocks
foreach ($carts as $cartItem) {
2026-05-14 11:55:22 +07:00
$product = Product::findOrFail($cartItem->id);
2026-03-15 17:08:23 +07:00
$batchNo = $cartItem->options['batch_no'] ?? null;
$expireDate = $cartItem->options['expire_date'] ?? null;
$variantName = $cartItem->options['variant_name'] ?? null;
2026-05-14 11:55:22 +07:00
$existingStock = Stock::where([
'business_id' => $user->business_id,
'batch_no' => $batchNo,
'product_id' => $cartItem->id,
'branch_id' => $branch_id,
])
->when($variantName, fn ($q) => $q->where('variant_name', $variantName))
2026-03-15 17:08:23 +07:00
->first();
$warehouseId = $cartItem->options['warehouse_id'] ?? ($existingStock->warehouse_id ?? null);
2026-05-14 11:55:22 +07:00
$newSerials = $cartItem->options['serial_numbers'] ?? [];
$existingSerials = $existingStock?->serial_numbers ?? [];
// Only serials that don't already exist
$uniqueNewSerials = array_diff($newSerials, $existingSerials);
if (! empty($newSerials) && count($uniqueNewSerials) === 0) {
return response()->json([
'success' => false,
'message' => 'Serial already exists in stock',
'duplicates' => array_values(array_intersect($newSerials, $existingSerials)),
], 400);
}
$mergedSerials = array_values(
array_unique(array_merge($existingSerials, $uniqueNewSerials))
);
// qty depends on serial
$qtyToAdd = count($uniqueNewSerials) > 0 ? count($uniqueNewSerials) : $cartItem->qty;
// price calculation
$vatRate = 0;
if ($product->vat_id) {
$vatData = Vat::find($product->vat_id);
$vatRate = $vatData?->rate ?? 0;
}
$purchasePrice = $cartItem->price;
// convert inclusive to exclusive
$exclusive_price = $vatRate > 0
? round($purchasePrice / (1 + ($vatRate / 100)), 4)
: $purchasePrice;
$isSerialProduct = ! empty($newSerials);
if ($isSerialProduct) {
// For serial products, use cart sales_price directly
$salesPrice = $cartItem->options['sales_price'];
$profitPercent = $cartItem->options['profit_percent'];
} else {
$profitPercent = ($cartItem->options['profit_percent'] ?? 0);
$profitOption = Option::where('key', 'business-settings')
->where('value', 'LIKE', '%"business_id":' . $business->id . '%')
->get()
->firstWhere('value.business_id', $business->id)
->value['product_profit_option'] ?? 'markup';
if ($profitOption === 'margin') {
$salesPrice = $exclusive_price / (1 - ($profitPercent / 100));
} else {
$salesPrice = $exclusive_price + ($exclusive_price * $profitPercent / 100);
}
if ($product->vat_type === 'inclusive' && $vatRate > 0) {
$salesPrice = $salesPrice * (1 + ($vatRate / 100));
}
$salesPrice = round($salesPrice, 2);
}
2026-03-15 17:08:23 +07:00
$stock = Stock::updateOrCreate(
[
'batch_no' => $batchNo,
'product_id' => $cartItem->id,
'variant_name' => $variantName,
'branch_id' => $branch_id,
'business_id' => $user->business_id,
],
[
'expire_date' => $expireDate,
2026-05-14 11:55:22 +07:00
'product_id' => $cartItem->id,
'warehouse_id' => $warehouseId,
'exclusive_price' => $exclusive_price,
'productPurchasePrice' => $purchasePrice,
'productSalePrice' => $salesPrice,
'profit_percent' => $profitPercent,
2026-03-15 17:08:23 +07:00
'productDealerPrice' => $cartItem->options['dealer_price'],
2026-05-14 11:55:22 +07:00
'serial_numbers' => ! empty($newSerials) ? $mergedSerials : null,
'productWholeSalePrice' => $cartItem->options['whole_sale_price'],
2026-03-15 17:08:23 +07:00
'productStock' => $cartItem->qty + ($existingStock->productStock ?? 0),
]
);
// purchase detail
2026-05-14 11:55:22 +07:00
$detail = PurchaseDetails::create([
2026-03-15 17:08:23 +07:00
'stock_id' => $stock->id,
'purchase_id' => $purchase->id,
'product_id' => $cartItem->id,
2026-05-14 11:55:22 +07:00
'quantities' => $qtyToAdd,
2026-03-15 17:08:23 +07:00
'expire_date' => $expireDate,
2026-05-14 11:55:22 +07:00
'productPurchasePrice' => $purchasePrice,
'productSalePrice' => $salesPrice,
'profit_percent' => $profitPercent,
'productDealerPrice' => $cartItem->options['dealer_price'],
2026-03-15 17:08:23 +07:00
'productWholeSalePrice' => $cartItem->options['whole_sale_price'],
2026-05-14 11:55:22 +07:00
'serial_numbers' => ! empty($uniqueNewSerials) ? array_values($uniqueNewSerials) : null,
]);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
$vatAmount = PurchaseVatTransaction::storeVatTransactions($business, $detail, $product, $party ?? null);
$detail->update([
'price_without_tax' => number_format($purchasePrice - ($vatAmount / $cartItem->qty), 3, '.', ''),
]);
}
2026-03-15 17:08:23 +07:00
// MultiPaymentProcessed Event
event(new MultiPaymentProcessed(
$request->payments ?? [],
$purchase->id,
'purchase',
$request->receive_amount ?? 0,
$request->party_id ?? null,
));
// Clear cart
foreach ($carts as $cartItem) {
Cart::remove($cartItem->rowId);
}
sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('New Purchase created.'), $user->business_id);
2026-05-14 11:55:22 +07:00
if (moduleCheck('MarketingAddon')) {
$this->purchaseMessage($business, $party ?? null, $purchase);
}
$tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($purchase->business_id) == 'standard_a4';
$invoice_route = $tax_invoice ? route('business.purchases.tax.invoice', $purchase->id) : route('business.purchases.invoice', $purchase->id);
2026-03-15 17:08:23 +07:00
DB::commit();
return response()->json([
'message' => __('Purchase created successfully.'),
'redirect' => route('business.purchases.index'),
2026-05-14 11:55:22 +07:00
'secondary_redirect_url' => $invoice_route,
2026-03-15 17:08:23 +07:00
]);
} catch (\Exception $e) {
DB::rollback();
2026-05-14 11:55:22 +07:00
return response()->json(['message' => $e->getMessage()], 500);
2026-03-15 17:08:23 +07:00
}
}
public function edit($id)
{
// Clears all cart items
Cart::destroy();
$business_id = auth()->user()->business_id;
2026-05-14 11:55:22 +07:00
$activeBranchId = auth()->user()->active_branch?->id;
2026-03-15 17:08:23 +07:00
$purchase = Purchase::with('details', 'details.product', 'details.stock:id,batch_no', 'details.product.unit', 'payment_type:id,name', 'transactions')
->where('business_id', $business_id)
->findOrFail($id);
$suppliers = Party::where('type', 'Supplier')
->where('business_id', $business_id)
->latest()
2026-05-14 11:55:22 +07:00
->get()
->transform(function ($supplier) use ($activeBranchId) {
$supplier->due = $supplier->dueForBranch($activeBranchId);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
return $supplier;
});
$products = Product::with(['category:id,categoryName', 'unit:id,unitName', 'brand:id,brandName'])
2026-03-15 17:08:23 +07:00
->withSum('stocks', 'productStock')
->where('business_id', $business_id)
->whereNot('product_type', 'combo')
->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();
2026-05-14 11:55:22 +07:00
$profit_option = Option::where('key', 'business-settings')
->where('value', 'LIKE', '%"business_id":' . $business_id . '%')
->get()
->firstWhere('value.business_id', $business_id)
->value['product_profit_option'] ?? '';
2026-03-15 17:08:23 +07:00
// Add purchase details to the cart
foreach ($purchase->details as $detail) {
$stock = Stock::where('id', $detail->stock_id)->first();
// Add to cart
Cart::add([
'id' => $detail->product_id,
'name' => $detail->product->productName ?? '',
'qty' => $detail->quantities,
'price' => $detail->productPurchasePrice,
'options' => [
'type' => 'purchase',
'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 ?? '',
'sales_price' => $detail->productSalePrice,
2026-05-14 11:55:22 +07:00
'profit_percent' => $detail->profit_percent,
2026-03-15 17:08:23 +07:00
'whole_sale_price' => $detail->productWholeSalePrice,
'dealer_price' => $detail->productDealerPrice,
'batch_no' => $detail->stock->batch_no ?? null,
'expire_date' => $detail->expire_date,
'variant_name' => $stock->variant_name ?? null,
'warehouse_id' => $stock->warehouse_id ?? null,
2026-05-14 11:55:22 +07:00
'price_without_tax' => $detail->price_without_tax ?? 0,
'serial_numbers' => $detail->serial_numbers ?? null,
'has_serial' => $detail->product->has_serial ?? null,
2026-03-15 17:08:23 +07:00
],
]);
}
2026-05-14 11:55:22 +07:00
$cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase');
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
return view('business::purchases.edit', compact('purchase', 'suppliers', 'products', 'cart_contents', 'categories', 'brands', 'vats', 'payment_types', 'warehouses', 'profit_option'));
2026-03-15 17:08:23 +07:00
}
public function update(Request $request, $id)
{
$request->validate([
'party_id' => 'required|exists:parties,id',
'invoiceNumber' => 'required|string',
'receive_amount' => 'nullable|numeric',
'vat_id' => 'nullable|exists:vats,id',
'discountAmount' => 'nullable|numeric',
'discount_type' => 'nullable|in:flat,percent',
'shipping_charge' => 'nullable|numeric',
'purchaseDate' => 'nullable|date',
]);
$user = auth()->user();
2026-05-14 11:55:22 +07:00
$business = Business::findOrFail($user->business_id);
$carts = Cart::content()->filter(fn ($item) => $item->options->type === 'purchase');
2026-03-15 17:08:23 +07:00
if ($carts->count() < 1) {
return response()->json(['message' => __('Cart is empty. Add items first!')], 400);
}
DB::beginTransaction();
try {
$purchase = Purchase::with('details')->findOrFail($id);
// Calculate amounts
2026-05-14 11:55:22 +07:00
$subtotal = $carts->sum(fn ($cartItem) => (float) $cartItem->subtotal);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
// Discount (applied first on subtotal)
2026-03-15 17:08:23 +07:00
$discountAmount = $request->discountAmount ?? 0;
if ($request->discount_type == 'percent') {
2026-05-14 11:55:22 +07:00
$discountAmount = ($subtotal * $discountAmount) / 100;
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
if ($discountAmount > $subtotal) {
2026-03-15 17:08:23 +07:00
return response()->json([
2026-05-14 11:55:22 +07:00
'message' => __('Discount cannot be more than subtotal!'),
2026-03-15 17:08:23 +07:00
], 400);
}
2026-05-14 11:55:22 +07:00
// Subtotal after discount (before VAT)
$subtotalAfterDiscount = $subtotal - $discountAmount;
// VAT (applied on subtotal after discount)
$vat = Vat::find($request->vat_id);
$vatAmount = 0;
if ($vat) {
$vatAmount = ($subtotalAfterDiscount * $vat->rate) / 100;
}
2026-03-15 17:08:23 +07:00
// Shipping Charge
$shippingCharge = $request->shipping_charge ?? 0;
// Total Amount
2026-05-14 11:55:22 +07:00
$totalAmount = $subtotalAfterDiscount + $vatAmount + $shippingCharge;
2026-03-15 17:08:23 +07:00
// Receive, Change, Due Amount Calculation
$receiveAmount = $request->receive_amount ?? 0;
$changeAmount = $receiveAmount > $totalAmount ? $receiveAmount - $totalAmount : 0;
$dueAmount = max($totalAmount - $receiveAmount, 0);
$paidAmount = $receiveAmount - $changeAmount;
if ($purchase->dueAmount || $dueAmount) {
$party = Party::findOrFail($request->party_id);
// If same party, adjust old due and add new one
$newDue = $request->party_id == $purchase->party_id ? ($party->due - $purchase->dueAmount) + $dueAmount : $party->due + $dueAmount;
// Check credit limit
if ($party->credit_limit > 0 && $newDue > $party->credit_limit) {
return response()->json(['message' => __('Party credit limit exceeded!')], 400);
}
$party->update(['due' => $newDue]);
// If changed to a new party, reduce previous partys due
if ($request->party_id != $purchase->party_id) {
$prev_party = Party::findOrFail($purchase->party_id);
$prev_party->update(['due' => $prev_party->due - $purchase->dueAmount]);
}
}
// branch/business balance
updateBalance($purchase->paidAmount - $paidAmount, 'decrement');
// Update purchase details
$purchase->update($request->except('discountAmount', 'discount_type', 'discount_percent', 'shipping_charge') + [
2026-05-14 11:55:22 +07:00
'business_id' => $user->business_id,
'user_id' => $user->id,
'vat_id' => $request->vat_id,
'vat_amount' => $vatAmount,
'discountAmount' => $discountAmount,
'totalAmount' => $totalAmount,
'paidAmount' => $paidAmount,
'change_amount' => $changeAmount,
'dueAmount' => $dueAmount,
'isPaid' => $dueAmount > 0 ? 0 : 1,
'shipping_charge' => $shippingCharge,
'payment_type_id' => $request->payment_type_id,
'discount_type' => $request->discount_type ?? 'flat',
'discount_percent' => $request->discount_type == 'percent' ? $request->discountAmount : 0,
'purchaseDate' => $request->purchaseDate ? Carbon::parse($request->purchaseDate)->setTimeFromTimeString(now()->format('H:i:s')) : now(),
]);
2026-03-15 17:08:23 +07:00
// Revert previous stock changes
foreach ($purchase->details as $detail) {
2026-05-14 11:55:22 +07:00
$stock = Stock::find($detail->stock_id);
if ($stock) {
if (! empty($detail->serial_numbers)) {
$stock->serial_numbers = array_values(array_diff($stock->serial_numbers, $detail->serial_numbers));
$stock->productStock = count($stock->serial_numbers);
} else {
$stock->productStock -= $detail->quantities;
}
$stock->save();
}
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
// Store old purchase details before deleting
$oldDetails = $purchase->details->keyBy('product_id');
2026-03-15 17:08:23 +07:00
// Delete existing purchase details
2026-05-14 11:55:22 +07:00
$prev_details_ids = $oldDetails->pluck('id')->toArray();
PurchaseDetails::where('purchase_id', $purchase->id)->delete();
// remove previous vat transactions
VatTransaction::where('vatable_id', $prev_details_ids)
->where('vatable_type', PurchaseDetails::class)
->delete();
2026-03-15 17:08:23 +07:00
// Insert updated purchase details and adjust stock
foreach ($carts as $cartItem) {
2026-05-14 11:55:22 +07:00
$product = Product::findOrFail($cartItem->id);
$batch_no = $cartItem->options['batch_no'] ?? null;
$expire_date = $cartItem->options['expire_date'] ?? null;
$variantName = $cartItem->options['variant_name'] ?? null;
2026-03-15 17:08:23 +07:00
$dealer_price = $cartItem->options['dealer_price'] ?? 0.0;
$whole_sale_price = $cartItem->options['whole_sale_price'] ?? 0.0;
$existingStock = Stock::where(['batch_no' => $batch_no, 'product_id' => $cartItem->id])
2026-05-14 11:55:22 +07:00
->when($variantName, fn ($q) => $q->where('variant_name', $variantName))
2026-03-15 17:08:23 +07:00
->first();
$warehouseId = $cartItem->options['warehouse_id'] ?? ($existingStock->warehouse_id ?? null);
2026-05-14 11:55:22 +07:00
// serial logic
$newSerials = $cartItem->options['serial_numbers'] ?? [];
$purchaseSerials = ! empty($newSerials) ? array_values($newSerials) : null;
$existingSerials = $existingStock?->serial_numbers ?? [];
// get previous purchase serials
$oldPurchaseSerials = $oldDetails[$cartItem->id]->serial_numbers ?? [];
$remainingSerials = array_diff($existingSerials, $oldPurchaseSerials);
$mergedSerials = ! empty($newSerials)
? array_values(array_unique(array_merge($remainingSerials, $newSerials)))
: array_values($remainingSerials);
if (! empty($newSerials)) {
// Serial Product
$stockQty = count($mergedSerials);
$purchaseQty = count($newSerials);
} else {
// Non Serial Product
$stockQty = ($existingStock->productStock ?? 0) + $cartItem->qty;
$purchaseQty = $cartItem->qty;
}
// Price Calculation
$purchasePrice = $cartItem->price;
// Exclusive Price
$exclusive_price = $purchasePrice;
$vatRate = 0;
if ($product->vat_id) {
$vat = Vat::find($product->vat_id);
$vatRate = $vat?->rate ?? 0;
if ($vatRate > 0) {
$exclusive_price = round($purchasePrice / (1 + ($vatRate / 100)), 3);
}
}
$isSerialProduct = ! empty($newSerials);
if ($isSerialProduct) {
// For serial products, use cart sales_price directly
$salesPrice = $cartItem->options['sales_price'] ?? $purchasePrice;
$profitPercent = 0;
} else {
$profitPercent = $cartItem->options['profit_percent'] ?? 0;
$profitOption = Option::where('key', 'business-settings')
->where('value', 'LIKE', '%"business_id":' . $business->id . '%')
->get()
->firstWhere('value.business_id', $business->id)
->value['product_profit_option'] ?? 'markup';
if ($profitOption === 'margin') {
$salesPrice = $exclusive_price / (1 - ($profitPercent / 100));
} else {
$salesPrice = $exclusive_price + ($exclusive_price * $profitPercent / 100);
}
if ($product->vat_type === 'inclusive' && $vatRate > 0) {
$salesPrice = $salesPrice * (1 + ($vatRate / 100));
}
$salesPrice = round($salesPrice, 3);
}
2026-03-15 17:08:23 +07:00
// update or create stock
$stock = Stock::updateOrCreate(
[
'batch_no' => $batch_no,
'product_id' => $cartItem->id,
'variant_name' => $variantName,
'business_id' => $user->business_id,
],
[
'product_id' => $cartItem->id,
'expire_date' => $expire_date,
'productDealerPrice' => $dealer_price,
2026-05-14 11:55:22 +07:00
'productPurchasePrice' => $purchasePrice,
'exclusive_price' => $exclusive_price,
'profit_percent' => $profitPercent,
'productSalePrice' => $salesPrice,
2026-03-15 17:08:23 +07:00
'productWholeSalePrice' => $whole_sale_price,
2026-05-14 11:55:22 +07:00
'productStock' => $stockQty,
2026-03-15 17:08:23 +07:00
'warehouse_id' => $warehouseId,
2026-05-14 11:55:22 +07:00
'serial_numbers' => $mergedSerials,
2026-03-15 17:08:23 +07:00
]
);
2026-05-14 11:55:22 +07:00
$detail = PurchaseDetails::create([
2026-03-15 17:08:23 +07:00
'purchase_id' => $purchase->id,
'product_id' => $cartItem->id,
2026-05-14 11:55:22 +07:00
'quantities' => $purchaseQty,
2026-03-15 17:08:23 +07:00
'stock_id' => $stock->id,
'expire_date' => $expire_date,
2026-05-14 11:55:22 +07:00
'productPurchasePrice' => $purchasePrice,
'profit_percent' => $profitPercent,
'productSalePrice' => $salesPrice,
'serial_numbers' => $purchaseSerials,
'productDealerPrice' => $dealer_price,
2026-03-15 17:08:23 +07:00
'productWholeSalePrice' => $whole_sale_price,
2026-05-14 11:55:22 +07:00
]);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
$vatAmount = PurchaseVatTransaction::storeVatTransactions($business, $detail, $product, $party ?? null);
$detail->update([
'price_without_tax' => number_format($purchasePrice - ($vatAmount / $purchaseQty), 3, '.', ''),
2026-03-15 17:08:23 +07:00
]);
}
// Multiple Payment Process
$oldTransactions = Transaction::where('business_id', $user->business_id)
->where('reference_id', $purchase->id)
->where('platform', 'purchase')
->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->increment('balance', $old->amount);
}
break;
case 'wallet_payment':
if ($request->party_id) {
$party = Party::find($request->party_id);
if ($party) {
$party->decrement('wallet', $old->amount);
}
}
break;
case 'cash_payment':
case 'cheque_payment':
// nothing to revert
break;
}
}
// Delete old transactions
Transaction::where('business_id', $user->business_id)
->where('reference_id', $purchase->id)
->where('platform', 'purchase')
->delete();
event(new MultiPaymentProcessed(
$request->payments ?? [],
$purchase->id,
'purchase',
$request->receive_amount ?? 0,
$request->party_id ?? null,
));
// Clear the cart
foreach ($carts as $cartItem) {
Cart::remove($cartItem->rowId);
}
sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('Purchase has been updated.'), $user->business_id);
2026-05-14 11:55:22 +07:00
$tax_invoice = moduleCheck('TaxInvoiceAddon') && invoice_setting($purchase->business_id) == 'standard_a4';
$invoice_route = $tax_invoice ? route('business.purchases.tax.invoice', $purchase->id) : route('business.purchases.invoice', $purchase->id);
2026-03-15 17:08:23 +07:00
DB::commit();
2026-05-14 11:55:22 +07:00
2026-03-15 17:08:23 +07:00
return response()->json([
'message' => __('Purchase updated successfully.'),
'redirect' => route('business.purchases.index'),
2026-05-14 11:55:22 +07:00
'secondary_redirect_url' => $invoice_route,
2026-03-15 17:08:23 +07:00
]);
} catch (\Exception $e) {
DB::rollback();
2026-05-14 11:55:22 +07:00
return response()->json(['message' => $e->getMessage()], 500);
2026-03-15 17:08:23 +07:00
}
}
public function destroy($id)
{
DB::beginTransaction();
try {
$purchase = Purchase::with('details')->findOrFail($id);
foreach ($purchase->details as $detail) {
2026-05-14 11:55:22 +07:00
VatTransaction::where('vatable_id', $detail->id)
->where('vatable_type', get_class($detail))
->delete();
2026-03-15 17:08:23 +07:00
Stock::where('id', $detail->stock_id)->decrement('productStock', $detail->quantities);
}
if ($purchase->party_id) {
$party = Party::findOrFail($purchase->party_id);
$party->update([
2026-05-14 11:55:22 +07:00
'due' => $party->due - $purchase->dueAmount,
2026-03-15 17:08:23 +07:00
]);
}
updateBalance($purchase->paidAmount, 'increment');
sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('Purchase has been deleted.'), $purchase->business_id);
$purchase->delete();
// Clears all cart items
Cart::destroy();
DB::commit();
return response()->json([
'message' => __('Purchase deleted successfully.'),
2026-05-14 11:55:22 +07:00
'redirect' => route('business.purchases.index'),
2026-03-15 17:08:23 +07:00
]);
} catch (\Exception $e) {
DB::rollback();
2026-05-14 11:55:22 +07:00
return response()->json(['message' => $e->getMessage()], 404);
2026-03-15 17:08:23 +07:00
}
}
public function deleteAll(Request $request)
{
DB::beginTransaction();
try {
2026-05-14 11:55:22 +07:00
$purchases = Purchase::where('business_id', auth()->user()->business_id)->whereIn('id', $request->ids)->get();
2026-03-15 17:08:23 +07:00
foreach ($purchases as $purchase) {
foreach ($purchase->details as $detail) {
2026-05-14 11:55:22 +07:00
VatTransaction::where('vatable_id', $detail->id)
->where('vatable_type', get_class($detail))
->delete();
2026-03-15 17:08:23 +07:00
Stock::where('id', $detail->stock_id)->decrement('productStock', $detail->quantities);
}
if ($purchase->party_id) {
$party = Party::findOrFail($purchase->party_id);
$party->update([
2026-05-14 11:55:22 +07:00
'due' => $party->due - $purchase->dueAmount,
2026-03-15 17:08:23 +07:00
]);
}
2026-05-14 11:55:22 +07:00
updateBalance($purchase->paidAmount, 'increment');
2026-03-15 17:08:23 +07:00
sendNotifyToUser($purchase->id, route('business.purchases.index', ['id' => $purchase->id]), __('Purchases has been deleted.'), $purchase->business_id);
$purchase->delete();
}
// Clears all cart items
Cart::destroy();
DB::commit();
return response()->json([
'message' => __('Selected purchases deleted successfully.'),
2026-05-14 11:55:22 +07:00
'redirect' => route('business.purchases.index'),
2026-03-15 17:08:23 +07:00
]);
} catch (\Exception $e) {
DB::rollback();
2026-05-14 11:55:22 +07:00
return response()->json(['message' => $e->getMessage()], 404);
2026-03-15 17:08:23 +07:00
}
}
public function acnooFilter(Request $request)
{
$business_id = auth()->user()->business_id;
$purchasesWithReturns = PurchaseReturn::where('business_id', $business_id)
->pluck('purchase_id')
->toArray();
$purchasesQuery = Purchase::with('user:id,name', 'party:id,name,email,phone,type', 'payment_type:id,name', 'branch:id,name')
->where('business_id', $business_id);
$purchasesQuery->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');
}
$purchasesQuery->whereDate('purchaseDate', '>=', $startDate)
->whereDate('purchaseDate', '<=', $endDate);
// Search Filter
if ($request->filled('search')) {
$purchasesQuery->where(function ($query) use ($request) {
2026-05-14 11:55:22 +07:00
$query->where('paymentType', 'like', '%'.$request->search.'%')
->orWhere('invoiceNumber', 'like', '%'.$request->search.'%')
2026-03-15 17:08:23 +07:00
->orWhereHas('party', function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('name', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
})
->orWhereHas('payment_type', function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('name', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
})
->orWhereHas('branch', function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('name', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
});
});
}
$perPage = $request->input('per_page', 10);
$purchases = $purchasesQuery->latest()->paginate($perPage);
if ($request->ajax()) {
return response()->json([
2026-05-14 11:55:22 +07:00
'data' => view('business::purchases.datas', compact('purchases', 'purchasesWithReturns'))->render(),
2026-03-15 17:08:23 +07:00
]);
}
return redirect(url()->previous());
}
public function productFilter(Request $request)
{
$business_id = auth()->user()->business_id;
$products = Product::where('business_id', $business_id)
->when($request->search, function ($query) use ($request) {
$query->where(function ($q) use ($request) {
2026-05-14 11:55:22 +07:00
$q->where('productName', 'like', '%'.$request->search.'%')
->orWhere('productCode', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
});
})
->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);
})
->withSum('stocks as total_stock', 'productStock')
->latest()
->get();
// Query categories for search options
$categories = Category::where('business_id', $business_id)
->when($request->search, function ($query) use ($request) {
2026-05-14 11:55:22 +07:00
$query->where('categoryName', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
})
->get();
// Query brands for search options
$brands = Brand::where('business_id', $business_id)
->when($request->search, function ($query) use ($request) {
2026-05-14 11:55:22 +07:00
$query->where('brandName', 'like', '%'.$request->search.'%');
2026-03-15 17:08:23 +07:00
})
->get();
$total_products = $products->count();
if ($request->ajax()) {
return response()->json([
'total_products' => $total_products,
'product_id' => $total_products == 1 ? $products->first()->id : null,
'data' => view('business::purchases.product-list', compact('products'))->render(),
'categories' => view('business::purchases.category-list', compact('categories'))->render(),
'brands' => view('business::purchases.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) {
2026-05-14 11:55:22 +07:00
$query->where('categoryName', 'like', '%'.$search.'%');
2026-03-15 17:08:23 +07:00
})
->get();
return response()->json([
'categories' => view('business::purchases.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) {
2026-05-14 11:55:22 +07:00
$query->where('brandName', 'like', '%'.$search.'%');
2026-03-15 17:08:23 +07:00
})
->get();
return response()->json([
'brands' => view('business::purchases.brand-list', compact('brands'))->render(),
]);
}
2026-05-14 11:55:22 +07:00
public function getInvoice($purchase_id, PurchaseInvoiceService $service)
2026-03-15 17:08:23 +07:00
{
2026-05-14 11:55:22 +07:00
$data = $service->getInvoiceData($purchase_id);
return view('business::purchases.invoice', $data);
}
public function getTaxInvoice($purchase_id, PurchaseTaxInvoiceService $service)
{
$data = $service->getInvoiceData($purchase_id);
return view('taxinvoiceaddon::purchases.tax', $data);
}
public function showPurchaseCart()
{
$cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase');
return view('business::purchases.cart-list', compact('cart_contents'));
}
public function getCartData()
{
$cart_contents = Cart::content()->filter(fn ($item) => $item->options->type == 'purchase');
$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 generatePDF($purchase_id)
{
$purchase = Purchase::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,productPurchasePrice,quantities,product_id,purchase_id,stock_id',
'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'
)
2026-03-15 17:08:23 +07:00
->findOrFail($purchase_id);
2026-05-14 11:55:22 +07:00
/**
* Transaction types (same helper used in Sale)
*/
$transactionTypes = transaction_types($purchase->transactions);
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
/**
* Purchase returns
*/
$purchase_returns = PurchaseReturn::with(
'purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber',
'purchase.party:id,name',
'details',
'details.purchaseDetail:id,productPurchasePrice,product_id,stock_id',
'details.purchaseDetail.product:id,productName',
'details.purchaseDetail.stock:id,batch_no'
)
2026-03-15 17:08:23 +07:00
->where('business_id', auth()->user()->business_id)
->where('purchase_id', $purchase_id)
->latest()
->get();
2026-05-14 11:55:22 +07:00
/**
* Return transaction type (same logic as Sale)
*/
2026-03-15 17:08:23 +07:00
$returnTransaction = $purchase_returns->first()?->transactions->first();
$returnTransactionType = '';
if ($returnTransaction) {
2026-05-14 11:55:22 +07:00
$returnTransactionType =
$returnTransaction->transaction_type === 'bank_payment'
? ($returnTransaction->paymentType?->name)
: ucfirst(explode('_', $returnTransaction->transaction_type)[0]);
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
/**
* Merge return qty into purchase details (same as Sale)
*/
2026-03-15 17:08:23 +07:00
$purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) {
$return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) {
2026-05-14 11:55:22 +07:00
return $return->details
->where('purchaseDetail.id', $detail->id)
->pluck('return_qty');
2026-03-15 17:08:23 +07:00
})->sum();
2026-05-14 11:55:22 +07:00
$detail->quantities += $return_qty_sum;
2026-03-15 17:08:23 +07:00
return $detail;
});
2026-05-14 11:55:22 +07:00
/**
* Discount calculation from returned items (same pattern as Sale)
*/
2026-03-15 17:08:23 +07:00
$total_discount = 0;
$product_discounts = [];
foreach ($purchase_returns as $return) {
foreach ($return->details as $detail) {
2026-05-14 11:55:22 +07:00
if (! isset($product_discounts[$detail->purchase_detail_id])) {
2026-03-15 17:08:23 +07:00
$product_discounts[$detail->purchase_detail_id] = [
'return_qty' => 0,
'return_amount' => 0,
'price' => $detail->purchaseDetail->productPurchasePrice,
];
}
$product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty;
$product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount;
}
}
foreach ($product_discounts as $data) {
$product_price = $data['price'] * $data['return_qty'];
2026-05-14 11:55:22 +07:00
$total_discount += ($product_price - $data['return_amount']);
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
/**
* Bank / Payment info
*/
$bank_detail = PaymentType::where('business_id', auth()->user()->business_id)
->latest()
->first();
2026-03-15 17:08:23 +07:00
2026-05-14 11:55:22 +07:00
/**
* CENTRALIZED PDF RENDER (same as Sale)
*/
return PdfService::render(
'business::purchases.pdf',
compact(
'purchase',
'purchase_returns',
'total_discount',
'transactionTypes',
'returnTransactionType',
'bank_detail'
),
'purchase-invoice.pdf'
);
2026-03-15 17:08:23 +07:00
}
2026-05-14 11:55:22 +07:00
public function generateTaxPDF($purchase_id)
2026-03-15 17:08:23 +07:00
{
$purchase = Purchase::where('business_id', auth()->user()->business_id)
->with(
2026-05-14 11:55:22 +07:00
'user:id,name,role', 'party:id,name,phone,address,email', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price_without_tax,productPurchasePrice,quantities,product_id,purchase_id,serial_numbers', 'details.stock:id,batch_no', 'details.product:id,vat_id,productName,has_serial', 'payment_type:id,name'
2026-03-15 17:08:23 +07:00
)
->findOrFail($purchase_id);
2026-05-14 11:55:22 +07:00
$business_id = auth()->check() ? auth()->user()->business_id : null;
2026-03-15 17:08:23 +07:00
/**
* Transaction types (same helper used in Sale)
*/
2026-05-14 11:55:22 +07:00
$detail_ids = $purchase->details->pluck('id');
$tax_items = VatTransaction::selectRaw('vat_id, vat_rate, SUM(vat_amount) as vat_amount')
->with('vat:id,name')
->whereIn('vatable_id', $detail_ids)
->where('vatable_type', PurchaseDetails::class)
->groupBy('vat_id', 'vat_rate')
->get();
$vatRowMap = VatTransaction::whereIn('vatable_id', $detail_ids)
->where('vatable_type', PurchaseDetails::class)
->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total')
->groupBy('vatable_id', 'vat_id')
->get()
->groupBy('vatable_id')
->map(function ($items) {
return $items->pluck('total', 'vat_id')->toArray();
})
->toArray();
2026-03-15 17:08:23 +07:00
$transactionTypes = transaction_types($purchase->transactions);
/**
* Purchase returns
*/
$purchase_returns = PurchaseReturn::with(
2026-05-14 11:55:22 +07:00
'purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'purchase.party:id,name,email', 'details', 'details.purchaseDetail.product:id,productName,has_serial', 'details.purchaseDetail.stock:id,batch_no'
)
2026-03-15 17:08:23 +07:00
->where('business_id', auth()->user()->business_id)
->where('purchase_id', $purchase_id)
->latest()
->get();
2026-05-14 11:55:22 +07:00
// collect return detail ids
$return_detail_ids = $purchase_returns->flatMap(function ($return) {
return $return->details->pluck('id');
});
// get VAT for purchase return details
$returnVatRowMap = VatTransaction::whereIn('vatable_id', $return_detail_ids)
->where('vatable_type', PurchaseReturnDetail::class)
->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total')
->groupBy('vatable_id', 'vat_id')
->get()
->groupBy('vatable_id')
->map(function ($items) {
return $items->pluck('total', 'vat_id')->toArray();
})
->toArray();
2026-03-15 17:08:23 +07:00
/**
* Return transaction type (same logic as Sale)
*/
$returnTransaction = $purchase_returns->first()?->transactions->first();
$returnTransactionType = '';
if ($returnTransaction) {
$returnTransactionType =
$returnTransaction->transaction_type === 'bank_payment'
? ($returnTransaction->paymentType?->name)
: ucfirst(explode('_', $returnTransaction->transaction_type)[0]);
}
/**
* Merge return qty into purchase details (same as Sale)
*/
$purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) {
$return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) {
return $return->details
->where('purchaseDetail.id', $detail->id)
->pluck('return_qty');
})->sum();
$detail->quantities += $return_qty_sum;
2026-05-14 11:55:22 +07:00
2026-03-15 17:08:23 +07:00
return $detail;
});
/**
* Discount calculation from returned items (same pattern as Sale)
*/
$total_discount = 0;
$product_discounts = [];
foreach ($purchase_returns as $return) {
foreach ($return->details as $detail) {
2026-05-14 11:55:22 +07:00
if (! isset($product_discounts[$detail->purchase_detail_id])) {
2026-03-15 17:08:23 +07:00
$product_discounts[$detail->purchase_detail_id] = [
'return_qty' => 0,
'return_amount' => 0,
'price' => $detail->purchaseDetail->productPurchasePrice,
];
}
$product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty;
$product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount;
}
}
foreach ($product_discounts as $data) {
$product_price = $data['price'] * $data['return_qty'];
$total_discount += ($product_price - $data['return_amount']);
}
/**
* Bank / Payment info
*/
$bank_detail = PaymentType::where('business_id', auth()->user()->business_id)
->latest()
->first();
/**
* CENTRALIZED PDF RENDER (same as Sale)
*/
return PdfService::render(
2026-05-14 11:55:22 +07:00
'taxinvoiceaddon::purchases.pdf',
2026-03-15 17:08:23 +07:00
compact(
'purchase',
'purchase_returns',
'total_discount',
'transactionTypes',
'returnTransactionType',
2026-05-14 11:55:22 +07:00
'bank_detail',
'tax_items',
'business_id',
'vatRowMap',
'returnVatRowMap'
2026-03-15 17:08:23 +07:00
),
2026-05-14 11:55:22 +07:00
'purchase-tax-invoice.pdf'
2026-03-15 17:08:23 +07:00
);
}
public function sendMail($purchase_id)
{
$purchase = Purchase::with('user:id,name,role', 'party:id,name,phone,address', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,productPurchasePrice,quantities,product_id,purchase_id', 'details.stock:id,batch_no', 'details.product:id,productName', 'payment_type:id,name')
->findOrFail($purchase_id);
$transactionTypes = $purchase->transactions
->map(function ($transaction) {
2026-05-14 11:55:22 +07:00
if ($transaction->transaction_type === 'bank_payment' && ! empty($transaction->paymentType?->name)) {
2026-03-15 17:08:23 +07:00
return $transaction->paymentType->name;
}
2026-05-14 11:55:22 +07:00
2026-03-15 17:08:23 +07:00
return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : '';
})
->unique()
->implode(', ');
$purchase_returns = PurchaseReturn::with('purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'purchase.party:id,name', 'details', 'details.purchaseDetail.product:id,productName', 'details.purchaseDetail.stock:id,batch_no')
->where('business_id', auth()->user()->business_id)
->where('purchase_id', $purchase_id)
->latest()
->get();
$returnTransaction = $purchase_returns->first()?->transactions->first();
$returnTransactionType = '';
if ($returnTransaction) {
$returnTransactionType = $returnTransaction->transaction_type === 'bank_payment'
? ($returnTransaction->paymentType?->name)
: ucfirst(explode('_', $returnTransaction->transaction_type)[0]);
}
// sum of return_qty
$purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) {
$return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) {
return $return->details->where('purchaseDetail.id', $detail->id)->pluck('return_qty');
})->sum();
$detail->quantities = $detail->quantities + $return_qty_sum;
return $detail;
});
// Calculate total discount based on return quantities and amounts
$total_discount = 0;
$product_discounts = [];
foreach ($purchase_returns as $return) {
foreach ($return->details as $detail) {
// Initialize discount tracking for the first occurrence of each purchase_detail_id
2026-05-14 11:55:22 +07:00
if (! isset($product_discounts[$detail->purchase_detail_id])) {
2026-03-15 17:08:23 +07:00
$product_discounts[$detail->purchase_detail_id] = [
'return_qty' => 0,
'return_amount' => 0,
'price' => $detail->purchaseDetail->productPurchasePrice,
];
}
// Accumulate return quantities and return amounts
$product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty;
$product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount;
}
}
// Calculate the total discount for each returned product
foreach ($product_discounts as $data) {
$product_price = $data['price'] * $data['return_qty'];
$discount = $product_price - $data['return_amount'];
$total_discount += $discount;
}
$bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first();
$pdf = Pdf::loadView('business::purchases.pdf', compact('purchase', 'purchase_returns', 'total_discount', 'transactionTypes', 'bank_detail', 'returnTransactionType'));
// Send email with PDF attachment
Mail::raw('Please find attached your Purchase invoice.', function ($message) use ($pdf) {
$message->to(auth()->user()->email)
->subject('Purchase Invoice')
->attachData($pdf->output(), 'purchase-invoice.pdf', [
'mime' => 'application/pdf',
]);
});
return response()->json([
'message' => __('Email Sent Successfully.'),
'redirect' => route('business.purchases.index'),
]);
}
2026-05-14 11:55:22 +07:00
public function sendTaxMail($purchase_id)
{
$purchase = Purchase::with('user:id,name,role', 'party:id,name,phone,address,email', 'business:id,phoneNumber,companyName,vat_name,vat_no,address,email,meta', 'details:id,price_without_tax,productPurchasePrice,quantities,product_id,purchase_id,serial_numbers', 'details.stock:id,batch_no', 'details.product:id,vat_id,productName,has_serial', 'payment_type:id,name')
->findOrFail($purchase_id);
$business_id = auth()->check() ? auth()->user()->business_id : null;
$detail_ids = $purchase->details->pluck('id');
$tax_items = VatTransaction::selectRaw('vat_id, vat_rate, SUM(vat_amount) as vat_amount')
->with('vat:id,name')
->whereIn('vatable_id', $detail_ids)
->where('vatable_type', PurchaseDetails::class)
->groupBy('vat_id', 'vat_rate')
->get();
$vatRowMap = VatTransaction::whereIn('vatable_id', $detail_ids)
->where('vatable_type', PurchaseDetails::class)
->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total')
->groupBy('vatable_id', 'vat_id')
->get()
->groupBy('vatable_id')
->map(function ($items) {
return $items->pluck('total', 'vat_id')->toArray();
})
->toArray();
$transactionTypes = $purchase->transactions
->map(function ($transaction) {
if ($transaction->transaction_type === 'bank_payment' && ! empty($transaction->paymentType?->name)) {
return $transaction->paymentType->name;
}
return $transaction->transaction_type ? ucfirst(explode('_', $transaction->transaction_type)[0]) : '';
})
->unique()
->implode(', ');
$purchase_returns = PurchaseReturn::with('purchase:id,party_id,isPaid,totalAmount,dueAmount,paidAmount,invoiceNumber', 'purchase.party:id,name,email', 'details', 'details.purchaseDetail.product:id,productName,has_serial', 'details.purchaseDetail.stock:id,batch_no')
->where('business_id', auth()->user()->business_id)
->where('purchase_id', $purchase_id)
->latest()
->get();
$return_detail_ids = $purchase_returns->flatMap(function ($return) {
return $return->details->pluck('id');
});
// get VAT for purchase return details
$returnVatRowMap = VatTransaction::whereIn('vatable_id', $return_detail_ids)
->where('vatable_type', PurchaseReturnDetail::class)
->selectRaw('vatable_id, vat_id, SUM(vat_amount) as total')
->groupBy('vatable_id', 'vat_id')
->get()
->groupBy('vatable_id')
->map(function ($items) {
return $items->pluck('total', 'vat_id')->toArray();
})
->toArray();
$returnTransaction = $purchase_returns->first()?->transactions->first();
$returnTransactionType = '';
if ($returnTransaction) {
$returnTransactionType = $returnTransaction->transaction_type === 'bank_payment'
? ($returnTransaction->paymentType?->name)
: ucfirst(explode('_', $returnTransaction->transaction_type)[0]);
}
// sum of return_qty
$purchase->details = $purchase->details->map(function ($detail) use ($purchase_returns) {
$return_qty_sum = $purchase_returns->flatMap(function ($return) use ($detail) {
return $return->details->where('purchaseDetail.id', $detail->id)->pluck('return_qty');
})->sum();
$detail->quantities = $detail->quantities + $return_qty_sum;
return $detail;
});
// Calculate total discount based on return quantities and amounts
$total_discount = 0;
$product_discounts = [];
foreach ($purchase_returns as $return) {
foreach ($return->details as $detail) {
// Initialize discount tracking for the first occurrence of each purchase_detail_id
if (! isset($product_discounts[$detail->purchase_detail_id])) {
$product_discounts[$detail->purchase_detail_id] = [
'return_qty' => 0,
'return_amount' => 0,
'price' => $detail->purchaseDetail->productPurchasePrice,
];
}
// Accumulate return quantities and return amounts
$product_discounts[$detail->purchase_detail_id]['return_qty'] += $detail->return_qty;
$product_discounts[$detail->purchase_detail_id]['return_amount'] += $detail->return_amount;
}
}
// Calculate the total discount for each returned product
foreach ($product_discounts as $data) {
$product_price = $data['price'] * $data['return_qty'];
$discount = $product_price - $data['return_amount'];
$total_discount += $discount;
}
$bank_detail = PaymentType::where('business_id', auth()->user()->business_id)->where('show_in_invoice', 1)->latest()->first();
$pdf = Pdf::loadView('taxinvoiceaddon::purchases.pdf', compact('purchase', 'purchase_returns', 'total_discount', 'transactionTypes', 'bank_detail', 'returnTransactionType', 'tax_items', 'business_id', 'vatRowMap', 'returnVatRowMap'));
// Send email with PDF attachment
Mail::raw('Please find attached your Purchase invoice.', function ($message) use ($pdf) {
$message->to(auth()->user()->email)
->subject('Purchase Invoice')
->attachData($pdf->output(), 'purchase-tax-invoice.pdf', [
'mime' => 'application/pdf',
]);
});
return response()->json([
'message' => __('Email Sent Successfully.'),
'redirect' => route('business.purchases.index'),
]);
}
2026-03-15 17:08:23 +07:00
public function createSupplier(Request $request)
{
$request->validate([
2026-05-14 11:55:22 +07:00
'phone' => 'nullable|max:20|'.Rule::unique('parties')->where('business_id', auth()->user()->business_id),
2026-03-15 17:08:23 +07:00
'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') + [
2026-05-14 11:55:22 +07:00
'due' => $request->due ?? 0,
'image' => $request->image ? $this->upload($request, 'image') : null,
'business_id' => auth()->user()->business_id,
]);
2026-03-15 17:08:23 +07:00
return response()->json([
'message' => __('Supplier created successfully'),
2026-05-14 11:55:22 +07:00
'redirect' => route('business.purchases.create'),
2026-03-15 17:08:23 +07:00
]);
}
public function bulkIndex()
{
return view('business::purchases.bulk-upload.index');
}
public function bulkCartStore(Request $request)
{
$request->validate([
2026-05-14 11:55:22 +07:00
'file' => 'required|file|mimes:xlsx,xls,csv',
2026-03-15 17:08:23 +07:00
]);
$businessId = auth()->user()->business_id;
$import = new PurchaseProductImport($businessId);
try {
Excel::import($import, $request->file('file'));
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage(),
], 422);
}
return response()->json([
'success' => true,
'message' => __('Bulk upload successfully.'),
2026-05-14 11:55:22 +07:00
'redirect' => route('business.purchases.create'),
2026-03-15 17:08:23 +07:00
]);
}
public function viewPayment(string $id)
{
$purchase = Purchase::with('transactions', 'transactions.paymentType:id,name')
->where('business_id', auth()->user()->business_id)
->findOrFail($id);
$transactions = $purchase->transactions->map(function ($transaction) {
return [
'date' => formatted_date($transaction->date),
'receipt_no' => $transaction->invoice_no ?? '-',
'amount' => currency_format($transaction->amount, currency: business_currency()),
'payment_type' => $transaction->transaction_type === 'bank_payment' ? (ucwords($transaction->paymentType->name) ?? '') : (ucfirst(explode('_', $transaction->transaction_type)[0] ?? '')),
];
});
return response()->json(['data' => $transactions]);
}
}