2026-03-15 17:08:23 +07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Business\App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Models\Branch;
|
|
|
|
|
use App\Models\Stock;
|
|
|
|
|
use App\Models\Transfer;
|
|
|
|
|
use App\Models\TransferProduct;
|
|
|
|
|
use App\Models\Warehouse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use App\Http\Controllers\Controller;
|
2026-05-14 11:55:22 +07:00
|
|
|
use App\Services\PdfService;
|
2026-03-15 17:08:23 +07:00
|
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
|
|
|
use Modules\Business\App\Exports\ExportTransfer;
|
|
|
|
|
|
|
|
|
|
class AcnooTransferController extends Controller
|
|
|
|
|
{
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->middleware('check.permission:transfers.read')->only(['index']);
|
|
|
|
|
$this->middleware('check.permission:transfers.create')->only(['create', 'store']);
|
|
|
|
|
$this->middleware('check.permission:transfers.update')->only(['edit', 'update']);
|
|
|
|
|
$this->middleware('check.permission:transfers.delete')->only(['destroy', 'deleteAll']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function index(Request $request)
|
|
|
|
|
{
|
2026-05-14 11:55:22 +07:00
|
|
|
$user = auth()->user();
|
|
|
|
|
$activeBranchId = $user->active_branch_id;
|
|
|
|
|
|
|
|
|
|
$branches = Branch::withTrashed()->where('business_id', $user->business_id)->latest()->get();
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
$transfers = Transfer::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'toBranch:id,name', 'fromBranch:id,name', 'transferProducts'])
|
2026-05-14 11:55:22 +07:00
|
|
|
->where('business_id', $user->business_id)
|
|
|
|
|
->when($activeBranchId, function ($q) use ($activeBranchId) {
|
|
|
|
|
$q->where(function ($query) use ($activeBranchId) {
|
|
|
|
|
$query->where('from_branch_id', $activeBranchId)
|
|
|
|
|
->orWhere('to_branch_id', $activeBranchId);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->when($request->branch_id && !$activeBranchId, function ($q) use ($request) {
|
2026-03-15 17:08:23 +07:00
|
|
|
$q->where(function ($query) use ($request) {
|
|
|
|
|
$query->where('from_branch_id', $request->branch_id)->orWhere('to_branch_id', $request->branch_id);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->when($request->search, function ($q) use ($request) {
|
|
|
|
|
$search = $request->search;
|
|
|
|
|
|
|
|
|
|
$q->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('note', 'like', '%' . $search . '%')
|
|
|
|
|
->orWhere('status', 'like', '%' . $search . '%')
|
|
|
|
|
->orWhere('invoice_no', 'like', '%' . $search . '%')
|
|
|
|
|
->orWhereHas('fromWarehouse', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('toWarehouse', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('toBranch', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('fromBranch', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->latest()
|
|
|
|
|
->paginate($request->per_page ?? 20)->appends($request->query());
|
|
|
|
|
|
|
|
|
|
if ($request->ajax()) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'data' => view('business::transfers.datas', compact('transfers','branches'))->render()
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return view('business::transfers.index', compact('transfers', 'branches'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function create()
|
|
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
$warehouses = Warehouse::where('business_id', $business_id)->latest()->get();
|
|
|
|
|
$branches = Branch::withTrashed()->where('business_id', auth()->user()->business_id)->latest()->get();
|
|
|
|
|
return view('business::transfers.create', compact('warehouses', 'branches'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function store(Request $request)
|
|
|
|
|
{
|
2026-05-14 11:55:22 +07:00
|
|
|
// Normalize serial_numbers
|
|
|
|
|
if ($request->has('products')) {
|
|
|
|
|
$products = $request->products;
|
|
|
|
|
|
|
|
|
|
foreach ($products as $key => $item) {
|
|
|
|
|
if (!empty($item['serial_numbers']) && is_string($item['serial_numbers'])) {
|
|
|
|
|
$products[$key]['serial_numbers'] = json_decode($item['serial_numbers'], true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$request->merge(['products' => $products]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
$request->validate([
|
|
|
|
|
'from_branch_id' => 'nullable|exists:branches,id',
|
|
|
|
|
'to_branch_id' => 'nullable|exists:branches,id|required_with:from_branch_id',
|
|
|
|
|
'from_warehouse_id' => 'nullable|exists:warehouses,id|required_with:to_warehouse_id',
|
|
|
|
|
'to_warehouse_id' => 'nullable|exists:warehouses,id|required_with:from_warehouse_id',
|
|
|
|
|
'transfer_date' => 'required|date',
|
|
|
|
|
'note' => 'nullable|string',
|
|
|
|
|
'status' => 'required|in:pending,completed,cancelled',
|
|
|
|
|
'shipping_charge' => 'nullable|numeric|min:0',
|
|
|
|
|
'products' => 'required|array|min:1',
|
|
|
|
|
'products.*.quantity' => 'required|integer|min:1',
|
|
|
|
|
'products.*.unit_price' => 'required|numeric|min:0',
|
|
|
|
|
'products.*.discount' => 'nullable|numeric|min:0',
|
|
|
|
|
'products.*.tax' => 'nullable|numeric|min:0',
|
2026-05-14 11:55:22 +07:00
|
|
|
'products.*.serial_numbers' => 'nullable|array',
|
2026-03-15 17:08:23 +07:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$user = auth()->user();
|
|
|
|
|
$fromBranch = $request->from_branch_id ?? $user->branch_id ?? $user->active_branch_id;
|
|
|
|
|
$toBranch = $request->to_branch_id;
|
|
|
|
|
$fromWh = $request->from_warehouse_id;
|
|
|
|
|
$toWh = $request->to_warehouse_id;
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Transfer validation
|
2026-03-15 17:08:23 +07:00
|
|
|
if ($user->active_branch_id && $toBranch && $toWh) {
|
2026-05-14 11:55:22 +07:00
|
|
|
return response()->json(['message' => 'You cannot transfer to another branch warehouse.'], 400);
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
if (!$toBranch && !$toWh) {
|
2026-05-14 11:55:22 +07:00
|
|
|
return response()->json(['message' => 'Please select a destination branch or warehouse.'], 400);
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
if ($fromBranch && !$fromWh && $fromBranch == $toBranch) {
|
|
|
|
|
return response()->json(['message' => 'Same branch transfer is not allowed.'], 400);
|
|
|
|
|
}
|
|
|
|
|
if ($fromWh && $fromWh == $toWh) {
|
|
|
|
|
return response()->json(['message' => 'Same warehouse transfer is not allowed.'], 400);
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-14 11:55:22 +07:00
|
|
|
$subTotal = $totalDiscount = $totalTax = 0;
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// products re-arrange
|
|
|
|
|
$products = collect($request->products)->map(function ($item, $key) {
|
|
|
|
|
return array_merge($item, ['stock_id' => $item['stock_id'] ?? $key]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calculate subtotal using serial count if available
|
|
|
|
|
foreach ($products as $item) {
|
|
|
|
|
$serials = !empty($item['serial_numbers'])
|
|
|
|
|
? (is_array($item['serial_numbers']) ? $item['serial_numbers'] : json_decode($item['serial_numbers'], true))
|
|
|
|
|
: [];
|
|
|
|
|
$qty = !empty($serials) ? count($serials) : $item['quantity'];
|
|
|
|
|
$subTotal += $qty * $item['unit_price'];
|
|
|
|
|
$totalTax += $item['tax'] ?? 0;
|
|
|
|
|
$totalDiscount += $item['discount'] ?? 0;
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$shipping = $request->shipping_charge ?? 0;
|
|
|
|
|
$grandTotal = $subTotal + $totalTax - $totalDiscount + $shipping;
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$transfer = Transfer::create([
|
|
|
|
|
'business_id' => $user->business_id,
|
|
|
|
|
'user_id' => auth()->id(),
|
2026-03-15 17:08:23 +07:00
|
|
|
'from_branch_id' => $fromBranch,
|
|
|
|
|
'from_warehouse_id' => $fromWh,
|
|
|
|
|
'to_branch_id' => $toBranch,
|
|
|
|
|
'to_warehouse_id' => $toWh,
|
2026-05-14 11:55:22 +07:00
|
|
|
'transfer_date' => $request->transfer_date,
|
|
|
|
|
'status' => $request->status,
|
|
|
|
|
'note' => $request->note,
|
2026-03-15 17:08:23 +07:00
|
|
|
'shipping_charge' => $shipping,
|
|
|
|
|
'sub_total' => $subTotal,
|
|
|
|
|
'total_discount' => $totalDiscount,
|
|
|
|
|
'total_tax' => $totalTax,
|
|
|
|
|
'grand_total' => $grandTotal,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$transferProducts = [];
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
foreach ($products as $item) {
|
|
|
|
|
$stock = Stock::find($item['stock_id']);
|
2026-03-15 17:08:23 +07:00
|
|
|
if (!$stock) {
|
2026-05-14 11:55:22 +07:00
|
|
|
return response()->json(['message' => "Invalid stock ID: {$item['stock_id']}"], 400);
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Ensure serial_numbers is array
|
|
|
|
|
$serials = !empty($item['serial_numbers'])
|
|
|
|
|
? (is_array($item['serial_numbers']) ? $item['serial_numbers'] : json_decode($item['serial_numbers'], true))
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
$quantity = !empty($serials) ? count($serials) : $item['quantity'];
|
|
|
|
|
|
|
|
|
|
$transferProducts[] = [
|
2026-03-15 17:08:23 +07:00
|
|
|
'transfer_id' => $transfer->id,
|
2026-05-14 11:55:22 +07:00
|
|
|
'stock_id' => $stock->id,
|
2026-03-15 17:08:23 +07:00
|
|
|
'product_id' => $stock->product_id,
|
2026-05-14 11:55:22 +07:00
|
|
|
'quantity' => $quantity,
|
|
|
|
|
'unit_price' => $item['unit_price'],
|
2026-03-15 17:08:23 +07:00
|
|
|
'discount' => $item['discount'] ?? 0,
|
|
|
|
|
'tax' => $item['tax'] ?? 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'serial_numbers' => !empty($serials) ? json_encode($serials) : null,
|
2026-03-15 17:08:23 +07:00
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($request->status === 'completed') {
|
2026-05-14 11:55:22 +07:00
|
|
|
// FROM stock
|
|
|
|
|
$fromStock = Stock::where('id', $stock->id)
|
|
|
|
|
->when($fromBranch, fn($q) => $q->where('branch_id', $fromBranch))
|
|
|
|
|
->when($fromWh, fn($q) => $q->where('warehouse_id', $fromWh))
|
|
|
|
|
->first();
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
if (!$fromStock || $fromStock->productStock < $quantity) {
|
|
|
|
|
return response()->json(['message' => 'Insufficient stock for product ID: ' . $stock->product_id], 400);
|
|
|
|
|
}
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Remove serials from source
|
|
|
|
|
if (!empty($serials)) {
|
|
|
|
|
$existingSerials = is_array($fromStock->serial_numbers) ? $fromStock->serial_numbers : json_decode($fromStock->serial_numbers ?? '[]', true);
|
|
|
|
|
$fromStock->serial_numbers = array_values(array_diff($existingSerials, $serials));
|
|
|
|
|
}
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$fromStock->decrement('productStock', $quantity);
|
|
|
|
|
$fromStock->save();
|
|
|
|
|
|
|
|
|
|
// TO stock
|
|
|
|
|
$toStock = Stock::where('product_id', $fromStock->product_id)
|
|
|
|
|
->when($toBranch, fn($q) => $q->where('branch_id', $toBranch))
|
|
|
|
|
->when($toWh, fn($q) => $q->where('warehouse_id', $toWh))
|
|
|
|
|
->where(function ($q) use ($fromStock) {
|
|
|
|
|
// if destination stock batch is NULL, skip batch compare
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
// otherwise match same batch
|
|
|
|
|
->orWhere('batch_no', $fromStock->batch_no);
|
|
|
|
|
})
|
|
|
|
|
->first();
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
if (!$toStock) {
|
|
|
|
|
$toStock = new Stock([
|
|
|
|
|
'business_id' => $user->business_id,
|
|
|
|
|
'product_id' => $fromStock->product_id,
|
|
|
|
|
'warehouse_id' => $toWh,
|
|
|
|
|
'productStock' => 0,
|
|
|
|
|
'productPurchasePrice' => $fromStock->productPurchasePrice,
|
|
|
|
|
'profit_percent' => $fromStock->profit_percent,
|
|
|
|
|
'productSalePrice' => $fromStock->productSalePrice,
|
|
|
|
|
'exclusive_price' => $fromStock->exclusive_price,
|
|
|
|
|
'productWholeSalePrice' => $fromStock->productWholeSalePrice,
|
|
|
|
|
'productDealerPrice' => $fromStock->productDealerPrice,
|
|
|
|
|
'mfg_date' => $fromStock->mfg_date,
|
|
|
|
|
'expire_date' => $fromStock->expire_date,
|
|
|
|
|
'serial_numbers' => !empty($serials) ? $serials : null,
|
|
|
|
|
]);
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Safe branch assignment
|
|
|
|
|
$toStock->branch_id = $toBranch ?? $user->active_branch_id;
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Save without events/global scopes
|
|
|
|
|
Stock::withoutEvents(function () use ($toStock) {
|
|
|
|
|
$toStock->save();
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Increment destination stock safely
|
|
|
|
|
$toStock->increment('productStock', $quantity);
|
2026-03-15 17:08:23 +07:00
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Merge serial numbers if exist
|
|
|
|
|
if (!empty($serials)) {
|
|
|
|
|
$toStockSerials = is_array($toStock->serial_numbers)
|
|
|
|
|
? $toStock->serial_numbers
|
|
|
|
|
: json_decode($toStock->serial_numbers ?? '[]', true);
|
|
|
|
|
$toStock->serial_numbers = array_values(array_unique(array_merge($toStockSerials, $serials)));
|
|
|
|
|
$toStock->save();
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
TransferProduct::insert($transferProducts);
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Transfer saved successfully.'),
|
2026-05-14 11:55:22 +07:00
|
|
|
'data' => $transfer,
|
2026-03-15 17:08:23 +07:00
|
|
|
'redirect' => route('business.transfers.index'),
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollBack();
|
|
|
|
|
return response()->json([
|
2026-05-14 11:55:22 +07:00
|
|
|
'success' => false,
|
|
|
|
|
'message' => $e->getMessage()
|
2026-03-15 17:08:23 +07:00
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function edit($id)
|
|
|
|
|
{
|
|
|
|
|
$user = auth()->user();
|
|
|
|
|
$business_id = $user->business_id;
|
|
|
|
|
|
|
|
|
|
$transfer = Transfer::with(['transferProducts.product', 'transferProducts.stock'])
|
|
|
|
|
->where('business_id', $business_id)
|
|
|
|
|
->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
// Determine the branch to filter warehouses
|
|
|
|
|
$branchId = $user->branch_id ?? $user->active_branch_id;
|
|
|
|
|
|
|
|
|
|
$warehousesQuery = Warehouse::where('business_id', $business_id);
|
|
|
|
|
if ($branchId) {
|
|
|
|
|
$warehousesQuery->where('branch_id', $branchId);
|
|
|
|
|
}
|
|
|
|
|
$warehouses = $warehousesQuery->latest()->get();
|
|
|
|
|
|
|
|
|
|
$branches = Branch::withTrashed()
|
|
|
|
|
->where('business_id', $business_id)
|
|
|
|
|
->latest()
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
return view('business::transfers.edit', compact('transfer', 'warehouses', 'branches'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function update(Request $request, $id)
|
|
|
|
|
{
|
2026-05-14 11:55:22 +07:00
|
|
|
// Normalize serial_numbers
|
|
|
|
|
if ($request->has('products')) {
|
|
|
|
|
$products = $request->products;
|
|
|
|
|
|
|
|
|
|
foreach ($products as $key => $item) {
|
|
|
|
|
if (!empty($item['serial_numbers']) && is_string($item['serial_numbers'])) {
|
|
|
|
|
$products[$key]['serial_numbers'] = json_decode($item['serial_numbers'], true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$request->merge(['products' => $products]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
$request->validate([
|
|
|
|
|
'from_branch_id' => 'nullable|exists:branches,id',
|
|
|
|
|
'to_branch_id' => 'nullable|exists:branches,id|required_with:from_branch_id',
|
|
|
|
|
'from_warehouse_id' => 'nullable|exists:warehouses,id|required_with:to_warehouse_id',
|
|
|
|
|
'to_warehouse_id' => 'nullable|exists:warehouses,id|required_with:from_warehouse_id',
|
|
|
|
|
'transfer_date' => 'required|date',
|
|
|
|
|
'note' => 'nullable|string',
|
|
|
|
|
'status' => 'required|in:pending,completed,cancelled',
|
|
|
|
|
'shipping_charge' => 'nullable|numeric|min:0',
|
|
|
|
|
'products' => 'required|array|min:1',
|
|
|
|
|
'products.*.quantity' => 'required|integer|min:1',
|
|
|
|
|
'products.*.unit_price' => 'required|numeric|min:0',
|
|
|
|
|
'products.*.discount' => 'nullable|numeric|min:0',
|
|
|
|
|
'products.*.tax' => 'nullable|numeric|min:0',
|
2026-05-14 11:55:22 +07:00
|
|
|
'products.*.serial_numbers' => 'nullable|array',
|
2026-03-15 17:08:23 +07:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$transfer = Transfer::findOrFail($id);
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
if ($request->status === 'cancelled') {
|
2026-03-15 17:08:23 +07:00
|
|
|
$transfer->update(['status' => 'cancelled']);
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Transfer cancelled successfully.'),
|
|
|
|
|
'redirect' => route('business.transfers.index')
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$user = auth()->user();
|
|
|
|
|
$fromBranch = $request->from_branch_id ?? $user->branch_id ?? $user->active_branch_id;
|
|
|
|
|
$toBranch = $request->to_branch_id;
|
|
|
|
|
$fromWh = $request->from_warehouse_id;
|
|
|
|
|
$toWh = $request->to_warehouse_id;
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
try {
|
|
|
|
|
$oldStatus = $transfer->status;
|
|
|
|
|
$subTotal = $totalDiscount = $totalTax = 0;
|
2026-05-14 11:55:22 +07:00
|
|
|
|
|
|
|
|
// Calculate totals using serial count
|
2026-03-15 17:08:23 +07:00
|
|
|
foreach ($request->products as $item) {
|
2026-05-14 11:55:22 +07:00
|
|
|
$serials = !empty($item['serial_numbers']) ? $item['serial_numbers'] : [];
|
|
|
|
|
$qty = !empty($serials) ? count($serials) : $item['quantity'];
|
|
|
|
|
|
|
|
|
|
$subTotal += $qty * $item['unit_price'];
|
2026-03-15 17:08:23 +07:00
|
|
|
$totalDiscount += $item['discount'] ?? 0;
|
|
|
|
|
$totalTax += $item['tax'] ?? 0;
|
|
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
$shipping = $request->shipping_charge ?? 0;
|
|
|
|
|
$grandTotal = $subTotal + $totalTax - $totalDiscount + $shipping;
|
|
|
|
|
|
|
|
|
|
// Update transfer
|
|
|
|
|
$transfer->update([
|
2026-05-14 11:55:22 +07:00
|
|
|
'user_id' => auth()->id(),
|
2026-03-15 17:08:23 +07:00
|
|
|
'from_branch_id' => $fromBranch,
|
|
|
|
|
'to_branch_id' => $toBranch,
|
|
|
|
|
'from_warehouse_id' => $fromWh,
|
|
|
|
|
'to_warehouse_id' => $toWh,
|
|
|
|
|
'transfer_date' => $request->transfer_date,
|
|
|
|
|
'note' => $request->note,
|
|
|
|
|
'status' => $request->status,
|
|
|
|
|
'shipping_charge' => $shipping,
|
|
|
|
|
'sub_total' => $subTotal,
|
|
|
|
|
'total_discount' => $totalDiscount,
|
|
|
|
|
'total_tax' => $totalTax,
|
|
|
|
|
'grand_total' => $grandTotal,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Delete old transfer products
|
2026-03-15 17:08:23 +07:00
|
|
|
TransferProduct::where('transfer_id', $transfer->id)->delete();
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
foreach ($request->products as $stockId => $item) {
|
2026-05-14 11:55:22 +07:00
|
|
|
|
|
|
|
|
$serials = !empty($item['serial_numbers']) ? $item['serial_numbers'] : [];
|
|
|
|
|
$quantity = !empty($serials) ? count($serials) : $item['quantity'];
|
|
|
|
|
|
|
|
|
|
// Insert updated transfer product
|
|
|
|
|
TransferProduct::create([
|
2026-03-15 17:08:23 +07:00
|
|
|
'transfer_id' => $transfer->id,
|
|
|
|
|
'stock_id' => $stockId,
|
2026-05-14 11:55:22 +07:00
|
|
|
'quantity' => $quantity,
|
2026-03-15 17:08:23 +07:00
|
|
|
'unit_price' => $item['unit_price'] ?? 0,
|
|
|
|
|
'discount' => $item['discount'] ?? 0,
|
|
|
|
|
'tax' => $item['tax'] ?? 0,
|
2026-05-14 11:55:22 +07:00
|
|
|
'serial_numbers' => !empty($serials) ? $serials : null,
|
|
|
|
|
]);
|
2026-03-15 17:08:23 +07:00
|
|
|
|
|
|
|
|
$fromStock = Stock::where('id', $stockId)
|
|
|
|
|
->when($fromBranch, fn($q) => $q->where('branch_id', $fromBranch))
|
|
|
|
|
->when($fromWh, fn($q) => $q->where('warehouse_id', $fromWh))
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$fromStock) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => "From stock not found for stock ID: {$stockId}"
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
$toStock = Stock::where('product_id', $fromStock->product_id)
|
|
|
|
|
->when($toBranch, fn($q) => $q->where('branch_id', $toBranch))
|
|
|
|
|
->when($toWh, fn($q) => $q->where('warehouse_id', $toWh))
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($fromStock) {
|
|
|
|
|
// if destination stock batch is NULL, skip batch compare
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
// otherwise match same batch
|
|
|
|
|
->orWhere('batch_no', $fromStock->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$toStock) {
|
|
|
|
|
$toStock = new Stock([
|
2026-05-14 11:55:22 +07:00
|
|
|
'business_id' => $user->business_id,
|
2026-03-15 17:08:23 +07:00
|
|
|
'product_id' => $fromStock->product_id,
|
|
|
|
|
'warehouse_id' => $toWh,
|
|
|
|
|
'productStock' => 0,
|
|
|
|
|
'productPurchasePrice' => $fromStock->productPurchasePrice,
|
2026-05-14 11:55:22 +07:00
|
|
|
'exclusive_price' => $fromStock->exclusive_price,
|
2026-03-15 17:08:23 +07:00
|
|
|
'profit_percent' => $fromStock->profit_percent,
|
|
|
|
|
'productSalePrice' => $fromStock->productSalePrice,
|
|
|
|
|
'productWholeSalePrice' => $fromStock->productWholeSalePrice,
|
|
|
|
|
'productDealerPrice' => $fromStock->productDealerPrice,
|
|
|
|
|
'mfg_date' => $fromStock->mfg_date,
|
|
|
|
|
'expire_date' => $fromStock->expire_date,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
$toStock->branch_id = $toBranch ?? $user->active_branch_id;
|
2026-03-15 17:08:23 +07:00
|
|
|
Stock::withoutEvents(fn() => $toStock->save());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 11:55:22 +07:00
|
|
|
// Only move stock if changing pending to completed
|
|
|
|
|
if ($oldStatus === 'pending' && $request->status === 'completed') {
|
|
|
|
|
|
|
|
|
|
// Handle serials for source
|
|
|
|
|
if (!empty($serials)) {
|
|
|
|
|
$existing = $fromStock->serial_numbers ?? [];
|
|
|
|
|
foreach ($serials as $s) {
|
|
|
|
|
if (!in_array($s, $existing)) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => "Serial {$s} not found in source stock."
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
// Remove from source
|
|
|
|
|
$fromStock->serial_numbers = array_values(array_diff($existing, $serials));
|
|
|
|
|
$fromStock->productStock = count($fromStock->serial_numbers);
|
|
|
|
|
if ($fromStock->productStock === 0) {
|
|
|
|
|
$fromStock->serial_numbers = null;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$fromStock->decrement('productStock', $quantity);
|
|
|
|
|
}
|
|
|
|
|
$fromStock->save();
|
|
|
|
|
|
|
|
|
|
// Handle serials for destination
|
|
|
|
|
if (!empty($serials)) {
|
|
|
|
|
$toExisting = $toStock->serial_numbers ?? [];
|
|
|
|
|
$toStock->serial_numbers = array_values(array_unique(array_merge($toExisting, $serials)));
|
|
|
|
|
$toStock->productStock = count($toStock->serial_numbers);
|
|
|
|
|
} else {
|
|
|
|
|
$toStock->increment('productStock', $quantity);
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
$toStock->save();
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Transfer updated successfully.'),
|
|
|
|
|
'redirect' => route('business.transfers.index'),
|
|
|
|
|
]);
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollBack();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => 'Error: ' . $e->getMessage(),
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
public function destroy(string $id)
|
2026-03-15 17:08:23 +07:00
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
$transfer = Transfer::with('transferProducts')->where('business_id', $business_id)->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if ($transfer->status == 'completed') {
|
|
|
|
|
foreach ($transfer->transferProducts as $tp) {
|
|
|
|
|
$stock = Stock::find($tp->stock_id);
|
|
|
|
|
|
|
|
|
|
// check if destination has enough stock to rollback
|
|
|
|
|
$toStock = Stock::where('product_id', $tp->product_id)
|
|
|
|
|
->where('warehouse_id', $transfer->to_warehouse_id)
|
|
|
|
|
->where('branch_id', $transfer->to_branch_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($stock) {
|
|
|
|
|
$q->whereNull('batch_no') // if batch NULL → skip compare
|
|
|
|
|
->orWhere('batch_no', optional($stock)->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$toStock || $toStock->quantity < $tp->quantity) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Cannot delete. Destination stock not enough to reverse this transfer'),
|
|
|
|
|
], 422);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($transfer->transferProducts as $tp) {
|
|
|
|
|
$stock = Stock::find($tp->stock_id);
|
|
|
|
|
|
|
|
|
|
// decrease destination stock
|
|
|
|
|
$toStock = Stock::where('product_id', $tp->product_id)
|
|
|
|
|
->where('warehouse_id', $transfer->to_warehouse_id)
|
|
|
|
|
->where('branch_id', $transfer->to_branch_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($stock) {
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
->orWhere('batch_no', optional($stock)->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
if ($toStock) {
|
|
|
|
|
$toStock->decrement('quantity', $tp->quantity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// increase source stock
|
|
|
|
|
$fromStock = Stock::where('product_id', $tp->product_id)
|
|
|
|
|
->where('warehouse_id', $transfer->from_warehouse_id)
|
|
|
|
|
->where('branch_id', $transfer->from_branch_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($stock) {
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
->orWhere('batch_no', optional($stock)->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
if ($fromStock) {
|
|
|
|
|
$fromStock->increment('quantity', $tp->quantity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$transfer->delete();
|
|
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Transfer deleted successfully'),
|
|
|
|
|
'redirect' => route('business.transfers.index'),
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollBack();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Error while deleting transfer: ') . $e->getMessage(),
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function deleteAll(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$business_id = auth()->user()->business_id;
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$transfers = Transfer::with('transferProducts')
|
|
|
|
|
->where('business_id', $business_id)
|
|
|
|
|
->whereIn('id', $request->ids)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
foreach ($transfers as $transfer) {
|
|
|
|
|
if ($transfer->status === 'completed') {
|
|
|
|
|
foreach ($transfer->transferProducts as $tp) {
|
|
|
|
|
$stock = Stock::find($tp->stock_id);
|
|
|
|
|
|
|
|
|
|
// check if destination has enough stock to rollback
|
|
|
|
|
$toStock = Stock::where('product_id', $tp->product_id)
|
|
|
|
|
->where('warehouse_id', $transfer->to_warehouse_id)
|
|
|
|
|
->where('branch_id', $transfer->to_branch_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($stock) {
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
->orWhere('batch_no', optional($stock)->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if (!$toStock || $toStock->quantity < $tp->quantity) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Cannot delete. Destination stock not enough to reverse transfer ID: ') . $transfer->id,
|
|
|
|
|
], 422);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($transfers as $transfer) {
|
|
|
|
|
if ($transfer->status === 'completed') {
|
|
|
|
|
foreach ($transfer->transferProducts as $tp) {
|
|
|
|
|
$stock = Stock::find($tp->stock_id);
|
|
|
|
|
|
|
|
|
|
// decrease destination stock
|
|
|
|
|
$toStock = Stock::where('product_id', $tp->product_id)
|
|
|
|
|
->where('warehouse_id', $transfer->to_warehouse_id)
|
|
|
|
|
->where('branch_id', $transfer->to_branch_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($stock) {
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
->orWhere('batch_no', optional($stock)->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
if ($toStock) {
|
|
|
|
|
$toStock->decrement('quantity', $tp->quantity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// increase source stock
|
|
|
|
|
$fromStock = Stock::where('product_id', $tp->product_id)
|
|
|
|
|
->where('warehouse_id', $transfer->from_warehouse_id)
|
|
|
|
|
->where('branch_id', $transfer->from_branch_id)
|
2026-05-14 11:55:22 +07:00
|
|
|
->where(function ($q) use ($stock) {
|
|
|
|
|
$q->whereNull('batch_no')
|
|
|
|
|
->orWhere('batch_no', optional($stock)->batch_no);
|
|
|
|
|
})
|
2026-03-15 17:08:23 +07:00
|
|
|
->first();
|
2026-05-14 11:55:22 +07:00
|
|
|
|
2026-03-15 17:08:23 +07:00
|
|
|
if ($fromStock) {
|
|
|
|
|
$fromStock->increment('quantity', $tp->quantity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$transfer->delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Selected transfers deleted successfully.'),
|
|
|
|
|
'redirect' => route('business.transfers.index'),
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollBack();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => __('Error while deleting transfers: ') . $e->getMessage(),
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function exportExcel()
|
|
|
|
|
{
|
|
|
|
|
return Excel::download(new ExportTransfer, 'transfer.xlsx');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function exportCsv()
|
|
|
|
|
{
|
|
|
|
|
return Excel::download(new ExportTransfer, 'transfer.csv');
|
|
|
|
|
}
|
2026-05-14 11:55:22 +07:00
|
|
|
|
|
|
|
|
public function exportPdf(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$user = auth()->user();
|
|
|
|
|
$activeBranchId = $user->active_branch_id;
|
|
|
|
|
|
|
|
|
|
$transfers = Transfer::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'toBranch:id,name', 'fromBranch:id,name', 'transferProducts'])
|
|
|
|
|
->where('business_id', $user->business_id)
|
|
|
|
|
->when($activeBranchId, function ($q) use ($activeBranchId) {
|
|
|
|
|
$q->where(function ($query) use ($activeBranchId) {
|
|
|
|
|
$query->where('from_branch_id', $activeBranchId)
|
|
|
|
|
->orWhere('to_branch_id', $activeBranchId);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->when($request->branch_id && !$activeBranchId, function ($q) use ($request) {
|
|
|
|
|
$q->where(function ($query) use ($request) {
|
|
|
|
|
$query->where('from_branch_id', $request->branch_id)->orWhere('to_branch_id', $request->branch_id);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->when($request->search, function ($q) use ($request) {
|
|
|
|
|
$search = $request->search;
|
|
|
|
|
|
|
|
|
|
$q->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('note', 'like', '%' . $search . '%')
|
|
|
|
|
->orWhere('status', 'like', '%' . $search . '%')
|
|
|
|
|
->orWhere('invoice_no', 'like', '%' . $search . '%')
|
|
|
|
|
->orWhereHas('fromWarehouse', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('toWarehouse', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('toBranch', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
})
|
|
|
|
|
->orWhereHas('fromBranch', function ($q) use ($search) {
|
|
|
|
|
$q->where('name', 'like', '%' . $search . '%');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->latest()
|
|
|
|
|
->limit(request('per_page') ?? 20)->get();
|
|
|
|
|
|
|
|
|
|
return PdfService::render('business::transfers.pdf', compact('transfers'),'transfers.pdf');
|
|
|
|
|
}
|
2026-03-15 17:08:23 +07:00
|
|
|
}
|