Commit d635a330 authored by wenwen.tang's avatar wenwen.tang 😕

chore: 租金通知系统首次纳入 Git 版本管理

纳入范围
- 应用源码 rentos/(19 个模块)、前端 web/、工具与测试 tools/(63 个脚本)
- 业务数据:台账 data/tables/rent_parking.json(85 行 × 36 列 A~AJ)、
  播种 data/seed/、配置 data/config.json 与 unit_map.json
- 版式资源:templates/ 四套模板、samples/ 历史样张、assets/ 中文字体 simhei.ttf
- 离线部署件:deploy/Dockerfile、deploy/wheels/(Linux wheel)、企微 CLI tgz
- 验收报告归档 output/*.md

忽略规则
- 运行时状态:data/sessions.json(含活跃登录 token)、scheduler_state.json、
  send_log.json、data/batches/、data/attachments/、logs/
- 生成产物:output/(仅保留 *.md 报告)、uploads/
- 快照与自动备份:data/_snap/、data/tables/.backup/
- 构建中间产物:deploy/_stage/、deploy/wecom/x/
- 截图与缓存:tools/shots*/、__pycache__/

换行策略
- 文本统一以 LF 存储:Dockerfile 的续行符若带 \r 会导致 Linux 构建解析错误
- *.bat 保持 GBK + CRLF 字节原样;二进制资源不做任何改写

