'=', '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); } /** * 采购单详情:头 + 明细矩阵(商品行 × 门店列) * 行 = 商品(按分类 sort → 商品 sort 排序),列 = 商品信息 + 每个门店一格(数量/金额) */ #[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), ]); } /** C4 修改采购单头信息(采购日期、状态、备注) */ #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function update(int $id, Request $request): JsonResponse { $data = $request->validate([ 'purchase_date' => 'nullable|date_format:Y-m-d', 'status' => 'nullable|integer|in:' . PurchaseOrderModel::STATUS_PENDING . ',' . PurchaseOrderModel::STATUS_COMPLETED, 'remark' => 'nullable|string|max:255', ], [ 'purchase_date.date_format' => '采购日期格式为 Y-m-d', 'status.in' => '采购单状态不正确', ]); $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } $purchase->update(array_filter($data, static fn ($v) => $v !== null)); return $this->success(); } /** * C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids), * 生成后源订单转为「采购中」并回写 purchase_id(订单与订货明细同步归集) */ #[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], '采购单已生成' ); } /** C2/C3 导出采购单:?type=all|category & format=xlsx|pdf */ #[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 app(ExportService::class)->download( 'purchase', $purchase, (string) $request->query('format', ExportService::FORMAT_XLSX), type: $type, ); } /** * C4 门店单元格修改:数量/称重回写订货明细,金额重算并同步订单/采购单汇总 */ #[PutRoute(route: '/cell/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function updateCell(int $id, PurchaseCellUpdateRequest $request): JsonResponse { $item = StoreOrderItemModel::find($id); if (empty($item)) { throw new RepositoryException('订货明细不存在'); } $validated = $request->validated(); $item = app(PurchaseEditService::class)->updateCell( $item, (int) $validated['quantity'], isset($validated['weight']) ? (float) $validated['weight'] : null, ); return $this->success([ 'quantity' => (int) $item->quantity, 'weight' => (float) $item->weight, 'amount' => (float) $item->amount, ]); } /** * C4 商品行修改:采购成本同步该商品全部订货明细;实际称重按数量比例分摊(尾差修正守恒) */ #[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('采购单不存在'); } $validated = $request->validated(); $costPrice = isset($validated['cost_price']) ? (float) $validated['cost_price'] : null; $weight = isset($validated['weight']) ? (float) $validated['weight'] : null; if ($costPrice === null && $weight === null) { throw new RepositoryException('采购成本与实际称重至少填写一项'); } $count = app(PurchaseEditService::class)->updateRow($purchase, $productId, $costPrice, $weight); return $this->success(['count' => $count], '已同步 ' . $count . ' 条订货明细'); } }