'=', 'purchase_no' => 'like', 'purchase_date' => 'betweenDate', ]; /** 采购单列表 */ #[GetRoute(authorize: 'query')] public function query(Request $request): JsonResponse { $params = $request->all(); $pageSize = $params['pageSize'] ?? 10; $data = $this->buildSearch($params, PurchaseOrderModel::query()->with('operator:id,nickname')) ->orderBy('purchase_date', 'desc') ->orderBy('id', 'desc') ->paginate($pageSize) ->toArray(); return $this->success($data); } /** * 采购单详情 */ #[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])] public function detail(int $id): JsonResponse { $purchase = PurchaseOrderModel::with('operator:id,nickname')->find($id); if (empty($purchase)) { return $this->error('采购单不存在'); } // 订货明细 $orderItems = StoreOrderItemModel::query() ->join('store_order', 'store_order.id', '=', 'store_order_item.order_id') ->where('store_order_item.purchase_id', $purchase->id) ->whereNull('store_order.deleted_at') ->with('supplier:id,name') ->select('store_order_item.*') ->get() ->makeVisible('cost_price'); // 门店列(含软删除门店,保证历史单据可见) $stores = StoreModel::withTrashed() ->whereIn('id', $orderItems->pluck('store_id')->unique()) ->get(['id', 'name']) ->map(static fn (StoreModel $store) => ['id' => $store->id, 'name' => $store->name]) ->values(); // 排序键:分类 sort → 商品 sort $products = ProductModel::withTrashed() ->with('category:id,sort') ->whereIn('id', $orderItems->pluck('product_id')->unique()) ->get() ->keyBy('id'); $rows = []; foreach ($orderItems->groupBy('product_id') as $productId => $group) { $first = $group->first(); // 商品 $product = $products->get((int) $productId); // 采购总数 $quantity = 0; // 采购总重量 $weight = '0'; // 门店明细 $cellsMap = []; foreach ($group as $item) { $storeId = $item->store_id; // 累加总量 $quantity += (int) $item->quantity; $weight = bcadd($weight, (string) $item->weight, 3); // 按门店合并 if (!isset($cellsMap[$storeId])) { $cellsMap[$storeId] = 0; } $cellsMap[$storeId] += (int) $item->quantity; } // 供应商 $supplier = $first->supplier ? ['id' => $first->supplier->id, 'name' => $first->supplier->name] : null; $rows[] = [ 'product_id' => (int) $productId, 'product_name' => $first->product_name, // 品名 'product_spec' => $first->product_spec, // 包规 'unit' => $first->unit, // 单位 'supplier_id' => (int) $first->supplier_id, // 供应商id 'supplier' => $supplier, // 供应商 'cost_price' => $first->cost_price, // 成本价 'quantity' => $quantity, 'weight' => (float) $weight, 'cells' => $cellsMap, 'category_sort' => (int) ($product->category->sort ?? 9999), 'product_sort' => (int) ($product->sort ?? 9999), ]; } usort($rows, static fn (array $a, array $b): int => [$a['category_sort'], $a['product_sort'], $a['product_id']] <=> [$b['category_sort'], $b['product_sort'], $b['product_id']]); return $this->success([ 'purchase' => $purchase->toArray(), 'stores' => $stores->toArray(), 'items' => array_map(static function (array $row): array { unset($row['category_sort'], $row['product_sort']); return $row; }, $rows), ]); } /** 修改采购单信息(采购日期、实际金额、备注) */ #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function update(int $id, Request $request): JsonResponse { $data = $request->validate([ 'actual_amount' => 'required|numeric|min:0', 'remark' => 'nullable|string|max:255', ], [ 'actual_amount.required' => '实际金额不能为空', 'actual_amount.numeric' => '实际金额格式错误', 'actual_amount.min' => '实际金额不能小于0', 'remark.max' => '备注超过最大长度', ]); $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { throw new RepositoryException('采购单已完成,不允许修改信息'); } $purchase->update(array_filter($data, static fn ($v) => $v !== null)); return $this->success(); } /** * 完成采购单 * @throws */ #[PutRoute(route: '/{id}/finish', authorize: 'update', where: ['id' => '[0-9]+'])] public function finish(int $id): JsonResponse { $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { throw new RepositoryException('采购单已完成,不允许修改信息'); } return DB::transaction(function () use ($purchase) { // 修改采购单状态 $purchase->status = PurchaseOrderModel::STATUS_COMPLETED; $purchase->save(); // 修改所有订单状态为配送中 StoreOrderModel::where('purchase_id', $purchase->id)->update([ 'status' => StoreOrderModel::STATUS_DISTRIBUTION ]); // 生成并发送账单 return $this->success(); }); } /** * 生成采购单 */ #[PostRoute('/generate', 'generate')] public function generate(Request $request): JsonResponse { $data = $request->validate([ 'purchase_date' => 'required|date_format:Y-m-d', 'order_ids' => 'sometimes|array|min:1', 'order_ids.*' => 'integer|exists:store_order,id', ], [ 'purchase_date.required' => '请选择采购日期', 'purchase_date.date_format' => '采购日期格式为 Y-m-d', 'order_ids.min' => '请选择要合并的订单', 'order_ids.*.exists' => '订单不存在', ]); $purchase = app(PurchaseGenerateService::class)->generate( $data['purchase_date'], (int) $request->user()->id, array_map('intval', $data['order_ids'] ?? []), ); return $this->success( ['id' => $purchase->id, 'purchase_no' => $purchase->purchase_no], '采购单已生成' ); } /** * 导出采购单 Excel 表格 * * @throws */ #[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])] public function export(int $id, Request $request): Response { $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } $type = (string) $request->query('type', 'all'); if (! in_array($type, ['all', 'category'], true)) { throw new RepositoryException('导出类型参数不正确(all 全品类 / category 蔬果分类)'); } return Excel::download( new PurchaseOrderExport($purchase, $type), $purchase->purchase_no . '_采购单.xlsx', ); } /** * 单元格下钻:采购单中某门店某商品的全部订货明细 */ #[GetRoute(route: '/{id}/cell', authorize: 'query', where: ['id' => '[0-9]+'])] public function cell(int $id, Request $request): JsonResponse { $productId = (int) $request->query('product_id', 0); $storeId = (int) $request->query('store_id', 0); if ($productId <= 0 || $storeId <= 0) { throw new RepositoryException('缺少商品或门店参数'); } $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } $items = StoreOrderItemModel::query() ->join('store_order', 'store_order.id', '=', 'store_order_item.order_id') ->where('store_order_item.purchase_id', $purchase->id) ->where('store_order_item.product_id', $productId) ->where('store_order_item.store_id', $storeId) ->whereNull('store_order.deleted_at') ->with('supplier:id,name') ->select( 'store_order_item.*', 'store_order.order_no', 'store_order.order_date', 'store_order.status as order_status', ) ->orderBy('store_order.id') ->get() ->makeVisible('cost_price'); $rows = []; foreach ($items as $item) { $rows[] = [ 'id' => $item->id, 'order_id' => $item->order_id, 'order_no' => $item->order_no, 'order_date' => (string) $item->order_date, 'order_status' => (int) $item->order_status, 'product_name' => $item->product_name, 'product_spec' => $item->product_spec, 'unit' => $item->unit, 'supplier_id' => (int) $item->supplier_id, 'supplier' => $item->supplier ? ['id' => $item->supplier->id, 'name' => $item->supplier->name] : null, 'price' => (string) $item->price, 'cost_price' => (string) $item->cost_price, 'quantity' => (int) $item->quantity, 'weight' => (string) $item->weight, 'amount' => (string) $item->amount, 'remark' => (string) $item->remark, 'image_ids' => $item->image_ids, ]; } app(ItemImageResolver::class)->resolve($rows); $store = StoreModel::withTrashed()->find($storeId); $product = ProductModel::withTrashed()->find($productId); return $this->success([ 'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null, 'product' => $product ? ['id' => $product->id, 'name' => $product->name] : null, 'items' => $rows, ]); } /** * 门店购买详情:采购单内指定门店的采购汇总(按商品聚合) * 单价为加权平均口径(Σ金额÷Σ数量),保证 单价×数量=预计金额 */ #[GetRoute(route: '/{id}/store', authorize: 'query', where: ['id' => '[0-9]+'])] public function storeSummary(int $id, Request $request): JsonResponse { $storeId = (int) $request->query('store_id', 0); if ($storeId <= 0) { throw new RepositoryException('缺少门店参数'); } $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } $items = StoreOrderItemModel::query() ->join('store_order', 'store_order.id', '=', 'store_order_item.order_id') ->where('store_order_item.purchase_id', $purchase->id) ->where('store_order_item.store_id', $storeId) ->whereNull('store_order.deleted_at') ->select('store_order_item.*') ->orderBy('store_order_item.id') ->get(); // 排序键:分类 sort → 商品 sort(与明细矩阵同序) $products = ProductModel::withTrashed() ->with('category:id,sort') ->whereIn('id', $items->pluck('product_id')->unique()) ->get() ->keyBy('id'); $rows = []; foreach ($items->groupBy('product_id') as $productId => $group) { $product = $products->get((int) $productId); $first = $group->first(); // 采购总数量(包数) $quantity = 0; // 预计金额 = Σ 明细 amount $amount = '0'; // 总重量 $weight = '0'; foreach ($group as $item) { $weight = bcadd($weight, (string) $item->weight, 3); $quantity += (int) $item->quantity; $amount = bcadd($amount, (string) $item->amount, 2); } $rows[] = [ 'product_id' => (int) $productId, 'product_name' => $first->product_name, 'product_spec' => $first->product_spec, // 包规 'unit' => $first->unit, // 单位 'price' => $quantity > 0 // 加权平均单价 ? bcdiv($amount, (string) $quantity, 2) : (string) $first->price, 'quantity' => $quantity, 'amount' => $amount, 'weight' => $weight, 'category_sort' => (int) ($product->category->sort ?? 9999), 'product_sort' => (int) ($product->sort ?? 9999), ]; } usort($rows, static fn (array $a, array $b): int => [$a['category_sort'], $a['product_sort'], $a['product_id']] <=> [$b['category_sort'], $b['product_sort'], $b['product_id']]); $store = StoreModel::withTrashed()->find($storeId); return $this->success([ 'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null, 'items' => array_map(static function (array $row): array { unset($row['category_sort'], $row['product_sort']); return $row; }, $rows), ]); } /** * 商品行修改:品名/供应商/包规/单位/成本 * @throws Throwable */ #[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])] public function updateRow(int $id, int $productId, PurchaseRowUpdateRequest $request): JsonResponse { $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { throw new RepositoryException('采购单已完成,不允许修改明细'); } $validated = $request->validated(); $items = StoreOrderItemModel::query() ->where('purchase_id', $purchase->id) ->where('product_id', $productId) ->lockForUpdate() ->get(); if ($items->isEmpty()) { throw new RepositoryException('该采购单下无此商品的订货明细'); } foreach ($items as $item) { $item->fill($validated)->save(); } // 重算采购单成本 $query = StoreOrderItemModel::query()->where('purchase_id', $purchase->id); $purchase->total_quantity = $query->sum('quantity'); $purchase->estimate_amount = $query->sum(DB::raw('quantity * cost_price')); $purchase->save(); return $this->success(['count' => $items->count()], '已同步 ' . $items->count() . ' 条订货明细'); } /** * 门店订单明细修改(订货量、重量、单价),自动重算价格 * @throws Throwable */ #[PutRoute(route: '/cell/{itemId}', authorize: 'update', where: ['itemId' => '[0-9]+'])] public function cellUpdate(int $itemId, PurchaseCellUpdateRequest $request): JsonResponse { $validated = $request->validated(); return DB::transaction(function () use ($itemId, $validated) { $item = StoreOrderItemModel::query()->lockForUpdate()->find($itemId); if (empty($item) || (int) $item->purchase_id === 0) { throw new RepositoryException('采购单明细不存在'); } $purchase = PurchaseOrderModel::find((int) $item->purchase_id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { throw new RepositoryException('采购单已完成,不允许修改明细'); } $item->quantity = $validated['quantity']; $item->price = $validated['price']; if (array_key_exists('weight', $validated)) { $item->weight = bcadd((string) $validated['weight'], '0', 3); } $item->amount = bcmul($validated['quantity'], $validated['price'], 3); $item->save(); // 重算订单金额 $order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id); $order->product_amount = $order->items()->sum('amount'); $order->total_weight = $order->items()->sum('weight'); $order->total_amount = bcadd($order->product_amount, $order->added_amount, 2); $order->save(); // 重算采购单重量 $purchase->total_weight = StoreOrderItemModel::query() ->where('purchase_id', $purchase->id) ->sum('weight'); $purchase->save(); return $this->success(); }); } }