功能现状
- 房租车位费 / 电费两套出单链,PDF 出单(reportlab 原生绘制,不依赖 Office)
- 批次收款开票登记、按「下一账单日」定时自动出单、批次数据导出
- 单元测试 147 项 / 端到端 / UI 回归(29 + 40 项)全部通过
parents
Pipeline #831 canceled with stages
# ============================================================
# 换行与二进制属性
# ------------------------------------------------------------
# 本项目:Windows 开发 + Linux(Docker) 部署,两侧必须行为一致。
# 策略:
# · 文本文件统一以 LF 存储 —— Dockerfile 的续行符 `\` 一旦
# 带上 \r 会被 Linux 构建器解析错误,因此必须 LF。
# · 批处理(GBK 编码)与二进制资源保持字节原样,禁止任何改写。
# 注:工作区现有的 CRLF 文件在首次 checkout 时会转为 LF,
# 对 Python / JSON 的运行行为无影响。
# ============================================================
# 默认:自动识别文本,统一以 LF 存储
* text=auto eol=lf
# ---- 保持字节原样(禁止换行转换与编码猜测)----
*.bat -text
*.cmd -text
# ---- 二进制资源 ----
*.ttf binary
*.jpeg binary
*.jpg binary
*.png binary
*.xlsx binary
*.xls binary
*.docx binary
*.doc binary
*.pdf binary
*.whl binary
*.tgz binary
*.gz binary
*.zip binary
# -*- coding: utf-8 -*-
# ============================================================
# 租金通知系统 —— 版本库忽略规则
# 原则:
# 1. 纳入:全部源码、模板、字体、播种数据、台账、配置、离线部署件
# 2. 忽略:运行时状态、生成产物、自动备份/快照、截图、构建中间产物
# ============================================================
# ---- Python ----
__pycache__/
*.py[cod]
*.pyo
.pytest_cache/
*.egg-info/
# ---- 运行时状态 ----
# sessions.json 内含【活跃登录 token】,严禁入库
data/sessions.json
data/scheduler_state.json
data/send_log.json
data/config.json.before-round3
data/batches/
data/attachments/
logs/
# ---- 台账自动备份与改动快照(体积随时间增长)----
data/_snap/
data/tables/.backup/
data/tables/retired/.backup/
# ---- 生成产物(验收报告 .md 例外,保留归档)----
output/*
!output/*.md
uploads/
# ---- 字体探测遗留文件 ----
assets/fonts/_probe.pdf
# ---- 截图与临时文件 ----
tools/shots/
tools/shots2/
tools/shots3/
tools/shots4/
tools/shots5/
tools/shots6/
*.bak
*.tmp
*~
# ---- 部署构建中间产物(Dockerfile / wheels / 企微 tgz 已在库内)----
deploy/_stage/
deploy/wecom/x/
# ---- 编辑器与系统 ----
.vscode/
.idea/
Thumbs.db
desktop.ini
.DS_Store
# 2026-10-08 工作日志
## 租金通知系统(D:/AI/rentos)— 需求确认阶段
### 已探明的数据来源(4 张原始表)
1. `收费通知书.xlsx`(192KB,43 sheet)— 租金/车位/物业费通知书生成器,两个主体(精锐/科博);明细靠外部 VLOOKUP,外部表未随文件附带 → 原表大量 #N/A。
2. `3F、6F、7F电费收款通知单(20260921).xlsx`(258MB)— **体积 99% 来自 99 张嵌入图片(26 jpg + 73 png,246.8MB)**,表格数据仅 ~250KB。含 3F/6F/7F 抄表表、8月/9月电费统计表、14 张电费通知单。
3. `精锐合约租金收款明细表(2026).xlsx`(主数据源)— 含 **收款提醒表(文件1 VLOOKUP 要找的就是它)**、收款时点(1934行)、合同收入明细(3F/6F/7F、8F/11F&车位)、汇总表、爱种草结算单、佣金明细表、出租率、目录、大写转换器。
### 关键业务规则
- 主体:精锐 (上海) 仓储有限公司|交通银行上海陆家嘴支行|310066771018170062380|客户单元 金沙江路1989号
- 主体:科博(上海)仓储有限公司|上海银行营业部|31600703001235523|客户单元 金沙江路1999号
- 收费项目:房屋租赁费 / 车位租赁费 / 物业费;电费单列
- 收款周期 1/2/3/4 个月;滞纳金 逾期每日千分之五;确认期 收单后 2 个工作日
- 电费:房间电费 = 用量 × 1.2;空调电费 = 用量 × 1.2 × 12.25;电价 1.2 元/度
- 电费通知单可同租户多房间合并出单(601A + 702 → 上海长盟贸易)
### 环境事实(复用)
- openpyxl 3.1.5 已在 managed python 3.13.12 可用;258MB xlsx 用 `read_only=True` 约 10s 读完。
- `wecom-cli` v1.3.4 **已授权**(机器人「租金机器人」),但 `sessions list` 返回 `sessions_count=0` → **当前只能发授权人本人**,发群需先把机器人拉进群。
- 本机无 LibreOffice、无 `formulas` 包 → recalc 不可用,**新 xlsx 不要依赖公式缓存值**,汇总用构建期静态值。
- 表格编辑器通道(sheetagent open_file)打不开这两个文件(`UNOPENABLE_LOCAL_PATH` / No workbook open),已改用 openpyxl 直读。
### 本次交付
`D:/AI/rentos/租金通知系统-需求确认清单.xlsx`(10 sheet):00总览+业务规则 / 01输入表清单(10张) / 02输出表清单(6类) / 03-06 字段字典(65条) / 07-08 单据模板 / 09待确认事项(7项)。
建表脚本:`.租金通知系统-需求确认清单.ref/build.py`
种子抽取脚本:`tools/extract_seed.py` → `data/seed/{rent_units,electricity}.json`
### 状态
**已停在需求确认关卡**:等巴特菲逐条拍板 09-待确认事项 后,才进入系统开发(任务 #2/#3/#4 pending)。
---
## 需求转向:改为 Excel 产出物(按公司)— 已完成
巴特菲明确:**不要先做 Web 系统,要的是 Excel 产出文件,按公司给出**,样板 = `D:/AI/rentos/租金.xlsx`(单公司一张《收费通知书》)。
### 租金.xlsx 样板解剖(版式基准)
- 单 sheet,一家公司一张通知书;A1 出租方 / A3:E3 「收费通知书」16pt / G3,G4 房号(下拉 $X$2:$X$73)/ I3,I4「公司名称:」/ J3,J4 公司名
- A5「租户单元:金沙江路1989号」+ B5 单元列表;A6:C6「公司名称:xxx」;E6「通知日期: Y年M月D日」
- A7:E7 正文「尊敬的租户,您好! 接到通知后,于 … 前支付到我司账户。您本次应缴费用如下:」
- A8:E8 费用大类(有物业费 → 「房屋租赁费、车位租赁费及物业费」,否则「…及其他费用」)
- 第9行表头 A9「项 目」/ B9:D9「所 属 时 间」/ E9「本次应收(人民币)」
- 第10行起明细;合计行紧随其后(D:E 合并放人民币大写),再往下 = 合计+1 空行、+2「请注意以下事项:」、+3/+4 两条注意、+5「汇款信息:」、+6~+8 公司/开户行/账号、+9 空行、+10 地址
- 字体 WPS灵秀黑(正文11pt/标题16pt);金额 `#,##0.00_ "元"`;日期 `yyyy"年"m"月"d"日";@`;细边框只在 A 左 / E 右 / 各行上下
- 行高:1=18.75, 2=41.1, 3=27.95, 4=27, 5~9=27.95, 明细/合计=27.95, 空=39, 注意标题=23.1, 两条注意=42, 汇款标题=20.1, 汇款3行=24, 空=89.1, 地址=39
- 列宽 A24.875 B18.375 C2.5 D18.375 E23.625 F15.5 G6.875 H1.375 I10.625 J9 X9.5
- 含 1 张 logo(E1:E2,1201B JPEG);3 个外链指向《精锐合约租金收款明细表(2026)》
### 取值规则(用 43 张历史通知书 + 租金.xlsx 反向校验通过,**铁律**)
| 通知书元素 | 来源(《收款提醒表》,表头第9行,数据第10行起) |
|---|---|
| 公司名称 | G 列「现租客」 |
| 所属时间 | **R/S 列「应收租期(起/止)」**(不是 O/P 已收租期) |
| 房屋租赁费 | J 列「每期租金」 |
| 物业费 | K 列「每期物业费」(>0 才出行) |
| 车位租赁费 | J 列「每期租金」 |
| 合计 | 明细行之和 → 人民币大写 |
| 通知日期 | 批次参数(历史批次 2026-09-17~20) |
| 缴费期限 | 批次参数(历史批次恒为 2026-09-30,与租期起无关) |
- 验证锚点:02(19710+3024=22734)、303(21000)、305(12000)、306(16000)、310(37386)、312(34270.6→…陆角整)、706B(14668)、1101+1115(64800)、伯凌132+227车位(3600) 全部与历史单一致。
- **出租方判定**:房号首位 4/5/8 且以「室」结尾 → 科博;其余(3F/6F/7F/11F/车位)→ 精锐。
- 科博历史单据把「开户银行 / 银行账号」标签写反(A20=账号、A21=银行),本表已纠正。
### 交付物(本轮)
- 生成器:`D:/AI/rentos/build_notices.py`(纯 openpyxl,零第三方依赖;含人民币大写、动态行位版式、logo 注入)
- `output/01-收费通知书(按公司)/` — **30 份**单公司 xlsx(文件名 `收费通知书-{精锐|科博}-{公司}.xlsx`,同公司多租期加 `(起-止)`)
- `output/00-收费通知书汇总清单.xlsx` — 4 sheet:通知书清单(30)+费用明细(57)+汇总+口径与待确认
- `output/02-收费通知书(单文件·按公司分表).xlsx` — 30 sheet 合订本
- `output/03-收费通知书(按公司·打包).zip`
- 本期合计应缴 **¥695,642.43**;参数:通知日期 2026-10-08、缴费期限 2026-10-31
### 技术要点(复用)
- openpyxl **不保留图片**(load/save 会丢),也读不出 `ws._images`(原样板同样为 0)→ logo 改用**注入 zip 部件**方式:`xl/media/image1.jpeg` + `xl/drawings/drawing1.xml` + `drawings/_rels` + sheets rels + `[Content_Types].xml` Override,并在 sheet1.xml 追加 `<drawing r:id="rIdDraw1"/>`。
- **坑**:openpyxl 生成的 sheet XML 默认不带 `xmlns:r`,追加 drawing 前必须手动补声明,否则 Excel/openpyxl 报 `unbound prefix`。
- 合并区内子单元格是 `MergedCell`,`value` 只读(写会抛 AttributeError),但 **样式可写** → 用 `isinstance(c, MergedCell)` 跳过赋值。
### 待巴特菲确认(已写入汇总表「口径与待确认」sheet)
1. 科博「客户单元」是 1999号 还是 1989号(历史单据两种都有,现按 1999号)
2. 通知日期/缴费期限批次参数是否按 2026-10-08 / 2026-10-31
3. 4F/5F 及其余 8F 租户、爱种草渠道口径单据本期未纳入
4. 每期租金为 0 的单元(193/194/148/199/200/225 车位)本期未出单
This diff is collapsed.
# 2026-10-11
## 本轮:房产税数据并入台账 + 出租方名册补全
巴特菲给了 4 份文件(原件留档 `samples/legacy/`):
| 文件 | 用途 |
|---|---|
| 三家仓储房产税(2026).xlsx | 国隆/科博/瑞格 三 sheet,48 个房产单元 |
| 收费通知书-国隆-901.docx | 国隆银行信息(新主体) |
| 收费通知书-科博-803.docx | 印证科博现有配置 |
| 收费通知书-瑞格501、502.docx | **瑞格嘉德银行账号**(此前遗留的缺口) |
### 巴特菲拍板的三条口径
1. 房产税数据**并入「房租车位费」台账**,不新建表
2. 台账缺的 21 个单元**全部补录**
3. 5 个单元的出租方**以房产税表为准**
> 重要口径差异:房产税表的「公司名稱」= **房产持有主体**;
> 台账 A 列「出租方」原本 = **出单收款主体**。证据:房产税表科博 sheet 里
> 301 室的「客户单位名称」写的就是「精锐(上海)仓储有限公司」,
> 即 3F 301-304 是精锐向科博整租后再分租。本轮按 (3) 统一到持有主体。
### 改动清单
**`rentos/tables.py`** —— 台账加 6 列(AD~AI,**不参与出单**),新增 `TAX_COLS` 常量
```
AD 产证面积(㎡)num | AE 取得成本(元)money | AF 车位对应房间 text
AG 季度计税租金 money | AH 从租季度房产税 money | AI 从价季度房产税 money
```
**`rentos/config.py`** —— `DEFAULT_OWNERS`:
- 瑞格嘉德:补 上海银行营业部 / 31600703001391851,名称改全角
- **新增「国隆」**:国隆(上海)仓储有限公司 / 上海银行营业部 / 31600703001237712
**`tools/import_property_tax.py`**(新增)—— 房产税表 → 台账导入器,干跑 / `--apply` 两段式。
踩到的坑:三张 sheet 表头排版各有瑕疵,**不能靠文字匹配表头**:
- `header_map` 盲扫前几行会被数据行覆盖 → 改成先定位含「公司名称/序号」的表头行
- 国隆的税额在**下方汇总区**(「901(车位部分)/(房屋部分)」要累加)
- **科博的从价房产税实际在 R 列**(表头写在 Q 列,Q 其实是备注列,如「本月整月仍按从租计算」)
- 瑞格的从价税在 Q 列(与表头一致)
→ 最终按**实测坐标** `SHEET_COLS` 读,不猜。
**台账数据** `data/tables/rent_parking.json`:64 → **85 行**
- 补录 21 行:国隆 8(901-904 + 125-128 车位)、科博 7(304/401-404/803/804)、瑞格嘉德 6(501-504/601/604)
- 更正 5 行出租方:301/302/303 → 科博;602/603 → 瑞格嘉德
- 回填 AD~AI:产证面积/取得成本 48 行、车位对应房间 24 行
- **新增行 AC(账单起始日)与 AB 一律留空** —— 账期不明,防止一开页面把历史欠期全发出去
- 繁体 0 残留;租客名「锦玉良源」半角括号统一全角;「陈俊(原祈俊,换人承租)」的括号内容移到备注列
**名册**:owners 3 → **4 家**(精锐/科博/瑞格嘉德/国隆);companies 26 → **33 条**
**测试**:97 → **99 项全绿**。改 4 处既有断言(列数 29→35、出租方 3→4 家),
新增 2 个:`test_tax_columns_defined`、`test_tax_columns_not_used_by_billing`(守住「房产列不参与出单」)。
### 验证
- 出单链路回归正常:新补单元因 AC 空自动落进「不可出单」,未产生任何单据
- HTTP 冒烟:`/api/tables` 85 行 35 列、`/api/companies` 33 条、`/api/owners` 4 家
- 服务已在 8899 用新代码重启(`--no-scheduler`)
- 备份:`data/tables/.backup/` + `data/_snap/rent_parking.before_tax_20261011-173409.json`
### 交付物
`output/验收报告-房产税数据并入台账.md`
### 待巴特菲确认
1. 新增 21 行的**账单起始日**要人工填;docx 里有 3 条线索(901室 / 803室 / 501+502室)
2. 901/902/904/304/604室、125/127车位的**每月租金**房产税表为空,补了账期也出不了单
3. 新行**物业费(O列)全空** → 出单只出「房屋租赁费」
4. 面积口径:产证面积 vs 台账 D/E 服务/建筑面积是两套口径(302室 444.43 vs 80/55.79)
### 规矩(沿用 2026-10-09)
**UI 回归不自动跑**,等巴特菲发话。
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"auth": {
"username": "admin",
"salt": "rentos-2026",
"password_hash": "3d4e486a8040fa5356c900c5485b93d4573c9d3b4a128dc40a91234fd4de79b7"
},
"notify": {
"enabled": true,
"interval_seconds": 3,
"plans": {
"elec": {
"enabled": true,
"advance_days": 3,
"send_time": "09:00",
"skip_rentfree": true
},
"rent_parking": {
"enabled": true,
"advance_days": 5,
"send_time": "09:00",
"skip_rentfree": true
}
},
"send_time": "09:00"
},
"wecom": {
"chat_id": "",
"chat_name": "(授权人本人)",
"send_mode": "both"
},
"elec": {
"price": 1.2,
"ac_factor": 12.25,
"room_factor": 1.0,
"show_logo": true
},
"remit": null,
"companies": [
{
"name": "上海伯凌建筑设计有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海向安实业有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海明境建筑安装有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海星塔文化传媒有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海柏晟昊服饰有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海爱种草科技发展有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海禧华堂建筑工程有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海绿炬环保科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海苒博文化传播有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海豪瞬企业发展有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海轩伟物业管理有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海鑫炙智能科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "上海长盟贸易发展有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海靖朴国际贸易有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "吉富投资咨询(上海)有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "吉欧富贸易(上海)有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "晟音汇(上海)健康管理咨询有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "杭州亿格云科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "杭州逢林科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "精锐(上海)仓储有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "逸腾远东制冷(江苏)有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "针知运纺织科技(上海)有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "锦众心诚(上海)美容科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "锦玉良源(上海)网络科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海即时-徐传勇",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-11 17:34:45"
},
{
"name": "刘毓敏",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "康宗挺",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "杨婷",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "王梅训",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "纪超",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "金银铸",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "陈俊",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "韩毅",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
}
],
"owners": {
"精锐": {
"name": "精锐 (上海) 仓储有限公司",
"bank": "交通银行上海陆家嘴支行",
"acct": "310066771018170062380",
"address": "上海市金沙江路1989号5楼"
},
"科博": {
"name": "科博(上海)仓储有限公司",
"bank": "上海银行营业部",
"acct": "31600703001235523",
"address": "上海市金沙江路1989号5楼 021-62488877"
},
"瑞格嘉德": {
"name": "瑞格嘉德(上海)仓储有限公司",
"bank": "上海银行营业部",
"acct": "31600703001391851",
"address": ""
},
"国隆": {
"name": "国隆(上海)仓储有限公司",
"bank": "上海银行营业部",
"acct": "31600703001237712",
"address": ""
}
}
}
\ No newline at end of file
{
"companies": [
{
"name": "上海伯凌建筑设计有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海向安实业有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海明境建筑安装有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海星塔文化传媒有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海柏晟昊服饰有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海爱种草科技发展有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海禧华堂建筑工程有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海绿炬环保科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海苒博文化传播有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海豪瞬企业发展有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海轩伟物业管理有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海长盟贸易发展有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "上海靖朴国际贸易有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "杭州亿格云科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "杭州逢林科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "逸腾远东制冷(江苏)有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "针知运纺织科技(上海)有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "锦玉良源(上海)网络科技有限公司",
"kind": "company",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "刘毓敏",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "康宗挺",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "杨婷",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "王梅训",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "纪超",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "金银铸",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "陈俊",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
},
{
"name": "韩毅",
"kind": "person",
"short": "",
"note": "",
"created_at": "2026-10-09 17:18:45"
}
],
"owners": {
"精锐": {
"name": "精锐 (上海) 仓储有限公司",
"bank": "交通银行上海陆家嘴支行",
"acct": "310066771018170062380",
"address": "上海市金沙江路1989号5楼"
},
"科博": {
"name": "科博(上海)仓储有限公司",
"bank": "上海银行营业部",
"acct": "31600703001235523",
"address": "上海市金沙江路1989号5楼 021-62488877"
},
"瑞格嘉德": {
"name": "瑞格嘉德(上海)仓储有限公司",
"bank": "",
"acct": "",
"address": ""
}
},
"_note": "首次启动时若名册为空,会从这里播种;之后以页面维护的为准。"
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# 已退休的旧台账(2026-10-09)
这三份数据被合并进 `../rent_parking.json`(「房租车位费」)后退休,**系统不再读取**。
保留于此仅供回溯比对,可直接删除。
| 文件 | 原表 | 去向 |
|---|---|---|
| `rent_summary_367.json` | 汇总表(3F、6F、7F) | 单元 / 面积 / 合约期 / 免租期 / 保证金 / 中介费 → 合并进新表 |
| `rent_contract_8f11f.json` | 合同收入明细(8F、11F&车位) 旧 60 列台账 | 只用于取「业主」(出租方),其余列未采纳 |
| `.backup/` | 上述两表的历次备份 | 同上 |
合并口径见 `tools/import_rent_parking.py` 与 `rentos/tables.py` 的模块说明。
This diff is collapsed.
This diff is collapsed.
{
"房间号": "现租客",
"301室": "(空置)",
"302室": "杭州逢林科技有限公司",
"303室": "上海绿炬环保科技有限公司",
"305室": "韩毅",
"306室": "上海轩伟物业管理有限公司",
"307室": "锦玉良源(上海)网络科技有限公司",
"308室": "(空置)",
"309室": "保洁阿姨(空置)",
"310室": "上海伯凌建筑设计有限公司",
"311室": "(空置)",
"312室": "上海星塔文化传媒有限公司",
"315室": "上海靖朴国际贸易有限公司",
"316室": "(空置)",
"601A室": "上海长盟贸易发展有限公司",
"601B室": "(空置)",
"602室": "(空置)",
"603室": "锦玉良源(上海)网络科技有限公司",
"605室": "(空置)",
"606A室": "(空置)",
"606B室": "(空置)",
"607室": "上海禧华堂建筑工程有限公司",
"608室": "杭州亿格云科技有限公司",
"701室": "(空置)",
"702室": "上海长盟贸易发展有限公司",
"703室": "(空置)",
"705室": "(空置)",
"706A室": "针知运纺织科技(上海)有限公司",
"706B室": "上海柏晟昊服饰有限公司",
"707室": "(空置)",
"708室": "(空置)"
}
\ No newline at end of file
# -*- coding: utf-8 -*-
# 租金通知系统 —— 云端镜像
# 底座用服务器本地已有的 python:3.11.3-slim(离线可用,无需拉取)
# 依赖全部以 wheel 形式随包带入(openpyxl / reportlab / pillow / charset-normalizer);
# wecom-cli 用官方 Linux 二进制随包带入。构建期不联网。
FROM python:3.11.3-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
TZ=Asia/Shanghai \
LANG=C.UTF-8 \
HOME=/root
WORKDIR /app
# ---- 1. 离线安装运行期依赖 ----
# openpyxl 台账 / 批次数据导出 xlsx
# reportlab + pillow 收费通知书直接绘制 PDF(2026-10-12 起统一 PDF,不再依赖 Office)
# charset-normalizer reportlab 的依赖
# 全部为 cp311 / py3 的 manylinux2014 wheel,apt 无新增(pillow wheel 自带图像库)
COPY wheels/ /wheels/
RUN pip install --no-index --find-links=/wheels \
openpyxl reportlab pillow charset-normalizer \
&& rm -rf /wheels
# ---- 2. 企业微信官方 CLI(Linux 静态二进制,@wecom/cli-linux-x64 1.3.4) ----
# 放到 PATH 上,rentos/wecom.py 的 shutil.which("wecom-cli") 即可命中,无需改代码。
# 凭据**不打进镜像**,运行时由 /opt/rentos/wecom-config 挂到 /root/.config/wecom。
COPY wecom/wecom-cli /usr/local/bin/wecom-cli
RUN chmod 755 /usr/local/bin/wecom-cli \
&& /usr/local/bin/wecom-cli --version
# ---- 3. 应用代码 ----
COPY app/rentos/ /app/rentos/
COPY app/build_notices.py /app/build_notices.py
COPY app/web/ /app/web/
COPY app/assets/ /app/assets/
COPY app/templates/ /app/templates/
# ---- 3b. 构建期自检:中文字体随包带上 + PDF 渲染链可用 ----
# simhei.ttf 是 asset 的一部分(约 9.7MB),缺失时会回落到 CID 字体、中文有缺字风险,
# 这里直接注册一次,构建失败即暴露问题,不要等到线上出单才发现。
RUN test -s /app/assets/fonts/simhei.ttf \
&& python -c "from reportlab.pdfbase import pdfmetrics; \
from reportlab.pdfbase.ttfonts import TTFont; \
pdfmetrics.registerFont(TTFont('simhei','/app/assets/fonts/simhei.ttf')); \
import PIL, reportlab; print('PDF 渲染链 OK: reportlab', reportlab.Version, '/ pillow', PIL.__version__)"
# ---- 4. 运行期目录(实际由 volume 覆盖,此处保证镜像可独立运行) ----
RUN mkdir -p /app/data /app/output /app/logs /app/uploads /app/output/exports \
/root/.config/wecom
EXPOSE 8899
# 定时发送已启用(容器内可用 wecom-cli 直发企业微信)
CMD ["python", "-m", "rentos.server", "--host", "0.0.0.0", "--port", "8899"]
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
# UI 回归报告 · 房租车位费出单前后全量
日期:2026-10-12 11:20 | 项目:`D:\AI\rentos` | 服务:`http://127.0.0.1:8899`
> 触发:巴特菲 **「执行UI 回归」**(按 2026-10-09 定下的规矩 —— 小改动不自动跑,等发话再跑)
---
## 一、结论
**7 个脚本全部通过,0 失败**;台账 / 批次 / 配置**逐格零残留**。
| 脚本 | 覆盖 | 结果 |
|---|---|---|
| `nav_verify.py` | 侧栏结构 / 深链 / 响应式抽屉 / 可访问性 | **61 / 0** |
| `ui_verify.py` | 台账改格与合计联动、增删行、搜索、电费手工录入 | **42 / 0** |
| `ui_verify2.py` | 设置页、锁定只读、确认框、撤销、删除恢复 | **62 / 0** |
| `ui_verify3.py` | 自动带入上期、类目/系数、图片附件、月度留档 | **42 / 0** |
| `ui_verify4.py` | 公司管理名册、出租方名册、候选池、自动带出 | **41 / 0** |
| `verify_elec_prev.py` | 电费「重新带入上期」定点 | **21 / 0** |
| `ui_verify5.py` 🆕 | **房租车位费出单全链**(本轮新增) | **40 / 0** |
全套件原始输出留档:`output/UI回归-20261012.log`(`grep -c FAIL` = 0)
全量单元测试:**99 项全绿**。
---
## 二、本轮新增:`tools/ui_verify5.py`(40 项)
房租车位费出单是 2026-10-09 交付的功能,此前**没有任何 UI 脚本覆盖**。新增覆盖:
| # | 验证内容 | 实测 |
|---|---|---|
| 1 | 三张卡齐全、三组按钮齐全 | ✅ |
| 2 | 默认值:基准日/通知日期 = 今天、缴费期限 = 当月月底 | ✅ `2026-10-12` / `2026-10-31` |
| 3 | 汇总条与明细/通知书份数三处数字一致 | ✅ 43 行 / 37 份 / ¥694,842.44 |
| 4 | 类型列只有「房间」「车位」,**车位参与出单** | ✅ 车位 22 行 + 房间 21 行 |
| 5 | 「显示未到期」开关行数联动、标记数 = 多出的行数 | ✅ |
| 6 | 勾选框默认全选;取消后全不选;单勾 1 行计数正确 | ✅ |
| 7 | 出单前确认框(写明账期推进)→ 生成成功 | ✅ 1 份 / ¥22,734.00 |
| 8 | **账期确实推进**(按付款周期 +3 月) | ✅ `2026-12-01 → 2027-03-01` |
| 9 | 批次详情:份数、发送/下载按钮、打包按钮已绑定 JS | ✅ |
| 10 | **打包接口真实可用**(`/api/batches/{id}/zip`) | ✅ HTTP 200,9,857 B |
| 11 | 类别隔离:批次表「账单」列标「房租车位费」、电费页不含该批次 | ✅ |
| 12 | 按最新配置重出单据可用 | ✅ |
| 13 | **删除批次 → 账期逐格回滚** | ✅ 85 行全部一致 |
| 14 | 全程无页面 JS 报错 | ✅ |
**安全设计**:只勾 1 行出单;删除批次时带 `rollback=true` 自动退账期;
`try/finally` 兜底清理批次;结束时逐格核对台账并把本次新增的产物目录删掉。
---
## 三、修掉的 6 类「脚本断言过期」
这些不是功能坏了,是**脚本里写死了数字/位置**,实现演进后没跟着改(因为规矩是"不自动跑")。
统一改成**以接口返回为准**或**按表头文字定位**,以后不会再假失败:
| 脚本 | 原断言 | 现在 |
|---|---|---|
| `nav_verify.py` | 「导航项 4 个」 | = 概览/电费/租金台账/公司管理/设置(5 项) |
| `nav_verify.py` | 「台账 64 行」 | = 后端返回行数(动态) |
| `ui_verify.py` | 台账 64 行 / 表头 26 项 / 跨度 31 / 合计格 31 / 增删后 65、64 行 | 一律用后端行数、列数推算 |
| `ui_verify.py` | 电费「第 10 列是金额」(表头 11 列) | **按表头文字找「金额」列号**(备注/出租方列加入后就错位了) |
| `ui_verify2.py` | 表头 26 项 / 跨度 31 / 每行 29 列 / 台账 64 行 | 同上,动态 |
| `ui_verify3.py` | 取「最新批次」当上一期 | 与前端同口径:电费批次里**优先取抄完表的**那一期 |
| `ui_verify3.py` | 批次表头首列是「批次」 | 首两列是「账单」「批次」(新增 kind 列) |
| `ui_verify4.py` | 名册 26 条 / 出租方 3 家 / 引用单元 42·14·8 / 302 室出租方=精锐 | 全部动态(现 33 条 / 4 家);302 室已按房产税口径改为**科博** |
> 注:`ui_verify4.py` 输出里的「被拒请求 `POST /api/companies → 400`」是脚本**故意**触发的
> 「重复录入被拒」用例,已在白名单里,不算失败。
---
## 四、查出的 2 个真问题
### 1. ⚠️ 回归脚本会误删真实数据行(已修)
`ui_verify.py` 的「增行 / 删行」用例原本这样删:
```python
last = page.query_selector_all("#tbWrap tbody tr")[-1] # 取 DOM 最后一行
last.query_selector("button[data-delrow]").click()
```
它假设「新增行一定落在末尾」。**本次回归中,一次运行里新增没生效,这一下就删掉了
真实数据行 `rent_parking-0089`(瑞格嘉德 5 楼 504 室 / 吉欧富贸易)** —— 台账从 85 行掉到 84 行。
已改为**按新增行的 id 精确定位**,并在找不到新行时跳过删除:
```python
before_ids = {...} # 进编辑模式后先取全部行 id
... 点新增、确认 ...
new_ids = sorted(after_ids - before_ids) # 只认刚出现的那一行
ck("新增行可按行 id 定位(恰好 1 行)", len(new_ids) == 1, str(new_ids))
if new_ids: ...点击该行的删除按钮...
else: print("! 未定位到新增行,跳过删除,避免误删真实数据")
```
**丢失的行已从快照恢复**(`504室` 与 `0001` 均已回位,85 行、与快照差异 0)。
### 2. 删除/改格确认框把「面积」当成行标识(已修)
确认框的「行」标签原为 `[row.D || row.C ...]` —— `D` 是**服务面积**列,于是 302 室显示成:
```
行 | 80 (rent_parking-0002)
```
用户根本认不出这是哪一行。已改为**房间号优先 + 现租客**:
```
行 | 302室 · 杭州逢林科技有限公司 (rent_parking-0002)
```
并在 `ui_verify2.py` 加了一条断言守住它。改动文件:`web/app.js`(两处,`delRow` 与单元格编辑确认框)。
---
## 五、数据回落核对(回归后)
| 项 | 结果 |
|---|---|
| `data/tables/rent_parking.json` | 85 行,与回归前快照**逐格差异 0** |
| `data/batches/` | 仍是原 3 个(2 个电费 + 1 个你 10-09 出的房租) |
| `data/config.json` | 与快照**完全一致** |
| `output/rent/` | 只剩 `20261009-175756`(你 10-09 出的那期),测试产物目录已自清 |
| 临时探针脚本 | 已删除(`tools/_probe_grid.py`、`tools/_probe_addrow.py`) |
---
## 六、发现但**未擅自改**的一项
**「取消全选」后汇总条不显示「已勾选 0」。**
点全选框取消时走的是 `renderRentPreview()`,会把汇总条重置成「可出单 43 行 / 通知书 37 份 / …」;
而**单个**勾选框的变化才更新成「已勾选 N / M 行」。功能正确(生成按钮会正确拦住 0 行),
只是状态反馈不一致。要不要统一成「已勾选 N / M 行」,等你发话。
---
## 七、复核要点(你可以自己验)
1. 打开 `http://127.0.0.1:8899` → **房租车位费** 页
2. 顶部「本期出单」:默认基准日是今天 —— 今天**没有到期的行**(10-09 那期已把账期推进走了),
想看出单效果就把「出单基准日」调到 **2027-01-01**(或勾「显示未到期」)
3. 批次详情里能看到 `20261009-175756` 那期(1 份通知书、账期 ¥12,000 的韩毅 305 室)
4. 台账里 302 室现在显示在**科博**名下(按你拍板的房产税口径)
# 验收报告 · 单台账改造 + 电费手工录入
日期:2026-10-09 | 范围:`D:\AI\rentos`
改造依据:巴特菲第 2 轮 5 条需求(账期补齐 / 两表汇总成「房租车位费」/ 只留出单必需字段 / 只初次导入 / 电费改手工录入)
---
## 1. 改造结论一览
| 需求 | 落地情况 | 证据 |
|---|---|---|
| ① 账期推不出来 → 用「合同收入明细(3F、6F、7F) 」补 | ✅ 已补齐 | 台账 AB「最近收款月份」+ AC「账单起始日」两列;302 室 → `2026-11` / `2026-12-01`(与历史样张一致) |
| ② 两张表汇总成一张,统称房租车位费 | ✅ 三张 sheet 合并 | `data/tables/rent_parking.json`:**64 单元 × 29 列** |
| ③ 只记录出单需要的数据 | ✅ 逐月网格未铺开 | 7 个月 × 5 子列 → 压缩成 AB/AC 两列 |
| ④ 只初次导入,之后不提供导入 | ✅ 导入入口全下线 | `/api/upload`、`/api/tables/sheets`、`/api/tables/{key}/import` 全部 **404** |
| ⑤ 电费支持手工录入 | ✅ 逐行录入 + 实时计费 | `POST /api/batches`;前端 `calcRow()` 与后端 `compute_row()` 同口径 |
**账期规则(已定稿)**:账单起始日 = 最后一个「发票打 √ 或 收款年月非空」月份的**次月 1 日**。
> 光有「租金金额」不算已收款——那是本期应收计划值。按此口径 706B 室修正为 2026-10-01,302 室为 2026-12-01。
## 2. 唯一台账「房租车位费」结构(29 列)
| 列 | 含义 | 列 | 含义 |
|---|---|---|---|
| A | 出租方(精锐 / 科博 / 瑞格嘉德) | P | 每月租金(元/月) |
| B | 楼层 | Q–T | 日租金 / 净租金 |
| C | 房间/车位号 | U–V | 保证金(保留) |
| D / E | 服务面积 / 建筑面积 | W | 附赠车位 |
| F | 现租客 | X | 月净租金 |
| G / H | 交付日期 / 是否交付 | Y–Z | 中介费(保留) |
| I / J | 合同期始 / 合同期止 | AA | 备注 |
| K / L / M | 租赁期 / 免租期 / 免租车位 | **AB** | **最近收款月份** |
| N | 付款周期(月) | **AC** | **账单起始日** |
| O | 每月物业费(元/月) | | |
数据构成:汇总表 3F/6F/7F 30 行 + 合同收入明细(8F、11F&车位) 34 行 → 64 单元,其中 19 个带收款进度。
## 3. 验证结果
| 测试 | 方式 | 结果 |
|---|---|---|
| `tools/test_rules.py` 等单元测试 | `unittest discover -s tools -p "test_*.py"` | **23 passed / 0 failed** |
| `tools/nav_verify.py` | Playwright 无头,13 组 | **61 / 61 通过**,页面 JS 错误 0 |
| `tools/ui_verify2.py` | Playwright 无头,10 组 | **61 / 61 通过**,页面 JS 错误 0 |
| `tools/ui_verify.py` | Playwright 无头,9 组 | **0 失败**,页面 JS 错误 0 |
关键子项:
- 台账渲染 **64 行 × 29 列**,首列「出租方」,无「序号」列
- 锁定态点击不进入编辑;保存/删除/撤销均弹二次确认;撤销后落盘
- 增行 65 → 删行 64,删行撤销后第 2 行房间号恢复为 `302室`
- 电费手工录入:`1000 → 1100` 房间 → 用量 **100**、金额 **¥120.00**;切「空调」→ **¥1,470.00**;只填上期 → 标「未抄表」不出单
- 「从上一期复制」带出 **60 行**,所属期间自动进位 **2026年9月 → 2026年10月**
- 免租期 3 段(315室 / 302室 / 303室),来源标注「房租车位费台账」
- 设置页两张账单(电费 / 房租车位费),退休键不再出现;脚本跑完把设置**原样还原**
## 4. 事故与处理(需要知晓)
排查中发现 **127.0.0.1:8899 上挂着一个改造前启动的旧进程(PID 31264)**:
1. 内存里仍是旧代码 —— `/api/tables` 返回两张退休表,`/api/upload` 仍存在;
2. **定时线程开着** —— 于 16:40:54–16:41:53 自动把「2026年8月」那批 **10 份电费通知书**发送到企业微信(唐文文·本人);
3. 探测接口时它把**已退休的 rent_summary_367 重新导入**进 `data/tables/`(该文件已删除,`data/tables/retired/` 下留有存档)。
处理(按你的选择):**结束 PID 31264 → 在 8899 用新代码重启**(`--no-scheduler`,不启定时线程)。
现状:8899 与 8901 均为新代码,`/api/tables` 只返回 `rent_parking`,旧接口全部 404。
> 教训:改完代码必须重启**所有**在跑的实例。旧进程会按旧逻辑写同一个 `data/`,甚至自动发消息。
## 5. 数据现状
| 项 | 状态 |
|---|---|
| `data/tables/` | 仅 `rent_parking.json`(64 行)+ `retired/`(旧表存档) |
| `data/config.json` | 计划只剩 `elec` / `rent_parking`,值已还原为 3 天 / 09:00 |
| `data/batches/` | 2 个批次完好(20261009-115501 / 20261009-120146) |
| 台账首行 | `精锐 / 3楼 / 301室 / 189 / (空置) / 2026-09 / 2026-10-01`(测试改动已还原) |
## 6. 仍未完成的事项
- **房租车位费尚未接入出单**:`plans.py` 里计划已配好,但没有从台账生成房租车位费通知书的代码,
设置页状态显示「还没有可发送的通知书」。
- 《电费通知单系统-流程说明.xlsx》未反映本轮改造(手工录入 / 单台账 / 导入下线)。
- 企微 `sessions_count = 0`,目前只能发给授权人本人。
- `config.py` 里「瑞格嘉德」的银行 / 账号 / 地址待补。
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff was suppressed by a .gitattributes entry.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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