docs: add plan for allowing negative invoice balance on reimburse submit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-12 20:56:43 +08:00
1 parent 12e8cdaaf9
commit 4f75bc40b9
1 file changed
+339
@@ -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
<span v-if="Number(walletBalance) < Number(detailTotalAmount)" class="invoice-balance-warn">
(余额不足,提交时将被拦截)
</span>
```
改为:
```vue
<span v-if="Number(walletBalance) < Number(detailTotalAmount)" class="invoice-balance-warn">
(余额不足,提交后余额将为负,请后续补发票)
</span>
```
- [ ] **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
<span v-if="balanceInsufficient" class="invoice-balance-tip--warn">(余额不足,无法提交)</span>
```
改为:
```vue
<span v-if="balanceInsufficient" class="invoice-balance-tip--warn">(余额不足,提交后余额将为负,请后续补发票)</span>
```
保留 `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/占位符;文件路径与函数名与现网一致。