Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
R
rentos
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
accounting
rentos
Commits
0b01156f
Commit
0b01156f
authored
Oct 15, 2026
by
wenwen.tang
😕
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
docs(memory): 加「合同原件」章节;精简 MEMORY.md(17.6KB→13.6KB 并保持口径完整)
parent
bcbfeeba
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
189 additions
and
124 deletions
+189
-124
2026-10-15.md
.workbuddy/memory/2026-10-15.md
+73
-0
MEMORY.md
.workbuddy/memory/MEMORY.md
+116
-124
No files found.
.workbuddy/memory/2026-10-15.md
0 → 100644
View file @
0b01156f
# 2026-10-15 · 房租车位费台账「合同原件」列
## 需求(巴特菲)
> @项目合约/ 房租车位费数据表需要支持上传合同留档并可查阅合同,增加合同原件的列。
## 交付
**新列 AL「合同原件」**
(
`type=files`
,排在 AJ 之后、最右),一格可挂多份
(租赁合同 + 补充协议 + 车位合同)。单元格存紧凑 JSON
`[{"id","name"}]`
,文件本体独立存放。
**新增 `rentos/contracts.py`**
(对照
`attachments.py`
,但白名单与上限不同)
-
`data/contracts/{id}.{ext}`
+
`index.json`
;id = uuid4 hex;
`clean_id`
挡路径穿越 +
`_safe_name`
去目录
-
白名单 pdf/doc/docx/xls/xlsx/png/jpg/gif/webp/bmp,单份
**≤ 20MB**
(合同扫描件比图片大)
-
`save / find / path_of / list_all / remove / parse / dump / describe`
-
`parse`
容错:JSON 数组 / 纯 id 串 / 脏项 / 去重;
`dump`
空列表 →
`""`
-
`describe`
给前端补 url / size / at / exists(文件被删也留位置提示)
-
`_decode`
兜底规则:data URL 带的类型不认识时
**回退按扩展名**
猜(否则 octet-stream 会被拒)
**`rentos/config.py`**
:加
`CONTRACT_DIR = data/contracts`
,进
`ensure_dirs()`
**`rentos/tables.py`**
-
`TYPES`
加
`files`
;
`_c("AL","合同原件","","files",168)`
排在
`AJ`
之后
-
`coerce(ctype='files')`
→
`_files_value()`
:list → 紧凑 JSON;
`""`
/
`"[]"`
→
`None`
-
新增
`files_text()`
:导出 xlsx 时该列输出
**文件名文本**
(多份换行 +
`wrap_text`
)
-
列定义以代码为准 →
**加列不需要数据迁移**
(实测线上/本地都无需 migrate)
**`rentos/server.py`**
-
`GET /api/contracts`
(列表)·
`GET /api/contracts/{id}`
(内联预览 PDF/图片,
`?dl=1`
下载,支持
`?token=`
)
·
`POST /api/contracts`
(上传)·
`DELETE /api/contracts/{id}`
-
`_CTYPES`
补 doc/docx/xls/xlsx/png/jpg/jpeg;
**权限**
:合同写接口不匹配任何
`CAP_ELEC`
规则 →
落到
`_write_cap`
兜底
`"*"`
=
**仅管理员**
;访客只读能查阅、不能增删
**前端 `web/app.js` / `admin.html` / `style.css`**
-
`filesOf / fileUrl / rowLabelOf / filesValueOf / ensureFileIndex / setFiles / openFilesDlg /
renderFilesDlg / pickContract / removeContract`
(
`FDLG`
当前行、
`FINDEX`
索引缓存,进页重置)
-
`td.files`
渲染按钮:有合同 →
`📎 N 份`
;编辑态空 →
`+ 合同`
;只读空 →
`—`
-
浮层:文件名 / 大小 / 上传时间 + 查阅 / 下载 / 移除,底部「上传合同」;
**增删走 `stageCell(rowId,'AL','files',…)` 暂存**
,与台账「保存 N 处改动」同一口径;
只读态浮层无上传/移除,文案改为「需要增删请先勾选编辑模式」
-
`startEdit`
对
`files`
列直接 return;
`delRow`
/
`startEdit`
的行标签统一走
`rowLabelOf()`
-
`fmtVal`
加
`files`
分支(历史/确认框里显示文件名列表)
**批量导入 `tools/import_contracts.py`**
(默认干跑)
-
解析文件名
**开头**
的单元号串:
`1101、1115`
/
`124.155号`
/
`132&227车位`
/
`501-502室`
/
`601A&702`
/
`169`
-
每个号按台账实际存在的
`n室`
/
`n车位`
定夺(两者都在时看「车位/号」线索)
-
干跑结果:
**29 个文件 → 35 个单元,零未匹配、零歧义**
(详见下方清单要点)
-
`--apply`
写入(追加;
`--replace`
覆盖)
**`.gitignore`**
:加
`data/contracts/`
(运行时数据)与
`项目合约/`
(93MB + 商业条款,不入库)
## 验证
-
单测
**328 项全绿**
:新增
`test_contracts.py`
**20 项**
(解码/存取/穿越/列值容错/coerce/files_text/
台账写读+导出 xlsx/目录隔离);
`test_rules`
3 处列数断言 31→32(含「合同列排最后」)
-
API 冒烟
**`tools/smoke_verify_contracts.py` 24 项全过**
(经 runner 沙箱):
上传→查阅 200+application/pdf+字节一致→
`?dl=1`
是 attachment→写 AL→导出 xlsx 是文件名→
访客上传/删 403 但可查阅→非法类型 400→穿越 404→清空 AL→删文件后 404
-
UI 回归
**`tools/ui_verify12.py` 30 项全过**
(自带沙箱,真浏览器):
表头「合同原件」在最后一列 / 只读无上传入口 / 编辑态
`+ 合同`
→ 浮层 → 真实文件选择器上传 →
浮层出现文件与三个操作 → 查阅链接带 token 且返回原字节 → 关浮层格子变
`📎 1 份`
且 pending →
保存后待保存条消失 + AL 落盘 → 只读浮层只有查阅/下载 → 移除 → AL 清空但
**服务器文件仍在**
→ 无 console 错误
-
**修掉一个真 bug**
:
`renderGrid`
的
`files`
分支原先读台账原值
`raw`
,
上传(暂存未保存)后格子仍显示「+ 合同」→ 改为优先读
`TB_PEND`
暂存值
## 代码提交
-
待提交:
`rentos/contracts.py`
(新)
`rentos/config.py`
`rentos/tables.py`
`rentos/server.py`
`web/app.js`
`web/admin.html`
`web/style.css`
`.gitignore`
`tools/test_contracts.py`
(新)
`tools/smoke_verify_contracts.py`
(新)
`tools/ui_verify12.py`
(新)
`tools/import_contracts.py`
(新)
`tools/test_rules.py`
`tools/ui_verify_runner.py`
`tools/ui_verify7/8/9/10/11.py`
`tools/check_scheduler.py`
## 待办
-
**29 份合同要不要真导入**
(本地 / 线上)——工具已就绪,干跑零误判,等巴特菲发话
-
9 月电费批次错计(10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记精锐)仍未处置
-
`ui_verify11`
里「保存修改」按钮的断言需同步改写(10-14 删了该按钮)
.workbuddy/memory/MEMORY.md
View file @
0b01156f
# 租金通知系统 · 项目长期记忆
## 目标 & 命名
三份 Excel → 固定模板 + 数据填充 →
**PDF 收费通知书**
(2026-10-12 起)
,自动发企微 + Web 维护。
三份 Excel → 固定模板 + 数据填充 →
**PDF 收费通知书**
,自动发企微 + Web 维护。
-
文件名 =
`{单元列表}-{费用类型}-收费通知书.pdf`
;单元按
`B.unit_sort_key`
排序、
`、`
连接;
**不含公司名**
-
费用类型词序固定:房租 → 物业费 → 车位费
(
`&`
合法)
-
费用类型词序固定:房租 → 物业费 → 车位费
## 数据模型
-
**唯一台账**
`rent_parking`
(85 行 ×
**3
1 列**
= A~AC +
**AK**
+ AJ
)
-
**唯一台账**
`rent_parking`
(85 行 ×
**3
2 列**
)
-
`BILL_COLS`
: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
-
**AK「免租类型」**
(2026-10-13 新增,
**显示在 L 之后**
):
`首N月免租`
/
`每年8-9月免租`
,空=无免租
-
AJ「下一账单日」
**只用于定时排期**
(推送日 = AJ − 提前天数);出单口径看 AC;出单后 AC 与 AJ 一起推进
-
**AD~AI 房产/房产税六列已删**
(
`RETIRED_TAX_COLS`
,守护
`test_retired_cols_not_used_by_billing`
)
last_paid=AB bill_start=AC next_bill=AJ;另
**AL「合同原件」**
(type=
`files`
,排最右)
-
**AK「免租类型」**
(显示在 L 之后):
`首N月免租`
/
`每年8-9月免租`
,空=无免租
-
AJ「下一账单日」
**只用于定时排期**
(推送日 = AJ − 提前天数);出单口径看 AC;出单后 AC/AJ 一起推进
-
**AD~AI 房产/房产税六列已删**
(
`RETIRED_TAX_COLS`
,有守护测试);列定义
**以代码为准**
(
`load_table`
用 spec 覆盖存档列)→
**加列不需要数据迁移**
-
默认锁定只读;改单元格/增行/删行必须
`confirm=true`
(否则 428);备份
`data/tables/.backup/`
-
**电费批次**
`data/batches/{id}.json`
:只留原始数据(details/totals/documents/发送状态),按账期月份留档;
测试残留移入
`data/batches/_trash/YYYYMMDD/`
(store 只 listdir 顶层
`*.json`
,子目录隐身、可恢复)
-
已有回收站:
`_trash/20261013/`
(17 张)、
`_trash/20261014/`
(14 张,output 同批移入
`output/elec/_trash/`
)
-
**账期口径**
(2026-10-14 确认):
**批次 = 点一次「生成通知书」的产物**
(号
`YYYYMMDD-HHMMSS`
·
`make_batch_id`
纯 ASCII 时间戳);
**账期 `period_label` 只是批次上的一个文本字段,不是独立实体**
,
`list_periods()`
按它归组即为页面上的「账期汇总」(组内按
`created_at`
倒序)
-
来源:① 录入页「所属期间」(默认今天年月;点「带入上一期」时
`nextPeriod()`
+1 月)—— 主要
② Excel 解析
`elec_parser._extract_period`
(先正则抓 A1 标题「YYYY年M月」,再退抄表日期年月)
③ 后端兜底
`ingest_rows`
:未传时用
**通知日期**
的年月
-
⚠️
**自由文本、无格式归一化**
:匹配是
`strip()`
后精确比较 → 「2026-11」与「2026年11月」会分成两组;
空值归「(未标账期)」
-
同账期多张批次是正常的(分批录 / 按公司分批 / 补录),故台账按账期汇总并默认折叠
## 免租(`rentos/rentfree.py`,2026-10-13 重写为两种类型)
-
**电费批次**
`data/batches/{id}.json`
:只留原始数据,按账期留档;测试残留移入
`data/batches/_trash/YYYYMMDD/`
(store 只 listdir 顶层
`*.json`
,子目录隐身、可恢复)
-
**账期口径**
:
**批次 = 点一次「生成通知书」的产物**
(
`YYYYMMDD-HHMMSS`
);账期
`period_label`
只是批次上的
**文本字段**
,
`list_periods()`
按它归组 = 页面「账期汇总」(组内
`created_at`
倒序)
-
来源:① 录入页「所属期间」(默认今天年月;带入上期时 +1 月)②
`elec_parser._extract_period`
③ 后端兜底用
**通知日期**
年月
-
⚠️
**自由文本、无归一化**
:
`strip()`
后精确比较 →「2026-11」与「2026年11月」会分成两组
-
同账期多张批次正常(分批录 / 按公司分批 / 补录)→ 台账按账期汇总并默认折叠
## 免租(`rentos/rentfree.py`)
-
`首N月免租`
:窗口
`[I, I+L月)`
,含起始不含结束;0.5 月 = 15 天
-
`每年8-9月免租`
:合约期
`[I, J]`
内每个自然年
`[max(该年08-01,
I), min(该年10-01, J+1天))`
(跨年裁剪)
-
`hit()`
按时点;
`overlaps()`
按账单期间交集(「每年8-9月」必需
,通知日通常早于该期
)
-
`每年8-9月免租`
:合约期
`[I, J]`
内每个自然年
`[max(该年08-01,I), min(该年10-01, J+1天))`
(跨年裁剪)
-
`hit()`
按时点;
`overlaps()`
按账单期间交集(「每年8-9月」必需)
-
⚠️
`windows()`
返回键是
**`begin`/`end`**
(非 from/to),另有
`type`
/
`months`
/
`year`
/
`key`
-
另有
`types()`
/
`windows()`
/
`active()`
/
`describe()`
/
`strip()`
## 名册(`data/config.json`,唯一入口 `rentos/directory.py`)
-
`companies`
租客
/往来单位(
`kind`
=company|person,
**名称即唯一键**
);
`owners`
出租方(
**键=简称**
)
-
`companies`
租客
(名称即唯一键,
`kind`
=company|person
);
`owners`
出租方(
**键=简称**
)
-
当前 4 家:精锐 37 / 科博 24 / 瑞格嘉德 16 / 国隆 8
-
`None`
/缺失 = 未维护过 → 回落
`DEFAULT_OWNERS`
;
**写过(哪怕 `{}`)= 权威**
,可真增删改
-
归一
`_norm_name`
;
改名
**联动台账**
(F 列/A 列);删除有引用 → 409,
`force=true`
才真删
-
`None`
/缺失 = 未维护过 → 回落
`DEFAULT_OWNERS`
;
**写过(哪怕 `{}`)= 权威**
-
改名
**联动台账**
(F 列/A 列);删除有引用 → 409,
`force=true`
才真删
-
归属三级优先:
**行内 `owner` > 台账 A 列 > 名单兜底(仅当名单恰好一家)**
-
⚠️ 写入必须
`store.set_config()`
(顶层整体替换);
`patch_config`
浅合并
**删不掉字典键**
## 业务口径
-
电费:
`用量 = 本期 − 上期`
;
`空调 = 用量 × 电价 × 12.25`
、
**`照明` = 用量 × 电价**
(类目统一叫「照明」)
-
电费:
`用量 = 本期 − 上期`
;
`空调 = 用量 × 电价 × 12.25`
、
**`照明` = 用量 × 电价**
-
只填上期不填本期 →
`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()`
)
## 电费核对(
2026-10-13 改「行」
粒度)
## 电费核对(
行
粒度)
-
行键 =
`单元|类目`
(如
`302室|照明`
);
`verify = {units:[], rows:[], done, at, by, scope}`
(旧
`units`
按整室自动展开,向后兼容;归一后只留
`rows`
,否则取消某行会被 units 抵消)
-
`verify_summary()["ok"] = done`
(
**已点确认即可,允许部分核对**
);
`partial`
= done 且有未勾行
-
**自动撤销**
:确认时把勾选快照存
`scope`
;之后进度保存若勾选少于 scope → 撤销确认
-
`verify_summary()["ok"] = done`
(
**确认即可,允许部分核对**
);
`partial`
= done 且有未勾行
-
**自动撤销**
:确认时把勾选快照存
`scope`
;之后勾选少于 scope → 撤销确认
-
发送闸门:未确认 → 409;部分核对发送前前端
`partialVerifyWarn()`
二次提醒
-
账期级:
`period_summary`
行并集,一行算已核对 ⇔
**含它的每张批次都勾了它**
;
`POST /api/periods/verify`
一次写全账期
-
前端:核对模式按行勾选 + 列显示菜单(
`localStorage['rentos_vcols']`
,「核对」列不可隐藏);
**重新编辑/核对入口只在批次详情**
;台账列表按账期汇总、默认折叠(
`tbody.pgrp.collapsed`
)
-
列显示菜单(
`setVColsOpen`
):开关状态存模块级
`VCOLS_OPEN`
(模式条重渲染不丢),
收起方式 = 再点按钮 /
`×`
/ 点外部 / Esc;空间不足自动
`.up`
向上弹
-
⚠️
**HTML `hidden` 属性不是 `!important`**
:组件类上只要写了
`display:flex/grid`
,
`hidden=true`
就
**完全失效**
→ 凡用
`hidden`
控制显隐,必须补
`[hidden]{display:none}`
-
⚠️
**会 `innerHTML` 重渲染的绑定函数里不要挂 document 级监听**
:每渲染一次堆一个,
旧闭包持有已移除的节点 → 用一次性模块标志(
`VCOLS_DOC_BOUND`
)或事件委托
## 电费「核对中改数据」(2026-10-14 · 当日晚改口径)
核对表
**可直接编辑**
,
**改数据不影响核对状态**
;点「确认核对」才把改动
**一并存盘**
。
前端
`confirmVerify()`
两步:①
`saveVerifyData()`
按
`_bid`
分组逐张
`PUT /api/batches/{id}`
(有改动才发请求)→ ②
`POST /api/periods/verify {rows, done:true}`
把勾选整期写成「已核对」。
于是「改过的行要不要重新核」由
**用户勾了没勾**
决定,服务端不猜。
-
**已删**
`keep_verify`
/
`_kept_verify`
/
`_detail_fp`
/
`_same_detail`
(旧「改过的行退回待核对」
口径被上面两步取代,留着会与新口径矛盾);
`reingest`
恢复为
**一律重置核对**
-
前端脏数据基线:
`ELEC_CMP_FIELDS`
+
`ESNAP`
/
`takeVerifySnapshot()`
/
`dirtyRows()`
/
`editSummary()`
-
⚠️ 基线键必须用稳定
`_rid`
,
**不能用数组下标**
(删中间行会让基线整体错位)
-
⚠️ 改动提示
**只改文字、绝不重渲染模式条**
(
`markVerifyDirty`
300ms 节流),否则输入框失焦
-
⚠️
`PUT`
是
**全量覆盖**
:每组必须带上该批次的
**全部行**
,只是「哪些批次要发」按有无改动筛
-
⚠️ 退出核对若有未保存改动要
**明确警告会丢失**
(数据只在确认时落盘)
## 电费录入「临时保存」草稿(2026-10-14)
录入页「临时保存」把当前明细整份落盘
`data/drafts/elec__{账号}.json`
(
**按账号隔离**
),
下次进电费页自动恢复、直接接着编辑;生成通知书 / 清空 / 手动丢弃时删除草稿。
-
接口
`GET|PUT|DELETE /api/drafts/elec`
;
`_write_cap`
里 PUT/DELETE 记
`CAP_ELEC`
(访客可用)
-
`store.draft_path/save_draft/load_draft/delete_draft`
;账号名净化
`[^0-9A-Za-z_\-] → _`
-
前端
`saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`
;
`applyNav`
里电费页改调
`elecBootFill()`
——
**有草稿就不「带入上期」**
(否则互相覆盖)
-
⚠️
**沙箱白名单必须加 `DRAFT_DIR`**
:
`ui_verify_runner`
与自带沙箱的
`7/8/9/10/11`
、
`check_scheduler`
都逐一补过 —— 漏了就会把草稿写进
**真实**
`data/drafts/`
(与批次泄漏同类坑)
-
账期级:一行算已核对 ⇔
**含它的每张批次都勾了它**
;
`POST /api/periods/verify`
一次写全账期
-
核对表
**可直接编辑**
,
**改数据不影响核对状态**
;
`confirmVerify()`
两步:
①
`saveVerifyData()`
按
`_bid`
分组逐张
`PUT /api/batches/{id}`
(有改动才发)
②
`POST /api/periods/verify {rows, done:true}`
整期写成「已核对」→ 要不要重核
**由用户勾选决定**
,服务端不猜
-
已删
`keep_verify`
那套(与新口径矛盾);
`reingest`
=
**一律重置核对**
-
脏基线:
`ELEC_CMP_FIELDS`
+
`ESNAP`
/
`takeVerifySnapshot()`
/
`dirtyRows()`
/
`editSummary()`
-
⚠️ 基线键用稳定
`_rid`
,
**不能用数组下标**
;⚠️ 改动提示只改文字、绝不重渲染模式条(否则输入框失焦)
-
⚠️
`PUT`
是
**全量覆盖**
:每组必须带该批次
**全部行**
;退出核对有未保存改动要
**明确警告会丢**
-
前端:核对模式按行勾选 + 列显示菜单(
`localStorage['rentos_vcols']`
);
**重新编辑/核对入口只在批次详情**
-
列显示菜单:开关状态存模块级
`VCOLS_OPEN`
;收起 = 再点按钮 /
`×`
/ 点外部 / Esc;空间不足自动
`.up`
-
⚠️
**HTML `hidden` 不是 `!important`**
:类里写了
`display:flex/grid`
就完全失效 → 必须补
`[hidden]{display:none}`
-
⚠️
**会 `innerHTML` 重渲染的绑定函数里别挂 document 级监听**
(用一次性模块标志或事件委托)
## 电费录入「临时保存」草稿
-
「临时保存」把明细整份落盘
`data/drafts/elec__{账号}.json`
(
**按账号隔离**
);下次进页
**自动恢复**
;
生成通知书 / 清空 / 手动丢弃时删除。
-
接口
`GET|PUT|DELETE /api/drafts/elec`
(
`_write_cap`
记
`CAP_ELEC`
,访客可用)
-
前端
`saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`
;
`applyNav`
改调
`elecBootFill()`
——
**有草稿就不「带入上期」**
## 合同原件(台账 AL 列,2026-10-15 新增)
一格可挂多份(租赁合同 + 补充协议 + 车位合同)。
-
单元格值 = 紧凑 JSON
`[{"id","name"}]`
;本体在
`data/contracts/{id}.{ext}`
+
`index.json`
-
`rentos/contracts.py`
:save/find/path_of/list_all/remove/parse/dump/describe;
白名单 pdf/doc/docx/xls/xlsx/png/jpg/gif/webp/bmp,单份
**≤ 20MB**
;id=uuid4 hex,
`clean_id`
挡路径穿越
-
接口:
`POST|GET /api/contracts`
·
`GET /api/contracts/{id}`
(内联预览;
`?dl=1`
下载;支持
`?token=`
)·
`DELETE /api/contracts/{id}`
——
**增删仅管理员**
(访客可查阅、不能增删)
-
前端:
`td.files`
渲染按钮(
`📎 N 份`
/ 编辑态
`+ 合同`
)→
`openFilesDlg()`
浮层;
增删走
`stageCell(rowId,'AL','files',…)`
暂存,
**与台账「保存 N 处改动」同一口径**
-
⚠️
**从某行移除只解关联、不删文件**
(多行可共享同一份)
-
⚠️
`renderGrid`
渲染
`files`
列
**必须读 `TB_PEND` 暂存值**
,否则上传后格子仍显示旧的「+ 合同」
-
导出 xlsx 该列输出
**文件名文本**
(
`tables.files_text`
),不是 JSON
-
批量导入:
`tools/import_contracts.py`
(默认干跑,
`--apply`
写入,
`--replace`
覆盖);
解析文件名
**开头**
的单元号串(
`1101、1115`
/
`124.155号`
/
`132&227车位`
/
`501-502室`
/
`601A&702`
),
再按台账实际存在的
`n室`
/
`n车位`
定夺;
`项目合约/`
(93MB)
**不入库**
## 房租车位费出单
链路:
`rent_parser.py`
→
`rent_render.py`
(→
`notice_pdf`
)→
`service.ingest_rent`
→ 批次
-
计费:期间起 =
**AC**
,止 = 起 + N 个自然月 − 1 天;
`房屋租赁费 = P × N`
、
`物业费 = O × N`
;
单元号含「车位」→ 车位租赁费;N 缺值
/非法
回落
`DEFAULT_MONTHS = 3`
单元号含「车位」→ 车位租赁费;N 缺值回落
`DEFAULT_MONTHS = 3`
-
**不可出单三条件**
:空置(F 空或含「空置」)/ 每月租金 ≤ 0 / AC 空(各带
`reasons`
)
-
**按免租数据跳过**
(2026-10-13):
`rent_preview`
对每行与每份单据标
`rentfree`
,汇总给
`rentfree_count/rows
`
-
**按免租数据跳过**
:
`rent_preview`
对每行与每份单据标
`rentfree
`
-
房间费与车位费
**合并一张**
(键
`(出租方, 租客, 期间)`
);批次按
`kind`
隔离
-
推进/回滚走
`tables.update_cells`
;
`rollback_rent_advance`
**只回滚当前值仍等于推进后的行**
-
**账期区间筛选**
:
`classify_period(rows, from, to)`
命中 = 账期起始日落在区间内,
**取代出单基准日**
(旧
`?on=`
保留为
`to`
别名);批次记
`period_filter_from/to`
-
**账期区间筛选**
:
`classify_period(from, to)`
命中 = 账期起始日落在区间内(旧
`?on=`
是
`to`
别名)
-
接口:
`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`
(收款/开票,
**只房租批次**
,
挂
`batch.documents[i]`
)·
`GET /api/batches/{id}/export`
(3 Sheet 落
`output/exports/`
)·
`PUT /api/tables/{key}/cells`
(见下节)
-
⚠️
`GET /api/batches/{id}`
的
`receipt`
在
**响应顶层**
,前端须显式接
`DELETE /api/batches/{id}`
`{rollback:true}`
·
`POST /api/batches/{id}/receipt`
(收款/开票,
**只房租批次**
)·
`GET /api/batches/{id}/export`
-
⚠️
`GET /api/batches/{id}`
的
`receipt`
在
**响应顶层**
## 台账编辑(
2026-10-13 改
批量暂存)
-
改格
**不
再
逐格弹确认**
:
`stageCell()`
暂存 + 黄底高亮(
`TB_PEND`
),顶条显示「N 处改动待保存」
## 台账编辑(批量暂存)
-
改格
**不逐格弹确认**
:
`stageCell()`
暂存 + 黄底高亮(
`TB_PEND`
),顶条显示「N 处改动待保存」
-
「保存全部」→ 一次
`confirmBox`
→ 单次
`PUT /api/tables/{key}/cells`
(
`{changes:[{row_id,col,value}], confirm:true, who}`
)→
`pushHist({t:'batch'})`
可
**整批撤销**
-
「放弃改动」全部还原;
`navTo()`
/
`beforeunload`
拦截未保存改动
-
⚠️
`renderGrid()`
末尾必须同时调
`paintPending()`
**和**
`renderPendBar()`
(漏后者 → 保存后待保存条不消失)
-
`navTo()`
/
`beforeunload`
拦截未保存改动
-
⚠️
`renderGrid()`
末尾必须同时调
`paintPending()`
**和**
`renderPendBar()`
-
确认框是
`.modal-mask.show`
(
**不是 `.open`**
)
## 系统骨架 / 渲染
-
启动
`python -m rentos.server --port 8899`
;
`--no-scheduler`
只开 Web;admin / 1989@rent05;
访客
**guest / guest@2026**
(
`role=viewer`
+
`perms=['elec']`
,只放开「电费通知单」页的写操作
)
-
**权限模型
(2026-10-13)**
:
`role`
(admin|viewer)+ 账号/会话的
`perms`
能力清单(目前只有
`elec`
)
访客
**guest / guest@2026**
(
`role=viewer`
+
`perms=['elec']`
)
-
**权限模型
**
:
`role`
(admin|viewer)+
`perms`
能力清单(目前只有
`elec`
)
-
管理员隐含
`{'*'}`
;
`viewer`
按
`perms`
**逐接口**
放行,
`perms=[]`
即纯只读
-
`auth.caps()/can()/normalize_perms()`
;
`server._write_cap(method,path)`
解析接口所需能力,
`_guard_write(user,cap)`
判定(403 文案「账号 X 只能操作:电费通知单」)
-
**批次类接口按批次 `kind` 复核**
:房租车位费的删/重出/发送/收款登记
**仍只有管理员**
能动
-
老配置一次性回填:
`auth.guest_perms_seeded`
标记 +
`DEFAULT_CONFIG`
同步加该键
-
前端:
`body.cap-elec`
才显示
`[data-cap="elec"]`
;
`data-ro`
仍是「仅管理员」;
账号管理新增「权限」列 + 一键开通/关闭(
`PUT /api/accounts/{name}`
);
**主账号不可降级**
-
导航 5 项:概览 / 电费通知单 / 房租车位费 / 公司管理 / 设置;真实
`<a href="#/...">`
+ hash 路由,
实现在
`app.js`
的
`PAGES`
+
`applyNav()`
;
**新增页面必须同时在 PAGES 与 `#nav` 登记**
-
CSS 表格类名
`edgrid`
;侧栏/顶栏读
`/api/overview`
(含 wecom-cli,慢)
-
`auth.caps()/can()`
;
`server._write_cap(method,path)`
解析所需能力,
`_guard_write()`
判定
-
**批次类接口按 `kind` 复核**
:房租车位费的删/重出/发送/收款
**仍只有管理员**
-
前端:
`body.cap-elec`
才显示
`[data-cap="elec"]`
;
`data-ro`
= 仅管理员;
**主账号不可降级**
-
导航 5 项:概览 / 电费通知单 / 房租车位费 / 公司管理 / 设置;hash 路由在
`app.js`
的
`PAGES`
+
`applyNav()`
;
**新增页面必须同时登记 PAGES 与 `#nav`**
-
**唯一渲染层**
`rentos/notice_pdf.py`
:reportlab + platypus 程序化绘制(
**不用 win32com / LibreOffice**
);
字体
`simhei.ttf`
子集嵌入
(单份 10~32KB);A4、
`MARGIN = 1.9cm`
;房租 5 列 / 电费 7 列,多单元按
`spans`
合并首列
字体
`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**
→
**单测与 UI 回归都用它**
-
Bash 先
`export PATH=…/PortableGit/versions/1.2.0/usr/bin:…/bin:$PATH`
-
`.bat`
必须
**cp936 + CRLF**
;分支用
`goto`
标签;echo 不放半角括号/
`!`
-
**改完后端代码必须 kill 所有在跑实例再重启**
(旧进程按旧逻辑写同一
`data/`
,甚至自动发消息)
-
**改完后端代码必须 kill 所有在跑实例再重启**
(旧进程会按旧逻辑写同一
`data/`
)
-
⚠️ 本工具链会把命令里的
`\n`
字面化 → 写多行脚本用 heredoc +
`textwrap.dedent`
,别用
`"\n".join`
## 启动 / 部署(192.168.68.121 · CentOS 7)
-
`start.bat [safe] [端口]`
/
`stop.bat [端口]`
;
`notify.enabled=True`
时双击
`start.bat`
**会开定时发送**
→
**会话内启动一律 `start.bat safe`**
-
⚠️ 会话里跑
`start.bat safe`
时,其内层
`start`
开新控制台会被环境拦(SIGTERM、无输出)→
改用后台任务直起等价命令:
`python -m rentos.server --port 8899 --no-scheduler`
(
`run_in_background`
)
-
`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/`
(admin / 1989@rent05),容器
`rentos`
,root / 12345678
-
**主用 Git 同步**
(⚠️
**必须用 venv python**
`…/envs/default/Scripts/python.exe`
,3.13.12 无 paramiko):
`RENTOS_SSH_PASSWORD=12345678 …python tools/deploy_git.py sync`
(setup/pull/build/run/status/logs)
-
push
需
绕代理:
`env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY git push`
-
**主用 Git 同步**
(⚠️
**必须用 venv python**
,3.13.12 无 paramiko):
`RENTOS_SSH_PASSWORD=12345678 …python tools/deploy_git.py sync`
-
push 绕代理:
`env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY git push`
-
代码落
`/opt/rentos/repo`
,与数据同级独立 →
`git reset --hard`
**不碰线上数据**
-
⚠️
**sync 只更代码、不动数据**
→
凡改列模型/加列的迁移,
**部署后必须在容器里手动再跑一次
**
(
如
`docker exec rentos python /app/tools/migrate_rentfree_type.py`
先干跑再加
`--apply`
)
-
构建必须在
**仓库根**
:
`docker build -f deploy/Dockerfile -t rentos:latest .`
,
构建期
零联网;
-
⚠️
**sync 只更代码、不动数据**
→
改列模型/加列的迁移
**部署后要在容器里手动再跑
**
(
`docker exec rentos python /app/tools/xxx.py`
,服务器
**无 Python 3**
)
-
构建必须在
**仓库根**
:
`docker build -f deploy/Dockerfile -t rentos:latest .`
,零联网;
**切勿 `docker build … | tail`**
(吞掉失败会拿旧镜像重启)
-
服务器
**无 Python 3**
→ 迁移只能
`docker exec rentos python /app/tools/xxx.py`
-
`docker -v`
的 SELinux 选项是
**冒号 `:z`**
(写成
`,z`
的症状:页面 200 但台账 0 行)
-
云端已能直发企微:
`/usr/local/bin/wecom-cli`
+ 凭据
`/opt/rentos/wecom-config/`
(700)
-
⚠️
**线上是唯一生产方,本机只作预览
;台账以线上为准
**
;
**本机与云端不要同时开定时器**
-
远端
`https://git.thiztech.com/accounting/rentos.git`
(
自建 Gitea;http 301 跳 https
),分支
`main`
;
纳入/排除见
`.gitignore`
(
`data/sessions.json`
含活跃 token 务必排除);
`.gitattributes`
:
`* text=auto eol=lf`
、
`*.bat -text`
、
**Dockerfile 必须 LF**
-
⚠️
**线上是唯一生产方,本机只作预览**
;
**本机与云端不要同时开定时器**
-
远端
`https://git.thiztech.com/accounting/rentos.git`
(
Gitea
),分支
`main`
;
`.gitignore`
排除
`data/sessions.json`
(含活跃 token)/
`batches`
/
`drafts`
/
`contracts`
/
`项目合约/`
;
`
.gitattributes`
:
`
* text=auto eol=lf`
、
`*.bat -text`
、
**Dockerfile 必须 LF**
## 验证脚本(tools/)
-
单测
**3
03
项**
:
`test_rules`
45 ·
`test_directory`
33 ·
`test_rent`
53 ·
`test_round5`
54 ·
`test_accounts`
60 ·
`test_round6`
39 ·
`test_masters`
19
-
单测
**3
28
项**
:
`test_rules`
45 ·
`test_directory`
33 ·
`test_rent`
53 ·
`test_round5`
54 ·
`test_accounts`
60 ·
`test_round6`
44 ·
`test_masters`
19 ·
**`test_contracts` 20**
(
`test_scheduler.py`
需活跃 8899 服务,无服务时失败属预期)
-
UI 回归:
**自带沙箱**
`ui_verify7/8/9/10/11.py`
;
**需连服务**
`ui_verify`
·
`2`
·
`3`
·
`4`
·
`5`
·
`6.py`
→ 依次覆盖 台账+电费录入 / 设置+锁定+批量暂存 / 带入+照明+图片 / 公司管理 / 房租出单全链 /
收款开票+导出 / 访客(纯只读 + guest 仅电费)+收款同步 / 账期区间 / 删房产列+免租两类型+批量编辑 /
访客能力三方对照 /
**行粒度核对+部分确认+账期汇总+列筛选(ui_verify11 85 项)**
-
⚠️
`ui_verify7`
的只读样本是脚本自建的
`ro_verify`
(无能力)—— guest 已改成「电费通知单操作员」,
不能再当纯只读样本用
-
**`tools/ui_verify_runner.py <脚本…>`**
:每个脚本起
**全新沙箱**
(复制
`data/`
+ 改写 config 常量 +
另起端口 + 注入
`RENTOS_BASE`
),真实
`data/`
零改动 —— 跑 1~6 必用
-
⚠️
**1~6 号脚本已加硬闸门**
:未设
`RENTOS_BASE`
且未设
`RENTOS_ALLOW_REAL=1`
直接退出
(2026-10-13 直连 8899 曾误写 17 个测试批次进真实
`data/batches/`
)
-
端到端
`e2e_round5.py`
(PDF 出单 → 收款开票 → 导出 xlsx → 排期 → 删除并回滚)
-
迁移:
`migrate_rentfree_type.py`
(删 AD~AI + 加 AK + 回填) ·
`import_park_notes.py`
(车位备注→AA) ·
`migrate_next_bill.py`
(AJ=AC)
-
**断言不要写死行数/列数/日期**
—— 读接口现取;表格
**按列 id / 表头文字定位**
,不按第 N 列
(2026-10-13 新增列使 DOM 位置整体后移,
`ui_verify3`
的
`tds[14]`
曾误读「付款周期」)
-
UI 回归:
**自带沙箱**
`ui_verify7~12.py`
;
**需连服务**
`ui_verify`
·
`2`
~
`6.py`
→ 台账+电费录入 / 设置+锁定+批量暂存 / 带入+照明+图片 / 公司管理 / 房租出单全链 /
收款开票+导出 / 访客能力 / 账期区间 / 批量编辑 / 行粒度核对+账期汇总 /
**合同原件(ui_verify12,30 项)**
-
API 冒烟(沙箱):
`smoke_verify_edit.py`
(核对态改数据 + 草稿)·
**`smoke_verify_contracts.py`(合同 24 项)**
-
⚠️
`ui_verify7`
的只读样本是脚本自建
`ro_verify`
(guest 已是「电费操作员」,不能当纯只读样本)
-
**`tools/ui_verify_runner.py <脚本…>`**
:每个脚本起
**全新沙箱**
(复制 data/ + 改写 config 常量 +
另起端口 + 注入
`RENTOS_BASE`
);⚠️
**1~6 号有硬闸门**
(未设
`RENTOS_BASE`
/
`RENTOS_ALLOW_REAL=1`
直接退出)
-
⚠️
**沙箱常量清单要同步补**
(
`BATCH_DIR`
/
`OUTPUT_DIR`
/
`DRAFT_DIR`
/
**`CONTRACT_DIR`**
)——
共 7 处:
`ui_verify_runner`
+
`ui_verify7/8/9/10/11`
+
`check_scheduler`
;漏了就会写进
**真实**
`data/`
(2026-10-13 直连 8899 曾误写 17 个测试批次)
-
迁移:
`migrate_rentfree_type.py`
·
`import_park_notes.py`
·
`migrate_next_bill.py`
-
端到端
`e2e_round5.py`
(PDF 出单 → 收款开票 → 导出 xlsx → 排期 → 删除回滚)
-
**断言不要写死行数/列数/日期**
,
**按列 id / 表头文字定位**
,不按第 N 列
(新增列使 DOM 位置后移,
`ui_verify3`
的
`tds[14]`
曾误读「付款周期」)
## 工作规矩(巴特菲)
-
**UI 回归不要每次小改都跑**
,等他发话;小改只跑单元测试
-
跑 UI 回归前先快照
`data/batches/`
、
`data/tables/rent_parking.json`
、
`data/config.json`
到
`data/_snap/`
-
回归脚本删行
**必须按行 id 定位**
(
`data-delrow`
=
`_id`
),
**绝不取「DOM 最后一行」**
-
`output/rent/<批次号>`
是
**目录**
;
台账确认框「行」标签 = 房间号优先 + 现租客(C 列)
-
台账确认框「行」标签 = 房间号优先 + 现租客(C 列)
-
**数据一致性铁律**
:线上台账权威,禁用本机数据覆盖线上
## 待巴特菲拍板
多办公地是否带公司名、车位是否算「办公地」、同名单据加期间、电费 0 用量是否出单、
4F/5F 与爱种草渠道口径、通知日期/缴费期限口径、是否做收款核销
(1934 行流水)
。
4F/5F 与爱种草渠道口径、通知日期/缴费期限口径、是否做收款核销。
**未处置**
:9 月电费批次多计 10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记「精锐」。
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment