From f73fb572d1c8e78699c5292c13c55dd44ba84b17 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 17 Aug 2026 15:48:02 +0800 Subject: [PATCH] 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 --- .../2026-08-17-reimburse-detail-occur-date.md | 552 ++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-17-reimburse-detail-occur-date.md diff --git a/docs/superpowers/plans/2026-08-17-reimburse-detail-occur-date.md b/docs/superpowers/plans/2026-08-17-reimburse-detail-occur-date.md new file mode 100644 index 000000000..dbf31597f --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-reimburse-detail-occur-date.md @@ -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: 明细表头与单元格** + +在「报销事由」`` / `` 之后、「操作」之前插入: + +```vue + 发生时间 +``` + +```vue + + + +``` + +- [ ] **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 + + + +``` + +- [ ] **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 + + + +``` + +若某页「报销事由」列模板写法略有不同,仍插在其后。 + +- [ ] **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 + +
+ + +
+``` + +- [ ] **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 +
+
发生时间
+
{{ d.occurDate || '—' }}
+
+``` + +插在「报销事由」字段之后。 + +- [ ] **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`。