docs: add implementation plan for reimburse detail occur date
Break the approved design into backend, web-admin, and bx-mobile tasks with concrete steps and verification. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
071281150d
commit
f73fb572d1
1 file changed
+552
@@ -0,0 +1,552 @@
|
||||
# 报销明细发生时间(occur_date)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:** 在报销明细上增加可选「发生时间」字段(仅年月日),贯通 backend / web-admin / bx-mobile 的录入与只读展示。
|
||||
|
||||
**Architecture:** 在 `bx_reimburse_detail` 新增可空 `DATE` 列 `occur_date`;创建/更新明细时透传写入;API 出参驼峰 `occurDate`。前端录入用日期选择器(可清空),只读场景始终展示该字段,空值显示 `—`。
|
||||
|
||||
**Tech Stack:** MySQL 5.7、FastAPI + SQLAlchemy + Pydantic v2、Vue 3 + Element Plus(web-admin)、Vue 3 + Vant(bx-mobile)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-17-reimburse-detail-occur-date-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 操作 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Create | `backend/scripts/migrate_20260817_bx_reimburse_detail_occur_date.sql` | DDL 加列 |
|
||||
| Modify | `docs/sql/shanxia2.0.sql` | 快照表结构同步 |
|
||||
| Modify | `backend/app/models/bx_models.py` | `BxReimburseDetail.occur_date` |
|
||||
| Modify | `backend/app/utils/bx_codegen.py` | 新增 `parse_optional_date`(空→None;勿复用 `parse_reimburse_date`,因其空值会变成今天) |
|
||||
| Modify | `backend/app/schemas/bx/reimburse.py` | Create/Out 增加 `occur_date` |
|
||||
| Modify | `backend/app/api/bx/reimburse.py` | `_build_details_data` 透传并解析 |
|
||||
| Modify | `backend/app/services/bx/reimburse.py` | create/update 明细写入 `occur_date` |
|
||||
| Create | `backend/tests/test_bx_optional_date.py` | `parse_optional_date` 单测 |
|
||||
| Modify | `web-admin/src/views/bx/reimburse/apply/index.vue` | 录入列 + 查看弹窗列 |
|
||||
| Modify | `web-admin/src/views/bx/reimburse/approve/index.vue` | 只读明细列 |
|
||||
| Modify | `web-admin/src/views/bx/view/mine/index.vue` | 只读明细列 |
|
||||
| Modify | `web-admin/src/views/bx/view/todo/index.vue` | 只读明细列 |
|
||||
| Modify | `web-admin/src/views/bx/view/all/index.vue` | 只读明细列 |
|
||||
| Modify | `bx-mobile/src/views/reimburse/ReimburseApplyPage.vue` | 录入字段 |
|
||||
| Modify | `bx-mobile/src/views/reimburse/ReimburseDetailPage.vue` | 只读字段 |
|
||||
| Modify | `bx-mobile/src/views/reimburse/ReimburseApprovePage.vue` | 只读字段 |
|
||||
|
||||
**不变:** 列表筛选、导出、`ApprovalDetailPage`、主表字段、必填校验、未来日期限制。
|
||||
|
||||
**注意:** `viewDetailSummary` 用列索引 `2` 汇总金额;新增列在报销事由之后,金额仍是 index `2`,无需改汇总逻辑。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: DDL + Model
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/scripts/migrate_20260817_bx_reimburse_detail_occur_date.sql`
|
||||
- Modify: `docs/sql/shanxia2.0.sql`(`CREATE TABLE bx_reimburse_detail` 段)
|
||||
- Modify: `backend/app/models/bx_models.py`(`BxReimburseDetail`)
|
||||
|
||||
- [ ] **Step 1: 写迁移脚本**
|
||||
|
||||
```sql
|
||||
-- backend/scripts/migrate_20260817_bx_reimburse_detail_occur_date.sql
|
||||
-- 报销明细增加可选发生日期 occur_date
|
||||
|
||||
ALTER TABLE `bx_reimburse_detail`
|
||||
ADD COLUMN `occur_date` date DEFAULT NULL COMMENT '费用发生日期'
|
||||
AFTER `amount`;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新 SQL 快照**
|
||||
|
||||
在 `docs/sql/shanxia2.0.sql` 的 `CREATE TABLE \`bx_reimburse_detail\`` 中,于 `` `amount` decimal(14,2) NOT NULL COMMENT '报销金额', `` 之后插入:
|
||||
|
||||
```sql
|
||||
`occur_date` date DEFAULT NULL COMMENT '费用发生日期',
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Model 加字段**
|
||||
|
||||
在 `BxReimburseDetail` 中 `amount` 与 `tenant_no` 之间增加:
|
||||
|
||||
```python
|
||||
occur_date = Column(Date, nullable=True, comment="费用发生日期")
|
||||
```
|
||||
|
||||
(`Date` 已在 `bx_models.py` 顶部从 sqlalchemy 导入。)
|
||||
|
||||
- [ ] **Step 4: 在目标库执行迁移**
|
||||
|
||||
```bash
|
||||
# 按本地习惯执行,例如:
|
||||
mysql -u... -p... your_db < backend/scripts/migrate_20260817_bx_reimburse_detail_occur_date.sql
|
||||
```
|
||||
|
||||
Expected: 成功;`SHOW COLUMNS FROM bx_reimburse_detail LIKE 'occur_date';` 可见该列。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/scripts/migrate_20260817_bx_reimburse_detail_occur_date.sql \
|
||||
docs/sql/shanxia2.0.sql \
|
||||
backend/app/models/bx_models.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(bx): add occur_date column on reimburse detail
|
||||
|
||||
Store optional expense occurrence date (DATE, nullable) for each
|
||||
reimburse line item.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: parse_optional_date + 单测(TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/test_bx_optional_date.py`
|
||||
- Modify: `backend/app/utils/bx_codegen.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测**
|
||||
|
||||
```python
|
||||
# backend/tests/test_bx_optional_date.py
|
||||
from datetime import date, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.bx_codegen import parse_optional_date
|
||||
|
||||
|
||||
def test_parse_optional_date_none_and_blank():
|
||||
assert parse_optional_date(None) is None
|
||||
assert parse_optional_date("") is None
|
||||
assert parse_optional_date(" ") is None
|
||||
|
||||
|
||||
def test_parse_optional_date_string_and_date():
|
||||
assert parse_optional_date("2026-08-17") == date(2026, 8, 17)
|
||||
assert parse_optional_date(date(2026, 1, 2)) == date(2026, 1, 2)
|
||||
assert parse_optional_date(datetime(2026, 3, 4, 12, 0, 0)) == date(2026, 3, 4)
|
||||
|
||||
|
||||
def test_parse_optional_date_invalid():
|
||||
with pytest.raises(ValueError):
|
||||
parse_optional_date("2026/08/17")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_optional_date.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(`parse_optional_date` 未定义)
|
||||
|
||||
- [ ] **Step 3: 实现 `parse_optional_date`**
|
||||
|
||||
在 `backend/app/utils/bx_codegen.py` 的 `parse_reimburse_date` 附近新增(**不要**改 `parse_reimburse_date` 行为):
|
||||
|
||||
```python
|
||||
def parse_optional_date(value: str | date | datetime | None) -> date | None:
|
||||
"""可选日期:空 → None;合法 YYYY-MM-DD / date / datetime → date。"""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
text_value = str(value).strip()
|
||||
if not text_value:
|
||||
return None
|
||||
return datetime.strptime(text_value, "%Y-%m-%d").date()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测确认通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_bx_optional_date.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/utils/bx_codegen.py backend/tests/test_bx_optional_date.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(bx): add parse_optional_date for nullable occur dates
|
||||
|
||||
Empty values map to None so optional detail dates do not default to today.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Schema + API + Service 写入
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/schemas/bx/reimburse.py`
|
||||
- Modify: `backend/app/api/bx/reimburse.py`
|
||||
- Modify: `backend/app/services/bx/reimburse.py`
|
||||
|
||||
- [ ] **Step 1: Schema**
|
||||
|
||||
`ReimburseDetailCreate` 增加:
|
||||
|
||||
```python
|
||||
occur_date: Optional[str] = None
|
||||
```
|
||||
|
||||
`ReimburseDetailOut` 增加(文件顶部已有 `OptDateToStr` 导入):
|
||||
|
||||
```python
|
||||
occur_date: OptDateToStr = None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: API `_build_details_data`**
|
||||
|
||||
在 `backend/app/api/bx/reimburse.py` 顶部增加导入:
|
||||
|
||||
```python
|
||||
from app.utils.bx_codegen import parse_optional_date
|
||||
```
|
||||
|
||||
将 `details_data.append({...})` 改为含解析与错误处理:
|
||||
|
||||
```python
|
||||
try:
|
||||
occur_date = parse_optional_date(d.occur_date)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="发生时间格式无效,请使用 YYYY-MM-DD",
|
||||
)
|
||||
details_data.append(
|
||||
{
|
||||
"expense_id": d.expense_item_id,
|
||||
"expense_name": item.item_name,
|
||||
"amount": d.amount,
|
||||
"remark": d.remark,
|
||||
"occur_date": occur_date,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Service create / update**
|
||||
|
||||
`create_with_details` 构造 `BxReimburseDetail` 时增加:
|
||||
|
||||
```python
|
||||
occur_date=d.get("occur_date"),
|
||||
```
|
||||
|
||||
`update_with_details` 内同样增加 `occur_date=d.get("occur_date")`。
|
||||
|
||||
- [ ] **Step 4: 冒烟(可选)**
|
||||
|
||||
重启后端后,用已有登录态调用创建/更新(或 Swagger `/docs`),body detail 含 `"occurDate": "2026-08-01"` 与省略该字段各一次;GET 详情确认 `occurDate` 分别为 `"2026-08-01"` / `null`。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/schemas/bx/reimburse.py \
|
||||
backend/app/api/bx/reimburse.py \
|
||||
backend/app/services/bx/reimburse.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(bx): persist and return reimburse detail occurDate
|
||||
|
||||
Wire optional occur_date through create/update detail payloads and
|
||||
API responses.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: web-admin 申请页录入 + 查看弹窗
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/bx/reimburse/apply/index.vue`
|
||||
|
||||
- [ ] **Step 1: 明细表头与单元格**
|
||||
|
||||
在「报销事由」`<th>` / `<td>` 之后、「操作」之前插入:
|
||||
|
||||
```vue
|
||||
<th class="col-occur">发生时间</th>
|
||||
```
|
||||
|
||||
```vue
|
||||
<td class="col-occur">
|
||||
<el-date-picker
|
||||
v-model="line.occurDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="可选"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</td>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 类型与表单初始化**
|
||||
|
||||
`DetailLine`:
|
||||
|
||||
```typescript
|
||||
interface DetailLine {
|
||||
expenseItemId: string
|
||||
amount: number | string
|
||||
remark: string
|
||||
occurDate: string
|
||||
}
|
||||
```
|
||||
|
||||
所有初始化 / `addDetailLine` / `reset` / 空行默认值改为含 `occurDate: ''`。
|
||||
|
||||
编辑回填:
|
||||
|
||||
```typescript
|
||||
addForm.details = (data.details || []).map((d: any) => ({
|
||||
expenseItemId: d.expenseId,
|
||||
amount: d.amount,
|
||||
remark: d.remark || '',
|
||||
occurDate: d.occurDate || ''
|
||||
}))
|
||||
```
|
||||
|
||||
提交 `buildPayload` 的 details map:
|
||||
|
||||
```typescript
|
||||
details: validDetails.map((d) => ({
|
||||
expenseItemId: String(d.expenseItemId),
|
||||
amount: Number(d.amount),
|
||||
remark: String(d.remark).trim(),
|
||||
occurDate: d.occurDate ? String(d.occurDate) : undefined
|
||||
}))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 查看弹窗列**
|
||||
|
||||
在报销事由列后:
|
||||
|
||||
```vue
|
||||
<el-table-column prop="occurDate" label="发生时间" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.occurDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 手工验收**
|
||||
|
||||
打开 `/#/bx/reimburse/apply`:新增一行填发生时间、一行不填 → 保存 → 查看弹窗分别显示日期与 `—`;编辑可清空。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/bx/reimburse/apply/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add optional occur date on reimburse apply details
|
||||
|
||||
Allow picking YYYY-MM-DD per detail line and show it in the view dialog.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: web-admin 只读明细列(审批 + view 三页)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/bx/reimburse/approve/index.vue`
|
||||
- Modify: `web-admin/src/views/bx/view/mine/index.vue`
|
||||
- Modify: `web-admin/src/views/bx/view/todo/index.vue`
|
||||
- Modify: `web-admin/src/views/bx/view/all/index.vue`
|
||||
|
||||
- [ ] **Step 1: 四处明细表在「报销事由」列后增加同一列**
|
||||
|
||||
```vue
|
||||
<el-table-column prop="occurDate" label="发生时间" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.occurDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
若某页「报销事由」列模板写法略有不同,仍插在其后。
|
||||
|
||||
- [ ] **Step 2: 手工抽查**
|
||||
|
||||
分别打开审批页与我的/待办/全部查看弹窗,确认列存在且空值为 `—`。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/bx/reimburse/approve/index.vue \
|
||||
web-admin/src/views/bx/view/mine/index.vue \
|
||||
web-admin/src/views/bx/view/todo/index.vue \
|
||||
web-admin/src/views/bx/view/all/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): show occur date on reimburse detail readonly tables
|
||||
|
||||
Keep the column always visible with an em dash when empty.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: bx-mobile 申请页
|
||||
|
||||
**Files:**
|
||||
- Modify: `bx-mobile/src/views/reimburse/ReimburseApplyPage.vue`
|
||||
|
||||
- [ ] **Step 1: 模板 — 报销事由后增加字段**
|
||||
|
||||
对齐已有 `reimburseDate` 的 `input type="date"`:
|
||||
|
||||
```vue
|
||||
<!-- 发生时间(可选) -->
|
||||
<div class="field">
|
||||
<label class="field-label">发生时间</label>
|
||||
<input
|
||||
class="form-input"
|
||||
type="date"
|
||||
v-model="line.occurDate"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: DetailLine 与数据流**
|
||||
|
||||
```typescript
|
||||
type DetailLine = {
|
||||
expenseItemId: string | number
|
||||
amount: string | number
|
||||
remark: string
|
||||
occurDate: string
|
||||
}
|
||||
```
|
||||
|
||||
初始化 / `addDetailLine` / 清空默认行:`occurDate: ''`。
|
||||
|
||||
回填:
|
||||
|
||||
```typescript
|
||||
occurDate: d.occurDate || '',
|
||||
```
|
||||
|
||||
提交 details:
|
||||
|
||||
```typescript
|
||||
remark: String(d.remark).trim(),
|
||||
occurDate: d.occurDate ? String(d.occurDate) : undefined,
|
||||
```
|
||||
|
||||
校验逻辑**不要**要求 `occurDate` 必填。
|
||||
|
||||
- [ ] **Step 3: 手工验收**
|
||||
|
||||
移动端申请:有值/无值各保存一次,编辑回填正确。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add bx-mobile/src/views/reimburse/ReimburseApplyPage.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(bx-mobile): add optional occur date on reimburse apply form
|
||||
|
||||
Mirror web-admin detail lines with a clearable YYYY-MM-DD input.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: bx-mobile 详情 / 审批只读
|
||||
|
||||
**Files:**
|
||||
- Modify: `bx-mobile/src/views/reimburse/ReimburseDetailPage.vue`
|
||||
- Modify: `bx-mobile/src/views/reimburse/ReimburseApprovePage.vue`
|
||||
|
||||
- [ ] **Step 1: 明细块在报销事由后始终展示发生时间**
|
||||
|
||||
两处都改为(与现有 `v-if="d.remark"` 的事由不同,发生时间**始终**渲染):
|
||||
|
||||
```vue
|
||||
<div class="adv-field">
|
||||
<div class="adv-field__label">发生时间</div>
|
||||
<div class="adv-field__value">{{ d.occurDate || '—' }}</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
插在「报销事由」字段之后。
|
||||
|
||||
- [ ] **Step 2: 手工验收**
|
||||
|
||||
详情页与审批页:有值显示日期,无值 `—`。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add bx-mobile/src/views/reimburse/ReimburseDetailPage.vue \
|
||||
bx-mobile/src/views/reimburse/ReimburseApprovePage.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(bx-mobile): display occur date on reimburse detail views
|
||||
|
||||
Always show the field and use an em dash when the date is empty.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 收尾 — 图谱更新 + 总验收
|
||||
|
||||
**Files:** 无业务代码(图谱增量)
|
||||
|
||||
- [ ] **Step 1: 更新知识图谱**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh all
|
||||
```
|
||||
|
||||
或按仓库惯例对改动端执行图谱更新。
|
||||
|
||||
- [ ] **Step 2: 总验收清单**
|
||||
|
||||
1. DB 列存在;旧数据 `NULL`
|
||||
2. Web 申请:填/不填/清空均可保存提交
|
||||
3. Web 查看/审批/view 三页:列可见,空为 `—`
|
||||
4. Mobile 申请 + 详情 + 审批一致
|
||||
5. `pytest tests/test_bx_optional_date.py -v` PASS
|
||||
|
||||
- [ ] **Step 3: Commit 图谱(若有变更)**
|
||||
|
||||
```bash
|
||||
git add backend/graphify-out web-admin/graphify-out bx-mobile/graphify-out graphify-out 2>/dev/null || true
|
||||
git status
|
||||
# 仅提交实际变更的图谱文件
|
||||
git commit -m "$(cat <<'EOF'
|
||||
chore: update graphify after reimburse occur_date changes
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec 覆盖自检
|
||||
|
||||
| Spec 项 | Task |
|
||||
|---------|------|
|
||||
| D1 `occur_date` DATE 可空 | Task 1 |
|
||||
| D2 仅 YYYY-MM-DD | Task 2–7 |
|
||||
| D3 可选、空→NULL | Task 2–3、4、6 |
|
||||
| D4 只读始终显示、空为 `—` | Task 4–5、7 |
|
||||
| D5 三端 | Task 3–7 |
|
||||
| D6 不做筛选/导出等 | 无对应 Task(刻意不做) |
|
||||
| 迁移 + shanxia2.0.sql | Task 1 |
|
||||
| `_build_details_data` + service | Task 3 |
|
||||
| ApprovalDetailPage 不改 | 无 Task |
|
||||
|
||||
**占位符扫描:** 无 TBD/TODO。
|
||||
**类型一致性:** DB/Model `occur_date`;API JSON `occurDate`;前端 `occurDate`。
|
||||
Reference in new issue
Block a user