Commit 066f0d99 authored by wenwen.tang's avatar wenwen.tang 😕

docs(memory): 用户交互流程图交付;MEMORY.md 二次精简(16 章节口径未丢)

parent 2dfe5556
...@@ -154,6 +154,16 @@ ...@@ -154,6 +154,16 @@
- `output/合同-台账核对报告.md`(可入库) - `output/合同-台账核对报告.md`(可入库)
- `output/合同-台账核对明细.csv`(35 行 × 17 列,output/*.csv 被忽略) - `output/合同-台账核对明细.csv`(35 行 × 17 列,output/*.csv 被忽略)
## 用户交互流程图(16:10)
- 需求:生成用户交互流程图,展示业务流程
- 产出 `output/rentos-用户交互流程图.html`:**自包含无外链**,含 4 张 SVG ——
① 全局交互地图(登录 → 概览 → 三条业务线)② 电费链路(录入→生成→逐行核对→确认存盘→发送→流水)
③ 房租车位费链路(台账/合同 → 账期预览 → 出单 → 收款发送)④ 自动排期(AJ − 提前天数 → 自动出单发送 → AC/AJ 推进)
外加角色权限对照表 + 贯穿性交互约定表;带 `@media print` 可直接打印
- 图按**真实** `PAGES` 5 项 + `admin.html` 按钮清单绘制(不凭印象);会话内另用内联图逐张展示
- ⚠️ 配色用固定浅色底 + 深字(不依赖主题变量),保证明暗主题下都清晰
- 顺带:MEMORY.md 二次精简(18.9KB → 17.7KB / 11143 字符),16 个章节口径未丢
## 待办 ## 待办
- 上面 17 个单元的修正要不要动手(涉及台账写操作,需 `confirm=true`) - 上面 17 个单元的修正要不要动手(涉及台账写操作,需 `confirm=true`)
- 4 张本机残留批次是否移入 `_trash/` - 4 张本机残留批次是否移入 `_trash/`
......
# 租金通知系统 · 项目长期记忆 # RentOS 租金通知系统 · 长期记忆
## 目标 & 命名 三份 Excel → 固定模板填充 → **PDF 收费通知书**(reportlab 程序化绘制),发企微 + Web 维护。
三份 Excel → 固定模板 + 数据填充 → **PDF 收费通知书**,自动发企微 + Web 维护。 文件名 `{单元列表}-{费用类型}-收费通知书.pdf`:单元按 `B.unit_sort_key` 排序、`、` 连接、**不含公司名**;
- 文件名 = `{单元列表}-{费用类型}-收费通知书.pdf`;单元按 `B.unit_sort_key` 排序、`、` 连接;**不含公司名** 费用类型词序固定 房租 → 物业费 → 车位费。
- 费用类型词序固定:房租 → 物业费 → 车位费
## 数据模型 ## 数据模型
- **唯一台账** `rent_parking`(85 行 × **32 列**) - **唯一台账** `rent_parking`(85 行 × **32 列**)。关键列:
- `BILL_COLS`:owner=A unit=C tenant=F begin=I end=J rentfree=L **free_type=AK** cycle=N fee=O rent=P owner=A unit=C tenant=F begin=I end=J rentfree=L **free_type=AK** cycle=N fee=O rent=P;
last_paid=AB bill_start=AC next_bill=AJ;另 **AL「合同原件」**(type=`files`,排最右) last_paid=AB bill_start=AC next_bill=AJ;**AL「合同原件」**(type=`files`,最右)
- **AK「免租类型」**(显示在 L 之后):`首N月免租` / `每年8-9月免租`,空=无免租
- AJ **只用于定时排期**(推送日 = AJ − 提前天数);出单口径看 AC;出单后 AC/AJ 一起推进 - AJ **只用于定时排期**(推送日 = AJ − 提前天数);出单口径看 AC;出单后 AC/AJ 一起推进
- **AD~AI 房产/房产税六列已删**(`RETIRED_TAX_COLS`,有守护测试);列定义**以代码为准** - ⚠️ 列定义**以代码为准**(`load_table` 用 spec 覆盖存档列)→ **加列不需要数据迁移**
(`load_table` 用 spec 覆盖存档列)→ **加列不需要数据迁移** - 默认只读;改格/增行/删行须 `confirm=true`(否则 428);备份 `data/tables/.backup/`
- 默认锁定只读;改单元格/增行/删行必须 `confirm=true`(否则 428);备份 `data/tables/.backup/` - **批次** `data/batches/{id}.json`(点一次「生成通知书」的产物,id=`YYYYMMDD-HHMMSS`);
- **批次** `data/batches/{id}.json`(= 点一次「生成通知书」的产物,id=`YYYYMMDD-HHMMSS`); 测试残留移 `data/batches/_trash/YYYYMMDD/`(store 只 listdir 顶层 `*.json`,子目录隐身可恢复)
测试残留移入 `data/batches/_trash/YYYYMMDD/`(store 只 listdir 顶层 `*.json`,子目录隐身可恢复) - **账期**:`period_label` 只是批次上的**文本字段**,`list_periods()` 按它归组 = 页面「账期汇总」。
- **账期口径**:`period_label` 只是批次上的**文本字段**,`list_periods()` 按它归组 = 页面「账期汇总」 来源 ① 录入页「所属期间」(默认今天年月,带入上期 +1 月)② `elec_parser._extract_period`
- 来源:① 录入页「所属期间」(默认今天年月;带入上期时 +1 月)② `elec_parser._extract_period` ③ 后端兜底用通知日期年月。⚠️ **自由文本无归一化**:`strip()` 精确比较 →「2026-11」≠「2026年11月」
③ 后端兜底用**通知日期**年月 - 同账期多张批次正常(分批/按公司/补录)→ 台账按账期汇总并默认折叠
- ⚠️ **自由文本、无归一化**:`strip()` 后精确比较 →「2026-11」与「2026年11月」会分成两组 - 8F/11F 付款周期统一 **3 个月**、**不收物业费**(O 列留空)
- 同账期多张批次正常(分批录 / 按公司分批 / 补录)→ 台账按账期汇总并默认折叠
## 免租(`rentos/rentfree.py`) ## 免租(`rentos/rentfree.py`)
- `首N月免租`:窗口 `[I, I+L月)`,含起始不含结束;0.5 月 = 15 天 - `首N月免租`:`[I, I+L月)`,含起始不含结束;0.5 月 = 15 天
- `每年8-9月免租`:合约期 `[I, J]` 内每个自然年 `[max(该年08-01,I), min(该年10-01, J+1天))`(跨年裁剪) - `每年8-9月免租`:合约期 `[I, J]` 内每年 `[max(该年08-01,I), min(该年10-01, J+1天))`(跨年裁剪)
- `hit()` 按时点;`overlaps()` 按账单期间交集(「每年8-9月」必需) - `hit()` 按时点;`overlaps()` 按账单期间交集(「每年8-9月」必需)
- ⚠️ `windows()` 返回键是 **`begin`/`end`**(非 from/to),另有 `type`/`months`/`year`/`key` - ⚠️ `windows()` 返回键是 **`begin`/`end`**(非 from/to);AK「免租类型」空 = 无免租
## 名册(`data/config.json`,唯一入口 `rentos/directory.py`) ## 名册(`data/config.json`,唯一入口 `rentos/directory.py`)
- `companies` 租客(名称即唯一键,`kind`=company|person);`owners` 出租方(**键=简称**) - `companies` 租客(名称即唯一键,`kind`=company|person);`owners` 出租方(键=简称)
- 当前 4 家:精锐 37 / 科博 24 / 瑞格嘉德 16 / 国隆 8 —— 当前 4 家:精锐 37 / 科博 24 / 瑞格嘉德 16 / 国隆 8
- `None`/缺失 = 未维护过 → 回落 `DEFAULT_OWNERS`;**写过(哪怕 `{}`)= 权威** - `None`/缺失 = 未维护过 → 回落 `DEFAULT_OWNERS`;**写过(哪怕 `{}`)= 权威**
- 改名**联动台账**(F/A 列);删除有引用 → 409,`force=true` 才真删 - 改名**联动台账**(F/A 列);删除有引用 → 409,`force=true` 才真删
- 归属三级优先:**行内 `owner` > 台账 A 列 > 名单兜底(仅当名单恰好一家)** - 归属三级优先:**行内 `owner` > 台账 A 列 > 名单兜底(仅当名单恰好一家)**
- ⚠️ 写入必须 `store.set_config()`(顶层整体替换);`patch_config` 浅合并**删不掉字典键** - ⚠️ 写入必须 `store.set_config()`(顶层整体替换);`patch_config` 浅合并**删不掉字典键**
## 业务口径 ## 业务口径
- 电费:`用量 = 本期 − 上期`;`空调 = 用量 × 电价 × 12.25`、**`照明` = 用量 × 电价** - 电费:`用量 = 本期 − 上期`;`空调 = 用量 × 电价 × 12.25`、`照明 = 用量 × 电价`;
- 只填上期不填本期 → `pending` 不出单;**车位不抄电表**(`P.is_park`,候选/带入/入库三处都要挡) 只填上期不填本期 → `pending` 不出单;**车位不抄电表**(`P.is_park`,候选/带入/入库三处都要挡)
- 8F/11F **付款周期统一 3 个月**、**不收物业费**(O 列留空) - `plans.py` 两张账单:`elec` 触发日 = 缴费期限 − 提前天数;`rent_parking` = **AJ − 提前天数**,
- 两张账单(`plans.py`):`elec` 触发日 = 缴费期限 − 提前天数;`rent_parking` = **AJ − 提前天数**,
按行判定(`rent_state()`),到点自动出单+发送(`service.auto_issue_rent()`) 按行判定(`rent_state()`),到点自动出单+发送(`service.auto_issue_rent()`)
## 电费核对(行粒度) ## 电费核对(行粒度)
- 行键 = `单元|类目`(如 `302室|照明`);`verify = {units:[], rows:[], done, at, by, scope}` - 行键 = `单元|类目`;`verify = {units, rows, done, at, by, scope}`
- `verify_summary()["ok"] = done`(**确认即可,允许部分核对**);`partial` = done 且有未勾行 - `ok = done`(**确认即可,允许部分核对**);`partial` = done 且有未勾行
- **自动撤销**:确认时把勾选快照存 `scope`;之后勾选少于 scope → 撤销确认 - **自动撤销**:确认时把勾选快照存 `scope`;之后勾选少于 scope → 撤销确认
- 发送闸门:未确认 → 409;部分核对发送前前端 `partialVerifyWarn()` 二次提醒 - 发送闸门:未确认 → 409;部分核对发送前 `partialVerifyWarn()` 二次提醒
- 账期级:一行算已核对 ⇔ **含它的每张批次都勾了它**;`POST /api/periods/verify` 一次写全账期 - 账期级:一行算已核对 ⇔ **含它的每张批次都勾了它**;`POST /api/periods/verify` 一次写全账期
- 核对表**可直接编辑**,**改数据不影响核对状态**;`confirmVerify()` 两步: - `confirmVerify()` 两步:① `saveVerifyData()` 按 `_bid` 分组逐张 `PUT /api/batches/{id}`(有改动才发)
① `saveVerifyData()` 按 `_bid` 分组逐张 `PUT /api/batches/{id}`(有改动才发) ② `POST /api/periods/verify {rows, done:true}` —— 要不要重核**由用户勾选决定**,服务端不猜
② `POST /api/periods/verify {rows, done:true}` → 要不要重核**由用户勾选决定**,服务端不猜
- 已删 `keep_verify` 那套;`reingest` = **一律重置核对** - 已删 `keep_verify` 那套;`reingest` = **一律重置核对**
- 脏基线:`ELEC_CMP_FIELDS` + `ESNAP`/`takeVerifySnapshot()`/`dirtyRows()`/`editSummary()` - ⚠️ 脏基线(`ELEC_CMP_FIELDS`/`ESNAP`/`takeVerifySnapshot`/`dirtyRows`/`editSummary`)用稳定 `_rid`,
- ⚠️ 基线键用稳定 `_rid`,**不能用数组下标**;⚠️ 改动提示只改文字、绝不重渲染模式条(否则输入框失焦) **不能用数组下标**;⚠️ 改动提示只改文字、**绝不重渲染模式条**(否则输入框失焦)
- ⚠️ `PUT` 是**全量覆盖**:每组必须带该批次**全部行**;退出核对有未保存改动要**明确警告会丢** - ⚠️ `PUT` 是**全量覆盖**:每组必须带该批次**全部行**;退出核对有未保存改动要**明确警告会丢**
- 前端:核对模式按行勾选 + 列显示菜单(`localStorage['rentos_vcols']`);**重新编辑/核对入口只在批次详情** - 核对表**可直接编辑**,**改数据不影响核对状态**;**重新编辑/核对入口只在批次详情**
- 列显示菜单:开关状态存模块级 `VCOLS_OPEN`;收起 = 再点按钮 / `×` / 点外部 / Esc;空间不足自动 `.up` - ⚠️ **HTML `hidden` 不是 `!important`**:类里写了 `display:flex/grid` 就失效 → 必须补 `[hidden]{display:none}`
- ⚠️ **HTML `hidden` 不是 `!important`**:类里写了 `display:flex/grid` 就完全失效 → 必须补 `[hidden]{display:none}`
- ⚠️ **会 `innerHTML` 重渲染的绑定函数里别挂 document 级监听**(用一次性模块标志或事件委托) - ⚠️ **会 `innerHTML` 重渲染的绑定函数里别挂 document 级监听**(用一次性模块标志或事件委托)
- 列显示菜单:`localStorage['rentos_vcols']`,开关存模块级 `VCOLS_OPEN`;
收起 = 再点按钮 / `×` / 点外部 / Esc;空间不足自动 `.up`
## 电费录入「临时保存」草稿 ## 电费录入「临时保存」草稿
- 「临时保存」把明细整份落盘 `data/drafts/elec__{账号}.json`(**按账号隔离**);下次进页**自动恢复**; - 明细整份落盘 `data/drafts/elec__{账号}.json`(**按账号隔离**);下次进页**自动恢复**;
生成通知书 / 清空 / 手动丢弃时删除。 生成通知书 / 清空 / 手动丢弃时删除;接口 `GET|PUT|DELETE /api/drafts/elec`(`CAP_ELEC`,访客可用)
- 接口 `GET|PUT|DELETE /api/drafts/elec`(`_write_cap` 记 `CAP_ELEC`,访客可用) - 前端 `saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`;`applyNav` 调 `elecBootFill()`
- 前端 `saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`;`applyNav` 改调 `elecBootFill()`
—— **有草稿就不「带入上期」** —— **有草稿就不「带入上期」**
## 合同原件(台账 AL 列,2026-10-15) ## 合同原件(台账 AL 列)
一格可挂多份(租赁合同 + 补充协议 + 车位合同)。 一格可挂多份(租赁合同 + 补充协议 + 车位合同)。单元格值 = 紧凑 JSON `[{"id","name"}]`;
- 单元格值 = 紧凑 JSON `[{"id","name"}]`;本体在 `data/contracts/{id}.{ext}` + `index.json` 本体 `data/contracts/{id}.{ext}` + `index.json`。
- `rentos/contracts.py`:save/find/path_of/list_all/remove/parse/dump/describe; - `rentos/contracts.py`:save/find/path_of/list_all/remove/parse/dump/describe;白名单
白名单 pdf/doc/docx/xls/xlsx/png/jpg/gif/webp/bmp,单份 **≤ 40MB**;id=uuid4 hex,`clean_id` 挡路径穿越 pdf/doc/docx/xls/xlsx/png/jpg/gif/webp/bmp,单份 **≤ 40MB**;id=uuid4 hex,`clean_id` 挡路径穿越
- ⚠️ 40MB 不是随手定的:扫描件能到 21.8MB,而 40MB base64 后 53MB 仍 < server `MAX_BODY=64MB` - ⚠️ 40MB 不是随手定的:扫描件能到 21.8MB,而 40MB base64 后 53MB 仍 < server `MAX_BODY=64MB`
- 接口:`POST|GET /api/contracts` · `GET /api/contracts/{id}`(内联预览;`?dl=1`;支持 `?token=`)· - 接口 `POST|GET /api/contracts` · `GET /api/contracts/{id}`(内联预览;`?dl=1`;支持 `?token=`)·
`DELETE /api/contracts/{id}` —— **增删仅管理员**(访客可查阅、不能增删) `DELETE` —— **增删仅管理员**(访客可查阅、不能增删)
- 前端:`td.files` 按钮(`📎 N 份` / 编辑态 `+ 合同`)→ `openFilesDlg()`; - 前端 `td.files` 按钮(`📎 N 份` / 编辑态 `+ 合同`)→ `openFilesDlg()`;增删走
增删走 `stageCell(rowId,'AL','files',…)`,**与「保存 N 处改动」同一口径** `stageCell(rowId,'AL','files',…)`(**与「保存 N 处改动」同一口径**)
- ⚠️ **从某行移除只解关联、不删文件**(多行可共享同一份) - ⚠️ **从某行移除只解关联、不删文件**(多行可共享同一份)
- ⚠️ `renderGrid` 渲染 `files` 列**必须读 `TB_PEND` 暂存值**,否则上传后格子仍显示旧按钮 - ⚠️ `renderGrid` 渲染 `files` 列**必须读 `TB_PEND` 暂存值**,否则上传后格子仍显示旧按钮
- 导出 xlsx 该列输出**文件名文本**(`tables.files_text`),不是 JSON - 导出 xlsx 该列输出**文件名文本**(`tables.files_text`),不是 JSON
- 导入:`tools/import_contracts.py`(默认干跑,`--apply` 写入,`--replace` 覆盖); - 导入 `tools/import_contracts.py`(默认干跑,`--apply` 写入,`--replace` 覆盖):解析文件名**开头**
解析文件名**开头**的单元号串(`1101、1115` / `124.155号` / `132&227车位` / `501-502室` / `601A&702`) 的单元号串(`1101、1115` / `124.155号` / `132&227车位` / `501-502室` / `601A&702`);
- ⚠️ **同一份文件只落盘一次**(按文件名缓存 meta),多单元挂同一个 id —— 否则 29 份膨胀成 35 份 ⚠️ **同一份文件只落盘一次**(按文件名缓存 meta),多单元挂同一 id —— 否则 29 份膨胀成 35 份
- **线上已导入**:29 份 → **35 个单元**,落盘 **29 份 / 92.3MB**、无失效关联; - **线上已导入**:29 份 → **35 个单元**,落盘 **29 份 / 92.3MB**、无失效关联;6 组共用:
6 组共用:307/603 · 601A/702 · 132/227车位 · 124/155车位 · 1101/1115 · 501/502 307/603 · 601A/702 · 132/227车位 · 124/155车位 · 1101/1115 · 501/502
- 线上跑法:文件放 `/opt/rentos/uploads/项目合约`(容器内 `/app/uploads/...`)→ - 线上跑法:文件放 `/opt/rentos/uploads/项目合约`(容器内 `/app/uploads/...`)→
`docker exec rentos python /app/tools/import_contracts.py --dir /app/uploads/项目合约 --apply` `docker exec rentos python /app/tools/import_contracts.py --dir /app/uploads/项目合约 --apply`;
- 源副本留服务器 `/opt/rentos/contracts-src/项目合约`(93MB),`uploads/` 已清空 源副本留 `/opt/rentos/contracts-src/项目合约`(93MB),`uploads/` 已清空
## 合同 × 台账核对(2026-10-15 下半场) ## 合同 × 台账核对
- **`tools/sync_pull.py`** —— 数据**回拉**通道(`deploy_git.py` 只管推代码)。 - **`tools/sync_pull.py`** = 数据**回拉**通道(`deploy_git.py` 只管推代码):`--dry` 列差异 →
`--dry` 列差异 → 备份 `data/_snap/pull-<ts>/` → SFTP 递归下载;默认跳 `attachments`、 备份 `data/_snap/pull-<ts>/` → SFTP 递归下载;默认跳 `attachments`、**永不回拉 `sessions.json`**;
**永不回拉 `sessions.json`**;⚠️ **只增/改,不删本地独有文件** → 本地残留需自己清。 ⚠️ **只增/改,不删本地独有文件** → 本地残留需自己清
- **`tools/check_contracts_vs_table.py`** —— 文件名级机械比对(复用 `import_contracts` 的单元解析) - **`tools/check_contracts_vs_table.py`** = 文件名级机械比对(复用 `import_contracts` 的单元解析)
- ⚠️ **合同文件名不可信**!已知 4 处与正文不符:119车位(租客/期限都写错)、 - ⚠️ **合同文件名不可信**!已知 4 处与正文不符:119车位(租客/期限都写错)、1102(漏「安装」)、
1102(漏「安装」)、307&603、601A&702(多主体)。**判断一律以正文为准** 307&603、601A&702(多主体)。**判断一律以正文为准**
- 读扫描件:`pypdfium2` 渲染成图人工判读。房屋合同的甲乙方在**第 2 页**, - 读扫描件:`pypdfium2` 渲染成图人工判读;房屋合同甲乙方多在**第 2 页**、「第三条 期限与交付」在
「第三条 期限与交付」在**第 3~4 页**(因模板而异,别写死页号) **第 3~4 页**(因模板而异,别写死页号)。读 `.doc`:`raw.decode("utf-16-le","ignore")` + 中文串抽取
- 读 `.doc`:`raw.decode("utf-16-le","ignore")` + 中文串抽取即可读到正文 - **结论(29 份 → 35 单元,18 对 / 17 有出入)**:明细见 `output/合同-台账核对报告.md`(入库)。
- **核对结论(29 份 → 35 单元,18 对 / 17 有出入)**: 影响出单 9 条:903室月租 36300/合同 16500;904室月租空/合同 19800;501室期空、502室标「(空置)」
- 影响出单:903室月租台账 36300/合同 16500;904室月租空/合同 19800; (合同 501+502 一体租给明境 2025-06-16~2028-06-15、合计 35648);126/128车位合同期整体早一年
501室期空、502室标「(空置)」(合同 501+502 一体租给明境 2025-06-16~2028-06-15、月租合计 35648); (应 2026-03-19~/2026-03-01~);154车位台账空置(合同 徐相天);601B室租客名带**「(空置)」后缀**
126/128车位合同期整体早一年(应 2026-03-19~/2026-03-01~);154车位台账空置(合同 徐相天); 会被出单逻辑跳过;315室期错(应 2026-11-01~2028-10-31);305室物业费 1512/合同 1312
601B室租客名带**「(空置)」后缀**会被出单逻辑跳过;315室期错(合同 2026-11-01~2028-10-31,JR202609315); - 待确认:132/227车位合同承租方=**上海伯凌建筑设计**而台账=爱种草(已按爱种草出单);
305室物业费台账 1512 / 合同 1312
- 待确认:132/227车位合同承租方=**上海伯凌建筑设计**而台账=上海爱种草(且已按爱种草出单);
119/124/155/191/227 车位合同甲方均为**精锐**,台账出租方是 瑞格嘉德/科博; 119/124/155/191/227 车位合同甲方均为**精锐**,台账出租方是 瑞格嘉德/科博;
903/904 面积都填 271.17(均值,应 247.24/295.10);306/310「租赁期(月)」12/19 应为 24 903/904 面积都填 271.17(均值,应 247.24/295.10);306/310「租赁期(月)」12/19 应为 24
- **当前不出单的 9 个单元**(未出现在任何房租批次):401 · 501 · 502 · 903 · 904 · 126车位 · - **当前不出单的 9 个单元**:401 · 501 · 502 · 903 · 904 · 126车位 · 128车位 · 154车位 · 601B室,
128车位 · 154车位 · 601B室,月租合计约 **¥98,262/月** 月租合计约 **¥98,262/月**
- 产出:`output/合同-台账核对报告.md`(入库)+ `output/合同-台账核对明细.csv`(output/*.csv 被忽略)
## 房租车位费出单 ## 房租车位费出单
链路:`rent_parser.py` → `rent_render.py`(→ `notice_pdf`)→ `service.ingest_rent` → 批次 链路 `rent_parser.py` → `rent_render.py`(→ `notice_pdf`)→ `service.ingest_rent` → 批次
- 计费:期间起 = **AC**,止 = 起 + N 个自然月 − 1 天;`房屋租赁费 = P × N`、`物业费 = O × N`; - 计费:期间起 = **AC**,止 = 起 + N 个自然月 − 1 天;`房屋租赁费 = P × N`、`物业费 = O × N`;
单元号含「车位」→ 车位租赁费;N 缺值回落 `DEFAULT_MONTHS = 3` 单元号含「车位」→ 车位租赁费;N 缺值回落 `DEFAULT_MONTHS = 3`
- **不可出单三条件**:空置(F 空或含「空置」)/ 每月租金 ≤ 0 / AC 空(各带 `reasons`) - **不可出单三条件**:空置(F 空或含「空置」)/ 每月租金 ≤ 0 / AC 空
- **按免租数据跳过**:`rent_preview` 对每行与每份单据标 `rentfree` - `rent_preview` 对每行与每份单据标 `rentfree` 供跳过;房间费与车位费**合并一张**
- 房间费与车位费**合并一张**(键 `(出租方, 租客, 期间)`);批次按 `kind` 隔离 (键 `(出租方, 租客, 期间)`);批次按 `kind` 隔离
- 推进/回滚走 `tables.update_cells`;`rollback_rent_advance` **只回滚当前值仍等于推进后的行** - 推进/回滚走 `tables.update_cells`;`rollback_rent_advance` **只回滚当前值仍等于推进后的行**
- **账期区间筛选**:`classify_period(from, to)` 命中 = 账期起始日落在区间内(旧 `?on=` 是 `to` 别名) - **账期区间筛选**:`classify_period(from, to)` 命中 = 账期起始日落在区间内(旧 `?on=` 是 `to` 别名)
- 接口:`GET /api/batches?kind=` · `GET /api/rent/preview?from=&to=&all=1` · `POST /api/rent/batches` · - 接口 `GET /api/batches?kind=` · `GET /api/rent/preview?from=&to=&all=1` · `POST /api/rent/batches` ·
`DELETE /api/batches/{id}` `{rollback:true}` · `POST /api/batches/{id}/receipt`(**只房租批次**)· `DELETE /api/batches/{id}` `{rollback:true}` · `POST /api/batches/{id}/receipt`(**只房租批次**)·
`GET /api/batches/{id}/export` `GET /api/batches/{id}/export` ⚠️ `GET /api/batches/{id}` 的 `receipt` 在**响应顶层**
- ⚠️ `GET /api/batches/{id}` 的 `receipt` 在**响应顶层**
## 台账编辑(批量暂存) ## 台账编辑(批量暂存)
- 改格**不逐格弹确认**:`stageCell()` 暂存 + 黄底高亮(`TB_PEND`),顶条显示「N 处改动待保存」 - 改格**不逐格弹确认**:`stageCell()` 暂存 + 黄底高亮(`TB_PEND`),顶条显示「N 处改动待保存」
- 「保存全部」→ 一次 `confirmBox` → 单次 `PUT /api/tables/{key}/cells` - 「保存全部」→ 一次 `confirmBox` → 单次 `PUT /api/tables/{key}/cells`
(`{changes:[{row_id,col,value}], confirm:true, who}`)→ `pushHist({t:'batch'})` 可**整批撤销** (`{changes:[{row_id,col,value}], confirm:true, who}`)→ `pushHist({t:'batch'})` 可**整批撤销**
- `navTo()` / `beforeunload` 拦截未保存改动 - `navTo()` / `beforeunload` 拦截未保存改动
- ⚠️ `renderGrid()` 末尾必须同时调 `paintPending()` **和** `renderPendBar()` - ⚠️ `renderGrid()` 末尾必须同时调 `paintPending()` **和** `renderPendBar()`;
- 确认框是 `.modal-mask.show`(**不是 `.open`**) 确认框是 `.modal-mask.show`(**不是 `.open`**)
## 系统骨架 / 渲染 ## 骨架 / 权限 / 渲染
- 启动 `python -m rentos.server --port 8899`;`--no-scheduler` 只开 Web;admin / 1989@rent05; - 启动 `python -m rentos.server --port 8899`(`--no-scheduler` 只开 Web);
访客 **guest / guest@2026**(`role=viewer` + `perms=['elec']`) admin / 1989@rent05;访客 **guest / guest@2026**(`role=viewer` + `perms=['elec']`)
- **权限模型**:`role`(admin|viewer)+ `perms` 能力清单(目前只有 `elec`) - **权限模型**:`role`(admin|viewer)+ `perms` 能力清单(目前只有 `elec`)
- 管理员隐含 `{'*'}`;`viewer` 按 `perms` **逐接口**放行,`perms=[]` 即纯只读 - 管理员隐含 `{'*'}`;`viewer` 按 `perms` **逐接口**放行,`perms=[]` 即纯只读
- `auth.caps()/can()`;`server._write_cap(method,path)` 解析所需能力,`_guard_write()` 判定 - `auth.caps()/can()`;`server._write_cap(method,path)` + `_guard_write()` 判定
- **批次类接口按 `kind` 复核**:房租车位费的删/重出/发送/收款**仍只有管理员** - **批次类接口按 `kind` 复核**:房租车位费的删/重出/发送/收款**仍只有管理员**
- 前端:`body.cap-elec` 才显示 `[data-cap="elec"]`;`data-ro` = 仅管理员;**主账号不可降级** - 前端:`body.cap-elec` 才显示 `[data-cap="elec"]`;`data-ro` = 仅管理员;**主账号不可降级**
- 导航 5 项:概览 / 电费通知单 / 房租车位费 / 公司管理 / 设置;hash 路由在 `app.js` 的 - 导航 5 项:概览 / 电费通知单 / 房租车位费 / 公司管理 / 设置;hash 路由在 `app.js` 的
`PAGES` + `applyNav()`;**新增页面必须同时登记 PAGES 与 `#nav`** `PAGES` + `applyNav()`;**新增页面必须同时登记 PAGES 与 `#nav`**
- **唯一渲染层** `rentos/notice_pdf.py`:reportlab + platypus 程序化绘制(**不用 win32com / LibreOffice**); - **唯一渲染层** `rentos/notice_pdf.py`:reportlab + platypus(**不用 win32com / LibreOffice**);
字体 `simhei.ttf` 子集嵌入;A4、`MARGIN = 1.9cm` 字体 `simhei.ttf` 子集嵌入;A4、`MARGIN = 1.9cm`
## 环境 ## 环境
- Python `…/python/versions/3.13.12/python.exe`(openpyxl / reportlab / pillow;**无 pypdfium2**) - venv `…/envs/default/Scripts/python.exe`:**Playwright + reportlab + pypdfium2 + PyPDF2**
- venv `…/envs/default/Scripts/python.exe`:**Playwright + reportlab + pypdfium2 + PyPDF2** → → **单测 / UI 回归 / 合同判读都用它**(3.13.12 无 pypdfium2、无 paramiko)
**单测 / UI 回归 / 合同判读都用它**
- Bash 先 `export PATH=…/PortableGit/versions/1.2.0/usr/bin:…/bin:$PATH` - Bash 先 `export PATH=…/PortableGit/versions/1.2.0/usr/bin:…/bin:$PATH`
- `.bat` 必须 **cp936 + CRLF**;分支用 `goto` 标签;echo 不放半角括号/`!` - `.bat` 必须 **cp936 + CRLF**;分支用 `goto` 标签;echo 不放半角括号/`!`
- **改完后端代码必须 kill 所有在跑实例再重启**(旧进程会按旧逻辑写同一 `data/`) - **改完后端代码必须 kill 所有在跑实例再重启**(旧进程会按旧逻辑写同一 `data/`)
- ⚠️ 本工具链会把命令里的 `\n` 字面化 → 写多行脚本用 heredoc + `textwrap.dedent`,别用 `"\n".join` - ⚠️ 本工具链会把命令里的 `\n` 字面化 → 多行脚本用 heredoc + `textwrap.dedent`
- ⚠️ 别用 `io.TextIOWrapper(sys.stdout.buffer,…)` 二次包 stdout(旧包装器 GC 时关掉 buffer → - ⚠️ 别用 `io.TextIOWrapper(sys.stdout.buffer,…)` 二次包 stdout;用 `sys.stdout.reconfigure`
「I/O operation on closed file」);用 `sys.stdout.reconfigure(encoding="utf-8")`
## 启动 / 部署(192.168.68.121 · CentOS 7) ## 部署(192.168.68.121 · CentOS 7)
- `start.bat [safe] [端口]` / `stop.bat`;`notify.enabled=True` 时双击 `start.bat` **会开定时发送**
→ **会话内一律 `start.bat safe`**;会话里它内层 `start` 会被拦 → 直接后台起
`python -m rentos.server --port 8899 --no-scheduler`
- 云端 `http://192.168.68.121:8899/`,容器 `rentos`,root / 12345678 - 云端 `http://192.168.68.121:8899/`,容器 `rentos`,root / 12345678
- ⚠️ **线上 admin 密码已不是 1989@rent05**(10-14 之后被改过,`auth.verify` 实测 False)→ - ⚠️ **线上 admin 密码已不是 1989@rent05**(`auth.verify` 实测 False)→ 部署自检改为
部署自检已改成登录失败时提示并跳过 HTTP 抽查,不再抛栈 登录失败时提示并跳过 HTTP 抽查,不再抛栈
- **主用 Git 同步**(⚠️ **必须用 venv python**,3.13.12 无 paramiko): - **主用 Git 同步**(⚠️ **必须用 venv python**):`RENTOS_SSH_PASSWORD=12345678 …python
`RENTOS_SSH_PASSWORD=12345678 …python tools/deploy_git.py sync` tools/deploy_git.py sync`;push 绕代理 `env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY git push`
- push 绕代理:`env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY git push`
- 代码落 `/opt/rentos/repo`,与数据同级独立 → `git reset --hard` **不碰线上数据** - 代码落 `/opt/rentos/repo`,与数据同级独立 → `git reset --hard` **不碰线上数据**
- ⚠️ **sync 只更代码、不动数据** → 改列模型/加列的迁移**部署后要在容器里手动再跑** - ⚠️ **sync 只更代码、不动数据** → 改列模型/加列的迁移**部署后要在容器里手动再跑**
(`docker exec rentos python /app/tools/xxx.py`,服务器**无 Python 3**) (`docker exec rentos python /app/tools/xxx.py`,服务器**无 Python 3**)
- 构建必须在**仓库根**:`docker build -f deploy/Dockerfile -t rentos:latest .`,零联网; - 构建在**仓库根**:`docker build -f deploy/Dockerfile -t rentos:latest .`,零联网;
**切勿 `docker build … | tail`**(吞掉失败会拿旧镜像重启) **切勿 `docker build … | tail`**(吞掉失败会拿旧镜像重启)
- `docker -v` 的 SELinux 选项是**冒号 `:z`**(写成 `,z` 的症状:页面 200 但台账 0 行) - `docker -v` 的 SELinux 选项是**冒号 `:z`**(写成 `,z` 的症状:页面 200 但台账 0 行)
- 云端已能直发企微:`/usr/local/bin/wecom-cli` + 凭据 `/opt/rentos/wecom-config/`(700) - 云端已能直发企微:`/usr/local/bin/wecom-cli` + 凭据 `/opt/rentos/wecom-config/`(700)
- ⚠️ **线上是唯一生产方,本机只作预览**;**本机与云端不要同时开定时器** - ⚠️ **线上是唯一生产方,本机只作预览**;**本机与云端不要同时开定时器**
- 远端 `https://git.thiztech.com/accounting/rentos.git`(Gitea),分支 `main`; - `start.bat [safe] [端口]` / `stop.bat`;`notify.enabled=True` 时双击 `start.bat` **会开定时发送**
`.gitignore` 排除 `data/sessions.json`/`batches`/`drafts`/`contracts`/`attachments`/`_snap`/`_trash`/ → 会话内一律 `start.bat safe`
`output/*`(**`!output/*.md` 保留验收报告**)/`项目合约/`;`.gitattributes`:`* text=auto eol=lf`、 - 远端 `https://git.thiztech.com/accounting/rentos.git`(Gitea)分支 `main`;`.gitignore` 排除
`data/sessions.json`/`batches`/`drafts`/`contracts`/`attachments`/`_snap`/`_trash`/`output/*`
(**`!output/*.md` 保留验收报告**)/`项目合约/`;`.gitattributes`:`* text=auto eol=lf`、
`*.bat -text`、**Dockerfile 必须 LF** `*.bat -text`、**Dockerfile 必须 LF**
## 验证脚本(tools/) ## 验证脚本(tools/)
...@@ -187,19 +174,18 @@ ...@@ -187,19 +174,18 @@
`test_accounts` 60 · `test_round6` 44 · `test_masters` 19 · `test_contracts` 21 `test_accounts` 60 · `test_round6` 44 · `test_masters` 19 · `test_contracts` 21
(`test_scheduler.py` 需活跃 8899 服务,无服务时失败属预期) (`test_scheduler.py` 需活跃 8899 服务,无服务时失败属预期)
- UI 回归:**自带沙箱** `ui_verify7~12.py`;**需连服务** `ui_verify`·`2`~`6.py` - UI 回归:**自带沙箱** `ui_verify7~12.py`;**需连服务** `ui_verify`·`2`~`6.py`
→ 台账+电费录入 / 设置+锁定+批量暂存 / 带入+照明+图片 / 公司管理 / 房租出单全链 / (台账+电费录入 / 设置+批量暂存 / 带入+图片 / 公司管理 / 房租出单全链 / 收款开票+导出 /
收款开票+导出 / 访客能力 / 账期区间 / 批量编辑 / 行粒度核对+账期汇总 / **合同原件(ui_verify12)** 访客能力 / 账期区间 / 批量编辑 / 行粒度核对 / **合同原件**)
- API 冒烟(沙箱):`smoke_verify_edit.py` · `smoke_verify_contracts.py`(合同 24 项) - API 冒烟(沙箱):`smoke_verify_edit.py` · `smoke_verify_contracts.py`(合同 24 项)
- ⚠️ `ui_verify7` 的只读样本是脚本自建 `ro_verify`(guest 已是「电费操作员」,不能当纯只读样本) - ⚠️ `ui_verify7` 的只读样本是脚本自建 `ro_verify`(guest 已是「电费操作员」,不能当纯只读样本)
- **`tools/ui_verify_runner.py <脚本…>`**:每个脚本起**全新沙箱**(复制 data/ + 改写 config 常量 + - **`tools/ui_verify_runner.py <脚本…>`**:每个脚本起**全新沙箱**(复制 data/ + 改写 config 常量 +
另起端口 + 注入 `RENTOS_BASE`);⚠️ **1~6 号有硬闸门**(未设 `RENTOS_BASE`/`RENTOS_ALLOW_REAL=1` 直接退出) 另起端口 + 注入 `RENTOS_BASE`);⚠️ **1~6 号有硬闸门**(未设 `RENTOS_BASE`/`RENTOS_ALLOW_REAL=1` 直接退出)
- ⚠️ **沙箱常量清单要同步补**(`BATCH_DIR`/`OUTPUT_DIR`/`DRAFT_DIR`/`CONTRACT_DIR`)—— - ⚠️ **沙箱常量清单要同步补**(`BATCH_DIR`/`OUTPUT_DIR`/`DRAFT_DIR`/`CONTRACT_DIR`)——
共 7 处:`ui_verify_runner` + `ui_verify7/8/9/10/11` + `check_scheduler`;漏了就会写进**真实** `data/` 共 7 处:`ui_verify_runner` + `ui_verify7/8/9/10/11` + `check_scheduler`;漏了会写进**真实** `data/`
(2026-10-13 直连 8899 曾误写 17 个测试批次) (2026-10-13 直连 8899 曾误写 17 个测试批次)
- 迁移:`migrate_rentfree_type.py` · `import_park_notes.py` · `migrate_next_bill.py` - 其他:迁移 `migrate_rentfree_type/import_park_notes/migrate_next_bill.py`;端到端 `e2e_round5.py`
- 端到端 `e2e_round5.py`(PDF 出单 → 收款开票 → 导出 xlsx → 排期 → 删除回滚) - **断言不要写死行数/列数/日期**,**按列 id / 表头文字定位**(新增列使 DOM 位置后移,
- **断言不要写死行数/列数/日期**,**按列 id / 表头文字定位**,不按第 N 列 `ui_verify3` 的 `tds[14]` 曾误读「付款周期」)
(新增列使 DOM 位置后移,`ui_verify3` 的 `tds[14]` 曾误读「付款周期」)
## 工作规矩(巴特菲) ## 工作规矩(巴特菲)
- **UI 回归不要每次小改都跑**,等他发话;小改只跑单元测试 - **UI 回归不要每次小改都跑**,等他发话;小改只跑单元测试
...@@ -211,6 +197,7 @@ ...@@ -211,6 +197,7 @@
## 待巴特菲拍板 ## 待巴特菲拍板
多办公地是否带公司名、车位是否算「办公地」、同名单据加期间、电费 0 用量是否出单、 多办公地是否带公司名、车位是否算「办公地」、同名单据加期间、电费 0 用量是否出单、
4F/5F 与爱种草渠道口径、通知日期/缴费期限口径、是否做收款核销; 4F/5F 与爱种草渠道口径、通知日期/缴费期限口径、是否做收款核销;
**合同核对发现的 17 处台账出入要不要动手修**;本机 4 张残留测试批次是否移 `_trash/`。 **合同核对发现的 17 处台账出入要不要动手修**;本机 4 张残留测试批次是否移 `_trash/`
(`20261014-134503`、`20261014-134504`、`20261015-134717`、`20261015-135238`)。
**未处置**:9 月电费批次多计 10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记「精锐」; **未处置**:9 月电费批次多计 10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记「精锐」;
`ui_verify11` 里「保存修改」按钮的断言待改写。 `ui_verify11` 里「保存修改」按钮的断言待改写。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment