'=', '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', 'items.supplier:id,name', 'items.allocations.store:id,name', ])->find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } return $this->success($purchase->toArray()); } /** 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', 'remark' => 'nullable|string|max:255', ], [ 'purchase_date.date_format' => '采购日期格式为 Y-m-d', ]); $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 采购明细修改:amount 后端重算(weight>0 ? weight×price : quantity×price), * 同步回写采购单头汇总(Σ total_weight / actual_amount) */ #[PutRoute(route: '/item/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function updateItem(int $id, PurchaseItemUpdateRequest $request): JsonResponse { $item = PurchaseOrderItemModel::find($id); if (empty($item)) { throw new RepositoryException('采购明细不存在'); } $validated = $request->validated(); $price = (string) $validated['price']; $quantity = (string) $validated['quantity']; $weight = (string) ($validated['weight'] ?? 0); $amount = (float) $weight > 0 ? bcmul($weight, $price, 2) : bcmul($quantity, $price, 2); $item->update([ 'product_name' => $validated['product_name'] ?? $item->product_name, 'product_spec' => $validated['product_spec'] ?? $item->product_spec, 'price' => $price, 'quantity' => $quantity, 'weight' => $weight, 'amount' => $amount, 'remark' => $validated['remark'] ?? $item->remark, ]); // 回写采购单头汇总 $sums = PurchaseOrderItemModel::query() ->where('purchase_id', $item->purchase_id) ->selectRaw('COALESCE(SUM(weight), 0) as total_weight, COALESCE(SUM(amount), 0) as actual_amount') ->first(); PurchaseOrderModel::whereKey($item->purchase_id)->update([ 'total_weight' => $sums->total_weight, 'actual_amount' => $sums->actual_amount, ]); return $this->success(['amount' => $amount]); } /** C5/C6 明细发送供应商:is_sent=1 + sent_at;联动采购单状态(全发送→ALL_SENT,否则 PART_SENT) */ #[PutRoute(route: '/item/{id}/send', authorize: 'send', where: ['id' => '[0-9]+'])] public function sendItem(int $id): JsonResponse { $item = PurchaseOrderItemModel::find($id); if (empty($item)) { throw new RepositoryException('采购明细不存在'); } if ($item->is_sent === PurchaseOrderItemModel::SENT) { throw new RepositoryException('该明细已发送,请勿重复操作'); } $item->is_sent = PurchaseOrderItemModel::SENT; $item->sent_at = now(); $item->save(); $purchase = $item->purchase; $hasUnsent = $purchase->items() ->where('is_sent', PurchaseOrderItemModel::NOT_SENT) ->exists(); $purchase->status = $hasUnsent ? PurchaseOrderModel::STATUS_PART_SENT : PurchaseOrderModel::STATUS_ALL_SENT; $purchase->save(); return $this->success(); } /** D3 执行金额分摊(按订货比例摊到门店/单品,尾差修正守恒;可重复执行) */ #[PostRoute(route: '/{id}/allocate', authorize: 'allocate', where: ['id' => '[0-9]+'])] public function allocate(int $id): JsonResponse { $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } $count = app(PurchaseAllocateService::class)->allocate($purchase); return $this->success(['count' => $count], '分摊完成'); } /** 分摊结果:按门店、按商品两个聚合维度 */ #[GetRoute(route: '/{id}/allocation', authorize: 'query', where: ['id' => '[0-9]+'])] public function allocation(int $id): JsonResponse { $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } $allocations = PurchaseAllocationModel::query() ->whereIn('purchase_item_id', $purchase->items()->pluck('id')) ->with(['store:id,name', 'product:id,name,unit']) ->get(); $byStore = $allocations->groupBy('store_id')->map(function ($group) { $first = $group->first(); return [ 'store_id' => $first->store_id, 'store_name' => $first->store?->name ?? '', 'quantity' => (float) $group->sum('quantity'), 'weight' => (float) $group->sum('weight'), 'amount' => (float) $group->sum('amount'), ]; })->values(); $byProduct = $allocations->groupBy('product_id')->map(function ($group) { $first = $group->first(); return [ 'product_id' => $first->product_id, 'product_name' => $first->product?->name ?? '', 'unit' => $first->product?->unit ?? '', 'quantity' => (float) $group->sum('quantity'), 'weight' => (float) $group->sum('weight'), 'amount' => (float) $group->sum('amount'), ]; })->values(); return $this->success([ 'by_store' => $byStore->toArray(), 'by_product' => $byProduct->toArray(), 'total_amount' => (float) $allocations->sum('amount'), ]); } }