From 4f75bc40b9b63e51727b4a7755ee622ab20aa4a8 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 12 Aug 2026 20:56:43 +0800 Subject: [PATCH] docs: add plan for allowing negative invoice balance on reimburse submit Co-authored-by: Cursor --- ...eimburse-allow-negative-invoice-balance.md | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-12-reimburse-allow-negative-invoice-balance.md diff --git a/docs/superpowers/plans/2026-08-12-reimburse-allow-negative-invoice-balance.md b/docs/superpowers/plans/2026-08-12-reimburse-allow-negative-invoice-balance.md new file mode 100644 index 000000000..2b922d3a4 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-reimburse-allow-negative-invoice-balance.md @@ -0,0 +1,339 @@ +# 报销提交允许发票夹余额为负 — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 去掉报销提交时「发票夹余额不足不可提交」的硬门槛;提交仍全额写 `debit_reimburse`,允许余额为负;Web 与 bx-mobile 改为弱提示。 + +**Architecture:** 后端仅删除 `debit_reimburse` 内余额不足抛错;前端删除提交前拦截与「将被拦截/无法提交」文案,保留余额对照与弱提示。扣/退时机、删票保护、幂等逻辑不变。 + +**Tech Stack:** FastAPI + SQLAlchemy;Vue 3(web-admin Element Plus / bx-mobile Vant);pytest + +**Spec:** `docs/superpowers/specs/2026-08-12-reimburse-allow-negative-invoice-balance-design.md` + +--- + +## 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| Modify | `backend/app/services/bx/invoice_ledger.py` | `debit_reimburse` 去掉余额不足拦截 | +| Modify | `backend/tests/test_bx_invoice_ledger.py` | 补充「占用可超总额 → 余额为负」单测 | +| Modify | `web-admin/src/views/bx/reimburse/apply/index.vue` | 弱提示 + 去掉提交拦截 | +| Modify | `bx-mobile/src/views/reimburse/ReimburseApplyPage.vue` | 同上 | +| Modify | `docs/business-inventory/03-报销业务明细.md` | 额度规则与流程图文案 | + +**不变:** `assert_can_delete_invoice`、`release_reimburse`、submit 幂等、关联发票可选。 + +--- + +### Task 1: 后端 — 允许超额占用 + +**Files:** +- Modify: `backend/tests/test_bx_invoice_ledger.py` +- Modify: `backend/app/services/bx/invoice_ledger.py`(约 L226–232) + +- [ ] **Step 1: 写失败/文档化用例 — 余额可为负** + +在 `backend/tests/test_bx_invoice_ledger.py` 末尾追加: + +```python +def test_summarize_allows_negative_balance(): + """提交占用可超过已上传总额,余额可为负(补票后再回升)。""" + rows = [ + {"entry_type": ENTRY_CREDIT_UPLOAD, "amount": Decimal("100")}, + {"entry_type": ENTRY_DEBIT_REIMBURSE, "amount": Decimal("300")}, + ] + s = summarize_entries(rows) + assert s["total_amount"] == Decimal("100") + assert s["used_amount"] == Decimal("300") + assert s["balance"] == Decimal("-200") +``` + +- [ ] **Step 2: 跑单测确认通过(公式本就可为负)** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_bx_invoice_ledger.py::test_summarize_allows_negative_balance -v +``` + +Expected: PASS + +- [ ] **Step 3: 去掉 `debit_reimburse` 余额不足抛错** + +在 `backend/app/services/bx/invoice_ledger.py` 的 `debit_reimburse` 中,删除余额校验与未再使用的局部变量。将: + +```python + totals = InvoiceLedgerService.calc_summary(db, tenant_no, owner_user_id) + balance = totals["balance"] + if balance < amt: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"发票额度不足(余额 {balance},本次 {amt})", + ) + entry = InvoiceLedgerService.add_entry( +``` + +改为直接: + +```python + entry = InvoiceLedgerService.add_entry( +``` + +保留:`amt <= 0` 校验、`_lock_summary`、`add_entry`、`refresh_summary`。 + +- [ ] **Step 4: 跑完整 ledger 单测** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_bx_invoice_ledger.py -v +``` + +Expected: 全部 PASS(含既有 3 个 + 新增 1 个) + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/services/bx/invoice_ledger.py backend/tests/test_bx_invoice_ledger.py +git commit -m "$(cat <<'EOF' +fix(bx): allow reimburse submit when invoice wallet balance is insufficient + +Drop the hard balance check in debit_reimburse so occupancy can exceed uploaded invoices; balance may go negative until later top-ups. +EOF +)" +``` + +--- + +### Task 2: Web 报销申请 — 弱提示、不拦截 + +**Files:** +- Modify: `web-admin/src/views/bx/reimburse/apply/index.vue` + +- [ ] **Step 1: 改弹窗余额弱提示文案** + +将模板中: + +```vue + + (余额不足,提交时将被拦截) + +``` + +改为: + +```vue + + (余额不足,提交后余额将为负,请后续补发票) + +``` + +- [ ] **Step 2: 去掉工具栏批量提交的余额拦截** + +在 `handleToolbarSubmit` 中删除整段「按申请人分别校验发票余额」逻辑(约从注释 `// 按申请人分别校验发票余额` 到 `for` 循环结束的 `return`),保留其后的 `loadingMap.submit = true` 与 `reimburseApi.submit` 调用。 + +删除后,`validTargets` 校验与 skip 提示之后应直接进入: + +```typescript + loadingMap.submit = true + try { + const results = await Promise.allSettled(validTargets.map((row) => reimburseApi.submit(row.id))) +``` + +若删除后 `refreshWalletBalance` 仅仍被弹窗打开/换申请人使用,保留该函数;不要误删 `refreshWalletBalance` 定义。 + +- [ ] **Step 3: 去掉行内提交的余额拦截** + +将 `handleSubmit` 从: + +```typescript +async function handleSubmit(row: any) { + await refreshWalletBalance(String(row.applicantId || currentUserId())) + if (Number(walletBalance.value) < Number(row.amount || 0)) { + ElMessage.warning( + `发票额度不足(余额 ${formatMoneyYuan(walletBalance.value)},本次 ${formatMoneyYuan(row.amount)})` + ) + return + } + loadingMap[`submit_${row.id}`] = true +``` + +改为: + +```typescript +async function handleSubmit(row: any) { + loadingMap[`submit_${row.id}`] = true +``` + +- [ ] **Step 4: 静态检查 — 无残留拦截文案** + +```bash +rg -n '发票额度不足|提交时将被拦截' web-admin/src/views/bx/reimburse/apply/index.vue +``` + +Expected: 无匹配(弱提示「提交后余额将为负」可存在) + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/views/bx/reimburse/apply/index.vue +git commit -m "$(cat <<'EOF' +fix(web-admin): allow reimburse submit with insufficient invoice balance + +Replace blocking tip with a soft warning and remove client-side submit guards. +EOF +)" +``` + +--- + +### Task 3: bx-mobile 报销申请 — 对齐 Web + +**Files:** +- Modify: `bx-mobile/src/views/reimburse/ReimburseApplyPage.vue` + +- [ ] **Step 1: 改余额区弱提示** + +将: + +```vue + (余额不足,无法提交) +``` + +改为: + +```vue + (余额不足,提交后余额将为负,请后续补发票) +``` + +保留 `balanceInsufficient` computed(仅用于展示弱提示)。 + +- [ ] **Step 2: 保存成功对话框 — 统一草稿文案** + +将: + +```typescript + message: balanceInsufficient.value + ? '已保存为草稿。发票余额不足,请先充值发票夹后再提交。' + : '报销单已保存为草稿', +``` + +改为: + +```typescript + message: '报销单已保存为草稿', +``` + +- [ ] **Step 3: 确认「提交审批」时不再因余额 return** + +将 `.then(() => { ... })` 内: + +```typescript + if (balanceInsufficient.value) { + showToast('余额不足,无法提交') + return + } + reimburseApi + .submit(id, invoicePayload) +``` + +改为直接调用 submit(无余额分支): + +```typescript + reimburseApi + .submit(id, invoicePayload) +``` + +- [ ] **Step 4: 静态检查** + +```bash +rg -n '无法提交|请先充值发票夹|余额不足,无法提交' bx-mobile/src/views/reimburse/ReimburseApplyPage.vue +``` + +Expected: 无匹配 + +- [ ] **Step 5: Commit** + +```bash +git add bx-mobile/src/views/reimburse/ReimburseApplyPage.vue +git commit -m "$(cat <<'EOF' +fix(bx-mobile): allow reimburse submit with insufficient invoice balance + +Align soft tip and submit flow with web-admin; no longer block on wallet balance. +EOF +)" +``` + +--- + +### Task 4: 业务清单文档 + +**Files:** +- Modify: `docs/business-inventory/03-报销业务明细.md` + +- [ ] **Step 1: 更新额度规则段(约 L128)** + +将: + +```markdown +**额度规则**:提交时按报销合计扣减本人发票余额;余额不足拒绝提交;审批驳回或本人撤回时退回额度。发票上传在「我的发票夹 / 发票夹管理」,申请单不再本地上传发票。 +``` + +改为: + +```markdown +**额度规则**:提交时按报销合计扣减本人发票余额(全额占用,余额允许为负;不足不拦截,可后续在发票夹补票回升);审批驳回或本人撤回时退回额度。发票上传在「我的发票夹 / 发票夹管理」,申请单不再本地上传发票。详见 `docs/superpowers/specs/2026-08-12-reimburse-allow-negative-invoice-balance-design.md`。 +``` + +- [ ] **Step 2: 更新流程图文案(约 L246)** + +将: + +```text + → 提交(余额 ≥ 合计才可;扣减发票余额) +``` + +改为: + +```text + → 提交(全额扣减发票余额,可为负;不足不拦截) +``` + +- [ ] **Step 3: Commit** + +```bash +git add docs/business-inventory/03-报销业务明细.md +git commit -m "$(cat <<'EOF' +docs(bx): document that reimburse submit may leave invoice balance negative + +EOF +)" +``` + +--- + +### Task 5: 手工验收清单(实现后勾选) + +不写自动化 E2E;由实现者或 QA 在本地联调: + +- [ ] **Step 1: 准备数据** — 某用户发票夹余额小于拟报销合计(或余额为 0)。 +- [ ] **Step 2: Web** — `/#/bx/reimburse/apply` 新建草稿,合计 > 余额;见弱提示;保存成功;提交审批成功;「我的发票夹」余额变为负或更低;流水有 `debit_reimburse`。 +- [ ] **Step 3: bx-mobile** — 同上路径提交成功,无「无法提交」toast。 +- [ ] **Step 4: 驳回或撤回** — 余额回升。 +- [ ] **Step 5: 删票** — 占用后余额不足时删票仍被拒绝。 +- [ ] **Step 6: 补票** — 上传发票后负余额回升。 + +(本 Task 无代码 commit;验收通过后可将 design 状态改为「已实现」可选。) + +--- + +## Spec 覆盖自检 + +| Spec 项 | Task | +|---------|------| +| D1 全额占用、余额可为负 | Task 1 | +| D2 全端一致(backend + web + bx-mobile) | Task 1–3 | +| D3 弱提示文案 | Task 2–3 | +| D4 删票保护不变 | Task 1 明确不改 `assert_can_delete_invoice`;Task 5 验收 | +| D5 非目标 | 计划未包含审批/发放提醒等 | +| 业务清单同步 | Task 4 | +| 验收标准 1–7 | Task 5 | + +无 TBD/占位符;文件路径与函数名与现网一致。