定开项目:台账化改造 + 统计/导出/搜索调整,新增本周进展与年度任务 tab
模块改名「定制交付→定开项目」,导入《定开项目.xlsx》市场台账(扩展 origin/project_code/office/industry/man_days/market_owner/project_status/accept_plan/risk_level/update_date/accept_date_real 等字段) 项目归属新增「提前交付」;已下单口径=新签订单/存量项目/汇智直签,并新增「项目归属」筛选 表格对齐台账字段并新增「进度」列;编辑/新建表单同步;名称搜索改为回车查询 下线交付流水线/阶段看板/财务跟踪;tab 调整为 项目总览 / 本周进展 / 年度任务 本周进展:验收情况 / 本周新增项目(按「下单时间」)/ 交付项目 年度任务:季度计收目标(《年度任务.xlsx》)+ 按验收/下单时间回填 统计口径:明细金额用元、汇总用万;下单只统计已下单;验收按「验收时间」accept_date 更新时间=提交时间;导出 Excel 对齐明细表并跟随筛选develop-chezhiqi
parent
e9854eb19b
commit
a77cd7bc81
|
|
@ -0,0 +1,48 @@
|
|||
# 2026-09-16
|
||||
|
||||
## 拉取代码仓
|
||||
- 从 `https://git.unissense.tech/mula/unis_manager` 克隆代码到本地工作目录 `C:\Users\Administrator\WorkBuddy\unis_manager`。
|
||||
- 当前分支 `main`,最新提交 `e9854eb v 0.0.1`。直接克隆进空的工作目录(`git clone <url> .`)。
|
||||
- 项目结构:FastAPI 后端(`app/`:main/config/models/schemas/routers/ai_parser/export 等)+ 原生 JS 前端(`web/`)+ SQLite 数据(`data/board.db`)+ 运维脚本(`scripts/`)。
|
||||
- 快速启动:`./run.sh`。依赖见 `requirements.txt`(fastapi / uvicorn / sqlalchemy / pydantic / httpx / openpyxl / python-multipart)。
|
||||
|
||||
## 本地启动(Windows)
|
||||
- `run.sh` 是为 macOS 写的(用了 `open`、`/Users/jiliu/...` 路径),Windows 上不能直接跑,需手动执行:
|
||||
1. 装依赖:`C:/Users/Administrator/.workbuddy/binaries/python/envs/default/Scripts/python.exe -m pip install -r requirements.txt`(已安装成功)
|
||||
2. 启动:`.../python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8770`
|
||||
- `data/board.db` 已有数据,无需 init/import。
|
||||
- 验证通过:`/health` 返回 `{"ok":true,"db":"sqlite"}`,首页 200,`/api/weekly` 正常返回周期与项目数据(当前周期「2026年9月第3周」)。
|
||||
- 服务运行中:http://127.0.0.1:8770
|
||||
|
||||
## 切换分支到 develop-chezhiqi
|
||||
- `git fetch --all` 拉到远端新分支 `origin/develop-chezhiqi`。
|
||||
- `git checkout -b develop-chezhiqi origin/develop-chezhiqi` → 已建本地跟踪分支并切换。
|
||||
- 该分支目前与 `main` 指向同一提交 `e9854eb v 0.0.1`(无代码差异),是后续开发的落脚分支。
|
||||
- 注:`.workbuddy/memory/` 下新增的每日日志与 MEMORY.md 属未跟踪文件,切分支时会保留。
|
||||
|
||||
## 在 develop-chezhiqi 分支上重启服务
|
||||
- 停掉旧进程(占用 8770 的 PID 8732;git bash 下 `taskkill //PID` 参数会被改写,改用 PowerShell `Stop-Process` 更稳)。
|
||||
- 重新启动:`uvicorn app.main:app --host 127.0.0.1 --port 8770`,新 boot id `dfafea7f...`,`/health` ok、首页 200。
|
||||
- 当前运行分支:`develop-chezhiqi`。
|
||||
|
||||
## 术语改名:定制交付 → 定开项目
|
||||
用户要求把模块③的称呼「定制交付」统一改成「定开项目」(公司内部正式叫法就是“定开”,见 `import_excel.py` 里 `/Users/jiliu/WorkSpace/定开管理/` 路径)。范围确认为**全量统一** + **项目分类一起改**。
|
||||
|
||||
替换规则(顺序敏感,带排除):
|
||||
1. `定制交付项目` → `定开项目`(先做,否则会出现“定开项目项目”)
|
||||
2. `定制交付` → `定开项目`
|
||||
3. `定制项目` → `定开项目`
|
||||
4. `定制(?!开发|产品组)` → `定开`
|
||||
|
||||
共改 21 个文件、137 行,涉及 `web/{index.html,app.js,styles.css,icons.js}`、`app/**`、`scripts/**`、`run.sh`、`README.md`。界面用词已统一:定开项目 / 定开项目明细 / 定开合同额 / 定开记录 / 定开动态 / 定开待计收。
|
||||
|
||||
**数据库**:`categories` 表 id=4 的 `name` 由「定制交付」改为「定开项目」(关联 3 个项目)。改前备份 `data/board.db.bak-before-rename-*`。
|
||||
|
||||
**刻意保留不动的字面量**(改了会破坏数据/逻辑):
|
||||
- `"定制开发"` —— 历史 Excel 里的项目容器名,出现在 `import_excel.py:KEY_PROJECTS`、`migrate_custom.py:CONTAINER`、`fix_period_split.py`、`add_entry_date.py` 的匹配条件里。
|
||||
- `import_excel.py:47` 的元组键 `("定制","交付","项目支撑")` —— 匹配 Excel 原始分组名,只改了映射目标值。
|
||||
- `constants.py:STAFF_GROUPS` 里的 `"定制产品组"` —— 团队名,`staff.group` 里有 7 人是这个值。
|
||||
- 业务数据(`projects.name` / `tasks.name` / `updates.content` / `staff.note` / `custom_projects.name`)一律未动。
|
||||
- 代码标识符(`custom` / `custom_projects` / API 路由 / CSS 类名)未动。
|
||||
|
||||
验证:`/api/categories` 返回 id=4 名称「定开项目」;首页 200;import 自检通过;服务已重启(boot `274bba9d...`)。
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# 2026-09-17
|
||||
|
||||
## 清空「定开项目」模块数据
|
||||
按用户要求清空 `data/board.db` 里定开项目模块的全部数据(保留表结构与其余模块)。
|
||||
|
||||
- 备份:`data/board.db.bak-before-clear-custom-20260917_105824`
|
||||
- 删除顺序:先子表 `custom_updates`(28 行)→ 再父表 `custom_projects`(119 行),避免外键孤儿。
|
||||
- 清空前摘要:119 个定开项目(归档 0),合同额合计约 395.2 万,累计计收 0。
|
||||
- 清空后校验:两表均 0 行,`custom_updates` 孤儿记录 0。
|
||||
- 接口验证:`/api/custom/projects` 返回 `[]`;`/api/weekly` 的 `custom` 段 0 条,KPI 中 `custom_total / custom_amount / custom_new / custom_updated / revenue_delta` 全部归零。项目管理模块(`projects`/`tasks`/`updates`)未受影响。
|
||||
- `ledger_items`(定开台账)本来就是 0 行,未动。
|
||||
|
||||
## 导入《定开项目.xlsx》市场台账 + 扩展 custom_projects 字段
|
||||
用户提供 `C:/Users/Administrator/Desktop/定开项目.xlsx`(Sheet1,120 行含表头 → **119 条数据**,14 列),要求导入定开项目库、按表格调整字段、并加「更新时间」字段便于按周查看变化。经确认采用:**新增字段(不重构)** + **每行一个更新时间** + **项目状态新增独立字段**。
|
||||
|
||||
### 表结构(10 个新字段,靠 `schema_patch.ensure_columns` 自动 ALTER TABLE)
|
||||
`origin`(项目归属) / `project_code`(项目ID, index) / `office`(办事处) / `industry`(行业) / `man_days`(市场下单人天) / `market_owner`(市场责任人) / `project_status`(项目状态) / `accept_plan`(推动验收计划) / `risk_level`(风险值) / `update_date`(更新时间 YYYY-MM-DD, index)
|
||||
|
||||
### 字段映射(表格列 → DB 列)
|
||||
- 项目归属→origin、项目名称→name、项目ID→project_code、行业→industry、市场下单人天→man_days、市场责任人→market_owner、项目状态→project_status、推动验收计划→accept_plan、风险值→risk_level、备注→remark
|
||||
- **下单时间→`sign_date`**(原字段注释就是「签单/下单时间」);**办事处→`region`(归一化)+ `office`(原样)**;**验收时间→`accept_date`**
|
||||
- `entry_date`(入库时间)兜底取下单时间,保证明细列表「按入库时间倒序」有排序键
|
||||
|
||||
### 数据质量(表格里就有的问题,未擅自改)
|
||||
- 重名 2 行:「甘肃省水利厅图像站建设项目二期」×2,**项目ID 不同**(HGSE25122401N / HGS025010202N)→ 导入脚本**有 ID 时只按 ID 匹配**,否则会互相覆盖
|
||||
- 1 行无项目ID(高新区感知源-钰田(定制开发))、1 行无办事处
|
||||
- 「验收时间/推动验收计划」是**季度文本**(Q1..Q4/27年),互补:60+59=119
|
||||
- 「下单时间」混格式:91 个真日期 + 28 个文本(`2024年3月前`、`2024/`)→ 日期转 YYYY-MM-DD,文本原样存
|
||||
- 「市场责任人」51 人**全部不在花名册**(销售侧人员,与 `owner` 交付负责人是两个概念)
|
||||
- 只有「深圳」不在区域词表 → 已在 `constants.REGION_ALIAS` 加 `深圳/深圳市 → 广东`
|
||||
|
||||
### 改动文件
|
||||
- `app/models.py`:CustomProject 加 10 字段 + `to_dict` 输出
|
||||
- `app/routers/custom.py`:PATCH 白名单加新字段;**台账字段有变化时刷新 `update_date`**(无变化不刷新,已实测);创建时写 `update_date`;`CustomProjectIn` 补字段
|
||||
- `app/routers/export.py`:导出追加 10 列(末尾追加,保持原 money_cols 索引不变)
|
||||
- `web/app.js`:明细列表 16→25 列(加 项目ID/行业/项目归属/市场责任人/项目状态/风险值/人天/推动验收/更新时间);新增 `statusPill`/`riskPill` 小色块;详情抽屉加「市场台账」区块(只读 + 编辑);KPI 提示加 人天/高风险
|
||||
- `scripts/import_custom_ledger.py`:**新建**,openpyxl 读取 + 按 ID/名称 upsert + 变更比对,支持 `--dry-run` / `--date`
|
||||
|
||||
### 验证
|
||||
- 导入:新增 119 / 更新 0 / 无变化 0;**重复导入幂等**(0/0/119)
|
||||
- 分布与源表一致:项目状态 已验收60/待开发31/交付完成17/交付中9/退单2;行业 安平51/政府25/企业22/教育11/交通6/医疗2/行业1/高速1
|
||||
- `/api/custom/projects` 返回 119 条含全部新字段;导出 34 列、合计行人天 5066
|
||||
- PATCH 改 industry → update_date 由 2026-01-01 刷新为今天;提交相同值 → 保持旧日期(变更检测正确),测试后已还原
|
||||
- 备份:`data/board.db.bak-before-ledger-import-20260917_141759`
|
||||
|
||||
### 遗留 / 待确认
|
||||
- 119 行 `stage` 全是默认 `signed`、`progress` 0、计收/开票/回款为空 —— 新台账不带这些业务字段,模块里依赖它们的旧列(进度%、可确认、已计收、开票、回款)目前是空的
|
||||
- 页面未做浏览器截图验证(agent-browser 未安装,装 Chromium 要 ~500MB 且规则禁止全局 npm),只做了 JS 语法校验 + 接口/导出功能验证
|
||||
|
||||
## 定开项目明细表改为按 Excel 表头顺序展示
|
||||
用户要求明细表字段参考导入的 Excel 表头。实际 Excel 表头顺序(14 列)为:项目归属 / 项目名称 / 项目ID / 下单时间 / 办事处 / 行业 / 市场下单人天 / 订单金额 / 市场责任人 / 项目状态 / 验收时间 / 推动验收计划 / 风险值 / 备注。
|
||||
- 重排 `renderCustomList` 表头+数据行,完全按此顺序;追加「更新时间」(用户此前要求,非 Excel 字段)与「操作」列 → 共 16 列。
|
||||
- 从主表移除 Excel 没有的旧列:区域、负责人(owner)、入库、交付、阶段、进度%、可确认/已计收/开票/回款 —— 这些仍保留在详情抽屉里。
|
||||
- 顶部统计行改为:合同额/万、人天、高风险数、已验收数(去掉空的计收/开票/回款)。
|
||||
- 纯前端改动(`web/app.js`),后端无需重启;接口此前已验证返回全部新字段。node `--check` 语法通过。
|
||||
|
||||
## 明细表交互改造:去按钮 / 状态筛选 / 新建表单对齐 Excel / 统计条
|
||||
用户四项要求:① 明细表去掉「详情」「归档」按钮 ② 新增「项目状态」筛选 ③「新建定开项目」字段参考 Excel 调整 ④ 表格上方加统计(项目数量+金额、本月/本年新增下单、本月/本年验收)。
|
||||
|
||||
### 新增字段 `accept_date_real`(验收日期,真实日期)
|
||||
Excel 里「验收时间」是季度文本(Q1..Q4),无法算「本月/本年」,故新增 **`accept_date_real`**(String(16), YYYY-MM-DD, index)专门用于验收统计,与 `accept_date`(季度文本,展示用)并存互补。
|
||||
- 改动:`models.py`(字段 + to_dict)、`custom.py`(CustomProjectIn / PATCH 白名单 / create 时 `region` 可由 `office` 归一化推导)、`scripts/import_custom_ledger.py`(导入时对「已验收」+季度文本的行补当年季度末占位;**仅在库中为空时补,不覆盖用户手改的真日期**)。
|
||||
- 一次性回填:已导入的 119 行中 60 条「已验收」补上 2026 季度末(Q1→03-31/Q2→06-30/Q3→09-30/Q4→12-31),不动 `update_date`。结果:本年验收 60、本月(9月)验收 17。
|
||||
|
||||
### 前端(web/app.js + styles.css)
|
||||
- **去按钮**:`renderCustomList` 行操作列只留「修改」(`data-cedit`);名称仍可点开详情(`data-copen`)。`bindCustomList` 移除 `data-carch` 绑定。
|
||||
- **状态筛选**:工具栏加 `<select id="cStatus">`(全部/已验收/待开发/交付完成/交付中/退单),state 加 `customStatus`,在 `renderCustomList` 内对已加载行做客户端筛选。
|
||||
- **新建表单**:`openNewCustomDialog` 重写为对齐 Excel 的字段(项目归属/项目名称*/项目ID/下单时间/办事处/行业/市场下单人天/订单金额/市场责任人/项目状态/验收时间(季度文本)/推动验收计划/风险值/验收日期/备注/重点项目);提交时把台账字段发给后端,`region` 由后端按 office 归一化(实测 深圳→广东)。更新录入页共用此函数,一并生效。
|
||||
- **统计条**:新增 `.stat-row/.stat`(styles.css),在 `renderCustomList` 表头上方展示 5 个格子:项目数量/合同额、本月新增下单、本年新增下单、本月验收、本年验收(均「个数 · 金额万」)。下单按 `sign_date`、验收按 `accept_date_real`,基于当前筛选后的行计算;card-head 的 period-hint 保留人天/高风险/已验收。
|
||||
|
||||
### 验证
|
||||
- 语法:node `--check`(app.js)、ast.parse(models.py/custom.py/export.py/import_custom_ledger.py/constants.py)全通过。
|
||||
- 接口:重启后 `/api/custom/projects` 119 条含 `accept_date_real`;POST 新建含全部台账字段 → region 由 office 推导正确(深圳→广东),测试项目已删除;`/api/export/custom` 200。
|
||||
- 统计口径实测:本年验收 60、本月(2026-09)验收 17、本年新增下单 31、本月新增下单 0。
|
||||
- 页面未做浏览器截图(agent-browser 未装),刷新浏览器查看即可。
|
||||
|
||||
## 明细表:去掉归档下拉 / 按更新时间排序 / 删除本周更新
|
||||
用户(附截图:归档状态下拉「在库/已归档/全部」)要求:① 删除该下拉 ② 新增展示并「按更新时间排序」 ③ 可按周删除「本周更新」的数据。
|
||||
|
||||
### 前端(web/app.js)
|
||||
- **移除**明细表 card-head 里的 `#cArchived` 归档状态下拉及 `bindCustomList` 里的绑定;`state.customArchived` 保留(恒为 "false",仍只查非归档)。
|
||||
- **排序**:`renderCustomList` 的 `view` 改为按 `update_date` 倒序(`String(b.update_date).localeCompare(...)`),表头副标题改为「按「更新时间」倒序」。
|
||||
- **删除本周更新**:card-head 加 `<button id="btnPurgeWeek" class="btn sm danger">删除本周更新</button>`;`purgeWeek()` 先 `GET /custom/weekly-preview` 拿数量/金额/名单 → `confirmDialog({danger:true, requireText:"删除"})` 二次确认(并要求手动输入「删除」)→ `POST /custom/purge-week` → `afterWrite()` 刷新。
|
||||
- `confirmDialog` 增加可选 `o.requirePlaceholder`(原来输入框 placeholder 硬编码「逐字输入项目名称」,批量删除场景不适用)。
|
||||
|
||||
### 后端(app/routers/custom.py)
|
||||
- 新增 `_sort_by_update()`(update_date 倒序,无则回落 entry_date);`GET /custom/projects` 改用之(原来是 `_sort_by_entry`)。
|
||||
- 新增 `_week_range()`(当前自然周 周一~周日,基于 `date.today()` 的 `weekday()`)。
|
||||
- 新增 `GET /custom/weekly-preview`(本周有 update_date 的项目数/金额/名单,供确认)。
|
||||
- 新增 `POST /custom/purge-week`(删除 update_date 落在本周的项目,`db.delete(cp)` 靠 relationship cascade 连带删 custom_updates),返回 `{deleted,start,end,names}`。
|
||||
- 路径用 `/custom/weekly-preview`、`/custom/purge-week`,**避开** `/custom/projects/{cid}`(int 路径参数会被非数字段 422),未放在 projects 前缀下。
|
||||
|
||||
### 验证
|
||||
- node `--check` + ast OK;服务重启后 `/health` ok。
|
||||
- `/api/custom/weekly-preview` → 本周 **2026-09-14 ~ 2026-09-20**,**119 个**(因导入把 119 行 update_date 都写成 2026-09-17=今天),合同额约 1130.7 万。
|
||||
- `/api/custom/projects` 已按 update_date 倒序返回。
|
||||
- ⚠️ 注意:当前所有行 update_date 都是导入当周,所以「删除本周更新」= 会删掉全部 119 条;已用「显示数量 + 需输入『删除』」做强确认。**未实际执行 purge**(只验证了 preview)。
|
||||
|
||||
## 定开金额单位:明细表用元、汇总统计用万
|
||||
用户要求表格列「订单金额/万」去掉「/万」,单位换成正常的个位(元);但**汇总统计仍需用「万」**。
|
||||
|
||||
- **关键事实**:`custom_projects.amount` 在库里本来就是**元**(实测 sum≈11,307,268、avg≈95,019、max=1,000,000),之前的「/万」只是展示约定——`fen()`/`wan()` 把元÷10000 显示成万。
|
||||
- **最终约定(定开金额)**:**明细表格每行 = 元**(`money(p.amount)`,¥+千分位);**所有汇总/统计 = 万**(`fen()`/`wan()`)。
|
||||
- 改动(纯前端 `web/app.js`,静态服务无需重启):
|
||||
- 定开项目明细表:表头 `订单金额/万` → `订单金额`;单元格 `fen(p.amount)` → `money(p.amount)`(元)。
|
||||
- 上方统计条、人力资源页「定开合同额/定开待计收」、工作看板 KPI「定开合同额」、`删除本周更新`确认框的「合同额合计」——**仍用万**(`fen()`/`wan()`),未改/已改回。
|
||||
- **未动**:项目管理(交付跟踪)模块的「合同额」仍用 wan/fen+万(另一套 `projects` 数据);导出沿用原始元值。
|
||||
- 服务 `/health` ok,浏览器刷新即可见(static 带 no-cache+ETag)。
|
||||
|
||||
## 更新时间 = 下单时间 + 修改表单对齐台账字段
|
||||
两项需求:(1) 更新时间全部改为项目下单时间;(2) 定开「修改」表单只保留《定开项目.xlsx》14 列表字段,删除交付跟踪类多余字段。
|
||||
|
||||
### 1) update_date = sign_date(全量)
|
||||
- 备份:`data/board.db.bak-before-updatedate-sign-20260917_000000`。
|
||||
- `UPDATE custom_projects SET update_date = sign_date`(119/119 行;0 个 null sign_date,91 个真日期 + 28 个文本如「2024年3月前」)。
|
||||
- 后端 `app/routers/custom.py` PATCH:原本改任何字段就 `update_date=today`,改为 `update_date = cp.sign_date if cp.sign_date else cp.update_date`(跟随下单时间)。已用 API 实测:改 remark 后 update_date 仍为 2024-04-30(=sign_date),不是今天。
|
||||
- `scripts/import_custom_ledger.py`:新增/更新行的 `update_date` 改默认取 `sign_date`(无则回退 args.date);同步改了文档注释。
|
||||
- 副作用:「删除本周更新」现在按 sign_date 周判断,历史台账(2024/2025)不会命中本周 → 之前「会删光 119 条」的危险消失。
|
||||
|
||||
### 2) 修改表单对齐台账(前端 `web/app.js`)
|
||||
- `openCustomDrawer` 的 edit 模式:section「定开项目信息」只放 Excel 14 列(项目归属/项目名称/项目ID/下单时间/办事处/行业/市场下单人天/订单金额/市场责任人/项目状态/验收时间/推动验收计划/风险值/备注)+ 验收日期 + 只读「更新时间(跟随下单时间)」。
|
||||
- **删除的多余字段**:基础信息里的 阶段/开发进度/已计收/确认比例/交付环节/区域/交付负责人/团队成员/客户;收入确认里的 开票金额/已回款 编辑框(收入确认 KPI 只读区仅在 view 模式显示,edit 模式整段不渲染)。edit 模式只渲染 ledgerSec 表单,baseSec/收入确认/周期执行记录/阶段流转 仅 view 模式渲染。
|
||||
- `cdSave` PATCH payload 精简为上述台账字段(不再传 stage/owner/region/members 等,未传字段不动;`set_people` 对 None 不改成员)。
|
||||
- view 模式补充显示 下单时间、验收时间(之前漏显示)。
|
||||
- **数据保护**:编辑表单的「下单时间」用 text 输入(非 date),因为 28 行 sign_date 是文本(date 控件会清空它们);新建对话框仍用 date。
|
||||
- 重启 uvicorn 使后端生效(原 PID 40088 → 新进程);`node --check` 与 `ast.parse` 均通过;前端 static 无需重启。
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# 2026-09-18
|
||||
|
||||
## 定开项目明细:4 项调整(更新时间 / 统计实时 / 搜索回车 / 季度口径)
|
||||
|
||||
用户提了 4 条:
|
||||
|
||||
### 1) 保存后「更新时间」= 提交时间
|
||||
- 昨天把 PATCH 改成 `update_date = sign_date`;今天改回**提交时间**。
|
||||
- `app/routers/custom.py`:`from datetime import date, datetime, timedelta`;
|
||||
- PATCH:`if changed: cp.update_date = datetime.now().strftime("%Y-%m-%d %H:%M")`
|
||||
- CREATE:`data["update_date"] = datetime.now().strftime("%Y-%m-%d %H:%M")`
|
||||
- 实测:PATCH 项目 1(原 2024-04-30)→ `2026-09-18 11:21`(含时分),已在库中还原为 2024-04-30。
|
||||
- 口径小结:导入/初始 = 下单时间;界面改/新建后 = 提交时间(`String(16)` 正好放「YYYY-MM-DD HH:MM」)。
|
||||
|
||||
### 2) 统计随修改实时更新
|
||||
- 保存走 `afterWrite() → refresh() → renderCustom()`,统计条本就重算;用户观感「项目状态改了统计没变」的根因是**旧口径(验收看 accept_date_real)与项目状态无关**。
|
||||
- 改为**验收口径 = 「项目状态 == 已验收」**,季度/年度按 `accept_date_real` 归属(无验收日期的计入本年)→ 改状态即可反映到「本季度/本年验收」。
|
||||
|
||||
### 3) 项目名称搜索改为「回车」触发
|
||||
- `web/app.js` 的 `bindKeyword(id, opts)` 增加 `opts.enter`:为真时只在 **Enter**(或 type=search 的清除 ×)才查询,否则维持 320ms 防抖。
|
||||
- 启用:`bindKeyword("cKeyword", { enter: true })`(定开)、`bindKeyword("pKeyword", { enter: true })`(项目)。fKeyword/stKeyword 维持实时。
|
||||
|
||||
### 4) 统计维度:本月 → 本季度
|
||||
- `renderCustomList` 统计条:`本月新增下单 → 本季度下单(Y年Qn)`、`本月验收项目 → 本季度验收(Y年Qn)`;本年两项保留。
|
||||
- 季季度按 1-3/4-6/7-9/10-12 划分;`ymOf()` 只认 `^\d{4}-\d{2}` 的日期(「2024年3月前」这类文本不参与)。
|
||||
- 核对(2026 Q3):本季度下单 12 / 本年下单 31 / 本季度验收 17 / 本年验收 62。
|
||||
|
||||
### 其他
|
||||
- 后端改了 `custom.py` → **重启 uvicorn**(新 PID 26784,旧 27492 已杀);前端 static 无需重启。
|
||||
- `node --check web/app.js`、`ast.parse(custom.py)` 通过。
|
||||
- 临时脚本 `scripts/_tmp_verify.py` 已删除。
|
||||
|
||||
## 定开明细:只留「项目明细」+ 统计全局化 + 导出对齐
|
||||
用户提了 3 条:去掉其它几个板块;筛选/统计只按项目明细;本季度统计要覆盖全部项目且不随搜索变化,并优化样式。
|
||||
|
||||
1. **只保留「项目明细」**:删掉「交付流水线 / 阶段看板 / 财务跟踪」三个 tab,以及 `renderCustomBoard` / `renderCustomPipeline` / `renderCustomFinance` 三个函数(共删 235 行,脚本 `_tmp_rmviews.py` 完成)。`state.customTab` 固定为 `"list"`。
|
||||
2. **筛选/统计只基于项目明细**:
|
||||
- `renderCustom` 改为**一次取全部**(`/custom/projects?archived=X`,不再带 keyword/region);
|
||||
- 列表在**客户端**按 关键字 + 区域 + 状态 过滤(`state.keyword/customRegion/customStatus`);
|
||||
- 统计条基于 `allRows`(**全部项目**),**不受搜索/筛选影响**(加 `.stat-note` 说明文字)。
|
||||
3. **导出 Excel 对齐「项目明细」**:`export_custom` 去掉 `stage` 参数、**新增 `status` 参数**;列改为 15 列(= 明细表列);改 `_sort_by_update` 排序;前端导出带 `archived/region/status/keyword`。实测 `status=已验收` → 64 行、表头 15 列正确(64 个已验收,其中 62 个有本年验收日期)。
|
||||
4. **统计样式优化**:`.stat` 左侧 3px 色条 + 金额重点色(`--c`);分色:总量=`--primary`、下单=`--violet`、验收=`--amber`。
|
||||
5. **清理**:删除未用的 `customQuery`、`state.customBoard/customStats`;归档确认文案去掉「阶段看板」措辞。
|
||||
6. 重启 uvicorn(PID 38440);`node --check` / `ast.parse` / health 均通过。前端 static 无需重启。
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# 2026-09-20
|
||||
|
||||
## 修正「本季度验收」统计口径 → 按「验收时间」
|
||||
用户反馈本季度验收数据不对,要求按「验收时间」统计;并启动服务。
|
||||
|
||||
- **启动**:uvicorn 之前已停;重新用 `<PY> -m uvicorn app.main:app --host 127.0.0.1 --port 8770` 启动,`/health` ok。前端 static 无需重启。
|
||||
- **排查**:`已验收` 共 64 条,全部带「验收时间」`accept_date`(季度文本):**Q1=8 / Q2=35 / Q3=21**。旧统计用 `accept_date_real`(真实日期),而 4 条 Q3 行(id 43 宜章县智慧城市、44 广安武胜智慧停车场、104 陕西-神木能源朱盖塔煤矿二期、120 解放碑地下环道AI智能视频巡检)`accept_date_real` 为空 → 本季度只算出 17,比实际 21 少 4。
|
||||
- **修正**(`web/app.js` `renderCustomList`,静态文件):验收统计改为按 `accept_date`(Q1~Q4):
|
||||
- `本季度验收` = `accept_date` == 当前季度(Q3)→ **21**
|
||||
- `本年验收` = 有「验收时间」的全部(Q1/Q2/Q3)→ **64**
|
||||
- 不再依赖 `accept_date_real`;新增 `qOf()` 归一化(trim + 大写 + `^Q[1-4]$`)。
|
||||
- **现值**(2026-09-20,全部 120 个项目):项目数量/合同额 120 / 本季度下单 12 / 本年下单 32 / 本季度验收 21 / 本年验收 64。
|
||||
- 备注:4 条 Q3 行的 `accept_date_real`(验收日期)仍为空,统计已不受影响;如需可补 2026-09-30。
|
||||
|
||||
## 定开项目 5 项新需求(进度 / 提前交付 / 归属筛选 / 下单口径 / 本周进展 tab)
|
||||
- **进度字段**:`custom_projects.progress`(Float, NOT NULL, 默认 0)复用,编辑/新建表单加「进度(%)」输入(`cdProgress`/`ncProgress`,提交 `progress`);表格加「进度」列,仅当 **状态=交付中** 或 progress>0 时显示 `N%`,否则「—」。
|
||||
- **项目归属新增「提前交付」**:统一常量 `CUSTOM_ORIGINS = [新签订单, 存量项目, 汇智直签, 提前交付, 其他]`;新建/编辑下拉都用它。
|
||||
- **项目归属筛选**:工具栏加 `#cOrigin`(state.customOrigin,客户端过滤);导出也带 `origin` 参数(后端 `export_custom` 新增 `origin`)。
|
||||
- **下单口径**:`CUSTOM_PLACED = [新签订单, 存量项目, 汇智直签]`(`isPlaced()`)。第 1 张统计卡改名「**已下单项目 / 合同额**」,「本季度下单 / 本年下单」也只统计已下单(提前交付不计入下单)。验收仍看 `accept_date`。
|
||||
- **新增「本周进展」tab**:定开现在两个 tab —— **项目明细 / 本周进展**(`state.customTab`)。`renderCustomWeekly()`:按 `update_date` 落在本周(周一~周日)筛项目,统计 **本周更新项目 / 验收情况 / 下单情况 / 提前交付情况 / 进行中项目情况**(各自数量+金额;进行中=待开发/交付中/交付完成),下面用表格列出本周项目。
|
||||
- **重构**:抽出 `customTableHtml(view)` 供两个 tab 共用(表格新增「进度」列);`renderCustom` 工具栏按 tab 切换(明细显示搜索/筛选/导出/新建,进展只显示周期)。
|
||||
- **清理**:删掉 `openCustomDrawer` 里 `baseSec` 的死 edit 分支(重复 id:cdName/cdAmount/cdProgress/cdRemark 等)与不再用的 `stageOpts`。
|
||||
- 现值(120 个项目,origin 分布:新签订单 83 / 存量项目 27 / 汇智直签 10,暂无提前交付);本周更新 5 个(id 23/43/44/104/120,均 已验收)。
|
||||
- 后端改了 `export.py` → 重启 uvicorn;`node --check` / `ast.parse` / health / 导出(origin) / PATCH progress 均通过。
|
||||
|
||||
### 「本周进展」tab 二次调整(同日稍后)
|
||||
- 去掉「本周更新项目」汇总卡(不再统计本周更新的数量/金额),只保留 验收 / 下单 / 提前交付 / 进行中 四张。
|
||||
- 本周项目列表**去掉「操作」列**:`customTableHtml(view, withOps)` 加开关,本周进展传 `false`;改数据靠点项目名称打开详情。
|
||||
- **「下单情况」改为「本周新增下单」**:按「下单时间」`sign_date` 落在本周统计(`signedWeek`),不再用「本周更新 ∩ 已下单」。其余三类(验收/提前交付/进行中)仍按「更新时间」在本周归属。
|
||||
- 纯前端改动(`web/app.js`),无需重启;当前 09-14~09-20 本周新增下单 0 个。
|
||||
|
||||
### 「本周进展」tab 三次调整(同日再稍后)
|
||||
- 「下单情况」→ **「本周新增项目」**:统计 本周更新 ∩(**已下单 或 提前交付**)——`isPlaced(p) || p.origin === "提前交付"`。
|
||||
- **去掉「提前交付情况」卡**(提前交付并入「本周新增项目」,不再单独统计)。
|
||||
- 「进行中项目情况」→ **「交付中项目」**:只统计 `project_status === "交付中"`(不再含 待开发/交付完成)。
|
||||
- 现在本周进展只有 **3 张卡:验收情况 / 本周新增项目 / 交付中项目**。纯前端改动,无需重启。
|
||||
|
||||
### 修正「本周新增项目」口径(关键)
|
||||
- 用户指出:**「本周新增项目」应按「下单时间」`sign_date` 统计**(本周新下单 / 提前交付的项目),**不能按「更新时间」的本周更新集**。
|
||||
- 最终实现:`newDeal = allRows.filter(p => inWeek(p.sign_date) && (isPlaced(p) || p.origin === "提前交付"))`。
|
||||
- 「验收情况」「交付中项目」仍按「更新时间」在本周的项目统计(用户此次未要求改)。
|
||||
- 数据:2026-09-20 有 1 条 sign_date=2026-09-20(18,000 元)→ 本周新增项目 = 1。
|
||||
|
||||
### 「交付中项目」→「交付项目」
|
||||
- 卡片改名 **「交付项目」**:统计**本周更新的**项目里 项目状态 ∈ {**交付中, 交付完成**} 的。
|
||||
- 最终本周进展 3 张卡:**验收情况**(本周更新 ∩ 已验收)/ **本周新增项目**(按下单时间 sign_date 在本周 ∩ 已下单或提前交付)/ **交付项目**(本周更新 ∩ 交付中+交付完成)。纯前端改动,无需重启。
|
||||
|
||||
## 新增「年度任务」tab(数据源《年度任务.xlsx》)
|
||||
- 用户提供 `C:/Users/Administrator/Desktop/年度任务.xlsx`,要求定开加一个 tab 展示年度任务,并按「验收/下单时间」回填。
|
||||
- **Excel 结构**(Sheet1,A1:G6):列 = 季度 / 计收目标(万元)/ 计收项目数量 / 计收项目金额(万元) / 计收完成率 / 新增签单项目数量 / 新增签单项目数量金额(万元);行 = Q1~Q4 + 合计。目标值 **Q1 35 / Q2 225 / Q3 160 / Q4 260(合计 680)万元**。
|
||||
- **读取方式**:`tencent-docs-routing` → `tencent-docs-sheetagent`(skill 不存在)→ `tencent-local-office-edit`(`edsdk.py` 可用,`open_file` 成功)但 `sheet_*` 一律报 **No workbook open**;按 skill 降级规则改用 **openpyxl** 直接读(已告知用户)。
|
||||
- **实现**(纯前端 `web/app.js`):
|
||||
- 新增常量 `CUSTOM_ANNUAL_TARGETS = {1:35,2:225,3:160,4:260}`(万元)。
|
||||
- 定开第三个 tab **「年度任务」**(`state.customTab === "annual"`);`renderCustomAnnual(allRows)` 输出与原表同列的表格 + 合计行。
|
||||
- **计收项目** = `accept_date`(验收时间,季度文本)== Qn;**新增签单** = `isPlaced` 且 `sign_date` 落在本年度该季度;金额 元→万元;**计收完成率** = 计收金额 ÷ 目标。
|
||||
- **回填现值(2026)**:Q1 计收 8个/92.64万(264.7%)、签单 8个/33.06万;Q2 计收 35个/190.09万(84.5%)、签单 12个/79.65万;Q3 计收 21个/88.68万(55.4%)、签单 12个/39.75万;Q4 全 0;合计 计收 64个/371.41万(54.6%)、签单 32个/152.47万。
|
||||
- 纯前端改动,无需重启;`node --check` 通过。
|
||||
|
||||
### tab 改名 + 列序调整
|
||||
- 定开第一个 tab **「项目明细」→「项目总览」**(chip 与卡片标题「定开项目总览」都改)。
|
||||
- `customTableHtml` 列序:把**「项目归属」移到「下单时间」之后**(新序:项目名称 / 项目ID / 下单时间 / 项目归属 / 办事处 / …)。项目总览与本周进展两个 tab 共用,故同时生效。
|
||||
- 后端导出标题仍是「定开项目明细」(未改,用户只说 tab 名)。纯前端改动,无需重启。
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# 项目长期记忆 · unis_manager
|
||||
|
||||
软件部关键任务追踪看板。FastAPI 后端 + 原生 JS 前端 + SQLite。
|
||||
|
||||
## 运行环境(Windows)
|
||||
|
||||
`run.sh` 是 macOS 专用(`open`、`/Users/jiliu/...` 绝对路径),Windows 上按以下方式手动跑:
|
||||
|
||||
- Python 解释器(已装好依赖):`C:/Users/Administrator/.workbuddy/binaries/python/envs/default/Scripts/python.exe`
|
||||
- 装依赖:`<PY> -m pip install -r requirements.txt`
|
||||
- 启动:`<PY> -m uvicorn app.main:app --host 127.0.0.1 --port 8770`
|
||||
- 端口/配置来自 `.env`(`PORT=8770`)。DB 用 SQLite,落在 `data/board.db`,仓库里已带数据。
|
||||
- 健康检查:`GET /health`;API 文档:`/api/docs`;首页:`/`。
|
||||
|
||||
## 关键结构
|
||||
|
||||
- `app/` 后端:`main.py`(入口,挂载 `/api` 子应用 + `/static`)、`models.py`、`schemas.py`、`constants.py`、`routers/{board,projects,custom,focus,weekly,staff,ai,export}.py`、`ai_parser.py`、`export.py`、`schema_patch.py`、`config.py`。
|
||||
- `web/` 前端:`index.html` / `app.js` / `styles.css` / `icons.js`(零构建,no-cache + ETag)。
|
||||
- `scripts/` 运维脚本:`init_db.py`、`import_excel.py`、`import_staff.py`、`migrate_custom.py`、`shift_year.py`、`fix_period_split.py`、`merge_project.py`、`add_entry_date.py`、**`import_custom_ledger.py`**(把《定开项目.xlsx》市场台账导入 `custom_projects`,支持 `--dry-run`/`--date`,按 项目ID→名称 匹配做 upsert + 变更比对)。
|
||||
- `.workbuddy/memory/` 存每日工作日志(随仓库提交,2026-09-12 起)。
|
||||
- `schema_patch.ensure_columns` 会在启动时自动给已有表补上模型里**新增的可空列**(`ALTER TABLE ... ADD COLUMN`),所以加字段只需改 `models.py`,不用写迁移脚本。
|
||||
|
||||
## 业务模型
|
||||
|
||||
四模块:① 工作看板(只读汇总 `/api/weekly`+`/api/focus`)② 项目管理(`projects`/`tasks`/`updates`)③ 定开项目(原「定制交付」,表 `custom_projects`/`custom_updates`)④ 每周重点工作(`weekly_focus`)。
|
||||
三支撑页:人力资源(`staff`,全系统负责人唯一来源)、更新录入、设置。
|
||||
②③ 两套独立项目体系,共用一个花名册。
|
||||
|
||||
### 定开项目的两套字段(2026-09-17 起)
|
||||
|
||||
`custom_projects` 同时承载两套口径,**互不覆盖**:
|
||||
- **交付跟踪口径**(原有):`stage`(商务阶段)/`progress`/`revenue_amount`/`invoice_amount`/`received_amount`/`owner`(交付负责人,来自花名册)
|
||||
- **市场台账口径**(2026-09-17 从《定开项目.xlsx》导入):`origin`(项目归属)/`project_code`(项目ID)/`office`(办事处)/`industry`(行业)/`man_days`(市场下单人天)/`market_owner`(市场责任人,销售侧,**不在花名册**)/`project_status`(已验收/待开发/交付完成/交付中/退单)/`accept_plan`(推动验收计划)/`risk_level`(1.高风险/2.低风险)/`update_date`(更新时间)。**`origin`(项目归属) 取值**:新签订单 / 存量项目 / 汇智直签 / **提前交付**(2026-09-20 增) / 其他;**`progress`**(Float, NOT NULL, 默认 0) 复用为「交付中」项目的**进度(%)**
|
||||
|
||||
映射要点:`下单时间→sign_date`、`办事处→region`(归一化)+`office`(原样)、`验收时间→accept_date`(存的是**季度文本** Q1..Q4,不是日期)、`备注→remark`。`accept_date` 与 `accept_plan` 互补(60+59=119)。
|
||||
|
||||
**`accept_date` vs `accept_date_real`**(2026-09-17 增):`accept_date` 存季度文本(Q1..Q4),只作展示;新增 **`accept_date_real`**(真实日期 YYYY-MM-DD)专门给「本月/本年验收」统计用。导入脚本会给「已验收 + 季度文本」的行补**当年季度末**占位(Q1→03-31…Q4→12-31),且**只在库中为空时补**,不覆盖界面手改的真日期。**统计口径(2026-09-20 定稿)**:下单看 `sign_date`(本季度/本年按日期归属);**验收看「验收时间」`accept_date`(季度文本 Q1~Q4)**——本季度验收 = `accept_date` 等于当前季度,本年验收 = 有验收时间(全部季度)。**不再用 `accept_date_real`**(它只作参考/展示,历史上有 4 条 Q3 行缺值,曾导致本季度验收少算)。
|
||||
|
||||
明细表(`renderCustomList`)列顺序对齐 Excel 14 列 + 更新时间 + 操作;行内**只有「修改」**(详情靠点名称;归档按钮已按要求去掉)。**定开视图有 3 个 tab:项目明细 / 本周进展 / 年度任务**(`state.customTab`;原交付流水线 / 阶段看板 / 财务跟踪已下线)。列表在客户端按 关键字 + **项目归属** + 区域 + 状态 过滤。统计条 `.stat-row` 基于**全部已下单项目**(不随搜索/筛选变化):**已下单项目 / 合同额** + 本季度下单 + 本年新增下单 + 本季度验收 + 本年验收——**已下单 = 项目归属 ∈ {新签订单, 存量项目, 汇智直签}**(`isPlaced()` / `CUSTOM_PLACED`),**提前交付不计入下单**。「本周进展」tab(`renderCustomWeekly`)列出「更新时间」在本周(周一~周日)的项目,上方 3 张卡:**验收情况**(本周更新 ∩ 已验收)/ **本周新增项目**(按「下单时间」`sign_date` 在本周 ∩ 已下单或提前交付)/ **交付项目**(本周更新 ∩ 交付中+交付完成)。「年度任务」tab(`renderCustomAnnual`)按《年度任务.xlsx》出表:季度 + 计收目标(万元,前端常量 `CUSTOM_ANNUAL_TARGETS` = Q1 35/Q2 225/Q3 160/Q4 260)+ 回填列——**计收项目**(`accept_date` == Qn)/ **新增签单**(`isPlaced` 且 `sign_date` 在本年该季度),金额元→万元,完成率 = 计收金额 ÷ 目标。表格 `customTableHtml(rows)` 两个 tab 共用,在「项目状态」后新增**进度**列(`progress`,仅 状态=交付中 或 >0 时显示 `N%`)。项目名称搜索框**回车才查询**(`bindKeyword(id,{enter:true})`;cKeyword 定开 / pKeyword 项目已启用)。导出 Excel 列与明细表一致(15 列),跟随 归档/区域/状态/关键字 筛选。`openNewCustomDialog`(新建,更新录入页共用)字段同样对齐 Excel,提交时 `region` 由后端按 `office` 归一化(深圳→广东)。
|
||||
|
||||
**`update_date`(更新时间)的语义**:只在台账字段**确有变化**时才刷新(无变化不刷新)。
|
||||
- **取值**:导入/初始 = 该行「下单时间」`sign_date`(2026-09-17 全量刷过);界面「修改 / 新建」提交后 = **提交时间** `YYYY-MM-DD HH:MM`(2026-09-18 起)。字段是 `String(16)`,正好放「YYYY-MM-DD HH:MM」。
|
||||
- 用于明细表按更新时间倒序 + 按周筛选/删除。
|
||||
- 与 `updated_at`(TimestampMixin 自动时间戳,任何改动都变)不是一回事。
|
||||
|
||||
导入「市场责任人」等销售侧人名**不走花名册校验**,不要往 `staff` 里塞。
|
||||
|
||||
## 术语(2026-09-16 起)
|
||||
|
||||
模块③ 的正式称呼是 **「定开项目」**(旧称「定制交付」),界面统一用「定开」:定开项目 / 定开项目明细 / 定开合同额 / 定开记录 / 定开动态 / 定开待计收。`categories` 表里也有一行分类叫「定开项目」。
|
||||
|
||||
**但这些字面量必须保留「定制」,改了会坏数据或逻辑**:
|
||||
- `"定制开发"` —— 历史 Excel 的项目容器名,是 `import_excel.py` / `migrate_custom.py` / `fix_period_split.py` / `add_entry_date.py` 里的匹配条件。
|
||||
- `import_excel.py` 中 `(("定制","交付","项目支撑"), "定开项目")` 的元组键 —— 匹配 Excel 原始分组名,只有映射目标值是「定开项目」。
|
||||
- `"定制产品组"` —— `staff.group` 的团队名,`constants.py:STAFF_GROUPS` 里有它,7 人属于该组。
|
||||
|
||||
代码标识符(`custom` / `custom_projects` / API 路由 / CSS 类名)保持英文原名,未随中文改名。
|
||||
|
||||
## 约定
|
||||
|
||||
- 当前开发分支:`develop-chezhiqi`(跟踪 `origin/develop-chezhiqi`;main 为发布线)。
|
||||
- 改代码后重启服务即可(未开 DEV 热加载)。
|
||||
- 动数据库前先备份:`cp data/board.db data/board.db.bak`。
|
||||
- 周期标签格式 `9月第1周`,可写 `2027年1月第1周` 显式指定年份。
|
||||
|
||||
## Windows 运维小抄
|
||||
|
||||
- 查端口占用:`netstat -ano | grep "127.0.0.1:8770.*LISTENING"`
|
||||
- 停服务:git bash 下 `taskkill //PID` 会被路径转换破坏参数,改用 PowerShell `Stop-Process -Id <PID> -Force` 更稳。
|
||||
- 服务重启后 `/health` 的 `BOOT_ID` 会变,可据此确认是新进程。
|
||||
70
README.md
70
README.md
|
|
@ -8,12 +8,12 @@
|
|||
|---|---|---|---|
|
||||
| **① 工作看板** | 每天进来的第一屏 | 三类工作本周期谁更新了、谁没动,点一条从一览看到详情 | 只读汇总(`/api/weekly` + `/api/focus`) |
|
||||
| **② 项目管理** | NEX云桌面、AI Agent、智飞无人机平台、内部运营… | 长期重点项目的迭代执行、子任务进展、风险阻塞 | `projects` / `tasks` / `updates` |
|
||||
| **③ 定制交付** | 各省市客户定制子项目 | 商务阶段流转(签单→开发→交付→验收→计收)+ 按进度确认收入 | `custom_projects` / `custom_updates` |
|
||||
| **③ 定开项目** | 各省市客户定开子项目 | 商务阶段流转(签单→开发→交付→验收→计收)+ 按进度确认收入 | `custom_projects` / `custom_updates` |
|
||||
| **④ 每周重点工作** | 不值得单独建项目档案的短期活儿 | 一条就是一件事,面板里录,归在**录入周**这一周,跟到完成 | `weekly_focus` |
|
||||
|
||||
支撑页三个:**人力资源**(`staff`,全系统「负责人」的唯一来源)、**更新录入**(执行 / 交付记录的唯一写入口:粘贴原文 → AI 整理 → 确认写入)、**设置**(周期管理、AI 配置)。
|
||||
|
||||
②③ 是**两套彼此独立的项目体系**(数据表也完全分开),共用一个**人力资源库**:项目与定制项目的「负责人」都从花名册里选,人员进出只维护一处。④ 的条目不是项目,不挂子任务与执行记录。① 只做汇总与跳转,自己不落数据。
|
||||
②③ 是**两套彼此独立的项目体系**(数据表也完全分开),共用一个**人力资源库**:项目与定开项目的「负责人」都从花名册里选,人员进出只维护一处。④ 的条目不是项目,不挂子任务与执行记录。① 只做汇总与跳转,自己不落数据。
|
||||
|
||||
## 周期管理
|
||||
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
| 入口 | 操作 |
|
||||
|---|---|
|
||||
| 顶部周期下拉 | 选「+ 新建周期…」 |
|
||||
| 更新录入页(常规 / 定制都有) | 周期下拉选「+ 新建周期…」 |
|
||||
| 更新录入页(常规 / 定开都有) | 周期下拉选「+ 新建周期…」 |
|
||||
| 设置页 → 周期管理 | 「+ 新建周期」按钮 |
|
||||
|
||||
输入格式 `9月第1周`(也可写 `2027年1月第1周` 显式指定年份),年份按以下规则自动推断:
|
||||
|
|
@ -60,7 +60,7 @@ cd /Users/jiliu/WorkSpace/unis_manager
|
|||
|
||||
打开 <http://127.0.0.1:8770>
|
||||
|
||||
首次运行 `run.sh` 会自动完成:选择带依赖的 Python 解释器 → 缺依赖则安装 → 建库 → 导入历史 Excel → 拆分定制交付项目,然后启动并打开浏览器。
|
||||
首次运行 `run.sh` 会自动完成:选择带依赖的 Python 解释器 → 缺依赖则安装 → 建库 → 导入历史 Excel → 拆分定开项目,然后启动并打开浏览器。
|
||||
|
||||
### 启动方式与排错
|
||||
|
||||
|
|
@ -101,15 +101,15 @@ python -m uvicorn app.main:app --port 8770
|
|||
|
||||
| 区域 | 内容 |
|
||||
|---|---|
|
||||
| 顶部 KPI | 重点项目(含本期已更新数)/ 重点项目未更新 / **本周重点工作**(本周到期的重点工作条数,脚注「逾期 N 条 · 已完成 N 条」)/ 执行记录 / 定制合同额(含项目数与本期计收)/ 风险阻塞 |
|
||||
| **左栏** | **三组**清单,顺序固定:① **本周工作重点**(列表,逾期的置顶)② **重点项目**(列表:`is_key` 的项目,另含本期补了执行记录的非重点项目,条目前用 ★ 区分)③ **定制项目**——**只给一个入口**「本周定制项目动态」,左栏不铺清单(100 多个项目会淹没看板),点开才在右栏列本周有动态的。组头显示条数与「几个已更新 / 几条逾期 / 本周动态几个」;每条带负责人、状态或阶段、本期摘要、风险条数,本期有记录的点亮标记 |
|
||||
| **右栏** | 点左栏任意一条即可联动。重点项目 → 本期执行记录 / 进度与状态(含子任务)/ 近 8 期趋势;**定制项目入口 → 本周动态总览**(顶部四枚标签:新入库 / 有交付动态 / 所列合同额 / 本期计收,下面分「本周新增」「本周有变化」两段,每条带阶段、负责人、金额、本期计收与摘要),点某一条再下钻到该项目 → 交付环节五段条 / 财务跟踪四联卡 + 开票·回款行内输入 / 本期交付记录 / 更早的交付记录,改开票、回款、环节日期即时保存(改日期会自动推进商务阶段);每周重点工作 → 只读的说明与状态,要改点「修改」开抽屉 |
|
||||
| 工具栏 | 「负责人」筛选、「导出本周期记录」,提示里带本期项目数与定制动态数;顶栏可切周 / 月视图(月视图自动汇总该月所有周)。「列入全部在途」开关(`all_custom`)不在工具栏,挪到了右栏定制总览的头部 |
|
||||
| 顶部 KPI | 重点项目(含本期已更新数)/ 重点项目未更新 / **本周重点工作**(本周到期的重点工作条数,脚注「逾期 N 条 · 已完成 N 条」)/ 执行记录 / 定开合同额(含项目数与本期计收)/ 风险阻塞 |
|
||||
| **左栏** | **三组**清单,顺序固定:① **本周工作重点**(列表,逾期的置顶)② **重点项目**(列表:`is_key` 的项目,另含本期补了执行记录的非重点项目,条目前用 ★ 区分)③ **定开项目**——**只给一个入口**「本周定开项目动态」,左栏不铺清单(100 多个项目会淹没看板),点开才在右栏列本周有动态的。组头显示条数与「几个已更新 / 几条逾期 / 本周动态几个」;每条带负责人、状态或阶段、本期摘要、风险条数,本期有记录的点亮标记 |
|
||||
| **右栏** | 点左栏任意一条即可联动。重点项目 → 本期执行记录 / 进度与状态(含子任务)/ 近 8 期趋势;**定开项目入口 → 本周动态总览**(顶部四枚标签:新入库 / 有交付动态 / 所列合同额 / 本期计收,下面分「本周新增」「本周有变化」两段,每条带阶段、负责人、金额、本期计收与摘要),点某一条再下钻到该项目 → 交付环节五段条 / 财务跟踪四联卡 + 开票·回款行内输入 / 本期交付记录 / 更早的交付记录,改开票、回款、环节日期即时保存(改日期会自动推进商务阶段);每周重点工作 → 只读的说明与状态,要改点「修改」开抽屉 |
|
||||
| 工具栏 | 「负责人」筛选、「导出本周期记录」,提示里带本期项目数与定开动态数;顶栏可切周 / 月视图(月视图自动汇总该月所有周)。「列入全部在途」开关(`all_custom`)不在工具栏,挪到了右栏定开总览的头部 |
|
||||
| 统计概览 | 页面最底部的折叠区(原「执行面板」,已不占主导航位):周期活跃度柱状图、分类环形图、状态分布、进度 Top10、低进度预警、风险清单,数据来自 `GET /api/stats`;加载失败也不影响看板主体 |
|
||||
|
||||
数据分两路:`GET /api/weekly?period_id=|year&month&owner=&all_custom=` 一次返回重点项目清单 + 定制项目清单 + 顶部 KPI(含 `custom_new` 本期新入库数、`custom_updated` 本期有交付记录数);**「本周工作重点」不在 `/api/weekly` 里**,前端并发取 `GET /api/focus` 后合并,分堆也在这一步判定(按条目的**录入周**:本周录入的 + 更早周次标了「延期」又没做完的结转条目)。
|
||||
数据分两路:`GET /api/weekly?period_id=|year&month&owner=&all_custom=` 一次返回重点项目清单 + 定开项目清单 + 顶部 KPI(含 `custom_new` 本期新入库数、`custom_updated` 本期有交付记录数);**「本周工作重点」不在 `/api/weekly` 里**,前端并发取 `GET /api/focus` 后合并,分堆也在这一步判定(按条目的**录入周**:本周录入的 + 更早周次标了「延期」又没做完的结转条目)。
|
||||
|
||||
定制项目的口径:`/api/weekly` 返回**本期有交付记录**、**被标为重点**或**本周期新入库**(`is_new`:入库日期落在本周期首末日之内)的定制项目;右栏总览默认只列「本周新增」与「本周有变化」两段,其余只在组头计数里出现,并在底部提示「另有 N 个在途定制项目本周没有动态」。勾选总览头部的「列入全部在途」后,后端补上处于在途阶段(商机 → 已验收)的项目,总览多出一段「其他在途项目」。头部那枚「合同额合计」只加总**当前列出来的行**,勾与不勾数字会跟着清单变。
|
||||
定开项目的口径:`/api/weekly` 返回**本期有交付记录**、**被标为重点**或**本周期新入库**(`is_new`:入库日期落在本周期首末日之内)的定开项目;右栏总览默认只列「本周新增」与「本周有变化」两段,其余只在组头计数里出现,并在底部提示「另有 N 个在途定开项目本周没有动态」。勾选总览头部的「列入全部在途」后,后端补上处于在途阶段(商机 → 已验收)的项目,总览多出一段「其他在途项目」。头部那枚「合同额合计」只加总**当前列出来的行**,勾与不勾数字会跟着清单变。
|
||||
|
||||
### 2. 项目管理
|
||||
|
||||
|
|
@ -126,12 +126,12 @@ python -m uvicorn app.main:app --port 8770
|
|||
| 操作 | 说明 |
|
||||
|---|---|
|
||||
| **面板分区** | 抽屉用标准对话框式标题栏:**标题在左(下面一行副标题),基础信息的操作按钮成排在右,✕ 在最右**,按钮条嵌在标题栏里(不悬浮、不吸顶,滚动时跟着标题栏常驻)。详情档是「删除 · 归档/恢复 · 标记重点 · **修改**」,修改与新增档是「取消 · **保存/添加**」,主按钮永远在最右、危险动作在最左,按钮文字不再重复对象名(标题栏已经写了是哪个项目/条目)。附表的写入另放一处:**绿虚线 = 附表 · 更新记录**条仍留在它自己的附表标题下面,只有「+ 录一笔「某周期」执行记录 / 交付记录」,点它跳到「更新录入」并自动带上该项目与顶栏当前选中的周期。附表的写入统一走更新录入(粘贴原文 → AI 整理 → 确认写入),抽屉里不另做一套表单,避免两条路写出两份数据。基础信息的按钮在标题栏、附表的更新按钮在附表上方,两类按钮不在同一个位置混着放;编辑与新增档的报错信息显示在正文最上方;周期反馈上方的 按周/按月 + 本周/近 4 周/… 标成「查看范围(只读筛选)」,只翻看不改数据 |
|
||||
| **改名** | 改名就是改基本信息:在详情面板点「修改」,表单第一行即项目名称(项目管理与定制交付同一套逻辑)。名称非空校验 + 重名检测,失败信息显示在面板正文最上方,不弹提示跑路 |
|
||||
| **负责人与团队** | **多负责人只适用于重点项目**:重点项目可以指定**多个负责人**与**团队成员**,定制交付只有一个**交付负责人**(单人下拉)外加**团队成员**。人名一律从人力资源库下拉里选。**界面上负责人不分主次**:名单横向排开自动换行,每一位都带 × 可删(没有「主」标记、没有不给删的首行),「+ 添加负责人 / + 添加成员」跟在名单末尾。名单只在详情抽屉的「修改」档维护;只读处一律一排对等的小标签:重点项目的列表与详情把负责人摆成对等的一排,定制交付的列表与详情只显示一个交付负责人加团队成员标签。存储与统计口径不变:**重点项目 `owner` 字段仍是 `owners[0]`**(删掉第一位就由后一位顶上),看板、导出、人力负载与 KPI 按它归属,修改档的提示行写明了这点;重点项目**所有负责人都算命中**,定制交付按交付负责人命中;团队成员不参与该筛选 |
|
||||
| **改名** | 改名就是改基本信息:在详情面板点「修改」,表单第一行即项目名称(项目管理与定开项目同一套逻辑)。名称非空校验 + 重名检测,失败信息显示在面板正文最上方,不弹提示跑路 |
|
||||
| **负责人与团队** | **多负责人只适用于重点项目**:重点项目可以指定**多个负责人**与**团队成员**,定开项目只有一个**交付负责人**(单人下拉)外加**团队成员**。人名一律从人力资源库下拉里选。**界面上负责人不分主次**:名单横向排开自动换行,每一位都带 × 可删(没有「主」标记、没有不给删的首行),「+ 添加负责人 / + 添加成员」跟在名单末尾。名单只在详情抽屉的「修改」档维护;只读处一律一排对等的小标签:重点项目的列表与详情把负责人摆成对等的一排,定开项目的列表与详情只显示一个交付负责人加团队成员标签。存储与统计口径不变:**重点项目 `owner` 字段仍是 `owners[0]`**(删掉第一位就由后一位顶上),看板、导出、人力负载与 KPI 按它归属,修改档的提示行写明了这点;重点项目**所有负责人都算命中**,定开项目按交付负责人命中;团队成员不参与该筛选 |
|
||||
| **归档** | 项目从看板与列表默认视图中隐藏,数据完整保留;勾选「含归档」可查看,随时可恢复 |
|
||||
| **删除** | 高危操作:弹窗列出将连带删除的子任务数与执行记录数,并且必须**逐字输入项目名称**,「永久删除」按钮才会点亮。**删除后不可恢复**,仅想隐藏请用归档 |
|
||||
|
||||
> 定制交付的「项目明细」表同理:列表只读,操作列只有 详情 / 修改 / 归档,改名 / 名单 / 删除都在抽屉里做。
|
||||
> 定开项目的「项目明细」表同理:列表只读,操作列只有 详情 / 修改 / 归档,改名 / 名单 / 删除都在抽屉里做。
|
||||
|
||||
#### 项目合并
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ python scripts/merge_project.py --from "智空无人机平台" --to "智飞无
|
|||
- 合并后按最新记录回算目标项目的进度与状态
|
||||
- 执行前建议备份:`cp data/board.db data/board.db.bak`
|
||||
|
||||
### 3. 定制交付
|
||||
### 3. 定开项目
|
||||
|
||||
| 模块 | 说明 |
|
||||
|---|---|
|
||||
|
|
@ -156,7 +156,7 @@ python scripts/merge_project.py --from "智空无人机平台" --to "智飞无
|
|||
| **阶段看板** | 按「商机 → 已签单 → 开发中 → 交付完成 → 已验收 → 已计收 → 暂停/异常」分列展示,卡片带金额、区域、开发进度、可确认收入;列头显示项目数与金额合计。卡片只是入口,点卡片开详情抽屉;卡上的 ★ 只读(重点标记在详情面板的基础信息区改),外层不做任何直接写入 |
|
||||
| **财务跟踪** | 页签内一排 KPI:合同总额 / 按进度可确认收入 / 已计收 / 待计收 / 已开票 / 已回款 / 待开票 / 待回款(各带比例脚注);再往下是阶段金额分布、周期活跃度、按负责人与区域汇总、待回款排行。财务口径的汇总只在这一页出现,环节与账期一屏对齐,方便财务逐月对账 |
|
||||
| **项目明细** | 表头一行给出当前列表的合同额 / 可确认 / 已计收 / 开票 / 回款合计;按入库时间倒序的**只读**总表:区域 / 负责人 / **入库 · 签单 · 交付 · 验收日期** / 阶段 / 合同额 / 进度 / 可确认 / 已计收 / **开票 / 回款**(金额列统一以「万」计)/ 备注;改数据点行尾「修改」 |
|
||||
| **详情抽屉** | 分「详情 / 修改」两档:详情只读,修改才给表单(含项目名称、手动确认比例、**交付环节五个日期**、区域下拉、**开票与回款金额**)+ 收入确认卡 + 周期执行记录 + 阶段流转时间线。负责人从人力资源库里选。基础信息的动作在标题栏按钮条上(修改/保存,正文里标明不会动下面的交付记录),开票与回款金额跟着标题栏的「保存」一起提交;附表的绿虚线条(「+ 录一笔」跳更新录入 · 定制交付并带上项目与周期)留在附表标题下 |
|
||||
| **详情抽屉** | 分「详情 / 修改」两档:详情只读,修改才给表单(含项目名称、手动确认比例、**交付环节五个日期**、区域下拉、**开票与回款金额**)+ 收入确认卡 + 周期执行记录 + 阶段流转时间线。负责人从人力资源库里选。基础信息的动作在标题栏按钮条上(修改/保存,正文里标明不会动下面的交付记录),开票与回款金额跟着标题栏的「保存」一起提交;附表的绿虚线条(「+ 录一笔」跳更新录入 · 定开项目并带上项目与周期)留在附表标题下 |
|
||||
|
||||
### 4. 每周重点工作
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ python scripts/merge_project.py --from "智空无人机平台" --to "智飞无
|
|||
| 列表 | 工作内容 / 说明(截断预览)/ 预计完成时间(下面小字标「录入 X月第N周」)/ 状态(标了延期且没完成的挂红色「延期」徽章)/ 更新日期 / 操作 **详情 · 修改 · 归档 · 删除**——和两张项目列表同一套规矩:列表只读,行内不放输入框 |
|
||||
| 筛选 | **本周 / 延期 / 已完成 / 全部** 四个堆(chip 上实时显示各堆条数)+「含归档条目」;顶栏切周即换基准周,工具栏标明「本周/延期按录入周统计」;顶部全局搜索同时匹配工作内容与说明 |
|
||||
| 详情抽屉 | 同样分「详情 / 修改」两档,**基础信息的按钮全在抽屉标题栏右侧的按钮条上**:详情档是「删除 · 归档 · **修改**」,修改与新增档是「取消 · **添加/保存**」,报错信息显示在正文最上方。点「修改」才出表单,**录入周只读**(改预计完成时间不会把条目搬去别的周)。重点工作条目没有附表,所以抽屉里不出现绿的更新条 |
|
||||
| 删除 | 列表行与抽屉顶部都能删,走同一个确认弹窗,并提示「只是这周不再跟进?请改用归档」。条目不挂子任务与执行记录,**不要求逐字输入名称**——逐字输入项目名称只用在项目管理与定制交付的项目删除上 |
|
||||
| 删除 | 列表行与抽屉顶部都能删,走同一个确认弹窗,并提示「只是这周不再跟进?请改用归档」。条目不挂子任务与执行记录,**不要求逐字输入名称**——逐字输入项目名称只用在项目管理与定开项目的项目删除上 |
|
||||
| 导出 | 右上角「导出 Excel」→ `GET /api/export/focus`,带上当前筛选条件 |
|
||||
| 联动 | 工作看板左栏**第一组**「本周工作重点」与 KPI「本周重点工作」读的就是这张表;看板右栏点条目给只读详情,改内容走抽屉 |
|
||||
|
||||
|
|
@ -182,12 +182,12 @@ python scripts/merge_project.py --from "智空无人机平台" --to "智飞无
|
|||
| 区块 | 说明 |
|
||||
|---|---|
|
||||
| 顶部 KPI | 在册人数与带项目人数、各归属小组人数、人员归属(汇智 / 云数 / 外服)分布,以及**花名册外的负责人**核对(历史数据里对不上花名册的人名,一个都不藏) |
|
||||
| 花名册 | 姓名 / 归属小组 / 人员归属 / 说明 + 该人的**在管项目数、定制项目数、定制合同额、名下待计收、最近更新**;小组与归属可行内改(下拉),说明行内编辑失焦即存 |
|
||||
| 花名册 | 姓名 / 归属小组 / 人员归属 / 说明 + 该人的**在管项目数、定开项目数、定开合同额、名下待计收、最近更新**;小组与归属可行内改(下拉),说明行内编辑失焦即存 |
|
||||
| 筛选与导出 | 在编 / 已归档 / 全部 + 按小组筛选;「导出 Excel」按当前筛选导出 |
|
||||
| 新增 | 弹窗录入:姓名必填且全库唯一,小组与归属下拉取自 `constants.STAFF_GROUPS` / `STAFF_AFFILIATIONS` |
|
||||
| 改名 | 走「编辑」改姓名时,**名下项目与定制项目的负责人、其他负责人、团队成员一并同步改掉**,历史数据不会断链 |
|
||||
| 改名 | 走「编辑」改姓名时,**名下项目与定开项目的负责人、其他负责人、团队成员一并同步改掉**,历史数据不会断链 |
|
||||
| 归档 / 恢复 | 归档后不再出现在「负责人」下拉里,但历史项目仍完整保留其负责人;随时可恢复。离职人员用归档,不要用删除 |
|
||||
| 删除 | 名下还挂着项目或定制项目时接口直接返回 400 并说明原因,防止把在管项目变成无主项目 |
|
||||
| 删除 | 名下还挂着项目或定开项目时接口直接返回 400 并说明原因,防止把在管项目变成无主项目 |
|
||||
|
||||
导入名单(表头按名称匹配:序号 / 姓名 / 归属小组 / 人员归属 / 说明):
|
||||
|
||||
|
|
@ -198,14 +198,14 @@ python scripts/import_staff.py --reset # 清空后重新导入
|
|||
python scripts/import_staff.py <xlsx> # 指定别的文件;首次运行 ./run.sh 会自动导入
|
||||
```
|
||||
|
||||
**负责人下拉**(`ownerSelectHtml`)在项目新建/详情、定制项目新建/详情、工作看板等位置统一使用,选项来自花名册:
|
||||
**负责人下拉**(`ownerSelectHtml`)在项目新建/详情、定开项目新建/详情、工作看板等位置统一使用,选项来自花名册:
|
||||
已归档的人不再作为可选项出现,只有他本来就是这条记录的负责人时才带「(已归档)」标注出现一次;
|
||||
历史数据里花名册没有的人名同样补一个「(不在花名册)」选项,保证打开老项目不会把负责人改没。
|
||||
**多负责人与团队成员**复用同一个下拉(`peopleEditorHtml`,现在只有重点项目的抽屉用它):人名下拉**横向排开、自动换行**,选项同样只有花名册里的人,所以新增成员不需要另外维护一份名单;名单里的每一位都带 × 可删,界面上不区分主次。定制交付的负责人是普通的单个 `ownerSelectHtml` 下拉,只有团队成员用这套名单编辑器。
|
||||
**多负责人与团队成员**复用同一个下拉(`peopleEditorHtml`,现在只有重点项目的抽屉用它):人名下拉**横向排开、自动换行**,选项同样只有花名册里的人,所以新增成员不需要另外维护一份名单;名单里的每一位都带 × 可删,界面上不区分主次。定开项目的负责人是普通的单个 `ownerSelectHtml` 下拉,只有团队成员用这套名单编辑器。
|
||||
|
||||
### 入库时间
|
||||
|
||||
定制项目新增「入库时间」字段,当前口径为 **该项目最早记录所在周期的第一天(周一)**:
|
||||
定开项目新增「入库时间」字段,当前口径为 **该项目最早记录所在周期的第一天(周一)**:
|
||||
|
||||
- 优先取原始《项目明细表》台账中该项目所在的周(该列是稀疏填写的,导入时按周分组向下继承)
|
||||
- 台账没有的,取《周工作情况总结》「定制开发」块中该项目首次出现的周
|
||||
|
|
@ -252,18 +252,18 @@ python scripts/add_entry_date.py --force # 全部重算
|
|||
|
||||
| 位置 | 导出内容 | 接口 |
|
||||
|---|---|---|
|
||||
| 定制交付 | 定制项目明细(含区域、金额、进度、收入确认、入库时间、备注 + 合计行) | `GET /api/export/custom` |
|
||||
| 定制交付 | 定制项目周期记录 | `GET /api/export/custom-updates` |
|
||||
| 定开项目 | 定开项目明细(含区域、金额、进度、收入确认、入库时间、备注 + 合计行) | `GET /api/export/custom` |
|
||||
| 定开项目 | 定开项目周期记录 | `GET /api/export/custom-updates` |
|
||||
| 项目管理 | 项目管理清单(分类、负责人、状态、标签、子任务数等) | `GET /api/export/projects` |
|
||||
| 工作看板 | 本周期(或本月)的执行记录明细 | `GET /api/export/updates` |
|
||||
| 每周重点工作 | 重点工作清单(工作内容 / 说明 / 预计完成时间 / 录入周 / 执行状态 / 是否延期 / 结果反馈 / 更新) | `GET /api/export/focus` |
|
||||
| 人力资源库 | 花名册(含在管 / 定制项目数、合同额、待计收) | `GET /api/export/staff` |
|
||||
| 人力资源库 | 花名册(含在管 / 定开项目数、合同额、待计收) | `GET /api/export/staff` |
|
||||
|
||||
导出的 xlsx 带表头样式、冻结首行、自动筛选,金额列按千分位格式;文件名含导出日期。
|
||||
|
||||
## 收入确认口径
|
||||
|
||||
定制项目按**完工百分比法**确认收入:
|
||||
定开项目按**完工百分比法**确认收入:
|
||||
|
||||
| 阶段 | 确认比例 |
|
||||
|---|---|
|
||||
|
|
@ -279,7 +279,7 @@ python scripts/add_entry_date.py --force # 全部重算
|
|||
- **可确认收入** = 合同额 × 上表比例
|
||||
- **待计收** = 可确认收入 − 已计收
|
||||
- **待开票** = 合同额 − 已开票、**待回款** = 合同额 − 已回款(均不小于 0,随开票 / 回款金额自动更新)
|
||||
- 开票与回款金额在项目详情抽屉、定制交付详情抽屉、工作看板右栏都能直接填
|
||||
- 开票与回款金额在项目详情抽屉、定开项目详情抽屉、工作看板右栏都能直接填
|
||||
- 项目上可填「手动确认比例」覆盖阶段推算(用于预付、分批收款等特殊情形)
|
||||
- 比例定义见 `app/constants.py::CUSTOM_STAGES` 与 `custom_revenue_ratio()`,改一处即可全局生效
|
||||
|
||||
|
|
@ -292,10 +292,10 @@ Period(周期: 年/月/周, sort_key)
|
|||
├── Task(子任务)
|
||||
└── Update(执行情况: 正文, 摘要, 进度, 状态, 风险, 下一步, 来源, 原文)
|
||||
|
||||
# 定制交付(独立)
|
||||
# 定开项目(独立)
|
||||
CustomProject(名称, 客户, 区域, 合同额, 阶段, 开发进度, 手动确认比例,
|
||||
已计收金额, 交付负责人(单人), 团队成员, 入库/签单/交付/验收/计收日期, 开票/回款金额, 重点, 归档)
|
||||
# 定制交付不做多负责人:other_owners 列保留在库里但不再读写
|
||||
# 定开项目不做多负责人:other_owners 列保留在库里但不再读写
|
||||
└── CustomUpdate(周期, 阶段, 内容, 进度, 本期新增下单, 本期新增计收, 风险, 下一步)
|
||||
|
||||
# 每周重点工作(独立:条目不是项目,不挂子任务与执行记录)
|
||||
|
|
@ -318,7 +318,7 @@ LedgerItem(客户项目台账) # 只剩模型与 Excel 导入解析,界面
|
|||
两套体系各有独立提示词:
|
||||
|
||||
- **项目管理**(常规执行记录):抽取 摘要 / 进度 / 状态 / 子任务列表 / 风险 / 下一步
|
||||
- **定制交付类**:抽取 商务阶段 / 开发进度 / 合同额 / 累计计收 / 本期新增计收 / 摘要 / 风险 / 下一步 / 本期明细
|
||||
- **定开项目类**:抽取 商务阶段 / 开发进度 / 合同额 / 累计计收 / 本期新增计收 / 摘要 / 风险 / 下一步 / 本期明细
|
||||
- **每周重点工作**不过 AI:在独立面板里直接填(工作内容 / 说明 / 预计完成时间填日历日期 / 执行状态)
|
||||
|
||||
- 默认走 **OpenAI 兼容协议**,支持 DeepSeek、通义千问(兼容模式)、Moonshot、智谱、OpenAI、本地 Ollama 等。
|
||||
|
|
@ -346,11 +346,11 @@ app/
|
|||
main.py FastAPI 入口(API 挂载在 /api,静态页面在 /)
|
||||
config.py 配置与 .env 加载(DATABASE_URL、AI 参数)
|
||||
db.py engine / Session(按 URL 自动适配方言)
|
||||
models.py ORM 模型(项目 + 定制项目 + 每周重点工作 + 花名册)
|
||||
models.py ORM 模型(项目 + 定开项目 + 每周重点工作 + 花名册)
|
||||
constants.py 状态/阶段常量、收入确认比例、区域与阶段识别
|
||||
ai_parser.py LLM 解析 + 本地规则兜底(通用 / 定制两套)
|
||||
ai_parser.py LLM 解析 + 本地规则兜底(通用 / 定开两套)
|
||||
routers/ board(周期/元数据/统计) · weekly(工作看板数据) · projects(项目管理) ·
|
||||
custom(定制交付) · focus(每周重点工作) · staff(人力) · ai · export
|
||||
custom(定开项目) · focus(每周重点工作) · staff(人力) · ai · export
|
||||
web/ index.html · styles.css · app.js(零依赖,无构建步骤)
|
||||
scripts/ init_db.py · import_excel.py · migrate_custom.py · import_staff.py · add_entry_date.py · merge_project.py · shift_year.py
|
||||
data/board.db SQLite 数据库
|
||||
|
|
@ -366,7 +366,7 @@ POST /api/periods 新增周期(传 label 如「9月第1周」
|
|||
DELETE /api/periods/{id} 删除空周期(有记录则拒绝)
|
||||
GET /api/board?period_id=|year&month 周期汇总(页面主链路不走它,统计走 /api/stats)
|
||||
GET /api/stats?period_id=|year&month
|
||||
GET /api/weekly?period_id=|year&month&owner=&all_custom= 工作看板:重点项目 + 定制项目本周动态 + KPI
|
||||
GET /api/weekly?period_id=|year&month&owner=&all_custom= 工作看板:重点项目 + 定开项目本周动态 + KPI
|
||||
(custom_new 本期新入库 / custom_updated 本期有交付记录;行上带 is_new)
|
||||
(左栏第一组「本周工作重点」前端另取 /api/focus 合并)
|
||||
GET /api/projects (?archived=false|true|all)
|
||||
|
|
@ -374,7 +374,7 @@ POST /api/projects PATCH /api/projects/{id} DELETE /api/projects/{id}
|
|||
(owners/members 传人名数组,整体替换:不传=不改,传 []=清空;owners[0] 回写 owner 列供归属统计,界面不再区分主次)
|
||||
GET /api/categories POST /api/categories PATCH /api/categories/{id} DELETE /api/categories/{id}
|
||||
|
||||
# 定制交付
|
||||
# 定开项目
|
||||
GET /api/custom/meta 阶段字典 + 区域列表 + 交付环节(pipeline) + 卡点阈值(aging_days)
|
||||
GET /api/custom/projects?stage=®ion=&keyword=
|
||||
POST /api/custom/projects PATCH /api/custom/projects/{id} DELETE /api/custom/projects/{id}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -223,10 +223,10 @@ def llm_parse(raw: str, project_name: str, period_label: str, db) -> dict[str, A
|
|||
|
||||
|
||||
# ==========================================================================
|
||||
# 定制交付项目:阶段 / 金额 / 计收
|
||||
# 定开项目:阶段 / 金额 / 计收
|
||||
# ==========================================================================
|
||||
CUSTOM_SYSTEM_PROMPT = """你是定制交付项目的管理助理,负责把项目周报原文整理成结构化记录。
|
||||
定制项目关注的是商务阶段流转(商机→签单→开发→交付→验收→计收)与收入确认。
|
||||
CUSTOM_SYSTEM_PROMPT = """你是定开项目的管理助理,负责把项目周报原文整理成结构化记录。
|
||||
定开项目关注的是商务阶段流转(商机→签单→开发→交付→验收→计收)与收入确认。
|
||||
请严格输出 JSON(不要 Markdown 代码块、不要解释),结构如下:
|
||||
{
|
||||
"stage": "developing",
|
||||
|
|
@ -247,7 +247,7 @@ CUSTOM_SYSTEM_PROMPT = """你是定制交付项目的管理助理,负责把项
|
|||
5. 不要编造原文没有的金额和数字。
|
||||
"""
|
||||
|
||||
CUSTOM_USER_TEMPLATE = """定制项目:{project}
|
||||
CUSTOM_USER_TEMPLATE = """定开项目:{project}
|
||||
周期:{period}
|
||||
当前阶段:{stage}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ LEDGER_CATEGORIES = [
|
|||
]
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 定制项目(交付制,与平台产品类项目分开管理)
|
||||
# 定开项目(交付制,与平台产品类项目分开管理)
|
||||
# (code, 名称, 颜色, 收入确认基准比例)
|
||||
# 开发中阶段:base + 0.4 * progress/100
|
||||
# 其余阶段:直接使用 base
|
||||
|
|
@ -74,7 +74,7 @@ def custom_revenue_ratio(stage: str, progress: float | None, manual_ratio=None)
|
|||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 定制项目:文本 → 阶段 / 金额 识别
|
||||
# 定开项目:文本 → 阶段 / 金额 识别
|
||||
# --------------------------------------------------------------------------
|
||||
CUSTOM_STAGE_RULES = [
|
||||
(("暂停", "终止", "未达成一致", "搁置"), "paused"),
|
||||
|
|
@ -259,6 +259,8 @@ REGION_ALIAS = {
|
|||
"天津市": "天津",
|
||||
"上海市": "上海",
|
||||
"重庆市": "重庆",
|
||||
"深圳": "广东",
|
||||
"深圳市": "广东",
|
||||
"未标注": None,
|
||||
"": None,
|
||||
}
|
||||
|
|
@ -339,7 +341,7 @@ DEFAULT_CATEGORY_COLORS = [
|
|||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 定制项目:全过程流水线(入库 → 签单 → 交付 → 验收 → 计收)
|
||||
# 定开项目:全过程流水线(入库 → 签单 → 交付 → 验收 → 计收)
|
||||
# key 环节代码(与 CUSTOM_STAGES 的阶段代码对齐,入库单独用 entry)
|
||||
# name 环节名
|
||||
# date_field 该环节的日期字段(CustomProject 上的属性名)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class TimestampMixin:
|
|||
|
||||
|
||||
class Category(Base, TimestampMixin):
|
||||
"""项目分类(如 平台产品 / AI / 定制交付 / 内部运营)。"""
|
||||
"""项目分类(如 平台产品 / AI / 定开项目 / 内部运营)。"""
|
||||
|
||||
__tablename__ = "categories"
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ class LedgerItem(Base, TimestampMixin):
|
|||
|
||||
|
||||
class CustomProject(Base, TimestampMixin):
|
||||
"""定制交付项目(与通用研发项目完全分离)。
|
||||
"""定开项目(与通用研发项目完全分离)。
|
||||
|
||||
关注:商务阶段流转(签单 → 开发 → 交付 → 验收 → 计收)与按进度确认收入。
|
||||
"""
|
||||
|
|
@ -394,7 +394,7 @@ class CustomProject(Base, TimestampMixin):
|
|||
revenue_amount: Mapped[float | None] = mapped_column(Float, nullable=True) # 已计收金额
|
||||
|
||||
owner: Mapped[str | None] = mapped_column(String(64), nullable=True) # 交付负责人(单人)
|
||||
other_owners: Mapped[str | None] = mapped_column(String(512), nullable=True) # 已停用:定制交付不做多负责人
|
||||
other_owners: Mapped[str | None] = mapped_column(String(512), nullable=True) # 已停用:定开项目不做多负责人
|
||||
members: Mapped[str | None] = mapped_column(String(512), nullable=True) # 团队成员
|
||||
entry_date: Mapped[str | None] = mapped_column(String(16), nullable=True) # 入库时间(YYYY-MM-DD)
|
||||
sign_date: Mapped[str | None] = mapped_column(String(16), nullable=True) # 签单/下单时间
|
||||
|
|
@ -413,6 +413,31 @@ class CustomProject(Base, TimestampMixin):
|
|||
archived: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
source: Mapped[str] = mapped_column(String(16), default="import", nullable=False)
|
||||
|
||||
# ---- 市场/销售侧台账字段(来源《定开项目.xlsx》,2026-09 起)----
|
||||
origin: Mapped[str | None] = mapped_column(String(32), nullable=True) # 项目归属:新签订单/存量项目/汇智直签
|
||||
project_code: Mapped[str | None] = mapped_column(
|
||||
String(64), nullable=True, index=True
|
||||
) # 项目ID(市场编号,导入时用于去重)
|
||||
office: Mapped[str | None] = mapped_column(String(64), nullable=True) # 办事处
|
||||
industry: Mapped[str | None] = mapped_column(String(32), nullable=True) # 行业:安平/政府/企业/教育…
|
||||
man_days: Mapped[float | None] = mapped_column(Float, nullable=True) # 市场下单人天
|
||||
market_owner: Mapped[str | None] = mapped_column(
|
||||
String(128), nullable=True
|
||||
) # 市场责任人(销售侧;多人用 / 分隔,与花名册无关)
|
||||
project_status: Mapped[str | None] = mapped_column(
|
||||
String(32), nullable=True
|
||||
) # 项目状态:已验收/待开发/交付完成/交付中/退单
|
||||
accept_plan: Mapped[str | None] = mapped_column(
|
||||
String(32), nullable=True
|
||||
) # 推动验收计划(Q1..Q4 / 27年)
|
||||
risk_level: Mapped[str | None] = mapped_column(String(32), nullable=True) # 风险值:1.高风险 / 2.低风险
|
||||
update_date: Mapped[str | None] = mapped_column(
|
||||
String(16), nullable=True, index=True
|
||||
) # 更新时间 YYYY-MM-DD:该行数据最后一次变动的日期,用于按周查看变化
|
||||
accept_date_real: Mapped[str | None] = mapped_column(
|
||||
String(16), nullable=True, index=True
|
||||
) # 验收日期 YYYY-MM-DD(真实日期):与 accept_date(季度文本)区分,用于「本月/本年验收」统计
|
||||
|
||||
updates: Mapped[list["CustomUpdate"]] = relationship(
|
||||
back_populates="project", cascade="all, delete-orphan"
|
||||
)
|
||||
|
|
@ -445,6 +470,18 @@ class CustomProject(Base, TimestampMixin):
|
|||
"is_key": self.is_key,
|
||||
"archived": self.archived,
|
||||
"source": self.source,
|
||||
# 市场/销售侧台账字段
|
||||
"origin": self.origin,
|
||||
"project_code": self.project_code,
|
||||
"office": self.office,
|
||||
"industry": self.industry,
|
||||
"man_days": self.man_days,
|
||||
"market_owner": self.market_owner,
|
||||
"project_status": self.project_status,
|
||||
"accept_plan": self.accept_plan,
|
||||
"risk_level": self.risk_level,
|
||||
"update_date": self.update_date,
|
||||
"accept_date_real": self.accept_date_real,
|
||||
"update_count": len(self.updates),
|
||||
"updated_at": self.updated_at.isoformat(timespec="seconds") if self.updated_at else None,
|
||||
}
|
||||
|
|
@ -464,7 +501,7 @@ class CustomProject(Base, TimestampMixin):
|
|||
|
||||
|
||||
class CustomUpdate(Base, TimestampMixin):
|
||||
"""定制项目的阶段流转 / 周期执行记录(独立表)。"""
|
||||
"""定开项目的阶段流转 / 周期执行记录(独立表)。"""
|
||||
|
||||
__tablename__ = "custom_updates"
|
||||
|
||||
|
|
@ -518,7 +555,7 @@ class CustomUpdate(Base, TimestampMixin):
|
|||
class Staff(Base, TimestampMixin):
|
||||
"""人力资源库成员。
|
||||
|
||||
项目管理 / 定制交付的负责人一律从这里选取;归档后不再出现在下拉里,
|
||||
项目管理 / 定开项目的负责人一律从这里选取;归档后不再出现在下拉里,
|
||||
历史记录中的人名保持不变。
|
||||
"""
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -156,7 +156,7 @@ def create_period(payload: PeriodCreate, db: Session = Depends(get_db)):
|
|||
|
||||
@router.delete("/periods/{pid}")
|
||||
def delete_period(pid: int, db: Session = Depends(get_db)):
|
||||
"""删除周期。仅允许删除没有任何执行记录(通用 / 定制)的空周期。"""
|
||||
"""删除周期。仅允许删除没有任何执行记录(通用 / 定开)的空周期。"""
|
||||
from ..models import CustomUpdate
|
||||
|
||||
p = db.get(Period, pid)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""定制交付项目 API(独立于通用项目体系)。
|
||||
"""定开项目 API(独立于通用项目体系)。
|
||||
|
||||
关注:商务阶段流转 与 按进度确认收入。
|
||||
"""
|
||||
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -39,7 +39,7 @@ class CustomProjectIn(BaseModel):
|
|||
progress: float = 0
|
||||
manual_ratio: float | None = None
|
||||
revenue_amount: float | None = None
|
||||
owner: str | None = None # 交付负责人(定制交付为单人,多负责人只用于重点项目)
|
||||
owner: str | None = None # 交付负责人(定开项目为单人,多负责人只用于重点项目)
|
||||
members: list[str] | None = None # 团队成员
|
||||
entry_date: str | None = None
|
||||
sign_date: str | None = None
|
||||
|
|
@ -50,6 +50,17 @@ class CustomProjectIn(BaseModel):
|
|||
received_amount: float | None = None
|
||||
remark: str | None = None
|
||||
is_key: bool = False
|
||||
# 市场/销售侧台账字段
|
||||
origin: str | None = None
|
||||
project_code: str | None = None
|
||||
office: str | None = None
|
||||
industry: str | None = None
|
||||
man_days: float | None = None
|
||||
market_owner: str | None = None
|
||||
project_status: str | None = None
|
||||
accept_plan: str | None = None
|
||||
risk_level: str | None = None
|
||||
accept_date_real: str | None = None # 验收日期(真实日期,区别于 accept_date 季度文本)
|
||||
|
||||
|
||||
class CustomParseIn(BaseModel):
|
||||
|
|
@ -102,13 +113,28 @@ def _sort_by_entry(projects):
|
|||
return sorted(projects, key=lambda c: (_entry_key(c), c.id), reverse=True)
|
||||
|
||||
|
||||
def _sort_by_update(projects):
|
||||
"""按「更新时间」倒序(最近变动的在前);无更新时间的回落到入库时间。"""
|
||||
def key(c):
|
||||
d = (c.update_date or "").strip()
|
||||
return (d if d else "", _entry_key(c), c.id)
|
||||
return sorted(projects, key=key, reverse=True)
|
||||
|
||||
|
||||
def _week_range(today: date | None = None) -> tuple[date, date]:
|
||||
"""当前自然周(周一~周日)。"""
|
||||
t = today or date.today()
|
||||
start = t - timedelta(days=t.weekday())
|
||||
return start, start + timedelta(days=6)
|
||||
|
||||
|
||||
def _unique_name(db: Session, name: str, exclude_id: int | None = None):
|
||||
"""同名会让录入按名称匹配时张冠李戴,禁止重名。"""
|
||||
q = db.query(CustomProject).filter(CustomProject.name == name)
|
||||
if exclude_id is not None:
|
||||
q = q.filter(CustomProject.id != exclude_id)
|
||||
if q.first():
|
||||
raise HTTPException(400, f"已存在同名定制项目「{name}」,请换个名称")
|
||||
raise HTTPException(400, f"已存在同名定开项目「{name}」,请换个名称")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 元信息
|
||||
|
|
@ -170,7 +196,7 @@ def list_custom_projects(
|
|||
if owner:
|
||||
q = q.filter(CustomProject.owner == owner)
|
||||
|
||||
return [c.to_dict() for c in _sort_by_entry(q.all())]
|
||||
return [c.to_dict() for c in _sort_by_update(q.all())]
|
||||
|
||||
|
||||
@router.post("/custom/projects")
|
||||
|
|
@ -181,8 +207,12 @@ def create_custom_project(payload: CustomProjectIn, db: Session = Depends(get_db
|
|||
raise HTTPException(400, "项目名称不能为空")
|
||||
data["name"] = name
|
||||
data["region"] = normalize_region(data.get("region"))
|
||||
# 新建表单只有「办事处」,没有单独的「区域」:用办事处归一化出区域
|
||||
if not data.get("region") and data.get("office"):
|
||||
data["region"] = normalize_region(data.get("office"))
|
||||
if not data.get("entry_date"):
|
||||
data["entry_date"] = date.today().isoformat()
|
||||
data["update_date"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
_unique_name(db, name)
|
||||
cp = CustomProject(**data, source="manual")
|
||||
set_people(cp, None, payload.members)
|
||||
|
|
@ -196,7 +226,7 @@ def create_custom_project(payload: CustomProjectIn, db: Session = Depends(get_db
|
|||
def get_custom_project(cid: int, db: Session = Depends(get_db)):
|
||||
cp = db.get(CustomProject, cid)
|
||||
if not cp:
|
||||
raise HTTPException(404, "定制项目不存在")
|
||||
raise HTTPException(404, "定开项目不存在")
|
||||
ups = (
|
||||
db.query(CustomUpdate, Period)
|
||||
.join(Period, Period.id == CustomUpdate.period_id)
|
||||
|
|
@ -225,12 +255,15 @@ def get_custom_project(cid: int, db: Session = Depends(get_db)):
|
|||
def patch_custom_project(cid: int, payload: dict, db: Session = Depends(get_db)):
|
||||
cp = db.get(CustomProject, cid)
|
||||
if not cp:
|
||||
raise HTTPException(404, "定制项目不存在")
|
||||
raise HTTPException(404, "定开项目不存在")
|
||||
allowed = (
|
||||
"name", "customer", "region", "amount", "stage", "progress", "manual_ratio",
|
||||
"revenue_amount", "invoice_amount", "received_amount", "owner",
|
||||
"entry_date", "sign_date", "deliver_date", "accept_date", "revenue_date",
|
||||
"remark", "is_key", "archived", "period_id",
|
||||
# 市场/销售侧台账字段
|
||||
"origin", "project_code", "office", "industry", "man_days",
|
||||
"market_owner", "project_status", "accept_plan", "risk_level", "accept_date_real",
|
||||
)
|
||||
payload = payload or {}
|
||||
if "name" in payload:
|
||||
|
|
@ -242,12 +275,18 @@ def patch_custom_project(cid: int, payload: dict, db: Session = Depends(get_db))
|
|||
payload["name"] = new_name
|
||||
if "region" in payload:
|
||||
payload["region"] = normalize_region(payload.get("region"))
|
||||
changed = False
|
||||
for k, v in payload.items():
|
||||
if k in allowed:
|
||||
if getattr(cp, k) != v:
|
||||
changed = True
|
||||
setattr(cp, k, v)
|
||||
# 团队成员:名单型字段,整体替换(不传即不改);负责人只有单人,走上面的 owner
|
||||
set_people(cp, None, payload.get("members"))
|
||||
_advance_stage(cp, payload)
|
||||
# 「更新时间」= 本次提交时间:台账字段有变化时刷新,便于按周查看最新改动
|
||||
if changed:
|
||||
cp.update_date = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
db.commit()
|
||||
db.refresh(cp)
|
||||
return cp.to_dict()
|
||||
|
|
@ -257,7 +296,7 @@ def patch_custom_project(cid: int, payload: dict, db: Session = Depends(get_db))
|
|||
def delete_custom_project(cid: int, db: Session = Depends(get_db)):
|
||||
cp = db.get(CustomProject, cid)
|
||||
if not cp:
|
||||
raise HTTPException(404, "定制项目不存在")
|
||||
raise HTTPException(404, "定开项目不存在")
|
||||
name = cp.name
|
||||
n_upd = len(cp.updates)
|
||||
db.delete(cp)
|
||||
|
|
@ -265,6 +304,51 @@ def delete_custom_project(cid: int, db: Session = Depends(get_db)):
|
|||
return {"ok": True, "name": name, "deleted_updates": n_upd}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 按周删除
|
||||
def _week_filtered(db: Session):
|
||||
"""本周(周一~周日)有「更新时间」的定开项目。"""
|
||||
start, end = _week_range()
|
||||
rows = (
|
||||
db.query(CustomProject)
|
||||
.filter(
|
||||
CustomProject.update_date.isnot(None),
|
||||
CustomProject.update_date >= start.isoformat(),
|
||||
CustomProject.update_date <= end.isoformat(),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return start, end, rows
|
||||
|
||||
|
||||
@router.get("/custom/weekly-preview")
|
||||
def weekly_preview(db: Session = Depends(get_db)):
|
||||
"""本周有更新的项目清单(供「删除本周更新」二次确认)。"""
|
||||
start, end, rows = _week_filtered(db)
|
||||
return {
|
||||
"start": start.isoformat(),
|
||||
"end": end.isoformat(),
|
||||
"count": len(rows),
|
||||
"amount": round(sum(r.amount or 0 for r in rows), 2),
|
||||
"names": [r.name for r in rows[:60]],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/custom/purge-week")
|
||||
def purge_week(db: Session = Depends(get_db)):
|
||||
"""删除本周(周一~周日)更新的定开项目,连带其记录。破坏性操作,前端须二次确认。"""
|
||||
start, end, rows = _week_filtered(db)
|
||||
names = [r.name for r in rows]
|
||||
for r in rows:
|
||||
db.delete(r) # custom_updates 走 relationship cascade="all, delete-orphan"
|
||||
db.commit()
|
||||
return {
|
||||
"deleted": len(names),
|
||||
"start": start.isoformat(),
|
||||
"end": end.isoformat(),
|
||||
"names": names,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 阶段看板
|
||||
@router.get("/custom/board")
|
||||
def custom_board(
|
||||
|
|
@ -459,7 +543,7 @@ def custom_stats(
|
|||
def create_custom_update(payload: dict, db: Session = Depends(get_db)):
|
||||
cp = db.get(CustomProject, payload.get("custom_project_id"))
|
||||
if not cp:
|
||||
raise HTTPException(404, "定制项目不存在")
|
||||
raise HTTPException(404, "定开项目不存在")
|
||||
u = CustomUpdate(
|
||||
custom_project_id=payload["custom_project_id"],
|
||||
period_id=payload["period_id"],
|
||||
|
|
@ -504,7 +588,7 @@ def delete_custom_update(uid: int, db: Session = Depends(get_db)):
|
|||
def custom_ai_parse(payload: CustomParseIn, db: Session = Depends(get_db)):
|
||||
cp = db.get(CustomProject, payload.custom_project_id) if payload.custom_project_id else None
|
||||
if payload.custom_project_id and not cp:
|
||||
raise HTTPException(404, "定制项目不存在")
|
||||
raise HTTPException(404, "定开项目不存在")
|
||||
period = db.get(Period, payload.period_id) if payload.period_id else None
|
||||
|
||||
parsed, mode, error = ai_parser.parse_custom(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""数据导出(Excel / xlsx)。
|
||||
|
||||
提供四类导出,均可带上页面当前的筛选条件:
|
||||
- /export/custom 定制交付项目明细
|
||||
- /export/custom-updates 定制项目周期记录
|
||||
- /export/custom 定开项目明细
|
||||
- /export/custom-updates 定开项目周期记录
|
||||
- /export/projects 常规项目管理
|
||||
- /export/updates 常规项目周期执行记录
|
||||
- /export/staff 人力资源库(含项目负载)
|
||||
|
|
@ -28,7 +28,7 @@ from ..models import (
|
|||
WeeklyFocus,
|
||||
)
|
||||
from .board import worst_status
|
||||
from .custom import _sort_by_entry
|
||||
from .custom import _sort_by_entry, _sort_by_update
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -42,11 +42,12 @@ def _resp(content: bytes, filename: str) -> Response:
|
|||
return Response(content=body, headers=headers)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 定制交付
|
||||
# ---------------------------------------------------------------- 定开项目
|
||||
@router.get("/export/custom")
|
||||
def export_custom(
|
||||
region: str | None = None,
|
||||
stage: str | None = None,
|
||||
origin: str | None = None,
|
||||
status: str | None = None,
|
||||
keyword: str | None = None,
|
||||
archived: str = Query("false", pattern="^(true|false|all)$"),
|
||||
db: Session = Depends(get_db),
|
||||
|
|
@ -58,50 +59,39 @@ def export_custom(
|
|||
q = q.filter(CustomProject.archived.is_(True))
|
||||
if region:
|
||||
q = q.filter(CustomProject.region == region)
|
||||
if stage:
|
||||
q = q.filter(CustomProject.stage == stage)
|
||||
if origin:
|
||||
q = q.filter(CustomProject.origin == origin)
|
||||
if status:
|
||||
q = q.filter(CustomProject.project_status == status)
|
||||
if keyword:
|
||||
q = q.filter(CustomProject.name.contains(keyword))
|
||||
projects = _sort_by_entry(q.all())
|
||||
projects = _sort_by_update(q.all())
|
||||
|
||||
periods = {p.id: p for p in db.query(Period).all()}
|
||||
# 列与「定开项目明细」表格一致
|
||||
headers = [
|
||||
"项目名称", "区域", "客户", "商务阶段", "负责人",
|
||||
"合同额(元)", "入库时间", "签单时间", "交付时间", "验收时间", "计收时间",
|
||||
"开发进度(%)", "收入确认比例(%)", "可确认收入(元)",
|
||||
"已开票(元)", "待开票(元)", "已回款(元)", "待回款(元)",
|
||||
"已计收(元)", "待计收(元)", "最近更新周期", "重点", "归档", "备注",
|
||||
"项目归属", "项目名称", "项目ID", "下单时间", "办事处", "行业",
|
||||
"市场下单人天", "订单金额(元)", "市场责任人", "项目状态",
|
||||
"验收时间", "推动验收计划", "风险值", "备注", "更新时间",
|
||||
]
|
||||
rows = []
|
||||
for c in projects:
|
||||
d = c.to_dict()
|
||||
p = periods.get(c.period_id)
|
||||
rows.append(
|
||||
[
|
||||
c.origin or "",
|
||||
c.name,
|
||||
c.region or "",
|
||||
c.customer or "",
|
||||
_stage_name(c.stage),
|
||||
c.owner or "",
|
||||
c.amount,
|
||||
c.entry_date or "",
|
||||
c.project_code or "",
|
||||
c.sign_date or "",
|
||||
c.deliver_date or "",
|
||||
c.office or "",
|
||||
c.industry or "",
|
||||
c.man_days if c.man_days is not None else "",
|
||||
c.amount if c.amount is not None else "",
|
||||
c.market_owner or "",
|
||||
c.project_status or "",
|
||||
c.accept_date or "",
|
||||
c.revenue_date or "",
|
||||
round(c.progress or 0, 1),
|
||||
d["revenue_ratio"],
|
||||
d["confirmed_amount"],
|
||||
c.invoice_amount,
|
||||
d["pending_invoice"],
|
||||
c.received_amount,
|
||||
d["pending_received"],
|
||||
c.revenue_amount,
|
||||
d["pending_amount"],
|
||||
p.to_dict()["full_label"] if p else "",
|
||||
"是" if c.is_key else "",
|
||||
"是" if c.archived else "",
|
||||
(c.remark or "").replace("\n", " ")[:200],
|
||||
c.accept_plan or "",
|
||||
c.risk_level or "",
|
||||
(c.remark or "").replace("\n", " "),
|
||||
c.update_date or "",
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -110,27 +100,21 @@ def export_custom(
|
|||
def total(key: str) -> float:
|
||||
return round(sum(d.get(key) or 0 for d in dicts), 2)
|
||||
|
||||
blank = [""] * 24
|
||||
totals = ["合计", "", "", f"{len(projects)} 个项目", ""]
|
||||
totals += [total("amount")] # 合同额
|
||||
totals += [""] * 6 # 五个环节日期 + 开发进度
|
||||
totals += ["", total("confirmed_amount")] # 确认比例 / 可确认收入
|
||||
totals += [total("invoice_amount"), total("pending_invoice")]
|
||||
totals += [total("received_amount"), total("pending_received")]
|
||||
totals += [total("revenue_amount"), total("pending_amount")]
|
||||
totals += ["", "", "", ""]
|
||||
rows.append(blank)
|
||||
rows.append(totals)
|
||||
rows.append([""] * 15)
|
||||
rows.append(
|
||||
["合计", f"{len(projects)} 个项目", "", "", "", "",
|
||||
total("man_days"), total("amount"), "", "", "", "", "", "", ""]
|
||||
)
|
||||
|
||||
content = build_xlsx(
|
||||
"定制交付项目",
|
||||
"定开项目明细",
|
||||
headers,
|
||||
rows,
|
||||
widths=[46, 10, 18, 14, 12, 14, 12, 12, 12, 12, 12, 11, 14, 16, 14, 14, 14, 14, 14, 14, 18, 7, 7, 40],
|
||||
title="定制交付项目明细(入库 → 签单 → 交付 → 验收 → 计收)",
|
||||
money_cols=(6, 14, 15, 16, 17, 18, 19, 20),
|
||||
widths=[10, 46, 18, 12, 10, 8, 13, 14, 14, 11, 11, 14, 10, 40, 18],
|
||||
title="定开项目明细",
|
||||
money_cols=(8,), # 订单金额(元)
|
||||
)
|
||||
return _resp(content, "定制交付项目明细")
|
||||
return _resp(content, "定开项目明细")
|
||||
|
||||
|
||||
@router.get("/export/custom-updates")
|
||||
|
|
@ -171,14 +155,14 @@ def export_custom_updates(
|
|||
)
|
||||
|
||||
content = build_xlsx(
|
||||
"定制项目记录",
|
||||
"定开项目记录",
|
||||
headers,
|
||||
rows,
|
||||
widths=[18, 42, 10, 14, 70, 10, 16, 16, 30, 30],
|
||||
title="定制项目周期记录",
|
||||
title="定开项目周期记录",
|
||||
money_cols=(7, 8),
|
||||
)
|
||||
return _resp(content, "定制项目周期记录")
|
||||
return _resp(content, "定开项目周期记录")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 常规项目
|
||||
|
|
@ -309,7 +293,7 @@ def export_staff(
|
|||
|
||||
headers = [
|
||||
"序号", "姓名", "归属小组", "人员归属", "在管项目", "重点项目",
|
||||
"定制项目", "定制合同额(元)", "定制待计收(元)", "最近更新", "说明", "归档",
|
||||
"定开项目", "定开合同额(元)", "定开待计收(元)", "最近更新", "说明", "归档",
|
||||
]
|
||||
rows = []
|
||||
for i, m in enumerate(members, 1):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
和项目管理(Project + Update)分开:不用先建项目档案,一条就是一个活儿。
|
||||
字段按需求——工作内容 / 说明 / 预计完成时间(日历日期)/ 执行状态 / 结果反馈 / 是否延期。
|
||||
period_id 是「录入周」——新建时顶栏选中的那一周,和项目管理/定制交付的周期归属同一个意思;
|
||||
period_id 是「录入周」——新建时顶栏选中的那一周,和项目管理/定开项目的周期归属同一个意思;
|
||||
预计完成时间只存 due_date,不再按日期反查周期。本周录一条两周后要干的事,本周照样看得见。
|
||||
「是否延期」是人工开关(delayed):更早周次录入、标了延期又没做完的,结转进当周视图。
|
||||
录入整理在「每周重点工作」页,汇总在「工作看板」。
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""人力资源库:人员维护 + 人员负载。
|
||||
|
||||
项目 / 定制项目的「负责人」统一取这里的姓名,因此:
|
||||
项目 / 定开项目的「负责人」统一取这里的姓名,因此:
|
||||
· 改名时同步刷新已有项目的负责人字段;
|
||||
· 被项目引用的人不允许直接删除,只能归档(归档后不再出现在下拉里)。
|
||||
"""
|
||||
|
|
@ -51,7 +51,7 @@ ALLOWED_PATCH = ("name", "group", "affiliation", "note", "sort_order", "archived
|
|||
|
||||
|
||||
def _workload(db: Session) -> dict[str, dict]:
|
||||
"""按姓名汇总在管项目 / 定制项目负载。"""
|
||||
"""按姓名汇总在管项目 / 定开项目负载。"""
|
||||
load: dict[str, dict] = {}
|
||||
|
||||
def slot(name: str) -> dict:
|
||||
|
|
@ -283,7 +283,7 @@ def delete_staff(sid: int, force: bool = False, db: Session = Depends(get_db)):
|
|||
if (n1 or n2) and not force:
|
||||
raise HTTPException(
|
||||
400,
|
||||
f"「{m.name}」名下还有 {n1} 个在管项目 / {n2} 个定制项目。"
|
||||
f"「{m.name}」名下还有 {n1} 个在管项目 / {n2} 个定开项目。"
|
||||
f"离职人员建议直接「归档」(归档后不再出现在负责人下拉里,历史记录保留);"
|
||||
f"确需删除请先转交这些项目。",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""周看板:重点项目 + 定制项目的单周总览。
|
||||
"""周看板:重点项目 + 定开项目的单周总览。
|
||||
|
||||
左侧总览清单(左栏)+ 右侧执行详情(右栏)一次给齐:
|
||||
· 重点项目 = 打了 ★ 的项目,以及本周期有执行记录的项目
|
||||
· 定制项目 = 打了 ★ 的定制项目,本周期有交付记录的,以及本周期新入库的(is_new,可切「全部在途」)
|
||||
· 定开项目 = 打了 ★ 的定开项目,本周期有交付记录的,以及本周期新入库的(is_new,可切「全部在途」)
|
||||
前端左栏只给一个入口,点开后在右栏按「本周新增 / 本周有变化」分组展示。
|
||||
右栏点开某一条时再按需拉 /projects/{id} 或 /custom/projects/{id} 看全量历史。
|
||||
工作看板左栏的「本周工作重点」不在本接口里:前端并发取 /api/focus 后合并成一组。
|
||||
|
|
@ -108,7 +108,7 @@ def weekly(
|
|||
}
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------- 定制项目
|
||||
# ---------------------------------------------------------------- 定开项目
|
||||
cups_by_project: dict[int, list[CustomUpdate]] = defaultdict(list)
|
||||
for u in (
|
||||
db.query(CustomUpdate).filter(CustomUpdate.period_id.in_(period_ids)).order_by(CustomUpdate.id).all()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""老库补列:create_all 只会建新表,不会给已存在的表加字段。
|
||||
|
||||
模型新增可空字段后(如定制项目的验收时间 / 开票 / 回款),启动时自动
|
||||
模型新增可空字段后(如定开项目的验收时间 / 开票 / 回款),启动时自动
|
||||
`ALTER TABLE ... ADD COLUMN` 补齐,不需要手写迁移脚本。
|
||||
|
||||
用法:在 `Base.metadata.create_all()` 之后调用 `ensure_columns(engine, Base.metadata)`。
|
||||
|
|
|
|||
BIN
data/board.db
BIN
data/board.db
Binary file not shown.
2
run.sh
2
run.sh
|
|
@ -60,7 +60,7 @@ if [ ! -f data/board.db ]; then
|
|||
if [ -f "$XLSX" ]; then
|
||||
echo "==> 导入历史数据:$XLSX"
|
||||
"$PY" scripts/import_excel.py "$XLSX"
|
||||
echo "==> 拆分定制交付项目"
|
||||
echo "==> 拆分定开项目"
|
||||
"$PY" scripts/migrate_custom.py
|
||||
else
|
||||
echo " (未找到 Excel 源文件,跳过历史数据导入:$XLSX)"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""为定制项目补充『入库时间』字段并回填历史数据。
|
||||
"""为定开项目补充『入库时间』字段并回填历史数据。
|
||||
|
||||
取值优先级(当前口径 = 原始数据的月/周第一天):
|
||||
1. 原始 Excel 里该项目**最早出现**的周期 → 换算成该周期第一天
|
||||
|
|
|
|||
|
|
@ -146,12 +146,12 @@ def main() -> None:
|
|||
# ---------- Step 2: 现有 9月第1周 数据 → 9月第2周 ----------
|
||||
moved_u = db.query(Update).filter(Update.period_id == p_last.id).all()
|
||||
moved_c = db.query(CustomUpdate).filter(CustomUpdate.period_id == p_last.id).all()
|
||||
print(f"[2] 9月第1周 现有数据 → 9月第2周:常规 {len(moved_u)} 条 / 定制 {len(moved_c)} 条")
|
||||
print(f"[2] 9月第1周 现有数据 → 9月第2周:常规 {len(moved_u)} 条 / 定开 {len(moved_c)} 条")
|
||||
for u in moved_u:
|
||||
print(f" 常规#{u.id} {u.project.name}"
|
||||
f"{'/' + u.task.name if u.task else ''}")
|
||||
for c in moved_c:
|
||||
print(f" 定制#{c.id} {c.project.name}")
|
||||
print(f" 定开#{c.id} {c.project.name}")
|
||||
|
||||
# ---------- Step 3: 拆分 8月第4周 ----------
|
||||
print(f"\n[3] 拆分 8月第4周(把属于 9月第1周 的内容移到 id={p_last.id})")
|
||||
|
|
@ -162,7 +162,7 @@ def main() -> None:
|
|||
plan_move, plan_new, plan_trim = [], [], []
|
||||
for (proj, task_name), content_last in last_block.items():
|
||||
if proj == "定制开发":
|
||||
continue # 定制项目走 Step 4,不进常规表
|
||||
continue # 定开项目走 Step 4,不进常规表
|
||||
key = (proj, task_name)
|
||||
u = cur_updates.get(key)
|
||||
if u is None:
|
||||
|
|
@ -195,8 +195,8 @@ def main() -> None:
|
|||
for proj, task_name, content in plan_new:
|
||||
print(f" 新建 {proj}/{task_name}({len(content)}字)")
|
||||
|
||||
# ---------- Step 4: 定制记录 ----------
|
||||
print(f"\n[4] 定制记录归类")
|
||||
# ---------- Step 4: 定开记录 ----------
|
||||
print(f"\n[4] 定开记录归类")
|
||||
cur_custom = db.query(CustomUpdate).filter(
|
||||
CustomUpdate.period_id == p_prev.id).all()
|
||||
last_names = {d for (_p, d) in last_block}
|
||||
|
|
@ -208,7 +208,7 @@ def main() -> None:
|
|||
custom_move.append(c)
|
||||
for c in cur_custom:
|
||||
tag = "→ 9月第1周" if c in custom_move else "留在 8月第4周"
|
||||
print(f" 定制#{c.id} {c.project.name[:26]:28s} {tag}")
|
||||
print(f" 定开#{c.id} {c.project.name[:26]:28s} {tag}")
|
||||
|
||||
# ---------- Step 5: 清理空的多余周期 ----------
|
||||
junk = [
|
||||
|
|
@ -289,8 +289,8 @@ def main() -> None:
|
|||
p.status = "in_progress"
|
||||
print(f" {p.name[:22]:24s} 进度={p.progress} 状态={p.status}")
|
||||
|
||||
# ---------- 重算受影响定制项目的入库时间 ----------
|
||||
print("\n[7] 重算受影响定制项目的入库时间")
|
||||
# ---------- 重算受影响定开项目的入库时间 ----------
|
||||
print("\n[7] 重算受影响定开项目的入库时间")
|
||||
from app.constants import period_first_day
|
||||
cid_touched = {c.custom_project_id for c in moved_c} | {
|
||||
c.custom_project_id for c in custom_move}
|
||||
|
|
@ -317,7 +317,7 @@ def main() -> None:
|
|||
for p in db.query(Period).order_by(Period.sort_key).all()[-5:]:
|
||||
n1 = db.query(Update).filter(Update.period_id == p.id).count()
|
||||
n2 = db.query(CustomUpdate).filter(CustomUpdate.period_id == p.id).count()
|
||||
print(f" {p.year}年{p.month}月第{p.week}周 常规 {n1:3d} / 定制 {n2:3d}")
|
||||
print(f" {p.year}年{p.month}月第{p.week}周 常规 {n1:3d} / 定开 {n2:3d}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
"""把《定开项目.xlsx》的市场台账导入定开项目表(custom_projects)。
|
||||
|
||||
字段映射(表格列 → 数据库列):
|
||||
|
||||
| 表格列 | 数据库列 | 说明 |
|
||||
|--------------|-----------------|------|
|
||||
| 项目归属 | origin | 新签订单 / 存量项目 / 汇智直签 |
|
||||
| 项目名称 | name | 匹配键之一 |
|
||||
| 项目ID | project_code | 匹配键(优先) |
|
||||
| 下单时间 | sign_date | 日期取 YYYY-MM-DD,像「2024年3月前」这种文本原样保留 |
|
||||
| 办事处 | office / region | office 存原样;region 归一化进区域词表,供「区域」筛选 |
|
||||
| 行业 | industry | |
|
||||
| 市场下单人天 | man_days | |
|
||||
| 订单金额 | amount | |
|
||||
| 市场责任人 | market_owner | 销售侧人员,与花名册无关,可「张宁/叶镇」这种多人写法 |
|
||||
| 项目状态 | project_status | 已验收 / 待开发 / 交付完成 / 交付中 / 退单 |
|
||||
| 验收时间 | accept_date | 存的是季度文本(Q1..Q4),不是日期 |
|
||||
| 推动验收计划 | accept_plan | Q1..Q4 / 27年 |
|
||||
| 风险值 | risk_level | 1.高风险 / 2.低风险 |
|
||||
| 备注 | remark | |
|
||||
| (脚本补) | update_date | 更新时间,跟随「下单时间」(sign_date),便于按周查看(sign_date 缺失时回退到导入日期) |
|
||||
|
||||
匹配与更新口径:
|
||||
|
||||
- 优先按「项目ID」匹配已有行;**有项目ID 时只按 ID 匹配**(表里存在同名不同 ID 的项目,
|
||||
退回名称匹配会把它们互相覆盖)。只有项目ID 为空才退回按「项目名称」匹配。
|
||||
- 同一行数据在一次运行内只匹配一个已有项目,避免重名互相吞掉。
|
||||
- **命中**:只比较上面这些台账字段,有变化才写库,并把 `update_date` 刷成「下单时间」(sign_date);
|
||||
没变化则整行不动(`update_date` 保持原值)。sign_date 缺失时回退到导入日期。
|
||||
- **未命中**:新增,`update_date` = 下单时间(sign_date),`entry_date` 兜底取下单时间。
|
||||
- 不触碰 stage / progress / 计收 / 开票 / 回款等业务字段。
|
||||
|
||||
用法:
|
||||
|
||||
python scripts/import_custom_ledger.py --dry-run # 只预演,不写库
|
||||
python scripts/import_custom_ledger.py # 实际写入
|
||||
python scripts/import_custom_ledger.py --date 2026-09-17 # 指定「更新时间」
|
||||
python scripts/import_custom_ledger.py --xlsx "D:/定开项目.xlsx"
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import openpyxl # noqa: E402
|
||||
|
||||
from app.constants import normalize_region # noqa: E402
|
||||
from app.db import SessionLocal # noqa: E402
|
||||
from app.models import CustomProject # noqa: E402
|
||||
|
||||
DEFAULT_XLSX = str(Path.home() / "Desktop" / "定开项目.xlsx")
|
||||
|
||||
# 表格列名 → 数据库字段
|
||||
COLMAP = {
|
||||
"项目归属": "origin",
|
||||
"项目名称": "name",
|
||||
"项目ID": "project_code",
|
||||
"下单时间": "sign_date",
|
||||
"办事处": "office",
|
||||
"行业": "industry",
|
||||
"市场下单人天": "man_days",
|
||||
"订单金额": "amount",
|
||||
"市场责任人": "market_owner",
|
||||
"项目状态": "project_status",
|
||||
"验收时间": "accept_date",
|
||||
"推动验收计划": "accept_plan",
|
||||
"风险值": "risk_level",
|
||||
"备注": "remark",
|
||||
}
|
||||
|
||||
NUMERIC = {"man_days", "amount"}
|
||||
# 参与「有没有变化」比较的字段(region 由 office 推导,一并比较)
|
||||
TRACKED = list(COLMAP.values()) + ["region"]
|
||||
|
||||
|
||||
def _text(value) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
s = str(value).strip()
|
||||
return s or None
|
||||
|
||||
|
||||
def _num(value) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
s = str(value).replace(",", "").strip()
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _date_text(value) -> str | None:
|
||||
"""下单时间:真日期 → YYYY-MM-DD;文本(如「2024年3月前」「2024/」)原样保留。"""
|
||||
if isinstance(value, datetime):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
if isinstance(value, date):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
return _text(value)
|
||||
|
||||
|
||||
def read_rows(xlsx: str) -> list[dict]:
|
||||
wb = openpyxl.load_workbook(xlsx, data_only=True)
|
||||
ws = wb.worksheets[0]
|
||||
header = [_text(ws.cell(1, c).value) for c in range(1, ws.max_column + 1)]
|
||||
|
||||
missing = [name for name in COLMAP if name not in header]
|
||||
if missing:
|
||||
raise SystemExit(f"[x] 表格缺少这些列:{missing}\n 实际表头:{header}")
|
||||
|
||||
col = {field: header.index(name) + 1 for name, field in COLMAP.items()}
|
||||
|
||||
rows: list[dict] = []
|
||||
for r in range(2, ws.max_row + 1):
|
||||
raw = {field: ws.cell(r, c).value for field, c in col.items()}
|
||||
if not any(v is not None and str(v).strip() for v in raw.values()):
|
||||
continue
|
||||
|
||||
row: dict = {}
|
||||
for field, value in raw.items():
|
||||
if field in NUMERIC:
|
||||
row[field] = _num(value)
|
||||
elif field == "sign_date":
|
||||
row[field] = _date_text(value)
|
||||
else:
|
||||
row[field] = _text(value)
|
||||
|
||||
if not row.get("name"):
|
||||
print(f"[warn] 第 {r} 行没有项目名称,跳过")
|
||||
continue
|
||||
|
||||
row["region"] = normalize_region(row.get("office"))
|
||||
# 验收日期(真实日期,区别于 accept_date 季度文本):仅对「已验收」且验收时间为季度文本的项目,
|
||||
# 补一个当年季度末占位,使「本年/本月验收」统计有数可算。用户后续在界面上改了真日期不会被覆盖。
|
||||
row["accept_date_real"] = None
|
||||
if row.get("project_status") == "已验收" and row.get("accept_date"):
|
||||
m = re.match(r"Q([1-4])", str(row["accept_date"]))
|
||||
if m:
|
||||
q = int(m.group(1))
|
||||
me = [(3, 31), (6, 30), (9, 30), (12, 31)][q - 1]
|
||||
row["accept_date_real"] = f"{date.today().year}-{me[0]:02d}-{me[1]:02d}"
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def apply_fields(cp: CustomProject, row: dict, only: list[str] | None = None) -> list[tuple[str, object, object]]:
|
||||
"""把 row 的字段写到 cp 上,返回 (字段, 旧值, 新值) 的变更列表。"""
|
||||
changes: list[tuple[str, object, object]] = []
|
||||
for field in (only or list(row)):
|
||||
if field not in row:
|
||||
continue
|
||||
old = getattr(cp, field)
|
||||
new = row[field]
|
||||
# 数值统一按 float 比,避免 1000000 / 1000000.0 被判成变化
|
||||
if field in NUMERIC:
|
||||
old_cmp = _num(old)
|
||||
new_cmp = _num(new)
|
||||
else:
|
||||
old_cmp = old
|
||||
new_cmp = new
|
||||
if old_cmp != new_cmp:
|
||||
changes.append((field, old, new))
|
||||
setattr(cp, field, new)
|
||||
return changes
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="导入定开项目台账")
|
||||
ap.add_argument("--xlsx", default=DEFAULT_XLSX, help="台账 Excel 路径")
|
||||
ap.add_argument("--date", default=date.today().isoformat(), help="写入的「更新时间」YYYY-MM-DD,默认今天")
|
||||
ap.add_argument("--dry-run", action="store_true", help="只预演,不写库")
|
||||
args = ap.parse_args()
|
||||
|
||||
xlsx = Path(args.xlsx)
|
||||
if not xlsx.exists():
|
||||
raise SystemExit(f"[x] 找不到文件:{xlsx}")
|
||||
|
||||
rows = read_rows(str(xlsx))
|
||||
print(f"==> 读取 {xlsx}")
|
||||
print(f" 有效数据 {len(rows)} 行;「更新时间」口径 = {args.date}")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
by_code: dict[str, CustomProject] = {}
|
||||
by_name: dict[str, CustomProject] = {}
|
||||
for cp in db.query(CustomProject).all():
|
||||
if cp.project_code:
|
||||
by_code.setdefault(cp.project_code, cp)
|
||||
by_name.setdefault((cp.name or "").strip(), cp)
|
||||
|
||||
matched_ids: set[int] = set()
|
||||
inserted = updated = unchanged = 0
|
||||
change_log: list[str] = []
|
||||
|
||||
for row in rows:
|
||||
cp = None
|
||||
code = row.get("project_code")
|
||||
if code:
|
||||
# 有项目ID 时只认 ID:表里存在同名不同 ID 的两个项目(如甘肃省水利厅二期),
|
||||
# 退回按名称匹配会把它们互相覆盖。
|
||||
cand = by_code.get(code)
|
||||
if cand is not None and cand.id not in matched_ids:
|
||||
cp = cand
|
||||
else:
|
||||
cand = by_name.get(row["name"])
|
||||
if cand is not None and cand.id not in matched_ids:
|
||||
cp = cand
|
||||
|
||||
if cp is None:
|
||||
inserted += 1
|
||||
change_log.append(f" [新增] {row['name']}({row.get('office') or '—'} / {row.get('project_status') or '—'})")
|
||||
if args.dry_run:
|
||||
continue
|
||||
cp = CustomProject(name=row["name"], source="import")
|
||||
cp.entry_date = row.get("sign_date") or args.date
|
||||
cp.update_date = row.get("sign_date") or args.date
|
||||
apply_fields(cp, row, [f for f in row if f != "name"])
|
||||
db.add(cp)
|
||||
db.flush() # 拿到 id,供 matched_ids
|
||||
if cp.project_code:
|
||||
by_code.setdefault(cp.project_code, cp)
|
||||
by_name.setdefault(row["name"], cp)
|
||||
matched_ids.add(cp.id)
|
||||
continue
|
||||
|
||||
changes = apply_fields(cp, row, TRACKED)
|
||||
matched_ids.add(cp.id)
|
||||
# 验收日期:仅当库里还为空时补占位(用户已在界面填了真日期则不覆盖)
|
||||
if cp.accept_date_real is None and row.get("accept_date_real"):
|
||||
cp.accept_date_real = row["accept_date_real"]
|
||||
changes.append(("accept_date_real", None, cp.accept_date_real))
|
||||
if not changes:
|
||||
unchanged += 1
|
||||
continue
|
||||
|
||||
updated += 1
|
||||
if not args.dry_run:
|
||||
cp.update_date = row.get("sign_date") or args.date
|
||||
detail = ";".join(f"{f} {old!r}→{new!r}" for f, old, new in changes)
|
||||
change_log.append(f" [更新] {row['name']}:{detail}")
|
||||
|
||||
if args.dry_run:
|
||||
db.rollback()
|
||||
else:
|
||||
db.commit()
|
||||
|
||||
print(f"\n{'[dry-run] 预演结果' if args.dry_run else '[ok] 导入完成'}")
|
||||
print(f" 新增 {inserted} 行 / 更新 {updated} 行 / 无变化 {unchanged} 行")
|
||||
|
||||
if change_log:
|
||||
print("\n明细:")
|
||||
for line in change_log[:40]:
|
||||
print(line)
|
||||
if len(change_log) > 40:
|
||||
print(f" ... 另有 {len(change_log) - 40} 条")
|
||||
|
||||
if not args.dry_run:
|
||||
total = db.query(CustomProject).count()
|
||||
print(f"\n 当前 custom_projects 共 {total} 行")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -44,7 +44,7 @@ CATEGORY_RULES = [
|
|||
(("无人机", "智飞"), "智能装备"),
|
||||
(("AI", "智能体", "Agent", "大模型"), "AI 能力"),
|
||||
(("NEX", "云桌面", "云屏", "平台开发", "UniFlow"), "平台产品"),
|
||||
(("定制", "交付", "项目支撑"), "定制交付"),
|
||||
(("定制", "交付", "项目支撑"), "定开项目"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ DEFAULT_CATEGORIES = [
|
|||
("平台产品", 0),
|
||||
("AI 能力", 1),
|
||||
("智能装备", 2),
|
||||
("定制交付", 3),
|
||||
("定开项目", 3),
|
||||
("内部运营", 4),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""把「定制开发」从通用项目体系拆出,独立成定制交付项目。
|
||||
"""把「定制开发」从通用项目体系拆出,独立成定开项目。
|
||||
|
||||
数据来源:
|
||||
1. 《项目明细表》台账(区域 / 金额 / 类别 / 状态说明)
|
||||
|
|
@ -151,13 +151,13 @@ def main():
|
|||
total = len(by_name)
|
||||
if args.dry_run:
|
||||
db.rollback()
|
||||
print(f"[dry-run] 将生成定制项目 {total} 个(台账 {created_from_ledger} + 子任务 {created_from_task}),迁移执行记录 {moved_updates} 条")
|
||||
print(f"[dry-run] 将生成定开项目 {total} 个(台账 {created_from_ledger} + 子任务 {created_from_task}),迁移执行记录 {moved_updates} 条")
|
||||
return
|
||||
|
||||
db.commit()
|
||||
|
||||
amount_sum = sum(c.amount or 0 for c in by_name.values())
|
||||
print(f"[ok] 定制项目 {total} 个(台账 {created_from_ledger} / 子任务 {created_from_task})")
|
||||
print(f"[ok] 定开项目 {total} 个(台账 {created_from_ledger} / 子任务 {created_from_task})")
|
||||
print(f"[ok] 迁移执行记录 {moved_updates} 条 → custom_updates")
|
||||
print(f"[ok] 合同金额合计 ¥{amount_sum:,.2f}")
|
||||
from collections import Counter
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def main():
|
|||
print("!! 平移后出现重复周期,已回滚。请先清理重复的周期数据。")
|
||||
return
|
||||
|
||||
# 定制项目的日期字段同步平移
|
||||
# 定开项目的日期字段同步平移
|
||||
date_changed = 0
|
||||
for cp in db.query(CustomProject).all():
|
||||
for field in ("sign_date", "deliver_date", "revenue_date"):
|
||||
|
|
|
|||
926
web/app.js
926
web/app.js
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,7 @@
|
|||
用法:Icon.svg("wallet") → 裸图标
|
||||
Icon.badge("wallet") → 图标 + 重点色底板
|
||||
Icon.status("in_progress") → 执行状态图标
|
||||
Icon.stage("signed") → 定制阶段图标
|
||||
Icon.stage("signed") → 定开阶段图标
|
||||
Icon.pipe("entry") → 交付环节图标
|
||||
静态标记里写 <i data-icon="wallet"></i>,加载时自动替换成 SVG。 */
|
||||
(function () {
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
["负责人", "users"], ["人员", "users"], ["人力", "users"], ["花名册", "users"], ["在册", "users"],
|
||||
["区域", "layers"], ["归属", "layers"], ["分类", "pie"],
|
||||
["明细", "list-checks"], ["待办", "list-checks"], ["本周新增", "inbox"], ["有变化", "refresh"],
|
||||
["在管项目", "layers"], ["定制项目", "package"], ["交付环节", "layers"], ["交付漏斗", "filter"],
|
||||
["在管项目", "layers"], ["定开项目", "package"], ["交付环节", "layers"], ["交付漏斗", "filter"],
|
||||
["交付", "package"], ["验收", "shield-check"], ["入库", "inbox"], ["签单", "pencil"],
|
||||
["阶段流转", "refresh"], ["阶段", "layers"], ["线索", "target"], ["开发", "code"],
|
||||
["重点项目", "star"], ["重点", "star"], ["周期反馈", "calendar-check"], ["周期管理", "calendar"],
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<nav class="nav" id="nav">
|
||||
<button class="nav-item active" data-view="board"><span class="ni" data-icon="board"></span>工作看板</button>
|
||||
<button class="nav-item" data-view="projects"><span class="ni" data-icon="list-checks"></span>重点项目</button>
|
||||
<button class="nav-item" data-view="custom"><span class="ni" data-icon="package"></span>定制交付</button>
|
||||
<button class="nav-item" data-view="custom"><span class="ni" data-icon="package"></span>定开项目</button>
|
||||
<button class="nav-item" data-view="focus"><span class="ni" data-icon="target"></span>每周重点工作</button>
|
||||
<div class="nav-label">支撑</div>
|
||||
<button class="nav-item" data-view="staff"><span class="ni" data-icon="users"></span>人力资源</button>
|
||||
|
|
|
|||
|
|
@ -219,6 +219,26 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
|||
.kpi.accent-amber { --c: var(--amber); }
|
||||
.kpi.accent-red { --c: var(--red); }
|
||||
|
||||
/* 定开项目明细表上方的统计条(左侧色条 + 金额重点色) */
|
||||
.stat-row {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 12px; padding: 2px 16px 16px;
|
||||
}
|
||||
.stat {
|
||||
position: relative; overflow: hidden;
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 12px 16px 12px 18px; box-shadow: var(--shadow);
|
||||
}
|
||||
.stat::before {
|
||||
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
||||
background: var(--c, var(--primary));
|
||||
}
|
||||
.stat .stat-num { font-size: 20px; font-weight: 700; letter-spacing: -.4px; color: var(--text); line-height: 1.25; }
|
||||
.stat .stat-money { color: var(--c, var(--primary)); }
|
||||
.stat .stat-unit { font-size: 11px; font-weight: 500; color: var(--muted); margin: 0 2px; }
|
||||
.stat .stat-label { font-size: 12px; color: var(--muted); margin-top: 5px; }
|
||||
.stat-note { padding: 0 16px 14px; font-size: 12px; color: var(--muted); }
|
||||
|
||||
/* ---------------- board ---------------- */
|
||||
.board-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
|
||||
.chip {
|
||||
|
|
@ -267,7 +287,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
|||
.progress > i { display: block; height: 100%; background: var(--primary); border-radius: 99px; transition: width .4s; }
|
||||
.progress-label { display: flex; justify-content: space-between; font-size: 11px; color: var(--muted); margin-top: 6px; }
|
||||
|
||||
/* ---------------- kanban (定制交付) ---------------- */
|
||||
/* ---------------- kanban (定开项目) ---------------- */
|
||||
.kanban { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 12px; align-items: flex-start; }
|
||||
.kcol {
|
||||
flex: 0 0 236px; background: var(--panel); border: 1px solid var(--border);
|
||||
|
|
@ -457,7 +477,7 @@ textarea.inp { height: auto; padding: 8px 10px; line-height: 1.6; }
|
|||
.btn.danger.solid { background: var(--red); border-color: var(--red); color: #fff; font-weight: 600; }
|
||||
.btn.danger.solid:hover { background: #b42318; color: #fff; }
|
||||
.arch-badge { font-size: 11px; padding: 1px 7px; border-radius: 999px; background: #f1f3f7; color: var(--muted); white-space: nowrap; flex: none; }
|
||||
/* 操作列固定在表格右侧:横向滚动时改名/归档/删除始终可见(项目管理、定制交付共用) */
|
||||
/* 操作列固定在表格右侧:横向滚动时改名/归档/删除始终可见(项目管理、定开项目共用) */
|
||||
table.tbl th.col-ops, table.tbl td.col-ops {
|
||||
position: sticky; right: 0; background: var(--panel); box-shadow: inset 1px 0 0 var(--border);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue