From bf8ac6839122e3deaddb8721c0894ff83b484d4b Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Fri, 14 Aug 2026 01:19:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E7=AD=90=E3=80=81=E8=B4=A6=E5=8D=95?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Exports/StatementExport.php | 67 --- app/Http/Controllers/Mini/BillController.php | 72 +++ app/Http/Controllers/Mini/OrderController.php | 1 - .../Controllers/Mini/StatementController.php | 86 ---- app/Http/Controllers/Recon/BillController.php | 116 +++++ .../Recon/ContainerReturnController.php | 125 ++++++ .../Controllers/Recon/StatementController.php | 49 -- .../Recon/ContainerReturnFormRequest.php | 41 ++ app/Models/BillModel.php | 34 ++ app/Models/ContainerReturnModel.php | 76 ++++ app/Models/StatementItemModel.php | 78 ---- app/Models/StatementModel.php | 65 --- app/Models/StoreModel.php | 8 +- app/Models/StoreOrderItemModel.php | 2 + app/Models/StoreOrderModel.php | 1 - app/Services/BillDetailService.php | 71 +++ app/Services/BillGenerateService.php | 24 +- app/Services/BillNumberService.php | 3 +- app/Services/StatementGenerateService.php | 101 ----- .../2026_07_23_030608_create_store_table.php | 2 + ..._07_23_030610_create_store_order_table.php | 2 + ..._23_030612_create_reconciliation_table.php | 46 +- .../2026_08_13_000001_create_bill_table.php | 5 + ...4_000001_create_container_return_table.php | 41 ++ database/seeders/PermissionSeeder.php | 46 +- resources/views/exports/statement.blade.php | 58 --- tests/Feature/StatementTest.php | 105 ----- web/api/recon/bill.ts | 19 + web/domain/iBill.ts | 41 ++ web/domain/iContainerReturn.ts | 26 ++ web/domain/iPurchaseOrder.ts | 19 + web/domain/iStatement.ts | 43 -- web/domain/iStore.ts | 4 + web/domain/iStoreOrder.ts | 1 - web/pages/customer/store.tsx | 24 + web/pages/purchase/order.tsx | 15 +- web/pages/recon/bill.tsx | 422 ++++++++++++++++++ web/pages/recon/container-return.tsx | 177 ++++++++ web/pages/recon/statement.tsx | 239 ---------- 39 files changed, 1392 insertions(+), 963 deletions(-) delete mode 100644 app/Exports/StatementExport.php create mode 100644 app/Http/Controllers/Mini/BillController.php delete mode 100644 app/Http/Controllers/Mini/StatementController.php create mode 100644 app/Http/Controllers/Recon/BillController.php create mode 100644 app/Http/Controllers/Recon/ContainerReturnController.php delete mode 100644 app/Http/Controllers/Recon/StatementController.php create mode 100644 app/Http/Requests/Recon/ContainerReturnFormRequest.php create mode 100644 app/Models/ContainerReturnModel.php delete mode 100644 app/Models/StatementItemModel.php delete mode 100644 app/Models/StatementModel.php create mode 100644 app/Services/BillDetailService.php delete mode 100644 app/Services/StatementGenerateService.php create mode 100644 database/migrations/2026_08_14_000001_create_container_return_table.php delete mode 100644 resources/views/exports/statement.blade.php delete mode 100644 tests/Feature/StatementTest.php create mode 100644 web/api/recon/bill.ts create mode 100644 web/domain/iBill.ts create mode 100644 web/domain/iContainerReturn.ts delete mode 100644 web/domain/iStatement.ts create mode 100644 web/pages/recon/bill.tsx create mode 100644 web/pages/recon/container-return.tsx delete mode 100644 web/pages/recon/statement.tsx diff --git a/app/Exports/StatementExport.php b/app/Exports/StatementExport.php deleted file mode 100644 index 128a684..0000000 --- a/app/Exports/StatementExport.php +++ /dev/null @@ -1,67 +0,0 @@ -statement->items()->orderBy('id')->get(); - } - - public function headings(): array - { - return ['品名', '单价', '数量', '重量', '金额', '对账状态', '备注']; - } - - public function map($item): array - { - return [ - $item->product_name, - (float) $item->price, - (float) $item->quantity, - (float) $item->weight, - (float) $item->amount, - $item->is_reconciled ? '已对账' : '未对账', - $item->store_remark, - ]; - } - - public function styles(Worksheet $sheet): array - { - $sheet->freezePane('A2'); - - return [ - 1 => ['font' => ['bold' => true]], - ]; - } - - /** - * PDF 模板视图数据 - * - * @return array{statement: StatementModel, storeName: string, items: Collection} - */ - public function viewData(): array - { - return [ - 'statement' => $this->statement, - 'storeName' => $this->statement->store?->name ?? '', - 'items' => $this->collection(), - ]; - } -} diff --git a/app/Http/Controllers/Mini/BillController.php b/app/Http/Controllers/Mini/BillController.php new file mode 100644 index 0000000..dba827e --- /dev/null +++ b/app/Http/Controllers/Mini/BillController.php @@ -0,0 +1,72 @@ +currentUser($request); + $store = $this->ensureStoreBound($user); + + $data = BillModel::query() + ->where('store_id', $store->id) + ->with('purchase:id,purchase_no,purchase_date') + ->orderBy('bill_date', 'desc') + ->orderBy('id', 'desc') + ->paginate((int) $request->input('pageSize', 10)) + ->toArray(); + + return $this->success($data); + } + + /** 账单详情(校验归属:仅能查看本店账单;含合并后的商品明细与关联订单) */ + #[GetRoute('/bill/{id}', authorize: true, where: ['id' => '[0-9]+'])] + public function detail(int $id, Request $request): JsonResponse + { + $user = $this->currentUser($request); + $store = $this->ensureStoreBound($user); + + $bill = BillModel::with('purchase:id,purchase_no,purchase_date') + ->where('store_id', $store->id) + ->find($id); + if ($bill === null) { + throw new RepositoryException('账单不存在'); + } + + $orders = $bill->orders() + ->orderBy('id') + ->get(['id', 'order_no', 'order_date', 'total_quantity', 'total_weight', 'total_amount', 'status']) + ->toArray(); + + return $this->success([ + 'bill' => $bill->toArray(), + 'items' => app(BillDetailService::class)->mergedItems($bill), + 'orders' => $orders, + ]); + } + + /** + * 在线支付(预留接口,本次不实现;当前为线下收款,由后台手动登记) + */ + #[PostRoute('/bill/{id}/pay', authorize: true, where: ['id' => '[0-9]+'])] + public function pay(int $id, Request $request): JsonResponse + { + throw new RepositoryException('在线支付暂未开通,请线下付款后由商家登记收款'); + } +} diff --git a/app/Http/Controllers/Mini/OrderController.php b/app/Http/Controllers/Mini/OrderController.php index 7bc28bf..ddece83 100644 --- a/app/Http/Controllers/Mini/OrderController.php +++ b/app/Http/Controllers/Mini/OrderController.php @@ -108,7 +108,6 @@ class OrderController extends BaseMiniController 'order_date' => $now->toDateString(), 'total_quantity' => $totalQuantity, 'total_weight' => 0, - 'product_amount' => $totalAmount, 'total_amount' => $totalAmount, 'status' => StoreOrderModel::STATUS_PENDING, 'remark' => $remark, diff --git a/app/Http/Controllers/Mini/StatementController.php b/app/Http/Controllers/Mini/StatementController.php deleted file mode 100644 index 3d2c0d7..0000000 --- a/app/Http/Controllers/Mini/StatementController.php +++ /dev/null @@ -1,86 +0,0 @@ -currentUser($request); - $store = $this->ensureStoreBound($user); - - $data = StatementModel::query() - ->where('store_id', $store->id) - ->orderBy('id', 'desc') - ->paginate((int) $request->input('pageSize', 10)) - ->toArray(); - - return $this->success($data); - } - - /** 生成对账单:快照当前回款周期,settlement_date = period_end + cycle 天 */ - #[PostRoute('/statement/generate', authorize: true)] - public function generate(Request $request): JsonResponse - { - $data = $request->validate([ - 'period_start' => 'required|date_format:Y-m-d', - 'period_end' => 'required|date_format:Y-m-d|after_or_equal:period_start', - ], [ - 'period_start.required' => '请选择对账周期开始日期', - 'period_start.date_format' => '开始日期格式为 Y-m-d', - 'period_end.required' => '请选择对账周期结束日期', - 'period_end.date_format' => '结束日期格式为 Y-m-d', - 'period_end.after_or_equal' => '结束日期不能早于开始日期', - ]); - - $user = $this->currentUser($request); - $store = $this->ensureStoreBound($user); - - $statement = app(StatementGenerateService::class)->generate( - $store, - $data['period_start'], - $data['period_end'], - ); - - return $this->success([ - 'id' => $statement->id, - 'statement_no' => $statement->statement_no, - 'total_amount' => $statement->total_amount, - 'settlement_date' => $statement->settlement_date?->toDateString(), - ], '对账单已生成'); - } - - /** 对账单详情(校验归属,含单品对账状态标识) */ - #[GetRoute('/statement/{id}', authorize: true, where: ['id' => '[0-9]+'])] - public function detail(int $id, Request $request): JsonResponse - { - $user = $this->currentUser($request); - $store = $this->ensureStoreBound($user); - - $statement = StatementModel::with('items') - ->where('store_id', $store->id) - ->find($id); - if ($statement === null) { - throw new RepositoryException('对账单不存在'); - } - - return $this->success($statement->toArray()); - } -} diff --git a/app/Http/Controllers/Recon/BillController.php b/app/Http/Controllers/Recon/BillController.php new file mode 100644 index 0000000..c39a19f --- /dev/null +++ b/app/Http/Controllers/Recon/BillController.php @@ -0,0 +1,116 @@ + '=', + 'status' => '=', + 'bill_no' => 'like', + 'bill_date' => 'betweenDate', + ]; + + /** 账单列表 */ + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $query = BillModel::query()->with([ + 'store:id,name', + 'purchase:id,purchase_no,purchase_date', + 'operator:id,nickname', + ])->withCount('orders'); + + // 按采购单号搜索 + $purchaseNo = trim((string) ($params['purchase_no'] ?? '')); + if ($purchaseNo !== '') { + $keyword = '%' . str_replace('%', '\%', $purchaseNo) . '%'; + $query->whereHas('purchase', static function ($purchaseQuery) use ($keyword) { + $purchaseQuery->where('purchase_no', 'like', $keyword); + }); + } + + $data = $this->buildSearch($params, $query) + ->orderBy('bill_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 + { + $bill = BillModel::with([ + 'store:id,name,address,contact,phone', + 'purchase:id,purchase_no,purchase_date,status', + 'operator:id,nickname', + 'paidOperator:id,nickname', + ])->find($id); + if (empty($bill)) { + throw new RepositoryException('账单不存在'); + } + + $orders = $bill->orders() + ->orderBy('id') + ->get(['id', 'order_no', 'order_date', 'total_quantity', 'total_weight', 'total_amount', 'status']) + ->toArray(); + + return $this->success([ + 'bill' => $bill->toArray(), + 'items' => app(BillDetailService::class)->mergedItems($bill), + 'orders' => $orders, + ]); + } + + /** + * 确认收款:线下收款后手动登记付款信息,支付状态置为已支付 + */ + #[PutRoute(route: '/{id}/pay', authorize: 'pay', where: ['id' => '[0-9]+'])] + public function pay(int $id, Request $request): JsonResponse + { + $data = $request->validate([ + 'paid_at' => 'sometimes|date_format:Y-m-d H:i:s', + 'pay_remark' => 'nullable|string|max:255', + ], [ + 'paid_at.date_format' => '付款时间格式为 Y-m-d H:i:s', + 'pay_remark.max' => '付款备注超过最大长度', + ]); + + $bill = BillModel::find($id); + if (empty($bill)) { + throw new RepositoryException('账单不存在'); + } + if ($bill->status === BillModel::STATUS_PAID) { + throw new RepositoryException('账单已支付,请勿重复收款'); + } + + $bill->status = BillModel::STATUS_PAID; + $bill->paid_at = $data['paid_at'] ?? now(); + $bill->pay_remark = (string) ($data['pay_remark'] ?? ''); + $bill->paid_operator_id = (int) $request->user()->id; + $bill->save(); + + return $this->success([], '收款已登记'); + } +} diff --git a/app/Http/Controllers/Recon/ContainerReturnController.php b/app/Http/Controllers/Recon/ContainerReturnController.php new file mode 100644 index 0000000..9ae3bb9 --- /dev/null +++ b/app/Http/Controllers/Recon/ContainerReturnController.php @@ -0,0 +1,125 @@ + '=', + 'type' => '=', + 'return_date' => 'betweenDate', + ]; + + /** 回筐记录列表 */ + #[GetRoute(authorize: 'query')] + public function query(Request $request): JsonResponse + { + $params = $request->all(); + $pageSize = $params['pageSize'] ?? 10; + $data = $this->buildSearch($params, ContainerReturnModel::query()->with([ + 'store:id,name,pending_box_num,pending_tray_num', + 'bill:id,bill_no', + 'operator:id,nickname', + ])) + ->orderBy('return_date', 'desc') + ->orderBy('id', 'desc') + ->paginate($pageSize) + ->toArray(); + return $this->success($data); + } + + /** + * 回筐登记:门店退回周转筐/托盘,扣减门店待回数量(超回拒绝) + * @throws Throwable + */ + #[PostRoute(authorize: 'create')] + public function create(ContainerReturnFormRequest $request): JsonResponse + { + $data = $request->validated(); + $boxNum = (int) $data['box_num']; + $trayNum = (int) $data['tray_num']; + if ($boxNum === 0 && $trayNum === 0) { + throw new RepositoryException('周转筐与周转托盘数量不能同时为 0'); + } + + return DB::transaction(function () use ($data, $boxNum, $trayNum, $request) { + $store = StoreModel::query()->lockForUpdate()->find((int) $data['store_id']); + if (empty($store)) { + throw new RepositoryException('门店不存在'); + } + if ($boxNum > (int) $store->pending_box_num) { + throw new RepositoryException( + '回筐数量超过该门店待回筐数量(当前待回 ' . (int) $store->pending_box_num . ' 个)' + ); + } + if ($trayNum > (int) $store->pending_tray_num) { + throw new RepositoryException( + '回托盘数量超过该门店待回托盘数量(当前待回 ' . (int) $store->pending_tray_num . ' 个)' + ); + } + + $store->pending_box_num = (int) $store->pending_box_num - $boxNum; + $store->pending_tray_num = (int) $store->pending_tray_num - $trayNum; + $store->save(); + + ContainerReturnModel::create([ + 'store_id' => $store->id, + 'bill_id' => 0, + 'type' => ContainerReturnModel::TYPE_RETURN, + 'box_num' => $boxNum, + 'tray_num' => $trayNum, + 'return_date' => $data['return_date'], + 'operator_id' => (int) $request->user()->id, + 'remark' => (string) ($data['remark'] ?? ''), + ]); + + return $this->success([], '回筐已登记'); + }); + } + + /** + * 删除回筐记录(仅手动登记的回筐记录可删,删除后恢复门店待回数量;压筐记录随账单生成不允许删除) + * @throws Throwable + */ + #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] + public function delete(int $id): JsonResponse + { + $record = ContainerReturnModel::find($id); + if (empty($record)) { + throw new RepositoryException('回筐记录不存在'); + } + if ($record->type !== ContainerReturnModel::TYPE_RETURN) { + throw new RepositoryException('压筐记录由账单生成,不允许删除'); + } + + return DB::transaction(function () use ($record) { + $store = StoreModel::query()->lockForUpdate()->find($record->store_id); + if ($store !== null) { + $store->pending_box_num = (int) $store->pending_box_num + (int) $record->box_num; + $store->pending_tray_num = (int) $store->pending_tray_num + (int) $record->tray_num; + $store->save(); + } + $record->delete(); + + return $this->success([], '回筐记录已删除,门店待回数量已恢复'); + }); + } +} diff --git a/app/Http/Controllers/Recon/StatementController.php b/app/Http/Controllers/Recon/StatementController.php deleted file mode 100644 index 281a567..0000000 --- a/app/Http/Controllers/Recon/StatementController.php +++ /dev/null @@ -1,49 +0,0 @@ - 'like', - 'store_id' => '=', - 'status' => '=', - 'period_start' => 'betweenDate', - ]; - - /** 对账单列表 */ - #[GetRoute(authorize: 'query')] - public function query(Request $request): JsonResponse - { - $params = $request->all(); - $pageSize = $params['pageSize'] ?? 10; - $data = $this->buildSearch($params, StatementModel::query()->with('store:id,name')) - ->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 - { - $statement = StatementModel::with(['store:id,name', 'items'])->find($id); - if (empty($statement)) { - throw new RepositoryException('对账单不存在'); - } - return $this->success($statement->toArray()); - } -} diff --git a/app/Http/Requests/Recon/ContainerReturnFormRequest.php b/app/Http/Requests/Recon/ContainerReturnFormRequest.php new file mode 100644 index 0000000..e25c492 --- /dev/null +++ b/app/Http/Requests/Recon/ContainerReturnFormRequest.php @@ -0,0 +1,41 @@ + 'required|integer|exists:store,id', + 'box_num' => 'required|integer|min:0', + 'tray_num' => 'required|integer|min:0', + 'return_date' => 'required|date_format:Y-m-d', + 'remark' => 'nullable|string|max:255', + ]; + } + + public function messages(): array + { + return [ + 'store_id.required' => '请选择门店', + 'store_id.exists' => '门店不存在', + 'box_num.required' => '周转筐数量不能为空', + 'box_num.integer' => '周转筐数量必须为整数', + 'box_num.min' => '周转筐数量不能小于 0', + 'tray_num.required' => '周转托盘数量不能为空', + 'tray_num.integer' => '周转托盘数量必须为整数', + 'tray_num.min' => '周转托盘数量不能小于 0', + 'return_date.required' => '请选择退回日期', + 'return_date.date_format' => '退回日期格式为 Y-m-d', + 'remark.max' => '备注超过最大长度', + ]; + } +} diff --git a/app/Models/BillModel.php b/app/Models/BillModel.php index 7783c92..10db8b7 100644 --- a/app/Models/BillModel.php +++ b/app/Models/BillModel.php @@ -15,6 +15,17 @@ class BillModel extends Model { use HasFactory; + /** 支付状态:未支付 */ + public const int STATUS_UNPAID = 0; + /** 支付状态:已支付 */ + public const int STATUS_PAID = 1; + + /** 支付状态中文名 */ + public const array STATUS_NAMES = [ + self::STATUS_UNPAID => '未支付', + self::STATUS_PAID => '已支付', + ]; + protected $table = 'bill'; protected $primaryKey = 'id'; @@ -31,6 +42,10 @@ class BillModel extends Model 'tray_price', 'added_amount', 'total_amount', + 'status', + 'paid_at', + 'pay_remark', + 'paid_operator_id', 'operator_id', 'remark', ]; @@ -47,6 +62,9 @@ class BillModel extends Model 'tray_price' => 'decimal:2', 'added_amount' => 'decimal:2', 'total_amount' => 'decimal:2', + 'status' => 'integer', + 'paid_at' => 'datetime:Y-m-d H:i:s', + 'paid_operator_id' => 'integer', 'operator_id' => 'integer', 'created_at' => 'datetime:Y-m-d H:i:s', ]; @@ -75,6 +93,14 @@ class BillModel extends Model return $this->belongsTo(SysUserModel::class, 'operator_id', 'id'); } + /** + * 收款操作人(后台系统用户,线下收款登记) + */ + public function paidOperator(): BelongsTo + { + return $this->belongsTo(SysUserModel::class, 'paid_operator_id', 'id'); + } + /** * 本账单关联的门店订单 */ @@ -82,4 +108,12 @@ class BillModel extends Model { return $this->hasMany(StoreOrderModel::class, 'bill_id', 'id'); } + + /** + * 本账单关联的门店订单明细 + */ + public function items(): HasMany + { + return $this->hasMany(StoreOrderItemModel::class, 'bill_id', 'id'); + } } diff --git a/app/Models/ContainerReturnModel.php b/app/Models/ContainerReturnModel.php new file mode 100644 index 0000000..5d98ef5 --- /dev/null +++ b/app/Models/ContainerReturnModel.php @@ -0,0 +1,76 @@ + '压筐', + self::TYPE_RETURN => '回筐', + ]; + + protected $table = 'container_return'; + protected $primaryKey = 'id'; + + protected $fillable = [ + 'store_id', + 'bill_id', + 'type', + 'box_num', + 'tray_num', + 'return_date', + 'operator_id', + 'remark', + ]; + + protected $casts = [ + 'store_id' => 'integer', + 'bill_id' => 'integer', + 'type' => 'integer', + 'box_num' => 'integer', + 'tray_num' => 'integer', + 'return_date' => 'date:Y-m-d', + 'operator_id' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + ]; + + /** + * 所属门店(含软删除门店,保证历史记录可见) + */ + public function store(): BelongsTo + { + return $this->belongsTo(StoreModel::class, 'store_id', 'id')->withTrashed(); + } + + /** + * 关联账单(压筐记录) + */ + public function bill(): BelongsTo + { + return $this->belongsTo(BillModel::class, 'bill_id', 'id'); + } + + /** + * 操作人(后台系统用户) + */ + public function operator(): BelongsTo + { + return $this->belongsTo(SysUserModel::class, 'operator_id', 'id'); + } +} diff --git a/app/Models/StatementItemModel.php b/app/Models/StatementItemModel.php deleted file mode 100644 index db9d21f..0000000 --- a/app/Models/StatementItemModel.php +++ /dev/null @@ -1,78 +0,0 @@ - 'integer', - 'order_id' => 'integer', - 'order_item_id' => 'integer', - 'product_id' => 'integer', - 'price' => 'decimal:2', - 'quantity' => 'decimal:2', - 'weight' => 'decimal:3', - 'amount' => 'decimal:2', - 'is_reconciled' => 'integer', - ]; - - /** - * 所属对账单 - */ - public function statement(): BelongsTo - { - return $this->belongsTo(StatementModel::class, 'statement_id', 'id'); - } - - /** - * 来源订单 - */ - public function order(): BelongsTo - { - return $this->belongsTo(StoreOrderModel::class, 'order_id', 'id'); - } - - /** - * 来源订单明细 - */ - public function orderItem(): BelongsTo - { - return $this->belongsTo(StoreOrderItemModel::class, 'order_item_id', 'id'); - } - - /** - * 对账商品 - */ - public function product(): BelongsTo - { - return $this->belongsTo(ProductModel::class, 'product_id', 'id'); - } -} diff --git a/app/Models/StatementModel.php b/app/Models/StatementModel.php deleted file mode 100644 index a5d8acf..0000000 --- a/app/Models/StatementModel.php +++ /dev/null @@ -1,65 +0,0 @@ - 'integer', - 'period_start' => 'date:Y-m-d', - 'period_end' => 'date:Y-m-d', - 'total_amount' => 'decimal:2', - 'payment_cycle_days' => 'integer', - 'settlement_date' => 'date:Y-m-d', - 'status' => 'integer', - 'reconciled_at' => 'datetime', - 'settled_at' => 'datetime', - ]; - - /** - * 所属门店 - */ - public function store(): BelongsTo - { - return $this->belongsTo(StoreModel::class, 'store_id', 'id'); - } - - /** - * 对账单明细 - */ - public function items(): HasMany - { - return $this->hasMany(StatementItemModel::class, 'statement_id', 'id'); - } -} diff --git a/app/Models/StoreModel.php b/app/Models/StoreModel.php index f24b298..3d4ea32 100644 --- a/app/Models/StoreModel.php +++ b/app/Models/StoreModel.php @@ -38,6 +38,8 @@ class StoreModel extends Model protected $casts = [ 'level_id' => 'integer', 'payment_cycle_days' => 'integer', + 'pending_box_num' => 'integer', + 'pending_tray_num' => 'integer', 'status' => 'integer', ]; @@ -66,10 +68,10 @@ class StoreModel extends Model } /** - * 门店对账单 + * 门店账单(采购单完成后按门店生成) */ - public function statements(): HasMany + public function bills(): HasMany { - return $this->hasMany(StatementModel::class, 'store_id', 'id'); + return $this->hasMany(BillModel::class, 'store_id', 'id'); } } diff --git a/app/Models/StoreOrderItemModel.php b/app/Models/StoreOrderItemModel.php index 442fe1c..70f40ec 100644 --- a/app/Models/StoreOrderItemModel.php +++ b/app/Models/StoreOrderItemModel.php @@ -21,6 +21,7 @@ class StoreOrderItemModel extends Model protected $fillable = [ 'order_id', 'purchase_id', + 'bill_id', 'store_id', 'product_id', 'category_id', @@ -42,6 +43,7 @@ class StoreOrderItemModel extends Model protected $casts = [ 'order_id' => 'integer', 'purchase_id' => 'integer', + 'bill_id' => 'integer', 'store_id' => 'integer', 'product_id' => 'integer', 'category_id' => 'integer', diff --git a/app/Models/StoreOrderModel.php b/app/Models/StoreOrderModel.php index a2aa379..8da6ae9 100644 --- a/app/Models/StoreOrderModel.php +++ b/app/Models/StoreOrderModel.php @@ -57,7 +57,6 @@ class StoreOrderModel extends Model protected $casts = [ 'store_id' => 'integer', 'purchase_id' => 'integer', - 'statement_id' => 'integer', 'bill_id' => 'integer', 'order_date' => 'date:Y-m-d', 'total_quantity' => 'integer', diff --git a/app/Services/BillDetailService.php b/app/Services/BillDetailService.php new file mode 100644 index 0000000..f5fea04 --- /dev/null +++ b/app/Services/BillDetailService.php @@ -0,0 +1,71 @@ + + */ + public function mergedItems(BillModel $bill): array + { + $items = StoreOrderItemModel::query() + ->where('bill_id', $bill->id) + ->orderBy('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; + $weight = '0'; + $amount = '0'; + foreach ($group as $item) { + $quantity += (int) $item->quantity; + $weight = bcadd($weight, (string) $item->weight, 3); + $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, + 'weight' => $weight, + 'amount' => $amount, + '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 array_map(static function (array $row): array { + unset($row['category_sort'], $row['product_sort']); + return $row; + }, $rows); + } +} diff --git a/app/Services/BillGenerateService.php b/app/Services/BillGenerateService.php index 0de80fd..5454b66 100644 --- a/app/Services/BillGenerateService.php +++ b/app/Services/BillGenerateService.php @@ -4,8 +4,10 @@ namespace App\Services; use App\Exceptions\RepositoryException; use App\Models\BillModel; +use App\Models\ContainerReturnModel; use App\Models\PurchaseOrderModel; use App\Models\StoreModel; +use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; use Illuminate\Support\Facades\DB; use Throwable; @@ -109,10 +111,30 @@ readonly class BillGenerateService 'operator_id' => $operatorId, ]); - // 关联该门店在采购单中的全部订单到账单 + // 关联该门店在采购单中的全部订单与订单明细到账单 StoreOrderModel::query() ->whereIn('id', $storeOrders->pluck('id')) ->update(['bill_id' => $bill->id]); + StoreOrderItemModel::query() + ->whereIn('order_id', $storeOrders->pluck('id')) + ->update(['bill_id' => $bill->id]); + + // 压筐:累加门店待回筐/托盘(行锁防并发),并写入压筐记录 + $store = StoreModel::query()->lockForUpdate()->find((int) $storeId); + if ($store !== null) { + $store->pending_box_num = (int) $store->pending_box_num + (int) $row['box_num']; + $store->pending_tray_num = (int) $store->pending_tray_num + (int) $row['tray_num']; + $store->save(); + } + ContainerReturnModel::create([ + 'store_id' => (int) $storeId, + 'bill_id' => $bill->id, + 'type' => ContainerReturnModel::TYPE_PRESS, + 'box_num' => (int) $row['box_num'], + 'tray_num' => (int) $row['tray_num'], + 'return_date' => $billDate, + 'operator_id' => $operatorId, + ]); $bills[] = $bill; } diff --git a/app/Services/BillNumberService.php b/app/Services/BillNumberService.php index aa93e39..7b0c30b 100644 --- a/app/Services/BillNumberService.php +++ b/app/Services/BillNumberService.php @@ -25,7 +25,6 @@ class BillNumberService 'PO' => ['purchase_order', 'purchase_no'], 'SO' => ['store_order', 'order_no'], 'RC' => ['reconciliation', 'recon_no'], - 'ST' => ['statement', 'statement_no'], 'JS' => ['settlement', 'settlement_no'], 'ZD' => ['bill', 'bill_no'], ]; @@ -33,7 +32,7 @@ class BillNumberService /** * 生成业务单号 * - * @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / ST 对账单 / JS 结算 / ZD 账单 + * @param string $prefix 业务前缀:PO 采购单 / SO 订货单 / RC 对账 / JS 结算 / ZD 账单 * @return string 如 PO202607230001 */ public function make(string $prefix): string diff --git a/app/Services/StatementGenerateService.php b/app/Services/StatementGenerateService.php deleted file mode 100644 index a20d268..0000000 --- a/app/Services/StatementGenerateService.php +++ /dev/null @@ -1,101 +0,0 @@ -join('store_order', 'store_order.id', '=', 'store_order_item.order_id') - ->where('store_order_item.store_id', $store->id) - ->whereDate('store_order.order_date', '>=', $periodStart) - ->whereDate('store_order.order_date', '<=', $periodEnd) - ->where('store_order.status', '<>', StoreOrderModel::STATUS_CANCELLED) - ->whereNull('store_order.deleted_at') - ->select('store_order_item.*') - ->get(); - - if ($orderItems->isEmpty()) { - throw new RepositoryException('周期内本店无订单数据,无法生成对账单'); - } - - // 防重复入账:剔除已计入过对账单的订单明细 - $usedItemIds = StatementItemModel::query() - ->whereIn('statement_id', StatementModel::where('store_id', $store->id)->pluck('id')) - ->pluck('order_item_id'); - $orderItems = $orderItems->reject(fn ($item) => $usedItemIds->contains($item->id)); - if ($orderItems->isEmpty()) { - throw new RepositoryException('周期内的订单明细均已生成过对账单'); - } - - // 快照回款周期 → 应结算日期 - $cycleDays = (int) $store->payment_cycle_days; - $totalAmount = $orderItems->reduce( - static fn (string $carry, $item): string => bcadd($carry, (string) $item->amount, 2), - '0' - ); - - $statement = StatementModel::create([ - 'statement_no' => $this->billNumberService->make('ST'), - 'store_id' => $store->id, - 'period_start' => $periodStart, - 'period_end' => $periodEnd, - 'total_amount' => $totalAmount, - 'payment_cycle_days' => $cycleDays, - 'settlement_date' => Carbon::parse($periodEnd)->addDays($cycleDays)->toDateString(), - 'status' => StatementModel::STATUS_PENDING, - ]); - - $rows = []; - $now = now(); - foreach ($orderItems as $item) { - $rows[] = [ - 'statement_id' => $statement->id, - 'order_id' => $item->order_id, - 'order_item_id' => $item->id, - 'product_id' => $item->product_id, - 'product_name' => $item->product_name, - 'price' => $item->price, - 'quantity' => $item->quantity, - 'weight' => $item->weight, - 'amount' => $item->amount, - 'is_reconciled' => StatementItemModel::NOT_RECONCILED, - 'store_remark' => '', - 'created_at' => $now, - 'updated_at' => $now, - ]; - } - StatementItemModel::insert($rows); - - return $statement; - }); - } -} diff --git a/database/migrations/2026_07_23_030608_create_store_table.php b/database/migrations/2026_07_23_030608_create_store_table.php index 5d7b3c1..dd551d9 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -60,6 +60,8 @@ return new class extends Migration $table->string('phone', 20)->default('')->comment('联系电话'); $table->string('address', 255)->default('')->comment('门店地址'); $table->integer('payment_cycle_days')->default(0)->comment('回款周期(天)'); + $table->integer('pending_box_num')->default(0)->comment('待回筐数量(生成账单压筐累加,回筐登记扣减)'); + $table->integer('pending_tray_num')->default(0)->comment('待回托盘数量'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); $table->string('remark', 255)->nullable()->default('')->comment('备注'); $table->timestamps(); diff --git a/database/migrations/2026_07_23_030610_create_store_order_table.php b/database/migrations/2026_07_23_030610_create_store_order_table.php index e718ba5..6db2a30 100644 --- a/database/migrations/2026_07_23_030610_create_store_order_table.php +++ b/database/migrations/2026_07_23_030610_create_store_order_table.php @@ -41,6 +41,7 @@ return new class extends Migration $table->integer('order_id')->comment('订单ID'); $table->integer('store_id')->comment('门店ID'); $table->integer('purchase_id')->default(0)->comment('归属采购单ID(0=未归集)'); + $table->integer('bill_id')->default(0)->comment('归属账单ID(0=未出账)'); $table->integer('product_id')->comment('商品ID'); $table->integer('category_id')->default(0)->comment('分类ID'); $table->integer('supplier_id')->default(0)->comment('供应商ID'); @@ -59,6 +60,7 @@ return new class extends Migration $table->timestamps(); $table->index(['order_id'], 'store_order_item_order_index'); $table->index(['purchase_id'], 'store_order_item_purchase_index'); + $table->index(['bill_id'], 'store_order_item_bill_index'); $table->index(['store_id', 'product_id'], 'store_order_item_store_product_index'); $table->comment('门店订货明细表'); }); diff --git a/database/migrations/2026_07_23_030612_create_reconciliation_table.php b/database/migrations/2026_07_23_030612_create_reconciliation_table.php index bc9c2d6..8ffd562 100644 --- a/database/migrations/2026_07_23_030612_create_reconciliation_table.php +++ b/database/migrations/2026_07_23_030612_create_reconciliation_table.php @@ -8,7 +8,7 @@ return new class extends Migration { /** * Run the migrations. - * 对账管理(D1-D10):财务对账、门店对账单、结算表 + * 财务管理(D1-D10):财务对账、结算表(门店对账单已下线,由采购单账单 bill 表替代) */ public function up(): void { @@ -58,48 +58,6 @@ return new class extends Migration }); } - // 门店对账单表(门店在小程序端自助生成,回款周期快照决定应结算日期) - if (! Schema::hasTable('statement')) { - Schema::create('statement', function (Blueprint $table) { - $table->increments('id')->comment('对账单ID'); - $table->string('statement_no', 32)->unique()->comment('对账单编号'); - $table->integer('store_id')->comment('门店ID'); - $table->date('period_start')->comment('对账周期开始'); - $table->date('period_end')->comment('对账周期结束'); - $table->decimal('total_amount', 10, 2)->default(0)->comment('对账总金额'); - $table->integer('payment_cycle_days')->default(0)->comment('回款周期(天),生成时从门店快照'); - $table->date('settlement_date')->nullable()->comment('应结算日期(按回款周期计算)'); - $table->integer('status')->default(0)->comment('状态(0未对账 1已对账 2已结算)'); - $table->timestamp('reconciled_at')->nullable()->comment('对账完成时间'); - $table->timestamp('settled_at')->nullable()->comment('结算时间'); - $table->string('remark', 255)->default('')->comment('备注'); - $table->timestamps(); - $table->index(['store_id', 'period_start'], 'statement_store_period_index'); - $table->comment('门店对账单表'); - }); - } - - // 门店对账单明细表(每个单品/订单的对账状态标识) - if (! Schema::hasTable('statement_item')) { - Schema::create('statement_item', function (Blueprint $table) { - $table->increments('id')->comment('明细ID'); - $table->integer('statement_id')->comment('对账单ID'); - $table->integer('order_id')->comment('订单ID'); - $table->integer('order_item_id')->comment('订货明细ID'); - $table->integer('product_id')->comment('商品ID'); - $table->string('product_name', 100)->comment('品名(快照)'); - $table->decimal('price', 10, 2)->default(0)->comment('单价'); - $table->decimal('quantity', 10, 2)->default(0)->comment('订货量'); - $table->decimal('weight', 10, 3)->default(0)->comment('重量'); - $table->decimal('amount', 10, 2)->default(0)->comment('单品金额'); - $table->integer('is_reconciled')->default(0)->comment('对账状态(1已对账 0未对账)'); - $table->string('store_remark', 255)->default('')->comment('门店备注'); - $table->timestamps(); - $table->index(['statement_id'], 'statement_item_statement_index'); - $table->comment('门店对账单明细表'); - }); - } - // 结算表(D9 对账结束后生成结算表/回框统计表,D10 下载存档) if (! Schema::hasTable('settlement')) { Schema::create('settlement', function (Blueprint $table) { @@ -131,8 +89,6 @@ return new class extends Migration { Schema::dropIfExists('reconciliation'); Schema::dropIfExists('reconciliation_item'); - Schema::dropIfExists('statement'); - Schema::dropIfExists('statement_item'); Schema::dropIfExists('settlement'); } }; diff --git a/database/migrations/2026_08_13_000001_create_bill_table.php b/database/migrations/2026_08_13_000001_create_bill_table.php index 85db8d1..f17dcc8 100644 --- a/database/migrations/2026_08_13_000001_create_bill_table.php +++ b/database/migrations/2026_08_13_000001_create_bill_table.php @@ -29,11 +29,16 @@ return new class extends Migration $table->decimal('tray_price', 10, 2)->default(0)->comment('周转托盘单价(生成时快照)'); $table->decimal('added_amount', 10, 2)->default(0)->comment('附加金额(周转筐/托盘金额)'); $table->decimal('total_amount', 10, 2)->default(0)->comment('账单总金额 = 商品金额 + 配送费 + 附加金额'); + $table->integer('status')->default(0)->comment('支付状态(0未支付 1已支付)'); + $table->timestamp('paid_at')->nullable()->comment('付款时间(线下收款手动登记)'); + $table->string('pay_remark', 255)->default('')->comment('付款备注(线下收款信息)'); + $table->integer('paid_operator_id')->default(0)->comment('收款操作人(后台系统用户ID)'); $table->integer('operator_id')->default(0)->comment('生成人(后台系统用户ID)'); $table->string('remark', 255)->default('')->comment('备注'); $table->timestamps(); $table->unique(['purchase_id', 'store_id'], 'bill_purchase_store_unique'); $table->index(['store_id', 'bill_date'], 'bill_store_date_index'); + $table->index(['status'], 'bill_status_index'); $table->comment('门店账单表(采购单完成后按门店生成)'); }); } diff --git a/database/migrations/2026_08_14_000001_create_container_return_table.php b/database/migrations/2026_08_14_000001_create_container_return_table.php new file mode 100644 index 0000000..3e7c760 --- /dev/null +++ b/database/migrations/2026_08_14_000001_create_container_return_table.php @@ -0,0 +1,41 @@ +increments('id')->comment('记录ID'); + $table->integer('store_id')->comment('门店ID'); + $table->integer('bill_id')->default(0)->comment('关联账单ID(0=手动回筐登记)'); + $table->integer('type')->comment('类型(1压筐-账单生成 2回筐-退回登记)'); + $table->integer('box_num')->default(0)->comment('周转筐数量'); + $table->integer('tray_num')->default(0)->comment('周转托盘数量'); + $table->date('return_date')->comment('记录日期(压筐=账单日期,回筐=退回日期)'); + $table->integer('operator_id')->default(0)->comment('操作人(后台系统用户ID)'); + $table->string('remark', 255)->default('')->comment('备注'); + $table->timestamps(); + $table->index(['store_id', 'type'], 'container_return_store_type_index'); + $table->index(['bill_id'], 'container_return_bill_index'); + $table->comment('门店回筐记录表(压筐=生成账单压出,回筐=门店退回登记)'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('container_return'); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 91aeaf9..27028ba 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -91,6 +91,18 @@ class PermissionSeeder extends Seeder ['type' => 'rule', 'key' => 'product.goods.batchPrice', 'name' => '批量调价'], ], ], + [ + 'type' => 'route', + 'key' => 'customer.supplier', + 'name' => '供应商', + 'path' => '/customer/supplier', + 'children' => [ + ['type' => 'rule', 'key' => 'customer.supplier.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'customer.supplier.create', 'name' => '新增'], + ['type' => 'rule', 'key' => 'customer.supplier.update', 'name' => '编辑'], + ['type' => 'rule', 'key' => 'customer.supplier.delete', 'name' => '删除'], + ], + ], ], ], [ @@ -123,18 +135,6 @@ class PermissionSeeder extends Seeder ['type' => 'rule', 'key' => 'customer.level.delete', 'name' => '删除'], ], ], - [ - 'type' => 'route', - 'key' => 'customer.supplier', - 'name' => '供应商', - 'path' => '/customer/supplier', - 'children' => [ - ['type' => 'rule', 'key' => 'customer.supplier.query', 'name' => '查询'], - ['type' => 'rule', 'key' => 'customer.supplier.create', 'name' => '新增'], - ['type' => 'rule', 'key' => 'customer.supplier.update', 'name' => '编辑'], - ['type' => 'rule', 'key' => 'customer.supplier.delete', 'name' => '删除'], - ], - ], [ 'type' => 'route', 'key' => 'customer.miniUser', @@ -202,7 +202,7 @@ class PermissionSeeder extends Seeder [ 'type' => 'menu', 'key' => 'procurement.recon', - 'name' => '对账管理', + 'name' => '财务管理', 'icon' => 'AccountBookOutlined', 'children' => [ [ @@ -223,11 +223,23 @@ class PermissionSeeder extends Seeder ], [ 'type' => 'route', - 'key' => 'recon.statement', - 'name' => '门店对账单', - 'path' => '/recon/statement', + 'key' => 'recon.bill', + 'name' => '门店账单', + 'path' => '/recon/bill', 'children' => [ - ['type' => 'rule', 'key' => 'recon.statement.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'recon.bill.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'recon.bill.pay', 'name' => '确认收款'], + ], + ], + [ + 'type' => 'route', + 'key' => 'recon.containerReturn', + 'name' => '回筐记录', + 'path' => '/recon/container-return', + 'children' => [ + ['type' => 'rule', 'key' => 'recon.containerReturn.query', 'name' => '查询'], + ['type' => 'rule', 'key' => 'recon.containerReturn.create', 'name' => '回筐登记'], + ['type' => 'rule', 'key' => 'recon.containerReturn.delete', 'name' => '删除'], ], ], [ diff --git a/resources/views/exports/statement.blade.php b/resources/views/exports/statement.blade.php deleted file mode 100644 index 88d1546..0000000 --- a/resources/views/exports/statement.blade.php +++ /dev/null @@ -1,58 +0,0 @@ - - -
- - - - -| 品名 | -单价 | -数量 | -重量 | -金额 | -对账状态 | -备注 | -
|---|---|---|---|---|---|---|
| {{ $item->product_name }} | -{{ $item->price }} | -{{ $item->quantity }} | -{{ $item->weight }} | -{{ $item->amount }} | -{{ $item->is_reconciled ? '已对账' : '未对账' }} | -{{ $item->store_remark }} | -
| 合计金额 | -¥{{ $statement->total_amount }} | -- | ||||