'=', 'category_id' => '=', 'supplier_id' => '=', 'title' => 'like', 'period_start' => 'date', ]; /** 对账单列表 */ #[GetRoute(authorize: 'query')] public function query(Request $request): JsonResponse { $params = $request->all(); $pageSize = $params['pageSize'] ?? 10; $data = $this->buildSearch($params, ReconciliationModel::query()->with('operator:id,nickname')) ->orderBy('id', 'desc') ->paginate($pageSize) ->toArray(); return $this->success($data); } /** 创建对账单(草稿,recon_no = RC…) */ #[PostRoute(authorize: 'create')] public function create(ReconciliationFormRequest $request): JsonResponse { $validated = $request->validated(); $recon = ReconciliationModel::create([ 'recon_no' => app(BillNumberService::class)->make('RC'), 'title' => $validated['title'], 'period_start' => $validated['period_start'], 'period_end' => $validated['period_end'], 'category_id' => $validated['category_id'], 'supplier_id' => $validated['supplier_id'], 'publish_amount' => 0, 'actual_amount' => 0, 'diff_amount' => 0, 'status' => ReconciliationModel::STATUS_DRAFT, 'operator_id' => (int) $request->user()->id, 'remark' => $validated['remark'] ?? '', ]); return $this->success(['id' => $recon->id]); } /** 编辑对账单(仅草稿/对账中) */ #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function update(int $id, ReconciliationFormRequest $request): JsonResponse { $recon = ReconciliationModel::find($id); if (empty($recon)) { throw new RepositoryException('对账单不存在'); } if ($recon->status === ReconciliationModel::STATUS_SETTLED) { throw new RepositoryException('对账单已结算,不能编辑'); } $validated = $request->validated(); $recon->update([ 'title' => $validated['title'], 'period_start' => $validated['period_start'], 'period_end' => $validated['period_end'], 'category_id' => $validated['category_id'], 'supplier_id' => $validated['supplier_id'], 'remark' => $validated['remark'] ?? '', ]); return $this->success(); } /** 删除对账单(仅草稿可删,连带明细) */ #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] public function delete(int $id): JsonResponse { $recon = ReconciliationModel::find($id); if (empty($recon)) { throw new RepositoryException('对账单不存在'); } if ($recon->status !== ReconciliationModel::STATUS_DRAFT) { throw new RepositoryException('仅草稿状态的对账单可以删除'); } DB::transaction(function () use ($recon) { $recon->items()->delete(); $recon->delete(); }); return $this->success(); } /** 生成对账明细(按周期 + 品类 + 供应商拉取采购分摊数据;可重复生成) */ #[PostRoute(route: '/{id}/build', authorize: 'build', where: ['id' => '[0-9]+'])] public function build(int $id): JsonResponse { $recon = ReconciliationModel::find($id); if (empty($recon)) { throw new RepositoryException('对账单不存在'); } if ($recon->status === ReconciliationModel::STATUS_SETTLED) { throw new RepositoryException('对账单已结算,不能重新生成明细'); } $count = app(ReconciliationBuildService::class)->build($recon); return $this->success(['count' => $count], '对账明细已生成'); } /** * D5 差额对比视图:按门店 / 按商品两个维度 + 合计行 */ #[GetRoute(route: '/{id}/diff', authorize: 'query', where: ['id' => '[0-9]+'])] public function diff(int $id): JsonResponse { $recon = ReconciliationModel::find($id); if (empty($recon)) { throw new RepositoryException('对账单不存在'); } $items = $recon->items()->with('store:id,name')->get(); $byStore = $items->groupBy('store_id')->map(function ($group) { $first = $group->first(); $publish = $group->sum('publish_amount'); $actual = $group->sum('actual_amount'); return [ 'store_id' => $first->store_id, 'store_name' => $first->store?->name ?? '', 'publish' => (float) $publish, 'actual' => (float) $actual, 'diff' => (float) bcsub((string) $publish, (string) $actual, 2), ]; })->values(); $byProduct = $items->groupBy('product_id')->map(function ($group) { $first = $group->first(); $publish = $group->sum('publish_amount'); $actual = $group->sum('actual_amount'); return [ 'product_id' => $first->product_id, 'product_name' => $first->product_name, 'publish' => (float) $publish, 'actual' => (float) $actual, 'diff' => (float) bcsub((string) $publish, (string) $actual, 2), ]; })->values(); $publishTotal = (string) $items->sum('publish_amount'); $actualTotal = (string) $items->sum('actual_amount'); return $this->success([ 'by_store' => $byStore->toArray(), 'by_product' => $byProduct->toArray(), 'total' => [ 'publish' => (float) $publishTotal, 'actual' => (float) $actualTotal, 'diff' => (float) bcsub($publishTotal, $actualTotal, 2), ], ]); } /** * D9 生成结算表:按门店聚合明细生成 settlement 记录,对账单 status → 已结算 * (回框统计表规则待业务确认,本次仅预留结构) */ #[PostRoute(route: '/{id}/settle', authorize: 'settle', where: ['id' => '[0-9]+'])] public function settle(int $id, Request $request): JsonResponse { $recon = ReconciliationModel::find($id); if (empty($recon)) { throw new RepositoryException('对账单不存在'); } if ($recon->status !== ReconciliationModel::STATUS_WORKING) { throw new RepositoryException('仅「对账中」的对账单可以生成结算表'); } $count = DB::transaction(function () use ($recon, $request) { $groups = $recon->items()->get()->groupBy('store_id'); if ($groups->isEmpty()) { throw new RepositoryException('对账单无明细,请先生成对账明细'); } $billNumber = app(BillNumberService::class); foreach ($groups as $storeId => $items) { $publish = $items->reduce( static fn (string $carry, $item): string => bcadd($carry, (string) $item->publish_amount, 2), '0' ); $actual = $items->reduce( static fn (string $carry, $item): string => bcadd($carry, (string) $item->actual_amount, 2), '0' ); SettlementModel::create([ 'settlement_no' => $billNumber->make('JS'), 'recon_id' => $recon->id, 'store_id' => (int) $storeId, 'period_start' => $recon->period_start, 'period_end' => $recon->period_end, 'total_amount' => $publish, 'actual_amount' => $actual, 'diff_amount' => bcsub($publish, $actual, 2), 'status' => SettlementModel::STATUS_SETTLED, 'operator_id' => (int) $request->user()->id, 'settled_at' => now(), ]); } $recon->status = ReconciliationModel::STATUS_SETTLED; $recon->save(); return $groups->count(); }); return $this->success(['count' => $count], '结算表已生成'); } }