chore: curate monthly assessment evidence and scripts

This commit is contained in:
kkfluous
2026-08-25 12:26:35 +08:00
parent 2956496fb0
commit 52dde9195a
117 changed files with 10743 additions and 179 deletions
+56
View File
@@ -6,3 +6,59 @@ __pycache__/
*.jpg
*.png
.localized
# Raw local downloads: retain only curated production evidence below.
*.xlsx
*.xls
*.zip
*.7z
*.rar
*.tar
*.gz
*.dmg
*.pkg
*.exe
*.pdf
*.doc
*.docx
*.rtf
# Production monthly-assessment evidence retained in this repository.
!20260713/**/*.xlsx
!20260713_1500/**/*.xlsx
!5月18T测算/**/*.xlsx
!6月18T测算/**/*.xlsx
!6月核算结果/**/*.xlsx
!7月核算结果/**/*.xlsx
!7月绩效重算_多源考核里程_20260817/**/*.xlsx
!租赁任务考核_*.xlsx
!里程任务考核_*.xlsx
!*干预情况说明*.xlsx
!docs/**
!**/*考核*.pdf
!**/*考核*.doc
!**/*考核*.docx
!**/*核算*.pdf
!**/*核算*.doc
!**/*核算*.docx
!**/*测试*.pdf
!**/*测试*.doc
!**/*测试*.docx
!**/*验证*.pdf
!**/*验证*.doc
!**/*验证*.docx
!**/*说明*.pdf
!**/*说明*.doc
!**/*说明*.docx
# Never commit credentials or local secret material.
.env
.env.*
*accesskey*
*AccessKey*
*secret*
*token*
*credential*
*password*
*private*key*
*backup*code*
+178
View File
@@ -0,0 +1,178 @@
# 养馋记物流合作 · 可行性与实施计划书
| 项 | 内容 |
|----|------|
| 版本 | V0.1 |
| 日期 | 2026-07-31 |
| 状态 | 评估稿(待管理层战略拍板;单价/税点待书面确认) |
| 配套 | [流程图集](./02-流程图集.md) · [分期实施计划](./03-分期实施计划.md) |
| 依据 | 养馋记合作交流纪要;`self-operated-logistics/assumptions.md``物流自营-MVP与版本计划.md``物流自营盈亏-粗方案-会议纪要落地.md``物流项目立项-合同交车衔接方案.md` |
---
## 1. 背景与问题
养馋记加快外区扩张,倾向**只选一家主物流商**,价格敏感,对标对象为顺丰(网络、回程、昼夜复用、单车多趟)。
羚牛已参与项目运营并具备氢能协同价值,但报价与综合成本仍有差距;差距主因不是「有没有车」,而是:
1. 外区回程空驶
2. 车辆全日利用率不足
3. 线路组合与系统化排线能力不足
4. 战略定位(短期利润 vs 规模客户)未内部拍板
同时会议提出 TMS / 智能调度诉求。现有 OneOS 物流条线已锁定为**轻量调度、非 TMS**,中期靶子是「合同 → 交车 → 出账 → 证据 → 盈亏」。
---
## 2. 计划目标
| 目标 | 说明 |
|------|------|
| G1 决策可下 | 管理层明确 Go / No-Go / 有条件 Go 及利润底线 |
| G2 主链可挂 | 养馋记作为物流项目接入现有主链,能出可解释账单 |
| G3 成本可谈 | 全成本与利用率、空驶可视,支撑对客报价 |
| G4 运力可扩 | 回程与昼夜复用有试点与台阶承诺,匹配门店扩张 |
| G5 系统不越界 | 不对外承诺「上线即对标顺丰 TMS」;算法另立项 |
**非目标:** 用绿色品牌替代价格竞争力;另起独立「养馋记 TMS」产品线抢占物流 MVP 资源。
---
## 3. 可行性结论
### 3.1 总判
| 维度 | 结论 |
|------|------|
| 商务/运营 | **有条件可行**——前提是回程走廊可拼、战略底线清晰、运力台阶可承诺 |
| 产品近中期 | **可行且应做**——经营底座 + 利用率/空驶分析,挂现有物流 MVP/V0.2 |
| 智能 TMS/算法 | **中长期可行、短期不可当作商务武器**——需专班与真实货源数据 |
### 3.2 衔接总策略(一句话)
**客户项目化接入现有物流主链;运营先解决货源与复用;系统先记账再调度台,最后才算法。**
```text
计价规则 → 立项 → 合同生效 → 交车 → 轻量调度 → 趟/段出账+证据 → 成本T+1 → 项目盈亏
养馋记:规则包 + 门店线路主数据 + 多趟/昼夜/空驶/返程字段
跨项目:人工排班 → 占用日历 →(后期)回程候选推荐
```
### 3.3 与顺丰对比(决策用)
| 能力 | 顺丰 | 羚牛现状/路径 |
|------|------|----------------|
| 区域网络与回程货 | 强 | 弱 → 靠现有项目走廊匹配 |
| 昼夜业务组合 | 成熟 | 需运营拼线 + 占用日历 |
| 单车多趟 | 常态 | 字段支持 + 调度台后置 |
| 单公里报价 | 未必最低 | 忌纯砍价 |
| 氢能/绿色 | 非其主叙事 | 羚牛差异化加分,不替代运价 |
| 系统排线 | 成熟网络调度 | 我方 S3/S4,不绑 S0 商务承诺 |
---
## 4. 范围与边界
### 4.1 范围内
- 养馋记项目:规则、合同、交车、调度任务、出账、证据、盈亏
- 主数据:门店、线路、频次、时间窗、货量、车型
- 经营指标:去程/返程/空驶、单车趟次与利用率、线路成本
- 跨项目占用(先人工后系统提示)
- 氢能第二账(能源/财务,与物流盈亏分表)
- 分期评估调度台与智能优化(另预算)
### 4.2 范围外(本期计划书明确不做)
- 完整 VRP/动态重调度作为商务交付承诺
- 承运商招标平台、客户系统 API 强对接(可单独立项)
- 租赁提车应收 / 还车应结混入物流资金流
- 推翻「非 TMS」假设、用演示算法换独家合同
### 4.3 与现版本对齐
| 物流版本 | 本计划对应 |
|----------|------------|
| MVP V0.1 | 阶段 S1 |
| V0.2 | 阶段 S2 |
| V0.3 规则/立项 | 规则包正式版、立项挂接 |
| V0.4 GPS | 在途可视(非智能排线) |
| 新立项 | 阶段 S3 调度台、S4 算法 |
---
## 5. 成本与投入(量级)
> 非正式报价;含产品研发量级,不含车辆购置、司机薪酬、商务让利。
| 阶段 | 内容 | 量级 | 费用感 |
|------|------|------|--------|
| S0 | 全成本、顺丰差拆解、回程走廊、战略底线 | 业务财务为主 · 0.5~1 人周~人月 | 很低 |
| S1 | MVP + 养馋规则/模板/空驶多趟字段 | 增量 1~2 人月 | 低 |
| S2 | 验真、成本挂接、利用率报表、占用日历 | 2~4 人月 | 中低 |
| S3 | 半自动调度台 | 4~8 人月 | 中 |
| S4 | 智能排线/回程推荐 | 6~12+ 人月 + 算法专班跨季 | 高 |
**隐性成本:** 回程货开发人力、薄利下备车、若过度承诺系统能力导致的信誉损失。
**预算建议:** 先批 S0+S1;战略 Go 后批 S2;S3/S4 单独立项,不写入对客必达条款。
---
## 6. 风险与难点
| 等级 | 风险/难点 | 应对 |
|------|-----------|------|
| 高 | 回程货源拼不成仍接独家 | S0 门禁;溢价或收缩范围 |
| 高 | 战略未拍板却锁低价长约 | 先内部底线再报价 |
| 中 | 系统预期=顺丰 TMS | 对外话术与分期绑定;禁演示忽悠 |
| 中 | 主数据(门店窗货量)缺失 | S3 入口条件 |
| 中 | 运力跟不上外区开店 | 台阶式运力承诺 |
| 低 | 多客户导入模板差异 | 规则中心+映射(既有规划) |
| 低 | 氢能归因不清 | 物流账 / 能源账分表 |
---
## 7. 组织与职责(RACI 摘要)
| 事项 | 业务 | 财务 | 物流运营 | 能源 | 数智/产品 | 管理层 |
|------|------|------|----------|------|-----------|--------|
| 战略定位与底线 | C | C | I | C | I | **A/R** |
| 全成本与报价 | R | **A** | C | C | I | C |
| 回程走廊匹配 | C | I | **A/R** | I | C | I |
| 主数据收集 | R | I | C | I | C | I |
| S1S2 系统 | C | C | C | C | **A/R** | I |
| S3S4 立项 | C | C | C | I | **R** | **A** |
| 对客方案 | **R** | C | C | C | C | A |
A=批准 R=执行 C=协商 I=知会
---
## 8. 成功标准
1. 管理层形成书面 Go / No-Go / 有条件 Go。
2. 任意试点趟次可回答:合同、车、司机、门店序列、收入分项、空驶公里、成本粗算。
3. 回程走廊表覆盖主要外区线路,可拼比例与不可拼溢价策略清晰。
4. 对内汇报不出现「已具备顺丰级智能排线」类越界表述。
5. S2 起单车日趟次、利用率、空驶率可周更。
---
## 9. 决策建议
1. **推荐路径:** 有条件推进——S0 过门 → 挂主链(S1)→ 经营可解释(S2)→ 视回程试点再开 S3/S4。
2. **不推荐:** 以智能 TMS 承诺换独家;或纯降单公里硬刚顺丰。
3. **差异化:** 运营熟悉度 + 氢能规模第二账 + 极简透明合作关系(对齐养馋记供应商理念)。
待拍板清单见 [实施计划 §6](./03-分期实施计划.md#6-待拍板清单)。
---
## 10. 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| V0.1 | 2026-07-31 | 首版:计划书 + 流程图 + 实施计划成套 |
+81
View File
@@ -0,0 +1,81 @@
# 养馋记物流派单与对账模块建设评估
> **管理层决策版|本次仅申请第一阶段立项,不进入全量开发承诺**
## 一、结论与建议
### 结论
**项目可做,建议分三阶段推进。** 先以养馋记订单与路线数据接入 OneOS,跑通“订单—人工派单—运输办结—费用归集—T+1 暂算盈亏”闭环;月度对账和 AI 智能调度分别后置,避免在业务规则、接口和数据尚未验证前一次性投入。
### 本次建议确认
1. **批准第一阶段 MVP**:周期 1 个月;人力成本 **11.4 万元**,首年全口径预算 **15.78 万元**
2. **批准启动接口与业务确认**:API/字段、费用试算样例、OneOS 成本匹配规则确认后正式开工
3. **第二、三阶段按门禁决策**:第一阶段跑通并验证数据口径后,再确认第二阶段投入;AI 不纳入本次预算
## 二、第一阶段:一个月先解决什么
| 管理层关心的问题 | 第一阶段交付 |
| --- | --- |
| 订单能否自动进入系统 | 对接养馋记 API;接口异常时支持 Excel/人工建单兜底 |
| 调度能否更快、更可控 | 自动生成待派任务,调度人工选择车辆和司机;司机端接收、执行和办结任务 |
| 每笔单是否赚钱 | 归集运费、OneOS 氢费和 ETC,形成逐单暂算;支持 **T+1 查看前一日收入、成本和盈亏** |
| 本期不做什么 | 完整月度对账、合作方在线确认/驳回、用友接口、全自动派单及 AI 调度 |
**第一阶段验收结果:** 养馋记订单可进入 OneOS,调度可完成派车派司机,司机可办结任务,管理层可在次日看到单据层面的暂算盈亏。
## 三、分阶段投入与边界
| 阶段 | 目标与主要交付 | 周期 | 阶段人力成本 | 决策边界 |
| --- | --- | ---: | ---: | --- |
| **第一阶段:MVP** | 订单接入、人工派单、司机执行、基础费用归集、T+1 暂算盈亏 | 1 个月 | **11.4 万元** | **本次申请批准** |
| **第二阶段:业务与对账闭环** | 改单取消与异常处理、完整费用及车辆成本、利润精算、月度账单、合作方在线对账 | 1—2 个月 | **11.4—22.8 万元** | 第一阶段验证通过后启动;累计 **22.8—34.2 万元** |
| **第三阶段:AI 智能调度试点** | 基于真实调度数据,试点串单/串点、复跑、车型匹配和成本优化建议 | 数据积累 2—3 个月;试点开发 1—2 个月 | **11.4—28.8 万元** | 单独立项;含 AI 的远期累计 **34.2—63.0 万元** |
## 四、成本口径与首年全口径预算
### 人力成本口径
- 第一、二阶段:产品 1 人、研发 2 人、测试 1 人
- 产品、研发 **1,500 元/人天**;测试 **1,200 元/人天**
- 按 20 个工作日/月测算,满配日成本 **5,700 元**
- AI 试点按产品 1 人、研发/算法 2—3 人、测试 1 人测算;数据积累随前两阶段实际运营完成,不单列开发人力
### 非人力成本预算(首年)
| 成本项 | 预算口径 | 首年预算 |
| --- | --- | ---: |
| 应用服务器 | 4核8G、100G 云盘、5Mbps 公网带宽,**2 台**(生产/测试或应用隔离) | **12,000 元** |
| 托管数据库 | RDS MySQL 2核8G、50G 存储、基础备份 | **6,000 元** |
| 对象存储、备份与日志 | 签收凭证、导入文件、备份、日志及基础流量 | **2,000 元** |
| 地图、路径与 GPS 服务 | 低量调用预留;优先复用 OneOS 现有车辆 GPS 数据 | **6,000 元** |
| 短信/消息 | 按 10,000 条通知短信预留 | **500 元** |
| 小程序认证 | 微信小程序主体认证 | **300 元** |
| 域名、SSL、安全监控 | 域名、证书、基础监控与告警 | **2,000 元** |
| 养馋记 API 联调/信息服务 | 对方可能收取的接口支持及联调服务费预留,待对方确认 | **10,000 元** |
| 上线部署与基础运维配置 | 环境初始化、备份策略、发布与巡检配置 | **5,000 元** |
| **非人力成本合计** | 其中年度运行成本约 28,800 元;一次性启动预留 15,000 元 | **43,800 元** |
### 项目首年投入总览
| 决策范围 | 人力成本 | 非人力成本 | 首年总投入 |
| --- | ---: | ---: | ---: |
| **本次:第一阶段 MVP** | 114,000 元 | 43,800 元 | **157,800 元** |
| 第一、二阶段完成 | 228,000—342,000 元 | 43,800 元 | **271,800—385,800 元** |
| 含 AI 试点的远期累计 | 342,000—630,000 元 | 43,800 元 | **385,800—673,800 元** |
**说明:** 上述为预算,不是供应商最终报价;云资源价格会受地域、带宽、磁盘和活动折扣影响。地图/GPS 预算基于低量调用并复用现有 OneOS 数据;如需单独采购地图商用授权、高频路径规划或新增 GPS 设备,应单独询价。用友接口及外部 AI 模型调用费用本期均不包含。
## 五、启动门禁与风险控制
| 启动前必须确认 | 不确认的影响 | 控制方式 |
| --- | --- | --- |
| API 文档、沙箱、可提供字段及同步时效 | 订单接入范围和工期无法锁定 | 先完成接口清单和联调验证;Excel/人工建单作为兜底 |
| 新单、改单、取消单及状态回传规则 | 运单状态和对账口径可能不一致 | 以业务样例确认状态规则后开发 |
| 运费、抽点、杂费、车辆成本试算样例 | 暂算与最终账可能存在差异 | 第一阶段先做暂算,第二阶段固化完整计费与对账规则 |
| OneOS 氢费、ETC 数据来源与匹配规则 | 无法形成可信的成本与利润 | 明确数据来源、匹配频率和异常处理规则 |
## 六、建议
**建议本次批准第一阶段首年全口径预算 15.78 万元**(人力 11.4 万元 + 非人力 4.38 万元)。用一个月验证接口可用性、人工调度闭环和 T+1 盈亏价值;通过后再投入第二阶段的对账闭环。AI 智能调度必须在真实订单、车辆、司机与成本数据积累 2—3 个月后单独评估,避免“先投入、后找场景”。
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,76 @@
# 2026年6月里程考核 — 源数据去重说明
## 背景
6月考核源数据从新系统同步时,由于部分车辆交还车流程未闭环,同一辆车同时存在两条考核任务记录。其中一条是早已还车产生的冗余记录。
经排查确认 **6辆车存在重复导入**,旧源文件 429 行 → 去重后新源文件 423 行,共删除 6 条冗余记录。
---
## 涉及的6辆车
| 车牌 | 销售 | 旧源 | 新源 | 考核目标 | 6月实际里程 | 是否达标 | 去重影响 |
|------|------|------|------|----------|------------|----------|----------|
| 粤AGP4335 | 刘念念 | 2条重复 | 1条 | 羚牛136辆4.5T冷链车 | 7,087 | 达标 | **奖金减半** |
| 粤AGP5159 | 金可鹏 | 1条 | 0条(删除) | 交投190辆4.5T冷链车 | 7,769 | 达标 | **奖金消失** |
| 粤AGP5685 | 金可鹏 | 1条 | 0条(删除) | 交投190辆4.5T冷链车 | 7,068 | 达标 | **奖金消失** |
| 粤AGR8799 | 钟祥 | 1条 | 0条(删除) | 交投190辆4.5T冷链车 | 2,869 | 未达标 | 无影响(0奖金) |
| 粤AGQ5808 | 金可鹏 | 2条重复 | 1条 | 交投190辆4.5T冷链车 | 1,360 | 未达标 | 无影响(0奖金) |
| 粤AGR9896 | 金可鹏 | 2条重复 | 1条 | 交投190辆4.5T冷链车 | 1,242 | 未达标 | 无影响(0奖金) |
> 后3辆虽重复,但考核结果为「未达标」,奖金为0,去重前后无差异。
---
## 发放金额差异(旧源含重复 vs 新源去重)
### 总体
| | 考核应发合计 |
|------|------------|
| 旧源(含6条重复) | **97,782.10** |
| 新源(去重后) | **96,665.27** |
| **多发金额** | **-1,116.83** |
### 分摊到人
| 销售 | 旧源(含重复) | 新源(去重) | 差异 | 说明 |
|------|-------------|-----------|------|------|
| **刘念念** | 32,590.31 | 32,027.25 | **-563.06** | 粤AGP4335 去重,当月520→260,补发1/2/4月消失 |
| **金可鹏** | 17,985.33 | 17,331.56 | **-653.77** | 粤AGP5159+粤AGP5685 删除,当月+补发消失;连锁粤A05776F新增+33.33 |
| **钟祥** | 2,928.02 | 3,028.02 | **+100.00** | 连锁反应:粤A01396F 累计变化后达标,新增当月100 |
| 其余9人 | — | — | **0.00** | 完全一致 |
### 逐条明细
**旧源多发(去重后消失,共990.16元):**
| 车牌 | 销售 | 发放类型 | 多发金额 |
|------|------|----------|----------|
| 粤AGP4335 | 刘念念 | 当月(重复导致翻倍) | 260.00 |
| 粤AGP4335 | 刘念念 | 补发1月 | 8.39 |
| 粤AGP4335 | 刘念念 | 补发2月 | 260.00 |
| 粤AGP4335 | 刘念念 | 补发4月 | 34.67 |
| 粤AGP5159 | 金可鹏 | 当月 | 150.00 |
| 粤AGP5685 | 金可鹏 | 当月 | 150.00 |
| 粤AGP5685 | 金可鹏 | 补发1月 | 150.00 |
| 粤AGP5685 | 金可鹏 | 补发2月 | 150.00 |
| 粤AGP5685 | 金可鹏 | 补发3月 | 48.39 |
| 粤AGP5685 | 金可鹏 | 补发5月 | 38.71 |
**新源新增(连锁反应,共133.33元):**
| 车牌 | 销售 | 发放类型 | 新增金额 |
|------|------|----------|----------|
| 粤A01396F | 钟祥 | 当月 | 100.00 |
| 粤A05776F | 金可鹏 | 当月 | 33.33 |
---
## 结论
- 旧源因重复记录**多发 1,116.83 元**,涉及刘念念(-563.06)、金可鹏(-653.77)、钟祥(+100.00)
- 新源去重后数据正确,以新源计算结果为准:**考核应发 96,665.27 元**
- GPS数据源无问题(本地G7+东方北斗与数据库一致)
- 其余9人发放金额完全不受影响
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+164
View File
@@ -0,0 +1,164 @@
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
FP = '/Users/kkfluous/Downloads/租赁任务考核_2026年5月.xlsx'
def num(x):
try: return float(x or 0)
except: return 0.0
# 读数据
wb_r = openpyxl.load_workbook(FP, data_only=True)
ws_r = wb_r['业务考核视图']
h = [c.value for c in next(ws_r.iter_rows(min_row=1, max_row=1))]
recs = [dict(zip(h, row)) for row in ws_r.iter_rows(min_row=2, values_only=True) if row[0]]
# 备份仪表盘
import os
bak = '/Users/kkfluous/Downloads/租赁任务考核_2026年5月_原版备份.xlsx'
bak_map = {}
if os.path.exists(bak):
wb_b = openpyxl.load_workbook(bak, data_only=True)
for row in wb_b['业务考核视图'].iter_rows(min_row=2, values_only=True):
p = str(row[0]).strip() if row[0] else ''
bak_map[p] = num(row[14])
wb_b.close()
test_recs = [r for r in recs if num(r.get('测试里程(km)')) > 0]
flipped = []
for r in test_recs:
p = str(r['车牌号']).strip()
old_km = bak_map.get(p, 0)
new_km = num(r.get('实际行驶里程(km)'))
t = num(r.get('应考核里程(km)'))
if t <= 0: continue
if old_km >= t and new_km < t:
flipped.append(r)
# 按车型+部门+销售分组
by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for r in flipped:
tgt = r.get('考核目标', '未知车型')
dept = r.get('部门名称', '')
sales = r.get('销售经理', '')
by_target[tgt][dept][sales] += 1
wb_r.close()
# 写sheet
wb = openpyxl.load_workbook(FP)
ws = wb['测试与考核情况说明']
SUB = PatternFill('solid', fgColor='D9E1F2')
HEAD = PatternFill('solid', fgColor='4472C4')
ALT = PatternFill('solid', fgColor='F2F2F2')
THIN = Side('thin', color='B4B4B4')
BD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
# 找到section 二所在的行
section2_row = None
for r in range(1, ws.max_row + 1):
v = ws.cell(row=r, column=1).value
if v and '二、测试车辆考核达标情况说明' in str(v):
section2_row = r
break
if section2_row:
# 删除section 二及之后的所有行(保留数据重新写)
# 找到从section2_row+1开始到section三之前的所有行
section3_row = None
for r in range(section2_row + 1, ws.max_row + 1):
v = ws.cell(row=r, column=1).value
if v and '三、里程' in str(v):
section3_row = r
break
if not section3_row:
section3_row = ws.max_row + 1
# 清除section2+1 到 section3-1 的内容
for r in range(section2_row + 1, section3_row):
for c in range(1, 7):
ws.cell(row=r, column=c).value = None
ws.cell(row=r, column=c).fill = PatternFill()
r = section2_row + 1
# 描述行
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
ws.cell(row=r, column=1,
value=f'{len(test_recs)}条考核车辆涉及测试;扣减其测试里程后,共 {len(flipped)} 条考核车辆记录由"达标"转判为"未达标"')
ws.cell(row=r, column=1).font = Font(size=10); ws.cell(row=r, column=1).alignment = LL
ws.row_dimensions[r].height = 28
r += 2
# 按车型展示,多车型一行
targets_sorted = sorted(by_target.keys())
triples = [targets_sorted[i:i+2] for i in range(0, len(targets_sorted), 2)]
for target_group in triples:
for ti, tgt in enumerate(target_group):
dept_data = by_target[tgt]
dept_order = sorted(dept_data.keys(), key=lambda d: -sum(dept_data[d].values()))
total_flip = sum(sum(s.values()) for s in dept_data.values())
base_col = ti * 3 + 1 # 每车型占3列(车型名|部门-销售|数量)
# 车型标题行
ws.merge_cells(start_row=r, start_column=base_col, end_row=r, end_column=base_col+2)
ws.cell(row=r, column=base_col, value=f'{tgt}{total_flip}辆)').fill = SUB
ws.cell(row=r, column=base_col).font = Font(bold=True, color='1F4E78')
ws.cell(row=r, column=base_col).alignment = CC
for j in range(base_col, base_col+3):
ws.cell(row=r, column=j).border = BD
r += 1
# 部门-销售-数量表头
for j, hh in enumerate(['部门', '销售经理', '数量'], base_col):
ws.cell(row=r, column=j, value=hh).fill = HEAD
ws.cell(row=r, column=j).font = Font(bold=True, color='FFFFFF')
ws.cell(row=r, column=j).alignment = CC
ws.cell(row=r, column=j).border = BD
r += 1
# 数据行
row_idx = 0
for dept in dept_order:
for sales in sorted(dept_data[dept].keys()):
cnt = dept_data[dept][sales]
ws.cell(row=r, column=base_col, value=dept).alignment = CC
ws.cell(row=r, column=base_col+1, value=sales).alignment = CC
ws.cell(row=r, column=base_col+2, value=cnt).alignment = CC
for j in range(base_col, base_col+3):
ws.cell(row=r, column=j).border = BD
if row_idx % 2 == 1:
ws.cell(row=r, column=j).fill = ALT
ws.row_dimensions[r].height = 22
r += 1
row_idx += 1
# 小计行
ws.cell(row=r, column=base_col, value='合计').font = Font(bold=True)
ws.cell(row=r, column=base_col).alignment = CC
ws.cell(row=r, column=base_col+2, value=total_flip).font = Font(bold=True)
ws.cell(row=r, column=base_col+2).alignment = CC
for j in range(base_col, base_col+3):
ws.cell(row=r, column=j).fill = SUB
ws.cell(row=r, column=j).border = BD
r += 2
# 跳到下一个triple的起始行
# Find max row reached in this triplet
pass # r already at right position
r += 1
# 列宽
for i, w in enumerate([18, 22, 22, 50, 18, 18], 1):
ws.column_dimensions[get_column_letter(i)].width = w
wb.save(FP)
print(f'✅ 更新完成')
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Fix May source: remove 4 vehicles from intervention, fix 粤A02239F GPS data."""
import os, shutil, openpyxl, warnings
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
warnings.filterwarnings('ignore')
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年5月.xlsx'
BAK = '租赁任务考核_2026年5月_原版备份.xlsx'
wb = openpyxl.load_workbook(SRC)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
print(f"Headers ({len(h)}):")
for i, hh in enumerate(h): print(f" [{i}] {hh}")
# Column indices
COL_PLATE = 0
COL_ACTUAL = h.index('实际行驶里程(km)') # GPS or仪表盘
COL_TBOX = h.index('原TBOX里程(km)')
COL_TEST = h.index('测试里程(km)')
COL_RATE = h.index('完成率(%)')
COL_PASS = h.index('是否达标')
YELLOW = PatternFill('solid', fgColor='FFFF00')
NO_FILL = PatternFill() # default/empty
# ── Fix 1: Remove 4 vehicles from intervention ──
REMOVE = {'粤A03186F', '粤A01128F', '粤A08391F', '粤A01396F'}
removed_count = 0
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
plate = str(row[COL_PLATE].value or '').strip()
if plate not in REMOVE:
continue
tbox = row[COL_TBOX].value # this is the original仪表盘里程
if tbox is None or float(tbox or 0) <= 0:
continue
original_km = float(tbox)
target_km = float(row[h.index('应考核里程(km)')].value or 0)
# Restore: 实际行驶里程 = original仪表盘
row[COL_ACTUAL].value = original_km
row[COL_TBOX].value = None # clear TBOX (non-intervention)
row[COL_TEST].value = 0 # clear test
# Recalculate
rate = round(original_km / target_km * 100, 2) if target_km > 0 else 0
new_pass = '达标' if original_km >= target_km and target_km > 0 else '未达标'
row[COL_RATE].value = rate
row[COL_PASS].value = new_pass
# Remove yellow highlight
for c in range(len(h)):
row[c].fill = NO_FILL
removed_count += 1
print(f" 移除: {plate} 实际恢复={original_km} 达标={new_pass}")
print(f"移除干预: {removed_count}")
# ── Fix 2: 粤A02239F GPS = 539.7 ──
plate = '粤A02239F'
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
p = str(row[COL_PLATE].value or '').strip()
if p != plate:
continue
tbox = float(row[COL_TBOX].value or 0)
gps_new = 539.7
target_km = float(row[h.index('应考核里程(km)')].value or 0)
row[COL_ACTUAL].value = gps_new
row[COL_TEST].value = round(tbox - gps_new, 2)
rate = round(gps_new / target_km * 100, 2) if target_km > 0 else 0
new_pass = '达标' if gps_new >= target_km and target_km > 0 else '未达标'
row[COL_RATE].value = rate
row[COL_PASS].value = new_pass
print(f"\n修复: {plate} GPS={gps_new} TBOX={tbox} Test={round(tbox-gps_new,2)} 达标={new_pass}")
break
# ── Save ──
wb.save(SRC)
print(f"\n✅ 已保存: {SRC}")
# ── Quick stats ──
wb2 = openpyxl.load_workbook(SRC, data_only=True)
ws2 = wb2['业务考核视图']
h2 = [c.value for c in next(ws2.iter_rows(min_row=1, max_row=1))]
total = 0; pass_count = 0
for row in ws2.iter_rows(min_row=2, values_only=True):
if not row[0]: continue
r = dict(zip(h2, row))
total += 1
if r.get('是否达标') == '达标': pass_count += 1
wb2.close()
print(f"总记录={total} 达标={pass_count}")
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""Generate 租赁任务考核_2026年5月_干预情况说明.xlsx based on updated source data."""
import os, openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
from copy import copy
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年5月.xlsx'
BAK = '租赁任务考核_2026年5月_原版备份.xlsx'
# ── Read source ──
wb_s = openpyxl.load_workbook(SRC, data_only=True)
ws_s = wb_s['业务考核视图']
src_headers = [c.value for c in next(ws_s.iter_rows(min_row=1, max_row=1))]
src_data = list(ws_s.iter_rows(min_row=2, values_only=True))
wb_s.close()
# ── Read backup ──
wb_b = openpyxl.load_workbook(BAK, data_only=True)
ws_b = wb_b['业务考核视图']
bak_data = list(ws_b.iter_rows(min_row=2, values_only=True))
wb_b.close()
print(f"Source: {len(src_data)} records")
print(f"Backup: {len(bak_data)} records")
# ── Compare ──
# Build plate -> row map for backup (keep all, including duplicates)
def make_key(row):
plate = str(row[0]).strip() if row[0] else ''
sales = str(row[4]).strip() if len(row) > 4 and row[4] else ''
target = str(row[9]).strip() if len(row) > 9 and row[9] else ''
return (plate, sales, target)
bak_map = {}
for r in bak_data:
k = make_key(r)
bak_map[k] = r
src_map = {}
for r in src_data:
k = make_key(r)
src_map[k] = r
# Find new/changed/removed
new_records = []
changed_records = []
removed_records = []
for k, sr in src_map.items():
if k not in bak_map:
new_records.append(sr)
else:
br = bak_map[k]
old_pass = str(br[16]).strip() if len(br) > 16 else ''
new_pass = str(sr[16]).strip() if len(sr) > 16 else ''
old_km = float(br[14] or 0) if len(br) > 14 else 0
new_km = float(sr[14] or 0) if len(sr) > 14 else 0
old_rate = float(br[15] or 0) if len(br) > 15 else 0
new_rate = float(sr[15] or 0) if len(sr) > 15 else 0
if old_pass != new_pass or abs(old_km - new_km) > 0.01 or abs(old_rate - new_rate) > 0.01:
changed_records.append((br, sr))
for k, br in bak_map.items():
if k not in src_map:
removed_records.append(br)
print(f"New: {len(new_records)}, Changed: {len(changed_records)}, Removed: {len(removed_records)}")
# Count达标 changes
old_pass_count = sum(1 for r in bak_data if str(r[16]).strip() == '达标' if len(r) > 16)
new_pass_count = sum(1 for r in src_data if str(r[16]).strip() == '达标' if len(r) > 16)
print(f"达标: {old_pass_count} -> {new_pass_count}")
# Flip analysis (达标 → 未达标)
flipped = []
for old_r, new_r in changed_records:
old_p = str(old_r[16]).strip() if len(old_r) > 16 else ''
new_p = str(new_r[16]).strip() if len(new_r) > 16 else ''
if old_p == '达标' and new_p == '未达标':
flipped.append((old_r, new_r))
# Reverse flip (未达标 → 达标)
reversed_flip = []
for old_r, new_r in changed_records:
old_p = str(old_r[16]).strip() if len(old_r) > 16 else ''
new_p = str(new_r[16]).strip() if len(new_r) > 16 else ''
if old_p == '未达标' and new_p == '达标':
reversed_flip.append((old_r, new_r))
print(f"Flipped (达标→未达标): {len(flipped)}")
print(f"Reversed (未达标→达标): {len(reversed_flip)}")
# ── Generate Excel ──
wb = openpyxl.Workbook()
# Styles
TITLE_FONT = Font(bold=True, size=14)
SECTION_FONT = Font(bold=True, size=12, color='1F4E78')
HEADER_FILL = PatternFill('solid', fgColor='4472C4')
HEADER_FONT = Font(bold=True, color='FFFFFF', size=10)
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
ALT_FILL = PatternFill('solid', fgColor='F2F2F2')
NOTE_FONT = Font(size=10)
BOLD_FONT = Font(bold=True, size=10)
THIN = Side('thin', color='B4B4B4')
BORDER = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
NUM_FMT = '#,##0.00'
def style_header_row(ws, row, ncols, fill=HEADER_FILL, font=HEADER_FONT):
for c in range(1, ncols + 1):
cell = ws.cell(row=row, column=c)
cell.fill = fill
cell.font = font
cell.alignment = CC
cell.border = BORDER
def style_data_row(ws, row, ncols, alt=False):
for c in range(1, ncols + 1):
cell = ws.cell(row=row, column=c)
cell.border = BORDER
cell.font = Font(size=10)
cell.alignment = CC
if alt:
cell.fill = ALT_FILL
# ── Sheet 1: 测试与考核情况说明 ──
ws1 = wb.active
ws1.title = '测试与考核情况说明'
ws1.sheet_properties.tabColor = '4472C4'
ws1.merge_cells('A1:G1')
ws1.cell(row=1, column=1, value='2026年5月 业务考核 — 数据更新说明').font = TITLE_FONT
ws1.cell(row=1, column=1).alignment = CC
ws1.row_dimensions[1].height = 30
ws1.merge_cells('A2:G2')
desc = (
'5月业务考核源数据已于近期更新。'
f'{len(src_data)}条考核记录,{new_pass_count}条达标(原{old_pass_count}条)。\n'
f'本次数据更新:新增{len(new_records)}条,变更{len(changed_records)}条,删除{len(removed_records)}条。\n'
f'其中达标→未达标翻转{len(flipped)}条,未达标→达标翻转{len(reversed_flip)}条。\n'
'本文件记录更新前后的数据对比,供核查参考。'
)
ws1.cell(row=2, column=1, value=desc).font = NOTE_FONT
ws1.cell(row=2, column=1).alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
ws1.row_dimensions[2].height = 80
r = 4
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
ws1.cell(row=r, column=1, value='一、5月业务考核概况').font = SECTION_FONT
ws1.cell(row=r, column=1).alignment = LL
r += 1
summary_data = [
('项目', '5月业务考核车辆', '变更记录', '占比/变化'),
('考核记录数(条)', len(src_data), len(changed_records), f'{len(changed_records)/len(src_data)*100:.1f}%' if len(src_data) > 0 else '0%'),
('达标记录数(条)', new_pass_count, new_pass_count - old_pass_count, f'{"+" if new_pass_count >= old_pass_count else ""}{new_pass_count - old_pass_count}'),
('未达标记录数(条)', len(src_data) - new_pass_count, (len(src_data) - new_pass_count) - (len(bak_data) - old_pass_count), ''),
('达标率', f'{new_pass_count/len(src_data)*100:.1f}%' if len(src_data) > 0 else '0%', '', ''),
]
for i, row_data in enumerate(summary_data):
for c, val in enumerate(row_data, 1):
ws1.cell(row=r, column=c, value=val)
if i == 0:
style_header_row(ws1, r, 4)
else:
style_data_row(ws1, r, 4, alt=(i % 2 == 0))
ws1.cell(row=r, column=1).alignment = LL
r += 1
r += 1
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
ws1.cell(row=r, column=1, value='二、考核记录变更明细').font = SECTION_FONT
ws1.cell(row=r, column=1).alignment = LL
r += 1
if flipped:
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
ws1.cell(row=r, column=1, value=f'2.1 达→未达标翻转({len(flipped)}条)').font = Font(bold=True, color='C00000')
r += 1
flip_headers = ['车牌号', '部门', '销售经理', '客户名称', '考核目标', '原实际里程', '新实际里程', '原是否达标', '新是否达标']
for c, h in enumerate(flip_headers, 1):
ws1.cell(row=r, column=c, value=h)
style_header_row(ws1, r, len(flip_headers))
r += 1
for i, (old_r, new_r) in enumerate(flipped):
vals = [
str(old_r[0]) if old_r[0] else '',
str(old_r[3]) if len(old_r) > 3 else '',
str(old_r[4]) if len(old_r) > 4 else '',
str(old_r[5]) if len(old_r) > 5 else '',
str(old_r[9]) if len(old_r) > 9 else '',
round(float(old_r[14] or 0), 2) if len(old_r) > 14 and old_r[14] else 0,
round(float(new_r[14] or 0), 2) if len(new_r) > 14 and new_r[14] else 0,
str(old_r[16]) if len(old_r) > 16 else '',
str(new_r[16]) if len(new_r) > 16 else '',
]
for c, val in enumerate(vals, 1):
ws1.cell(row=r, column=c, value=val)
style_data_row(ws1, r, len(flip_headers), alt=(i % 2 == 1))
r += 1
r += 1
if reversed_flip:
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
ws1.cell(row=r, column=1, value=f'2.2 未达标→达标翻转({len(reversed_flip)}条)').font = Font(bold=True, color='008000')
r += 1
for c, h in enumerate(flip_headers, 1):
ws1.cell(row=r, column=c, value=h)
style_header_row(ws1, r, len(flip_headers))
r += 1
for i, (old_r, new_r) in enumerate(reversed_flip):
vals = [
str(old_r[0]) if old_r[0] else '',
str(old_r[3]) if len(old_r) > 3 else '',
str(old_r[4]) if len(old_r) > 4 else '',
str(old_r[5]) if len(old_r) > 5 else '',
str(old_r[9]) if len(old_r) > 9 else '',
round(float(old_r[14] or 0), 2) if len(old_r) > 14 and old_r[14] else 0,
round(float(new_r[14] or 0), 2) if len(new_r) > 14 and new_r[14] else 0,
str(old_r[16]) if len(old_r) > 16 else '',
str(new_r[16]) if len(new_r) > 16 else '',
]
for c, val in enumerate(vals, 1):
ws1.cell(row=r, column=c, value=val)
style_data_row(ws1, r, len(flip_headers), alt=(i % 2 == 1))
r += 1
r += 1
if other_changes := [(o, n) for o, n in changed_records
if not (str(o[16]).strip() == '达标' and str(n[16]).strip() == '未达标')
and not (str(o[16]).strip() == '未达标' and str(n[16]).strip() == '达标')]:
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7)
ws1.cell(row=r, column=1, value=f'2.3 其他变更({len(other_changes)}条,里程变化但达标状态不变)').font = BOLD_FONT
r += 1
col_widths = [14, 12, 12, 30, 22, 16, 16, 12, 12]
for i, w in enumerate(col_widths, 1):
ws1.column_dimensions[get_column_letter(i)].width = w
# ── Sheet 2: 业务考核视图 (current data) ──
ws2 = wb.create_sheet('业务考核视图')
ws2.sheet_properties.tabColor = '70AD47'
for c, h in enumerate(src_headers, 1):
ws2.cell(row=1, column=c, value=h)
style_header_row(ws2, 1, len(src_headers))
for i, row in enumerate(src_data):
for c, val in enumerate(row, 1):
ws2.cell(row=i + 2, column=c, value=val)
style_data_row(ws2, i + 2, len(src_headers), alt=(i % 2 == 1))
for i, w in enumerate([12, 8, 8, 12, 10, 28, 20, 24, 10, 22, 12, 12, 10, 16, 16, 10, 10, 10, 12, 12, 12], 1):
ws2.column_dimensions[get_column_letter(i)].width = w
# ── Sheet 3: 5月业务考核车辆总览 ──
ws3 = wb.create_sheet('5月业务考核车辆总览')
ws3.sheet_properties.tabColor = 'ED7D31'
for c, h in enumerate(src_headers, 1):
ws3.cell(row=1, column=c, value=h)
style_header_row(ws3, 1, len(src_headers))
for i, row in enumerate(src_data):
for c, val in enumerate(row, 1):
ws3.cell(row=i + 2, column=c, value=val)
style_data_row(ws3, i + 2, len(src_headers), alt=(i % 2 == 1))
for i, w in enumerate([12, 8, 8, 12, 10, 28, 20, 24, 10, 22, 12, 12, 10, 16, 16, 10, 10, 10, 12, 12, 12], 1):
ws3.column_dimensions[get_column_letter(i)].width = w
# ── Sheet 4: 涉及变更的业务考核车辆 ──
ws4 = wb.create_sheet('涉及变更的业务考核车辆')
ws4.sheet_properties.tabColor = 'FFC000'
ws4.merge_cells('A1:U1')
ws4.cell(row=1, column=1, value=f'变更记录汇总 — 新增{len(new_records)}条 / 变更{len(changed_records)}条 / 删除{len(removed_records)}').font = Font(bold=True, size=11)
ws4.cell(row=1, column=1).alignment = CC
ws4.row_dimensions[1].height = 24
# Source headers + change type col
ext_headers = list(src_headers) + ['变更类型', '原实际里程', '原是否达标']
for c, h in enumerate(ext_headers, 1):
ws4.cell(row=3, column=c, value=h)
style_header_row(ws4, 3, len(ext_headers))
r = 4
# New records
for i, rec in enumerate(new_records):
for c, val in enumerate(rec, 1):
ws4.cell(row=r, column=c, value=val)
ws4.cell(row=r, column=len(src_headers) + 1, value='新增')
style_data_row(ws4, r, len(ext_headers), alt=(i % 2 == 1))
ws4.row_dimensions[r].height = 20
r += 1
# Changed records
for i, (old_r, new_r) in enumerate(changed_records):
for c, val in enumerate(new_r, 1):
ws4.cell(row=r, column=c, value=val)
ws4.cell(row=r, column=len(src_headers) + 1, value='变更')
old_km = round(float(old_r[14] or 0), 2) if len(old_r) > 14 and old_r[14] else 0
old_pass = str(old_r[16]) if len(old_r) > 16 else ''
ws4.cell(row=r, column=len(src_headers) + 2, value=old_km)
ws4.cell(row=r, column=len(src_headers) + 3, value=old_pass)
style_data_row(ws4, r, len(ext_headers), alt=(i % 2 == 1))
ws4.row_dimensions[r].height = 20
r += 1
for i, w in enumerate([12, 8, 8, 12, 10, 28, 20, 24, 10, 22, 12, 12, 10, 16, 16, 10, 10, 10, 12, 12, 12, 10, 14, 12], 1):
ws4.column_dimensions[get_column_letter(i)].width = w
# ── Save ──
out_path = 'new-kpi-202605_1/租赁任务考核_2026年5月_干预情况说明.xlsx'
wb.save(out_path)
print(f'\n{out_path} ({len(wb.sheetnames)} sheets)')
+330
View File
@@ -0,0 +1,330 @@
#!/usr/bin/env python3
"""Generate 租赁任务考核_2026年5月_干预情况说明.xlsx — GPS干预后的完整说明"""
import os, openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年5月.xlsx'
BAK = '租赁任务考核_2026年5月_原版备份.xlsx'
INTV = '标黄为5月干预车辆.xlsx'
# Styles
HEAD = PatternFill('solid', fgColor='4472C4')
HFONT = Font(bold=True, color='FFFFFF', size=10)
SUB = PatternFill('solid', fgColor='D9E1F2')
ALT = PatternFill('solid', fgColor='F2F2F2')
TITLE_FONT = Font(bold=True, size=14)
SEC_FONT = Font(bold=True, size=12, color='1F4E78')
NOTE_FONT = Font(size=10)
BOLD_FONT = Font(bold=True, size=10)
RED_FONT = Font(size=10, color='C00000')
BORD = Border(Side('thin','B4B4B4'), Side('thin','B4B4B4'), Side('thin','B4B4B4'), Side('thin','B4B4B4'))
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
# ── Load intervention plates ──
wb_i = openpyxl.load_workbook(INTV)
ws_i = wb_i['Sheet1']
yellow_plates = set()
for row in ws_i.iter_rows(min_row=2, values_only=False):
cell = row[0]
fill = cell.fill
is_yellow = (fill and fill.patternType == 'solid' and fill.start_color
and fill.start_color.rgb and 'FFFF00' in str(fill.start_color.rgb))
if is_yellow:
p = str(cell.value).strip() if cell.value else ''
if p:
yellow_plates.add(p)
wb_i.close()
# ── Load source (post-intervention) and backup (pre-intervention) ──
def load_records(fp):
wb = openpyxl.load_workbook(fp, data_only=True)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0]:
recs.append(dict(zip(h, row)))
wb.close()
return recs
src_recs = load_records(SRC)
bak_recs = load_records(BAK)
# Build plate -> {backup rec, source rec}
bak_by_plate = {}
for r in bak_recs:
p = str(r['车牌号']).strip()
# For duplicate plates, keep last
key = (p, str(r.get('销售经理','')), str(r.get('项目名称','')))
bak_by_plate[key] = r
src_by_plate = {}
for r in src_recs:
p = str(r['车牌号']).strip()
key = (p, str(r.get('销售经理','')), str(r.get('项目名称','')))
src_by_plate[key] = r
# ── Analyze intervention impact ──
# For each source record that's an intervention vehicle, find its backup
intv_records = []
for key, sr in src_by_plate.items():
p = key[0]
if p not in yellow_plates:
continue
br = bak_by_plate.get(key)
tbox = sr.get('原TBOX里程(km)')
gps = sr.get('实际行驶里程(km)')
test = sr.get('测试里程(km)')
old_actual = float(br['实际行驶里程(km)'] or 0) if br else 0
old_pass = str(br['是否达标']).strip() if br else '?'
new_pass = str(sr['是否达标']).strip() if sr else '?'
target = float(sr.get('应考核里程(km)', 0) or 0)
tbox_val = float(tbox or 0)
gps_val = float(gps or 0)
test_val = float(test or 0)
flip = ''
if old_pass == '达标' and new_pass == '未达标':
flip = '达→未达标'
elif old_pass == '未达标' and new_pass == '达标':
flip = '未→达标'
has_gps = '' if gps_val > 0 else '无GPS数据'
intv_records.append({
'plate': p, 'dept': sr.get('部门名称', ''), 'sales': sr.get('销售经理', ''),
'client': sr.get('客户名称', ''), 'target_name': sr.get('考核目标', ''),
'target_km': target, 'old_actual': old_actual, 'tbox': tbox_val,
'gps': gps_val, 'test': test_val, 'old_pass': old_pass, 'new_pass': new_pass,
'flip': flip, 'has_gps': has_gps
})
print(f"Intervention records: {len(intv_records)}")
flip_down = [r for r in intv_records if r['flip'] == '达→未达标']
flip_up = [r for r in intv_records if r['flip'] == '未→达标']
no_gps = [r for r in intv_records if r['gps'] == 0]
print(f"达→未达标: {len(flip_down)}, 未→达标: {len(flip_up)}, 无GPS: {len(no_gps)}")
# ── Intervention vehicles by target type ──
by_target = defaultdict(list)
for r in intv_records:
by_target[r['target_name']].append(r)
# ── Generate workbook ──
wb = openpyxl.Workbook()
def style_header(ws, row, ncols):
for c in range(1, ncols+1):
cell = ws.cell(row=row, column=c)
cell.fill = HEAD; cell.font = HFONT; cell.alignment = CC; cell.border = BORD
def style_row(ws, row, ncols, alt=False):
for c in range(1, ncols+1):
cell = ws.cell(row=row, column=c)
cell.border = BORD; cell.font = Font(size=10); cell.alignment = CC
if alt: cell.fill = ALT
# ── Sheet 1: 测试与考核情况说明 ──
ws1 = wb.active
ws1.title = '测试与考核情况说明'
ws1.sheet_properties.tabColor = '4472C4'
ws1.merge_cells('A1:H1')
ws1.cell(row=1, column=1, value='2026年5月 业务考核 — GPS干预与考核情况说明').font = TITLE_FONT
ws1.cell(row=1, column=1).alignment = CC
ws1.row_dimensions[1].height = 32
desc = (
f'5月共计{len(src_recs)}条考核记录,其中{len(yellow_plates)}辆车/{len(intv_records)}条记录涉及GPS里程干预。\n'
'干预规则:对于标黄车辆,使用G7平台GPS每日里程(考核期内逐日累加)替代原仪表盘里程作为实际行驶里程参与考核。\n'
f'GPS数据来源:hydrogen_energy.ln_vehicle_g7_mileage101.133.130.65),按考核起止时间汇总每日total_mileage/100000=km。\n'
f'干预结果:{len(flip_down)}条由达标翻转为未达标,{len(flip_up)}条由未达标翻转为达标,{len(no_gps)}条车辆无GPS数据(里程记为0)。'
)
ws1.merge_cells('A2:H2')
ws1.cell(row=2, column=1, value=desc).font = NOTE_FONT
ws1.cell(row=2, column=1).alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
ws1.row_dimensions[2].height = 80
r = 4
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=8)
ws1.cell(row=r, column=1, value='一、5月考核概况(GPS干预后)').font = SEC_FONT
r += 1
# Summary table
summary_headers = ['指标', '干预前', '干预后', '变化']
for c, h in enumerate(summary_headers, 1):
ws1.cell(row=r, column=c, value=h)
style_header(ws1, r, 4)
r += 1
old_pass_count = sum(1 for r in bak_recs if str(r.get('是否达标','')).strip() == '达标')
new_pass_count = sum(1 for r in src_recs if str(r.get('是否达标','')).strip() == '达标')
old_fail = len(bak_recs) - old_pass_count
new_fail = len(src_recs) - new_pass_count
summary_rows = [
('考核记录数(条)', len(bak_recs), len(src_recs), ''),
('达标记录数(条)', old_pass_count, new_pass_count, f'{new_pass_count - old_pass_count:+d}'),
('未达标记录数(条)', old_fail, new_fail, f'{new_fail - old_fail:+d}'),
('达标率', f'{old_pass_count/len(bak_recs)*100:.1f}%', f'{new_pass_count/len(src_recs)*100:.1f}%', ''),
('干预车辆数', len(yellow_plates), len(yellow_plates), ''),
('涉及考核记录', len(intv_records), len(intv_records), ''),
('有GPS数据', '', sum(1 for r in intv_records if r['gps'] > 0), ''),
('无GPS数据(里程=0)', '', len(no_gps), ''),
]
for row_data in summary_rows:
for c, val in enumerate(row_data, 1):
ws1.cell(row=r, column=c, value=val)
style_row(ws1, r, 4)
r += 1
r += 1
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=8)
ws1.cell(row=r, column=1, value='二、干预车型达标翻转统计').font = SEC_FONT
r += 1
# By target type
flip_headers = ['考核目标', '干预车辆数', '达→未达标', '未→达标', '有GPS数据', '无GPS数据']
for c, h in enumerate(flip_headers, 1):
ws1.cell(row=r, column=c, value=h)
style_header(ws1, r, 6)
r += 1
# Sort: most affected first
target_order = sorted(by_target.keys(), key=lambda t: -len(by_target[t]))
for i, tgt in enumerate(target_order):
recs = by_target[tgt]
ws1.cell(row=r, column=1, value=tgt)
ws1.cell(row=r, column=2, value=len(recs))
ws1.cell(row=r, column=3, value=sum(1 for x in recs if x['flip'] == '达→未达标'))
ws1.cell(row=r, column=4, value=sum(1 for x in recs if x['flip'] == '未→达标'))
ws1.cell(row=r, column=5, value=sum(1 for x in recs if x['gps'] > 0))
ws1.cell(row=r, column=6, value=sum(1 for x in recs if x['gps'] == 0))
style_row(ws1, r, 6, alt=(i%2==1))
r += 1
col_widths1 = [14, 15, 15, 15, 18, 18, 18, 18]
for i, w in enumerate(col_widths1, 1):
ws1.column_dimensions[get_column_letter(i)].width = w
# ── Sheet 2: 干预车辆明细 ──
ws2 = wb.create_sheet('干预车辆明细')
ws2.sheet_properties.tabColor = 'ED7D31'
ws2.merge_cells('A1:N1')
ws2.cell(row=1, column=1, value='5月干预车辆 — 原仪表盘里程 vs GPS里程 逐条对比').font = TITLE_FONT
ws2.cell(row=1, column=1).alignment = CC
ws2.row_dimensions[1].height = 28
detail_headers = ['车牌号', '部门', '销售经理', '客户名称', '考核目标',
'考核天数', '应考核里程(km)', '原仪表盘里程(km)', 'GPS行驶里程(km)',
'测试里程(km)', '原是否达标', '干预后达标', '翻转', 'GPS数据']
r2 = 3
for c, h in enumerate(detail_headers, 1):
ws2.cell(row=r2, column=c, value=h)
style_header(ws2, r2, 14)
r2 += 1
# Sort: flip-down first, then no-GPS, then others
intv_records.sort(key=lambda r: (0 if r['flip'] == '达→未达标' else 1 if r['gps'] == 0 else 2, r['plate']))
for i, rec in enumerate(intv_records):
ws2.cell(row=r2, column=1, value=rec['plate'])
ws2.cell(row=r2, column=2, value=rec['dept'])
ws2.cell(row=r2, column=3, value=rec['sales'])
ws2.cell(row=r2, column=4, value=rec['client'])
ws2.cell(row=r2, column=5, value=rec['target_name'])
ws2.cell(row=r2, column=6, value=rec.get('days', ''))
ws2.cell(row=r2, column=7, value=round(rec['target_km'], 2))
ws2.cell(row=r2, column=8, value=round(rec['tbox'], 2))
ws2.cell(row=r2, column=9, value=round(rec['gps'], 2))
ws2.cell(row=r2, column=10, value=round(rec['test'], 2))
ws2.cell(row=r2, column=11, value=rec['old_pass'])
ws2.cell(row=r2, column=12, value=rec['new_pass'])
# Flip column with color
flip_cell = ws2.cell(row=r2, column=13, value=rec['flip'])
if rec['flip'] == '达→未达标':
flip_cell.font = RED_FONT
ws2.cell(row=r2, column=14, value=rec['has_gps'])
style_row(ws2, r2, 14, alt=(i%2==1))
r2 += 1
col_widths2 = [12, 10, 10, 26, 20, 8, 14, 16, 16, 14, 10, 10, 10, 10]
for i, w in enumerate(col_widths2, 1):
ws2.column_dimensions[get_column_letter(i)].width = w
# ── Sheet 3: 业务考核视图 (current, post-intervention) ──
ws3 = wb.create_sheet('业务考核视图')
ws3.sheet_properties.tabColor = '70AD47'
src_h = list(src_recs[0].keys())
for c, h in enumerate(src_h, 1):
ws3.cell(row=1, column=c, value=h)
style_header(ws3, 1, len(src_h))
for i, rec in enumerate(src_recs):
for c, h in enumerate(src_h, 1):
val = rec.get(h)
if val is not None:
ws3.cell(row=i+2, column=c, value=val)
style_row(ws3, i+2, len(src_h), alt=(i%2==1))
for i in range(1, len(src_h)+1):
ws3.column_dimensions[get_column_letter(i)].width = 12
# ── Sheet 4: 干预车辆汇总(按部门+销售) ──
ws4 = wb.create_sheet('干预汇总(按部门销售)')
ws4.sheet_properties.tabColor = 'FFC000'
ws4.merge_cells('A1:F1')
ws4.cell(row=1, column=1, value='干预车辆 — 按部门+销售经理汇总').font = TITLE_FONT
ws4.cell(row=1, column=1).alignment = CC
# Aggregate
by_dept_sales = defaultdict(lambda: {'total': 0, 'flip_down': 0, 'flip_up': 0, 'no_gps': 0, 'has_gps': 0, 'gps_amount': 0})
for rec in intv_records:
key = (rec['dept'], rec['sales'])
by_dept_sales[key]['total'] += 1
if rec['flip'] == '达→未达标':
by_dept_sales[key]['flip_down'] += 1
elif rec['flip'] == '未→达标':
by_dept_sales[key]['flip_up'] += 1
if rec['gps'] > 0:
by_dept_sales[key]['has_gps'] += 1
by_dept_sales[key]['gps_amount'] += rec['gps']
else:
by_dept_sales[key]['no_gps'] += 1
agg_headers = ['部门', '销售经理', '干预车辆数', '达→未达标', '未→达标', '无GPS数据']
r4 = 3
for c, h in enumerate(agg_headers, 1):
ws4.cell(row=r4, column=c, value=h)
style_header(ws4, r4, 6)
r4 += 1
for (dept, sales), data in sorted(by_dept_sales.items()):
ws4.cell(row=r4, column=1, value=dept)
ws4.cell(row=r4, column=2, value=sales)
ws4.cell(row=r4, column=3, value=data['total'])
ws4.cell(row=r4, column=4, value=data['flip_down'])
ws4.cell(row=r4, column=5, value=data['flip_up'])
ws4.cell(row=r4, column=6, value=data['no_gps'])
style_row(ws4, r4, 6)
r4 += 1
col_widths4 = [14, 12, 12, 12, 12, 12]
for i, w in enumerate(col_widths4, 1):
ws4.column_dimensions[get_column_letter(i)].width = w
# ── Save ──
out_path = 'new-kpi-202605_1/租赁任务考核_2026年5月_干预情况说明.xlsx'
wb.save(out_path)
print(f'{out_path} ({len(wb.sheetnames)} sheets)')
+410
View File
@@ -0,0 +1,410 @@
#!/usr/bin/env python3
"""生成 租赁任务考核_2026年5月_干预情况说明.xlsx — 公式计算 + %格式 + 完整匹配参考样式"""
import os, openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
from openpyxl.utils import get_column_letter
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年5月.xlsx'
BAK = '租赁任务考核_2026年5月_原版备份.xlsx'
# ─── Styles ───
DARK_BLUE = PatternFill('solid', fgColor='1F4E78')
LIGHT_BLUE = PatternFill('solid', fgColor='D9E1F2')
HD_BLUE = PatternFill('solid', fgColor='4472C4')
YELLOW_BG = PatternFill('solid', fgColor='FFF2CC')
ALT_GRAY = PatternFill('solid', fgColor='F2F2F2')
YELLOW_HL = PatternFill('solid', fgColor='FFFF00')
NO_FILL = PatternFill('solid', fgColor='00000000')
TITLE_F = Font(bold=True, size=14, color='FFFFFF')
SEC_F = Font(bold=True, size=12, color='1F4E78')
SUB_F = Font(bold=True, size=11, color='1F4E78')
HDR_F = Font(bold=True, size=11, color='FFFFFF')
BODY_F = Font(size=11)
BOLD_F = Font(bold=True, size=11)
RED_F = Font(size=10, color='FF0000')
DESC_F = Font(size=10)
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
LT = Alignment(horizontal='left', vertical='top', wrap_text=True)
# ─── Load yellow intervention plates ───
wb_i = openpyxl.load_workbook('标黄为5月干预车辆.xlsx')
ws_i = wb_i['Sheet1']
YELLOW_PLATES = set()
for row in ws_i.iter_rows(min_row=2, values_only=False):
cell = row[0]
fill = cell.fill
is_y = (fill and fill.patternType == 'solid' and fill.start_color
and fill.start_color.rgb and 'FFFF00' in str(fill.start_color.rgb))
if is_y:
p = str(cell.value).strip() if cell.value else ''
if p: YELLOW_PLATES.add(p)
wb_i.close()
# 手动移除4辆已确认非干预
YELLOW_PLATES.difference_update({'粤A03186F', '粤A01128F', '粤A08391F', '粤A01396F'})
print(f"Yellow plates: {len(YELLOW_PLATES)} (removed 4)")
# ─── Load data ───
def load_recs(fp):
wb = openpyxl.load_workbook(fp, data_only=True)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0]: recs.append(dict(zip(h, row)))
wb.close()
return recs, h
src_recs, src_h = load_recs(SRC)
bak_recs, bak_h = load_recs(BAK)
print(f"Source: {len(src_recs)} rows, cols={len(src_h)}")
print(f"Backup: {len(bak_recs)} rows, cols={len(bak_h)}")
# Identify intervention records by plate (yellow list)
intv_recs = []
for sr in src_recs:
plate = str(sr['车牌号']).strip()
if plate not in YELLOW_PLATES:
continue
gps = float(sr.get('实际行驶里程(km)', 0) or 0)
tbox = float(sr.get('原TBOX里程(km)', 0) or 0)
test = float(sr.get('测试里程(km)', 0) or 0)
tgt = float(sr.get('应考核里程(km)', 0) or 0)
# Find backup counterpart
key = (plate, str(sr.get('销售经理','')), str(sr.get('项目名称','')))
br = None
for b in bak_recs:
bk = (str(b['车牌号']).strip(), str(b.get('销售经理','')), str(b.get('项目名称','')))
if bk == key: br = b; break
old_actual = float(br['实际行驶里程(km)'] or 0) if br else 0
old_pass = str(br['是否达标']).strip() if br else '?'
new_pass = str(sr.get('是否达标','')).strip()
flip = ''
if old_pass == '达标' and new_pass == '未达标': flip = '达→未达标'
elif old_pass == '未达标' and new_pass == '达标': flip = '未→达标'
intv_recs.append(dict(plate=plate, dept=sr.get('部门名称',''), sales=sr.get('销售经理',''),
client=sr.get('客户名称',''), target_name=sr.get('考核目标',''),
tgt_km=tgt, tbox=tbox, gps=gps, test=test, flip=flip,
days=sr.get('考核天数'), rate=sr.get('完成率(%)'),
new_pass=new_pass, old_pass=old_pass))
print(f"Intervention records: {len(intv_recs)}")
flip_down = [r for r in intv_recs if r['flip'] == '达→未达标']
flip_up = [r for r in intv_recs if r['flip'] == '未→达标']
print(f"达→未达标: {len(flip_down)}, 未→达标: {len(flip_up)}")
# ─── Reference column layout (O=GPS, P=TBOX, Q=Test) ───
# Build full records with columns in reference order
REF_HEADERS = [
'车牌号', '考核年份', '考核月份', '部门名称', '销售经理', '客户名称',
'合同编号', '项目名称', '考核目标ID', '考核目标', '考核开始日期',
'考核结束日期', '考核天数', '应考核里程(km)',
'实际行驶里程(km)', # O = GPS (考核口径)
'原TBOX里程(km)', # P = TBOX (仪表盘)
'测试里程(km)', # Q = test
'完成率(%)', '是否达标', '考核状态',
'交车日期', '预计还车日期', '实际还车日期',
]
def reorder_rec(src_rec):
"""Map source record to reference column order. O=GPS, P=TBOX, Q=Test.
For non-intv: O=仪表盘, P=空, Q=0. For intv: O=GPS, P=仪表盘, Q=GPS-仪表盘."""
out = {}
plate = str(src_rec.get('车牌号','')).strip()
is_intv = plate in YELLOW_PLATES
for h in REF_HEADERS:
if h == '实际行驶里程(km)':
# O column: always use source's actual (which is仪表盘 for non-intv, GPS for intv)
out[h] = src_rec.get('实际行驶里程(km)')
elif h == '原TBOX里程(km)':
# P column: only fill if intv, else empty
out[h] = src_rec.get('原TBOX里程(km)') if is_intv else None
elif h == '测试里程(km)':
out[h] = src_rec.get('测试里程(km)') if is_intv else 0
else:
out[h] = src_rec.get(h)
return out
all_ref = [reorder_rec(r) for r in src_recs]
intv_ref = [reorder_rec(r) for r in src_recs if str(r['车牌号']).strip() in YELLOW_PLATES]
# ─── Build workbook ───
wb = openpyxl.Workbook()
def hdr_row(ws, row, ncols):
for c in range(1, ncols+1):
cell = ws.cell(row=row, column=c)
cell.fill = HD_BLUE; cell.font = HDR_F; cell.alignment = CC; cell.border = BORD
def dat_row(ws, row, ncols, alt=False):
for c in range(1, ncols+1):
cell = ws.cell(row=row, column=c)
cell.font = BODY_F; cell.alignment = CC; cell.border = BORD
if alt: cell.fill = ALT_GRAY
# ═══════════════════════════════
# Sheets 2,3,4 first (so Sheet1 formulas can reference them)
# ═══════════════════════════════
def write_data_sheet(wb, name, records, highlight_intv=False):
ws = wb.create_sheet(name)
for ci, h in enumerate(REF_HEADERS, 1):
ws.cell(row=1, column=ci, value=h)
hdr_row(ws, 1, len(REF_HEADERS))
for ri, rec in enumerate(records):
r = ri + 2
is_intv = str(rec.get('车牌号','')).strip() in YELLOW_PLATES
for ci, h in enumerate(REF_HEADERS, 1):
val = rec.get(h)
if val is not None:
ws.cell(row=r, column=ci, value=val)
ws.cell(row=r, column=ci).border = BORD
ws.cell(row=r, column=ci).font = Font(size=10)
ws.cell(row=r, column=ci).alignment = CC
if highlight_intv and is_intv:
for ci in range(1, len(REF_HEADERS)+1):
ws.cell(row=r, column=ci).fill = YELLOW_HL
for ci in range(1, len(REF_HEADERS)+1):
ws.column_dimensions[get_column_letter(ci)].width = 12
return ws
# Sheet 2: 业务考核视图 (all records, intv highlighted yellow)
ws2 = write_data_sheet(wb, '业务考核视图', all_ref, highlight_intv=True)
# Sheet 3: 5月业务考核车辆总览 (all records, no highlight)
ws3 = write_data_sheet(wb, '5月业务考核车辆总览', all_ref, highlight_intv=False)
# Sheet 4: 涉及测试的业务考核车辆 (intv only)
ws4 = write_data_sheet(wb, '涉及测试的业务考核车辆', intv_ref, highlight_intv=False)
# ═══════════════════════════════
# Sheet 1: 测试与考核情况说明 (with formulas)
# ═══════════════════════════════
ws1 = wb.active
ws1.title = '测试与考核情况说明'
MAX_R = 500 # generous row range for formulas
# R1: Title
ws1.merge_cells('A1:G1')
c = ws1.cell(row=1, column=1, value='2026年5月 业务考核 — 测试与考核情况说明')
c.fill = DARK_BLUE; c.font = TITLE_F; c.alignment = CC
ws1.row_dimensions[1].height = 32
# R2-R3: Description
ws1.merge_cells('A2:G3')
desc = ('5月业务考核车辆中,部分车辆当月存在测试行为(仪表盘里程 > GPS 实际行驶里程),其差额作为测试里程剥离,不参与考核。\n'
'对于有测试行为的车辆,使用GPS里程作为实际业务考核里程并参与绩效计算。\n'
'对于无5月GPS数据的干预车辆,实际行驶里程记为0,判定未达标。\n'
'下表数据由公式实时计算,源sheet数据更新后自动刷新。')
c = ws1.cell(row=2, column=1, value=desc)
c.fill = YELLOW_BG; c.font = DESC_F; c.alignment = LT
ws1.row_dimensions[2].height = 72
# R4: empty spacer
# R5: Section 一
ws1.merge_cells('A5:G5')
c = ws1.cell(row=5, column=1, value='一、5月业务考核与干预情况')
c.fill = LIGHT_BLUE; c.font = SEC_F; c.alignment = LL
ws1.row_dimensions[5].height = 24
# R6: Table header
for ci, h in enumerate(['项目', '5月业务考核车辆', '涉及测试的车辆', '占比', '', '', ''], 1):
ws1.cell(row=6, column=ci, value=h)
hdr_row(ws1, 6, 4)
ws1.row_dimensions[6].height = 24
# R7-R11: Formula rows
# Sheet references for formulas (openpyxl writes them as-is)
S3 = "'5月业务考核车辆总览'"
S4 = "'涉及测试的业务考核车辆'"
formula_rows = [
# (row, label, B_formula, C_formula, B_fmt, C_fmt, D_fmt)
(7, '车辆数(辆)',
f'=SUMPRODUCT(({S3}!A2:A{MAX_R}<>"")/(COUNTIF({S3}!A2:A{MAX_R},{S3}!A2:A{MAX_R})+({S3}!A2:A{MAX_R}="")))',
f'=SUMPRODUCT(({S4}!A2:A{MAX_R}<>"")/(COUNTIF({S4}!A2:A{MAX_R},{S4}!A2:A{MAX_R})+({S4}!A2:A{MAX_R}="")))',
'#,##0', '#,##0', '0.00%'),
(8, '产生考核记录数(条)',
f'=COUNTA({S3}!A2:A{MAX_R})',
f'=COUNTA({S4}!A2:A{MAX_R})',
'#,##0', '#,##0', '0.00%'),
(9, '仪表盘里程(km)',
f'=SUM({S3}!O2:O{MAX_R})+SUM({S4}!Q2:Q{MAX_R})',
f'=SUM({S4}!O2:O{MAX_R})+SUM({S4}!Q2:Q{MAX_R})',
'#,##0.0', '#,##0.0', '0.00%'),
(10, '业务考核里程 GPS(km)',
f'=SUM({S3}!O2:O{MAX_R})',
f'=SUM({S4}!O2:O{MAX_R})',
'#,##0.0', '#,##0.0', '0.00%'),
(11, '测试里程(km)',
f'=SUM({S4}!Q2:Q{MAX_R})',
f'=SUM({S4}!Q2:Q{MAX_R})',
'#,##0.0', '#,##0.0', '0.00%'),
]
for ri, (row_num, label, b_f, c_f, b_fmt, c_fmt, d_fmt) in enumerate(formula_rows):
r = row_num
ws1.cell(row=r, column=1, value=label).font = BOLD_F
ws1.cell(row=r, column=1).alignment = LL
ws1.cell(row=r, column=1).border = BORD
ws1.cell(row=r, column=2, value=b_f).number_format = b_fmt
ws1.cell(row=r, column=2).font = BODY_F
ws1.cell(row=r, column=2).alignment = CC
ws1.cell(row=r, column=2).border = BORD
ws1.cell(row=r, column=3, value=c_f).number_format = c_fmt
ws1.cell(row=r, column=3).font = BODY_F
ws1.cell(row=r, column=3).alignment = CC
ws1.cell(row=r, column=3).border = BORD
ws1.cell(row=r, column=4, value=f'=IFERROR(C{r}/B{r},0)').number_format = d_fmt
ws1.cell(row=r, column=4).font = BODY_F
ws1.cell(row=r, column=4).alignment = CC
ws1.cell(row=r, column=4).border = BORD
if ri % 2 == 1:
for c in range(1, 5):
ws1.cell(row=r, column=c).fill = ALT_GRAY
ws1.row_dimensions[r].height = 22
# R12: empty spacer
# R13: Section 二
ws1.merge_cells('A13:G13')
c = ws1.cell(row=13, column=1, value='二、干预车辆考核达标情况说明')
c.fill = LIGHT_BLUE; c.font = SEC_F; c.alignment = LL
ws1.row_dimensions[13].height = 24
# R14: Summary description
ws1.merge_cells('A14:G14')
flip_desc = (f'{len(intv_recs)} 条考核记录涉及里程干预;替换为GPS里程后,'
f'{len(flip_down)} 条考核车辆记录由"达标"转判为"未达标"。按车型分类如下:')
c = ws1.cell(row=14, column=1, value=flip_desc)
c.font = RED_F; c.alignment = LL
ws1.row_dimensions[14].height = 28
# Group by vehicle type
by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
flip_by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for r in intv_recs:
by_target[r['target_name']][r['dept']][r['sales']] += 1
for r in flip_down:
flip_by_target[r['target_name']][r['dept']][r['sales']] += 1
target_order = sorted(by_target.keys(), key=lambda t: -sum(sum(s.values()) for s in flip_by_target.get(t, {}).values()))
row = 15
for tgt in target_order:
dept_sales = flip_by_target.get(tgt, {})
total_count = sum(sum(s.values()) for s in by_target[tgt].values())
flip_count = sum(sum(s.values()) for s in dept_sales.values())
if flip_count == 0: continue # only show types with flips
# Vehicle type title
if flip_count > 0:
title = f'{tgt}{flip_count} 辆)'
else:
title = f'{tgt}{total_count} 辆)'
ws1.merge_cells(start_row=row, start_column=1, end_row=row, end_column=7)
c = ws1.cell(row=row, column=1, value=title)
c.fill = LIGHT_BLUE; c.font = SUB_F; c.alignment = LL
for ci in range(1, 8): ws1.cell(row=row, column=ci).border = BORD
ws1.row_dimensions[row].height = 22
row += 1
# Headers (two columns side-by-side: A-C and E-G)
for base in [1, 5]:
for ci, h in enumerate(['部门', '销售经理', '数量'], base):
ws1.cell(row=row, column=ci, value=h)
for ci in range(1, 8):
ws1.cell(row=row, column=ci).fill = HD_BLUE
ws1.cell(row=row, column=ci).font = HDR_F
ws1.cell(row=row, column=ci).alignment = CC
ws1.cell(row=row, column=ci).border = BORD
ws1.row_dimensions[row].height = 22
row += 1
# Flatten sales list sorted by department then count
dept_order = sorted(dept_sales.keys(), key=lambda d: -sum(dept_sales[d].values()))
sales_list = []
for dept in dept_order:
for sales in sorted(dept_sales[dept].keys()):
sales_list.append((dept, sales, dept_sales[dept][sales]))
n = len(sales_list)
left = sales_list[:(n+1)//2]
right = sales_list[(n+1)//2:]
for ri2 in range(max(len(left), len(right))):
if ri2 < len(left):
d, s, cnt = left[ri2]
ws1.cell(row=row, column=1, value=d)
ws1.cell(row=row, column=2, value=s)
ws1.cell(row=row, column=3, value=cnt)
if ri2 < len(right):
d, s, cnt = right[ri2]
ws1.cell(row=row, column=5, value=d)
ws1.cell(row=row, column=6, value=s)
ws1.cell(row=row, column=7, value=cnt)
for ci in range(1, 8):
cell = ws1.cell(row=row, column=ci)
cell.border = BORD; cell.font = BODY_F; cell.alignment = CC
if ri2 % 2 == 1: cell.fill = ALT_GRAY
ws1.row_dimensions[row].height = 22
row += 1
row += 1 # blank between vehicle types
# Section 三
ws1.merge_cells(start_row=row, start_column=1, end_row=row, end_column=7)
c = ws1.cell(row=row, column=1, value='三、里程三列数据来源说明')
c.fill = LIGHT_BLUE; c.font = SEC_F; c.alignment = LL
ws1.row_dimensions[row].height = 24
row += 1
for ci, h in enumerate(['', '字段名', '数据来源', '说明', '', '', ''], 1):
ws1.cell(row=row, column=ci, value=h)
hdr_row(ws1, row, 4)
ws1.row_dimensions[row].height = 24
row += 1
ref_data = [
('O', '实际行驶里程(km)', 'GPS 轨迹还原',
'基于 G7 GPS 定位轨迹按考核期逐日汇总计算的运营里程(total_mileage/100000=km),本月里程考核口径。干预车辆已替换为GPS里程,非干预车辆沿用原仪表盘里程。'),
('P', '原TBOX里程(km)', '车辆 T-Box / 仪表盘读数',
'当月仪表总里程(业务系统原始数据),含运营+测试+调试里程。仅干预车辆填充,非干预车辆为空。'),
('Q', '测试里程(km)', 'P O(原TBOX GPS,差额)',
'仪表盘跑了但 GPS 未记录到的部分,判定为厂内测试/调试/路试等非运营里程,不计入考核。仅干预车辆填充,非干预车辆为0。'),
]
for ri, (col_letter, name, source, note) in enumerate(ref_data):
ws1.cell(row=row, column=1, value=col_letter).font = BOLD_F
ws1.cell(row=row, column=1).alignment = CC
ws1.cell(row=row, column=2, value=name)
ws1.cell(row=row, column=3, value=source)
ws1.cell(row=row, column=4, value=note).alignment = LL
dat_row(ws1, row, 4, alt=(ri%2==1))
ws1.cell(row=row, column=1).font = BOLD_F
ws1.row_dimensions[row].height = 42
row += 1
# Column widths
for ci, w in enumerate([12, 22, 13, 12, 13, 22, 13], 1):
ws1.column_dimensions[get_column_letter(ci)].width = w
# ─── Save ───
out = 'new-kpi-202605_1/租赁任务考核_2026年5月_干预情况说明_Final.xlsx'
wb.save(out)
print(f'{out} ({len(wb.sheetnames)} sheets)')
+375
View File
@@ -0,0 +1,375 @@
#!/usr/bin/env python3
"""重新生成 离职人员5月奖金处理明细.xlsx — 当月转嫁,其他不发了"""
import os, openpyxl, warnings
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
from calc_engine import *
warnings.filterwarnings('ignore')
os.chdir('/Users/kkfluous/Downloads')
# ── Full calc ──
print("读取源数据...")
D = {}
for m in [1,2,3,4,5]:
D[m] = read_file(f'租赁任务考核_2026年{m}月.xlsx', m)
G = {m: grp(D[m], m) for m in [1,2,3,4,5]}
calc_jan_carryover(G[1])
feb_data = calc_feb(G[1], G[2])
mar_data = calc_mar(G[1], G[2], G[3], feb_data)
apr_data = calc_apr(G[1], G[2], G[3], G[4], mar_data)
may_data = calc_may(G[1], G[2], G[3], G[4], G[5], apr_data)
RESIGNATIONS = {
'赵连飞': {'dept': '业务二部', 'transfer_to': '刘念念'},
'伍仲文': {'dept': '业务六部', 'transfer_to': '钟祥'},
'岑彦': {'dept': '业务六部', 'transfer_to': '钟祥'},
}
resigned = set(RESIGNATIONS.keys())
# ── Collect resigned payments ──
detail_records = []
for ptype, items in may_data.items():
for it in items:
if it['销售'] not in resigned:
continue
name = it['销售']
is_dangyue = (ptype == '当月')
transfer_to = RESIGNATIONS[name]['transfer_to']
handling = f'{transfer_to}(需确认)' if is_dangyue else '不发了'
transfer_disp = transfer_to if is_dangyue else ''
note = '5月当月达标,转新销售' if is_dangyue else f'{ptype},不发了'
# Get additional info from G5
g5 = G[5].get((it['车牌'], name))
client = ''
for m in [1,2,3,4,5]:
for r in D[m]:
if str(r['车牌号']).strip() == it['车牌'] and r['销售经理'] == name:
client = r.get('客户名称', '')
break
if client: break
monthly_bonus = g5['奖励额'] if g5 else 0
days = g5['天数'] if g5 else ''
target_name = ''
if g5 and g5.get('recs'):
target_name = g5['recs'][0].get('考核目标', '')
# 盈亏查询月
if ptype == '当月' or ptype == '累计补发5月':
loss_month = 5
elif ptype == '结转':
loss_month = 5
elif ptype.startswith('补发'):
loss_month = int(ptype.replace('补发','').replace('',''))
else:
loss_month = 5
detail_records.append({
'name': name, 'plate': it['车牌'], 'pay_type': ptype,
'amount': it[''], 'handling': handling, 'transfer': transfer_disp,
'note': note, 'client': client, 'loss_month': loss_month,
'loss_status': '', 'monthly_bonus': monthly_bonus,
'days': days, 'target': target_name,
})
print(f"离职人员发放记录: {len(detail_records)}")
# ── Styles ──
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10)
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
ALT_FILL = PatternFill('solid', fgColor='F2F2F2')
TITLE_FNT = Font(bold=True, size=14)
NOTE_FNT = Font(size=10, color='C00000')
NORM_FNT = Font(size=10)
BOLD_FNT = Font(bold=True, size=10)
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
def hdr(ws, r, nc):
for c in range(1, nc+1):
cell = ws.cell(row=r, column=c)
cell.fill = HEAD_FILL; cell.font = HEAD_FONT; cell.alignment = CC; cell.border = BORD
def drw(ws, r, nc, alt=False):
for c in range(1, nc+1):
cell = ws.cell(row=r, column=c)
cell.font = NORM_FNT; cell.alignment = CC; cell.border = BORD
if alt: cell.fill = ALT_FILL
wb = openpyxl.Workbook()
# ═══════════════════════════════════════
# Sheet 1: 离职人员发放处理总览
# ═══════════════════════════════════════
ws1 = wb.active
ws1.title = '离职人员发放处理总览'
ws1.merge_cells('A1:G1')
ws1.cell(row=1, column=1, value='2026年5月核算 — 离职人员奖金处理方案').font = TITLE_FNT
ws1.cell(row=1, column=1).alignment = CC
ws1.row_dimensions[1].height = 30
ws1.merge_cells('A2:G2')
ws1.cell(row=2, column=1,
value='处理规则:仅5月当月达标奖金转给新接手的销售(需与其他部门确认是否发放);其余结转、补发、累计补发等一律不发了。').font = NOTE_FNT
ws1.cell(row=2, column=1).alignment = LL
ws1.row_dimensions[2].height = 22
r = 4
oh = ['离职人员', '部门', '业务转嫁至', '不发了', '转新销售(需确认)', '原考核应发合计']
for c, h in enumerate(oh, 1):
ws1.cell(row=r, column=c, value=h)
hdr(ws1, r, 6)
r += 1
total_not = 0; total_trf = 0
for name in ['赵连飞', '伍仲文', '岑彦']:
info = RESIGNATIONS[name]
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
transfer = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
total_not += not_pay; total_trf += transfer
ws1.cell(row=r, column=1, value=name)
ws1.cell(row=r, column=2, value=info['dept'])
ws1.cell(row=r, column=3, value=info['transfer_to'])
ws1.cell(row=r, column=4, value=round(not_pay, 2))
ws1.cell(row=r, column=5, value=round(transfer, 2))
ws1.cell(row=r, column=6, value=round(not_pay+transfer, 2))
drw(ws1, r, 6)
ws1.row_dimensions[r].height = 22
r += 1
# 合计
for c in range(1, 7):
ws1.cell(row=r, column=c).fill = SUB_FILL
ws1.cell(row=r, column=c).border = BORD
ws1.cell(row=r, column=c).font = BOLD_FNT
ws1.cell(row=r, column=c).alignment = CC
ws1.cell(row=r, column=1, value='合计')
ws1.cell(row=r, column=4, value=round(total_not, 2))
ws1.cell(row=r, column=5, value=round(total_trf, 2))
ws1.cell(row=r, column=6, value=round(total_not+total_trf, 2))
r += 2
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
ws1.cell(row=r, column=1, value='转嫁接收方汇总(需与其他部门确认是否发放)').font = Font(bold=True, size=12, color='1F4E78')
ws1.cell(row=r, column=1).alignment = CC
r += 1
for c, hh in enumerate(['接收人', '来源离职人员', '转嫁金额', '备注'], 1):
ws1.cell(row=r, column=c, value=hh)
hdr(ws1, r, 4)
r += 1
by_recv = defaultdict(lambda: {'sources':[], 'amt':0})
for rec in detail_records:
if rec['pay_type'] == '当月':
t = rec['transfer']
by_recv[t]['sources'].append(rec['name'])
by_recv[t]['amt'] += rec['amount']
for receiver in ['刘念念', '钟祥']:
d = by_recv[receiver]
ws1.cell(row=r, column=1, value=receiver)
ws1.cell(row=r, column=2, value=''.join(sorted(set(d['sources']))))
ws1.cell(row=r, column=3, value=round(d['amt'], 2))
ws1.cell(row=r, column=4, value='需与相关部门确认是否发放给新销售')
drw(ws1, r, 4)
r += 1
for i, w in enumerate([14, 22, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheet 2: 发放明细(逐条)
# ═══════════════════════════════════════
ws2 = wb.create_sheet('发放明细(逐条)')
ws2.merge_cells('A1:M1')
ws2.cell(row=1, column=1, value='离职人员 — 5月考核应发逐条明细').font = TITLE_FNT
ws2.cell(row=1, column=1).alignment = CC
ws2.row_dimensions[1].height = 28
dh = ['离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称',
'客户盈亏', '处理方式', '转嫁接收人', '5月考核天数', '月度奖励', '考核目标', '备注']
r2 = 3
for c, h in enumerate(dh, 1):
ws2.cell(row=r2, column=c, value=h)
hdr(ws2, r2, 13)
r2 += 1
name_order = ['赵连飞', '伍仲文', '岑彦']
detail_records.sort(key=lambda x: (name_order.index(x['name']), 0 if x['pay_type']=='当月' else 1, x['plate']))
for i, rec in enumerate(detail_records):
ws2.cell(row=r2, column=1, value=rec['name'])
ws2.cell(row=r2, column=2, value=rec['plate'])
ws2.cell(row=r2, column=3, value=rec['pay_type'])
ws2.cell(row=r2, column=4, value=round(rec['amount'], 2))
ws2.cell(row=r2, column=5, value=f"{rec['loss_month']}")
ws2.cell(row=r2, column=6, value=rec['client'])
ws2.cell(row=r2, column=7, value=rec['loss_status'])
ws2.cell(row=r2, column=8, value=rec['handling'])
ws2.cell(row=r2, column=9, value=rec['transfer'])
ws2.cell(row=r2, column=10, value=rec['days'] if rec['days'] else '')
ws2.cell(row=r2, column=11, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws2.cell(row=r2, column=12, value=rec['target'])
ws2.cell(row=r2, column=13, value=rec['note'])
drw(ws2, r2, 13, alt=(i%2==1))
ws2.row_dimensions[r2].height = 22
r2 += 1
# 合计行
ws2.cell(row=r2, column=3, value='合计')
ws2.cell(row=r2, column=4, value=round(sum(r['amount'] for r in detail_records), 2))
for c in range(1, 14):
ws2.cell(row=r2, column=c).fill = SUB_FILL
ws2.cell(row=r2, column=c).border = BORD
ws2.cell(row=r2, column=c).font = BOLD_FNT
ws2.cell(row=r2, column=c).alignment = CC
for i, w in enumerate([10, 12, 14, 14, 10, 24, 8, 18, 14, 12, 10, 22, 28], 1):
ws2.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheets 3-5: 个人明细
# ═══════════════════════════════════════
for name, title_txt, transfer_to in [
('赵连飞', '赵连飞(2026-04-30离职,业务转→刘念念)', '刘念念'),
('伍仲文', '伍仲文(2026-05-09离职,业务转→钟祥)', '钟祥'),
('岑彦', '岑彦(2026-05-09离职,业务转→钟祥)', '钟祥'),
]:
ws = wb.create_sheet(name)
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = TITLE_FNT
ws.cell(row=1, column=1).alignment = CC
ws.row_dimensions[1].height = 28
ph = ['车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏',
'处理方式', '5月考核天数', '月度奖励', '考核目标', '备注']
r3 = 3
for c, h in enumerate(ph, 1):
ws.cell(row=r3, column=c, value=h)
hdr(ws, r3, 11)
r3 += 1
person_recs = [p for p in detail_records if p['name'] == name]
dangyue_recs = [p for p in person_recs if p['pay_type'] == '当月']
other_recs = [p for p in person_recs if p['pay_type'] != '当月']
for i, rec in enumerate(person_recs):
h_disp = rec['handling']
ws.cell(row=r3, column=1, value=rec['plate'])
ws.cell(row=r3, column=2, value=rec['pay_type'])
ws.cell(row=r3, column=3, value=round(rec['amount'], 2))
ws.cell(row=r3, column=4, value=f"{rec['loss_month']}")
ws.cell(row=r3, column=5, value=rec['client'])
ws.cell(row=r3, column=6, value=rec['loss_status'])
ws.cell(row=r3, column=7, value=h_disp)
ws.cell(row=r3, column=8, value=rec['days'] if rec['days'] else '')
ws.cell(row=r3, column=9, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws.cell(row=r3, column=10, value=rec['target'])
ws.cell(row=r3, column=11, value=rec['note'])
drw(ws, r3, 11, alt=(i%2==1))
ws.row_dimensions[r3].height = 22
r3 += 1
dn = sum(p['amount'] for p in dangyue_recs)
ot = sum(p['amount'] for p in other_recs)
gt = dn + ot
# 不发了小计
ws.cell(row=r3, column=2, value='不发了小计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(ot, 2)).font = BOLD_FNT
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
r3 += 1
# 转嫁小计
ws.cell(row=r3, column=2, value=f'{transfer_to}小计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(dn, 2)).font = BOLD_FNT
ws.cell(row=r3, column=11, value='需与其他部门确认').font = Font(size=9, color='C00000')
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
r3 += 1
# 合计
ws.cell(row=r3, column=2, value='合计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(gt, 2)).font = BOLD_FNT
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
for i, w in enumerate([12, 14, 14, 10, 26, 8, 18, 12, 10, 22, 30], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheets 6-7: 转嫁明细
# ═══════════════════════════════════════
for title_txt, sources, receiver in [
('转嫁至刘念念的奖金 — 来源:赵连飞(2026-04-30离职)', ['赵连飞'], '刘念念'),
('转嫁至钟祥的奖金 — 来源:伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)', ['伍仲文', '岑彦'], '钟祥'),
]:
ws = wb.create_sheet(f'{receiver}明细')
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = TITLE_FNT
ws.cell(row=1, column=1).alignment = CC
ws.row_dimensions[1].height = 28
ws.merge_cells('A2:K2')
ws.cell(row=2, column=1, value='⚠ 以下金额需与其他部门确认是否可发放给新销售').font = NOTE_FNT
ws.cell(row=2, column=1).alignment = LL
th = ['来源离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称',
'客户盈亏', '5月考核天数', '月度奖励', '考核目标', '备注']
r4 = 4
for c, h in enumerate(th, 1):
ws.cell(row=r4, column=c, value=h)
hdr(ws, r4, 11)
r4 += 1
trf_recs = [p for p in detail_records if p['name'] in sources and p['pay_type'] == '当月']
for i, rec in enumerate(trf_recs):
ws.cell(row=r4, column=1, value=rec['name'])
ws.cell(row=r4, column=2, value=rec['plate'])
ws.cell(row=r4, column=3, value='当月')
ws.cell(row=r4, column=4, value=round(rec['amount'], 2))
ws.cell(row=r4, column=5, value=f"{rec['loss_month']}")
ws.cell(row=r4, column=6, value=rec['client'])
ws.cell(row=r4, column=7, value=rec['loss_status'])
ws.cell(row=r4, column=8, value=rec['days'] if rec['days'] else '')
ws.cell(row=r4, column=9, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws.cell(row=r4, column=10, value=rec['target'])
ws.cell(row=r4, column=11, value='需确认是否发给新销售')
drw(ws, r4, 11, alt=(i%2==1))
ws.row_dimensions[r4].height = 22
r4 += 1
ws.cell(row=r4, column=3, value='合计')
ws.cell(row=r4, column=4, value=round(sum(r['amount'] for r in trf_recs), 2))
for c in range(1, 12):
ws.cell(row=r4, column=c).fill = SUB_FILL; ws.cell(row=r4, column=c).border = BORD
ws.cell(row=r4, column=c).font = BOLD_FNT; ws.cell(row=r4, column=c).alignment = CC
for i, w in enumerate([14, 12, 14, 14, 10, 26, 8, 12, 10, 22, 22], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ── Save ──
out = 'new-kpi-202605_1/离职人员5月奖金处理明细.xlsx'
wb.save(out)
print(f'\n{out} ({len(wb.sheetnames)} sheets)')
print(f'\n汇总:')
for name in ['赵连飞', '伍仲文', '岑彦']:
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
trf = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
print(f' {name}: 不发了={not_pay:.2f}{RESIGNATIONS[name]["transfer_to"]}={trf:.2f} 合计={not_pay+trf:.2f}')
print(f' 总计: 不发了={total_not:.2f} 转新销售={total_trf:.2f}')
+275
View File
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""Process May intervention: replace mileage with GPS, add TBOX/test columns."""
import os, shutil, openpyxl, pymysql, warnings
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from datetime import datetime, date, timedelta
from collections import defaultdict
warnings.filterwarnings('ignore')
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年5月.xlsx'
BAK = '租赁任务考核_2026年5月_原版备份.xlsx'
OUT = SRC # overwrite
# ── Step 1: Backup ──
if not os.path.exists(BAK):
shutil.copy2(SRC, BAK)
print(f"✅ Backup: {BAK}")
else:
print(f"跳过备份(已存在)")
# ── Step 2: Load intervention plates ──
wb_i = openpyxl.load_workbook('标黄为5月干预车辆.xlsx')
ws_i = wb_i['Sheet1']
yellow_plates = set()
for row in ws_i.iter_rows(min_row=2, values_only=False):
cell = row[0]
fill = cell.fill
is_yellow = (fill and fill.patternType == 'solid' and fill.start_color
and fill.start_color.rgb and 'FFFF00' in str(fill.start_color.rgb))
if is_yellow:
p = str(cell.value).strip() if cell.value else ''
if p:
yellow_plates.add(p)
wb_i.close()
print(f"干预车辆: {len(yellow_plates)}")
# ── Step 3: Read source records ──
wb_s = openpyxl.load_workbook(SRC, data_only=True)
ws_s = wb_s['业务考核视图']
src_h = [c.value for c in next(ws_s.iter_rows(min_row=1, max_row=1))]
rows_raw = list(ws_s.iter_rows(min_row=2, values_only=True))
wb_s.close()
records = []
for row in rows_raw:
if not row[0]:
continue
records.append(dict(zip(src_h, row)))
print(f"源数据: {len(records)}")
# ── Step 4: GPS from DB ──
print("查询数据库...")
conn = pymysql.connect(host='101.133.130.65', user='root', password='ln123!@#',
database='hydrogen_energy', charset='utf8mb4')
cur = conn.cursor()
cur.execute("SELECT plate_number, dates, total_mileage FROM ln_vehicle_g7_mileage WHERE dates >= '2026-05-01' AND dates <= '2026-06-08'")
db_rows = cur.fetchall()
conn.close()
# Group: plate -> {date: mileage}
pmap = defaultdict(lambda: defaultdict(int))
for p, d, m in db_rows:
dd = d if isinstance(d, date) else (d.date() if isinstance(d, datetime) else date.fromisoformat(str(d)[:10]))
pmap[p][dd] = m or 0
print(f"DB plates loaded: {len(pmap)}")
# Compute GPS km per plate per考核期
def get_gps_km(plate, start_v, end_v):
"""GPS里程(km) = sum of daily total_mileage / 100000"""
if plate not in pmap:
return 0.0
# Parse dates
if isinstance(start_v, datetime):
sd = start_v.date()
elif isinstance(start_v, date):
sd = start_v
else:
return 0.0
if isinstance(end_v, datetime):
ed = end_v.date()
elif isinstance(end_v, date):
ed = end_v
else:
return 0.0
total = 0
d = sd
daily = pmap[plate]
while d <= ed:
total += daily.get(d, 0)
d += timedelta(days=1)
return total / 100000.0
# Build plate -> gps for intervention vehicles
plate_gps = {}
for rec in records:
p = str(rec['车牌号']).strip()
if p not in yellow_plates:
continue
if p in plate_gps:
continue
km = get_gps_km(p, rec.get('考核开始日期'), rec.get('考核结束日期'))
plate_gps[p] = round(km, 2)
print(f"GPS数据获取: {len(plate_gps)}/{len(yellow_plates)}")
gps_zero = sum(1 for v in plate_gps.values() if v == 0)
print(f"GPS=0: {gps_zero}")
# ── Step 5: Generate new workbook ──
YELLOW = PatternFill('solid', fgColor='FFFF00')
RED = PatternFill('solid', fgColor='FF0000')
REDF = Font(color='FFFFFF', bold=True)
HFILL = PatternFill('solid', fgColor='4472C4')
HFONT = Font(bold=True, color='FFFFFF', size=10)
BORD = Border(Side('thin','B4B4B4'), Side('thin','B4B4B4'), Side('thin','B4B4B4'), Side('thin','B4B4B4'))
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
NFONT = Font(size=10)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = '业务考核视图'
# New header layout:
# 车牌号(0) 考核年份(1) 考核月份(2) 部门名称(3) 销售经理(4) 客户名称(5) 合同编号(6) 项目名称(7)
# 考核目标ID(8) 考核目标(9) 考核开始日期(10) 考核结束日期(11) 考核天数(12) 应考核里程(13)
# 原TBOX里程(14) 实际行驶里程=GPS(15) 测试里程(16) 完成率(17) 是否达标(18) 考核状态(19) ...
# Column map: src_col -> dst_col (0-indexed)
# 0-13 stay same, 14(old actual) -> split into 14(TBOX) 15(new actual) 16(test), 15(old rate) -> 17, 16(old pass)->18, 17(status)->19, 18-20 -> 20-22
new_h = ['车牌号', '考核年份', '考核月份', '部门名称', '销售经理', '客户名称',
'合同编号', '项目名称', '考核目标ID', '考核目标', '考核开始日期',
'考核结束日期', '考核天数', '应考核里程(km)',
'原TBOX里程(km)', '实际行驶里程(km)', '测试里程(km)',
'完成率(%)', '是否达标', '考核状态',
'交车日期', '预计还车日期', '实际还车日期']
for ci, h in enumerate(new_h):
c = ci + 1
cell = ws.cell(row=1, column=c, value=h)
cell.fill = HFILL
cell.font = HFONT
cell.alignment = CC
cell.border = BORD
# Write data
for ri, rec in enumerate(records):
r = ri + 2
plate = str(rec['车牌号']).strip()
is_intv = plate in yellow_plates
# Columns 0-13: copy directly (0-indexed in src_h)
for si in range(14): # 车牌号 ~ 应考核里程(km)
val = rec.get(src_h[si])
if val is not None:
ws.cell(row=r, column=si + 1, value=val)
ws.cell(row=r, column=si + 1).border = BORD
ws.cell(row=r, column=si + 1).alignment = CC
ws.cell(row=r, column=si + 1).font = NFONT
# Column 14 (0-indexed): 原TBOX里程(km) - old actual mileage
tbox = float(rec.get('实际行驶里程(km)', 0) or 0)
ws.cell(row=r, column=15, value=tbox).border = BORD
ws.cell(row=r, column=15).alignment = CC
ws.cell(row=r, column=15).font = NFONT
# Column 15 (0-indexed:16): 实际行驶里程(km) - GPS for intv, TBOX for others
target_km = float(rec.get('应考核里程(km)', 0) or 0)
if is_intv:
new_actual = plate_gps.get(plate, 0)
else:
new_actual = tbox
cell_actual = ws.cell(row=r, column=16, value=new_actual)
cell_actual.border = BORD
cell_actual.alignment = CC
cell_actual.font = NFONT
# Column 16 (0-indexed:17): 测试里程(km) = TBOX - GPS
test_km = round(tbox - new_actual, 2) if is_intv else 0
ws.cell(row=r, column=17, value=test_km).border = BORD
ws.cell(row=r, column=17).alignment = CC
ws.cell(row=r, column=17).font = NFONT
# Column 17 (0-indexed:18): 完成率(%)
rate = round(new_actual / target_km * 100, 2) if target_km > 0 else 0
ws.cell(row=r, column=18, value=rate).border = BORD
ws.cell(row=r, column=18).alignment = CC
ws.cell(row=r, column=18).font = NFONT
# Column 18 (0-indexed:19): 是否达标
new_pass = '达标' if new_actual >= target_km and target_km > 0 else '未达标'
ws.cell(row=r, column=19, value=new_pass).border = BORD
ws.cell(row=r, column=19).alignment = CC
ws.cell(row=r, column=19).font = NFONT
# Column 19 (0-indexed:20): 考核状态
val = rec.get('考核状态')
ws.cell(row=r, column=20, value=val if val else '进行中').border = BORD
ws.cell(row=r, column=20).alignment = CC
ws.cell(row=r, column=20).font = NFONT
# Columns 20-22 (0-indexed): 交车日期, 预计还车日期, 实际还车日期
for si in range(18, 21):
val = rec.get(src_h[si])
if val is not None:
ws.cell(row=r, column=si + 1 + 5, value=val) # +5 offset for new cols
else:
ws.cell(row=r, column=si + 1 + 5, value='')
ws.cell(row=r, column=si + 1 + 5).border = BORD
ws.cell(row=r, column=si + 1 + 5).alignment = CC
ws.cell(row=r, column=si + 1 + 5).font = NFONT
# ── Formatting: yellow for intervention, red for GPS=0 ──
if is_intv:
for c in range(1, len(new_h) + 1):
cell = ws.cell(row=r, column=c)
# Don't override red cells
existing_fill = cell.fill
is_red = (existing_fill.patternType == 'solid' and
existing_fill.start_color and
existing_fill.start_color.rgb and
'FF0000' in str(existing_fill.start_color.rgb))
if not is_red:
cell.fill = YELLOW
else:
# Red stays, but also yellow background? No, red means GPS=0, keep it
pass
# Red highlight for GPS=0
if plate_gps.get(plate, 0) == 0:
ws.cell(row=r, column=16).fill = RED # 实际行驶里程 column
ws.cell(row=r, column=16).font = REDF
# Column widths
widths = [12, 8, 8, 12, 10, 26, 18, 22, 10, 22, 13, 13, 10, 14, 16, 16, 14, 12, 10, 8, 12, 12, 12]
for i, w in enumerate(widths):
ws.column_dimensions[get_column_letter(i+1)].width = w
# ── Save ──
wb.save(OUT)
print(f"\n✅ 已保存: {OUT} ({len(records)} rows, {len(new_h)} cols)")
# ── Stats ──
old_p = sum(1 for r in records if str(r.get('是否达标','')).strip() == '达标')
new_p = 0
flip_down = []
flip_up = []
for rec in records:
p = str(rec['车牌号']).strip()
tgt = float(rec.get('应考核里程(km)', 0) or 0)
old_status = str(rec.get('是否达标','')).strip()
if p in yellow_plates:
actual = plate_gps.get(p, 0)
else:
actual = float(rec.get('实际行驶里程(km)', 0) or 0)
n = '达标' if actual >= tgt and tgt > 0 else '未达标'
if n == '达标':
new_p += 1
if old_status == '达标' and n == '未达标':
flip_down.append((p, float(rec.get('实际行驶里程(km)',0) or 0), actual, tgt))
elif old_status == '未达标' and n == '达标':
flip_up.append((p, float(rec.get('实际行驶里程(km)',0) or 0), actual, tgt))
print(f"达标变化: {old_p} -> {new_p} ({new_p - old_p:+d})")
print(f"达→未达标: {len(flip_down)}")
for p, tb, gp, tg in flip_down[:10]:
print(f" {p} TBOX={tb:.1f} GPS={gp:.2f} 目标={tg:.0f}")
print(f"未→达标: {len(flip_up)}")
for p, tb, gp, tg in flip_up[:10]:
print(f" {p} TBOX={tb:.1f} GPS={gp:.2f} 目标={tg:.0f}")
+70
View File
@@ -0,0 +1,70 @@
from copy import copy
from pathlib import Path
import shutil
from openpyxl import load_workbook
OUTPUT = Path('/Users/kkfluous/Downloads/7月核算结果/7月考核车辆多源里程汇总.xlsx')
BACKUP = Path('/Users/kkfluous/Downloads/7月核算结果/7月考核车辆多源里程汇总_增加车型前.xlsx')
SOURCE = Path('/Users/kkfluous/Downloads/7月核算结果/租赁任务考核_2026年7月.xlsx')
def plate(value):
return ''.join(str(value or '').upper().split())
src_wb = load_workbook(SOURCE, read_only=True, data_only=True)
src_ws = src_wb['业务考核视图']
headers = [c.value for c in src_ws[1]]
idx = {name: i for i, name in enumerate(headers)}
types = {}
for row in src_ws.iter_rows(min_row=2, values_only=True):
if not row[idx['车牌号']]:
continue
start = row[idx['考核开始日期']]
end = row[idx['考核结束日期']]
key = (plate(row[idx['车牌号']]), start.date(), end.date(), str(row[idx['客户名称']] or '').strip())
types[key] = row[idx['考核目标']] or '无来源'
src_wb.close()
if not BACKUP.exists():
shutil.copy2(OUTPUT, BACKUP)
wb = load_workbook(OUTPUT)
ws = wb.active
header_row = next(r for r in range(1, 8) if ws.cell(r, 2).value == '车牌')
data_start = header_row + 1
for merged in list(ws.merged_cells.ranges):
if str(merged) in ('A1:L1', 'A1:M1'):
ws.unmerge_cells(str(merged))
ws.insert_cols(4, 1)
ws.cell(header_row, 4).value = '车型'
ws.cell(header_row, 4)._style = copy(ws.cell(header_row, 3)._style)
ws.cell(header_row, 4).font = copy(ws.cell(header_row, 3).font)
ws.cell(header_row, 4).fill = copy(ws.cell(header_row, 3).fill)
ws.cell(header_row, 4).border = copy(ws.cell(header_row, 3).border)
ws.cell(header_row, 4).alignment = copy(ws.cell(header_row, 3).alignment)
missing = []
for row in range(data_start, ws.max_row + 1):
start = ws.cell(row, 5).value
end = ws.cell(row, 6).value
key = (plate(ws.cell(row, 2).value), start.date(), end.date(), str(ws.cell(row, 3).value or '').strip())
ws.cell(row, 4).value = types.get(key, '无来源')
ws.cell(row, 4)._style = copy(ws.cell(row, 3)._style)
ws.cell(row, 4).font = copy(ws.cell(row, 3).font)
ws.cell(row, 4).fill = copy(ws.cell(row, 3).fill)
ws.cell(row, 4).border = copy(ws.cell(row, 3).border)
ws.cell(row, 4).alignment = copy(ws.cell(row, 3).alignment)
if key not in types:
missing.append(key)
ws.merge_cells('A1:M1')
ws.column_dimensions['D'].width = 24
ws.auto_filter.ref = f'A{header_row}:M{ws.max_row}'
ws.print_title_rows = f'1:{header_row}'
wb.save(OUTPUT)
print({'records': ws.max_row - header_row, 'type_missing': len(missing), 'gps_column': ws.cell(header_row, 13).value})
+90
View File
@@ -0,0 +1,90 @@
from ast import parse, walk, Call, Constant, keyword
from collections import defaultdict
from datetime import datetime, date
from pathlib import Path
from zipfile import ZipFile
from xml.etree import ElementTree as ET
import re
import openpyxl
import pymysql
BASE = Path('/Users/kkfluous/Downloads')
ASSESSMENT = BASE / '7月核算结果/租赁任务考核_2026年7月.xlsx'
TBOX = BASE / '车辆里程查询_20260701-20260731_1024辆.xlsx'
DETAILS = [BASE / '里程统计[天][2026-07-01至2026-07-31].xlsx', BASE / '里程统计[天][2026-07-01至2026-07-31] (1).xlsx']
NS = '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'
def norm(v): return re.sub(r'\s+', '', str(v or '')).upper()
def cfg():
tree=parse((BASE/'replace_gps_jun.py').read_text())
for node in walk(tree):
if isinstance(node,Call) and getattr(node.func,'attr',None)=='connect':
d={x.arg:x.value.value for x in node.keywords if isinstance(x.value,Constant)}
if {'host','user','password','database'}<=d.keys(): return d
raise RuntimeError('no db config')
def read_details():
out=defaultdict(dict)
for path in DETAILS:
with ZipFile(path) as z:
root=ET.fromstring(z.read('xl/worksheets/sheet1.xml'))
for row in root.findall(f'.//{NS}row'):
if int(row.get('r'))<4: continue
d={}
for c in row.findall(f'{NS}c'):
col=re.match(r'[A-Z]+',c.get('r')).group()
if c.get('t')=='inlineStr': d[col]=''.join(t.text or '' for t in c.findall(f'.//{NS}t'))
else:
v=c.find(f'{NS}v'); d[col]=v.text if v is not None else None
p=norm(d.get('H')); ds=str(d.get('O') or '').strip()
if p and p!='合计' and re.fullmatch(r'2026-07-\d{2}',ds):
out[p][datetime.strptime(ds,'%Y-%m-%d').date()]=float(d.get('P') or 0)
return out
awb=openpyxl.load_workbook(ASSESSMENT,data_only=True)
aws=awb['业务考核视图']
h=[c.value for c in aws[1]]; ix={v:i for i,v in enumerate(h)}
candidates=set(); records=[]
for ri,row in enumerate(aws.iter_rows(min_row=2,values_only=True),2):
fills={aws.cell(ri,c).fill.fgColor.rgb for c in range(1,aws.max_column+1) if aws.cell(ri,c).fill.fill_type=='solid'}
is_candidate=any(str(x).upper().endswith('FFFF00') for x in fills if x)
p=norm(row[ix['车牌号']]); start=row[ix['考核开始日期']].date(); end=row[ix['考核结束日期']].date()
records.append((p,start,end,is_candidate))
if is_candidate:candidates.add(p)
awb.close()
twb=openpyxl.load_workbook(TBOX,read_only=True,data_only=True); tws=twb['里程查询']
daycols={i:v.date() for i,v in enumerate(next(tws.iter_rows(min_row=6,max_row=6,values_only=True))) if isinstance(v,datetime)}
tbox=defaultdict(dict)
for row in tws.iter_rows(min_row=7,values_only=True):
p=norm(row[0])
if p:
for i,d in daycols.items():
if row[i] is not None:tbox[p][d]=float(row[i])
twb.close()
gps=read_details()
dc=cfg();dc.update(charset='utf8mb4',connect_timeout=8,read_timeout=30,write_timeout=30)
conn=pymysql.connect(**dc)
try:
with conn.cursor() as cur:
ph=','.join(['%s']*len(candidates))
cur.execute(f'SELECT plate_number,dates,total_mileage FROM ln_vehicle_g7_mileage WHERE dates >= %s AND dates <= %s AND plate_number IN ({ph})',[date(2026,7,1),date(2026,7,31),*sorted(candidates)])
rows=cur.fetchall()
finally:conn.close()
for p,d,m in rows:
if isinstance(d,datetime):d=d.date()
gps[norm(p)].setdefault(d,float(m or 0)/100000)
candidate_days=0; comparable=0; flagged=0; plates_no_tbox=set(); plates_no_gps=set(); flagged_plates=set()
for p,start,end,cand in records:
if not cand:continue
for n in range((end-start).days+1):
d=start.fromordinal(start.toordinal()+n);candidate_days+=1
if d not in tbox.get(p,{}):plates_no_tbox.add(p);continue
if d not in gps.get(p,{}):plates_no_gps.add(p);continue
comparable+=1
if tbox[p][d]-gps[p][d]>50:flagged+=1;flagged_plates.add(p)
print({'candidate_records':sum(x[3] for x in records),'candidate_plates':len(candidates),'candidate_days':candidate_days,'comparable_days':comparable,'flagged_days':flagged,'flagged_plates':len(flagged_plates),'plates_with_any_tbox_gap':len(plates_no_tbox),'plates_with_any_gps_gap':len(plates_no_gps),'no_tbox_sample':sorted(plates_no_tbox)[:20],'no_gps_sample':sorted(plates_no_gps)[:20]})
+59
View File
@@ -0,0 +1,59 @@
from ast import parse, walk, Call, Constant, keyword
from datetime import date
from pathlib import Path
import re
import openpyxl
import pymysql
def connection_kwargs_from_existing_script():
tree = parse(Path('/Users/kkfluous/Downloads/replace_gps_jun.py').read_text())
for node in walk(tree):
if not isinstance(node, Call):
continue
func = node.func
if getattr(func, 'attr', None) != 'connect':
continue
values = {}
for item in node.keywords:
if isinstance(item, keyword) and isinstance(item.value, Constant):
values[item.arg] = item.value.value
if {'host', 'user', 'password', 'database'} <= values.keys():
return values
raise RuntimeError('未在既有脚本中找到数据库连接配置')
src = openpyxl.load_workbook(
'/Users/kkfluous/Downloads/7月核算结果/租赁任务考核_2026年7月.xlsx',
read_only=True,
data_only=True,
)['业务考核视图']
headers = [c.value for c in src[1]]
plate_col = headers.index('车牌号')
plates = sorted({re.sub(r'\s+', '', str(r[plate_col] or '')).upper() for r in src.iter_rows(min_row=2, values_only=True) if r[plate_col]})
cfg = connection_kwargs_from_existing_script()
cfg.update(charset='utf8mb4', connect_timeout=8, read_timeout=30, write_timeout=30)
conn = pymysql.connect(**cfg)
try:
with conn.cursor() as cur:
placeholders = ','.join(['%s'] * len(plates))
cur.execute(
f'''SELECT plate_number, dates, total_mileage
FROM ln_vehicle_g7_mileage
WHERE dates >= %s AND dates <= %s
AND plate_number IN ({placeholders})''',
[date(2026, 7, 1), date(2026, 7, 31), *plates],
)
rows = cur.fetchall()
finally:
conn.close()
db_plates = {re.sub(r'\s+', '', str(r[0] or '')).upper() for r in rows}
print({
'assessment_plates': len(plates),
'database_rows': len(rows),
'database_plates': len(db_plates),
'uncovered_plates': len(set(plates) - db_plates),
})
@@ -0,0 +1,49 @@
import openpyxl
import re
from collections import Counter
from datetime import datetime
from pathlib import Path
NEW = Path('/Users/kkfluous/Downloads/7月考核车辆多源里程汇总_用户确认版.xlsx')
OLD = Path('/Users/kkfluous/Downloads/7月核算结果/租赁任务考核_2026年7月.xlsx')
def norm(v):
return re.sub(r'\s+', '', str(v or '')).upper()
def day(v):
return v.date() if isinstance(v, datetime) else v
nw = openpyxl.load_workbook(NEW, data_only=True, read_only=False)
ns = nw['7月考核车辆里程汇总']
nh = {ns.cell(2, c).value: c for c in range(1, ns.max_column + 1)}
ow = openpyxl.load_workbook(OLD, data_only=True, read_only=False)
os = ow['业务考核视图']
oh = {os.cell(1, c).value: c for c in range(1, os.max_column + 1)}
N = [(norm(ns.cell(r, nh['车牌']).value), day(ns.cell(r, nh['考核开始日期']).value),
day(ns.cell(r, nh['考核结束日期']).value), ns.cell(r, nh['核算里程(km)']).value)
for r in range(3, ns.max_row + 1)]
O = [(norm(os.cell(r, oh['车牌号']).value), day(os.cell(r, oh['考核开始日期']).value),
day(os.cell(r, oh['考核结束日期']).value), os.cell(r, oh['实际行驶里程(km)']).value)
for r in range(2, os.max_row + 1)]
nk, ok = Counter(x[:3] for x in N), Counter(x[:3] for x in O)
nd = {x[:3]: x[3] for x in N}
changes = []
for x in O:
nv, ov = nd.get(x[:3]), x[3]
if isinstance(nv, (int, float)) and abs(float(nv) - float(ov or 0)) > 0.005:
changes.append((*x[:3], ov, nv, nv - float(ov or 0)))
print({
'new_rows': len(N), 'old_rows': len(O),
'new_dup_keys': sum(v > 1 for v in nk.values()), 'old_dup_keys': sum(v > 1 for v in ok.values()),
'missing_in_new': len(set(ok) - set(nk)), 'extra_in_new': len(set(nk) - set(ok)),
'changed_rows': len(changes),
'old_total': round(sum(float(x[3] or 0) for x in O), 2),
'new_total': round(sum(float(x[3] or 0) for x in N if isinstance(x[3], (int, float))), 2),
})
print('missing', list(set(ok) - set(nk))[:10])
print('extra', list(set(nk) - set(ok))[:10])
for x in sorted(changes, key=lambda x: abs(x[5]), reverse=True)[:15]:
print(x)
+101
View File
@@ -0,0 +1,101 @@
from ast import parse, walk, Call, Constant, keyword
from collections import defaultdict
from datetime import datetime, date
from pathlib import Path
import re
import shutil
import openpyxl
import pymysql
OUTPUT = Path('/Users/kkfluous/Downloads/7月核算结果/7月考核车辆多源里程汇总.xlsx')
BACKUP = Path('/Users/kkfluous/Downloads/7月核算结果/7月考核车辆多源里程汇总_GPS全量回补前.xlsx')
def norm(value):
return re.sub(r'\s+', '', str(value or '')).upper()
def db_config():
tree = parse(Path('/Users/kkfluous/Downloads/replace_gps_jun.py').read_text())
for node in walk(tree):
if not isinstance(node, Call) or getattr(node.func, 'attr', None) != 'connect':
continue
values = {}
for item in node.keywords:
if isinstance(item, keyword) and isinstance(item.value, Constant):
values[item.arg] = item.value.value
if {'host', 'user', 'password', 'database'} <= values.keys():
return values
raise RuntimeError('未找到既有GPS数据库配置')
wb = openpyxl.load_workbook(OUTPUT)
ws = wb.active
header_row = next(r for r in range(1, 8) if ws.cell(r, 12).value == 'GPS里程(km)')
data_start = header_row + 1
plates = sorted({norm(ws.cell(r, 2).value) for r in range(data_start, ws.max_row + 1) if ws.cell(r, 2).value})
cfg = db_config()
cfg.update(charset='utf8mb4', connect_timeout=8, read_timeout=30, write_timeout=30)
conn = pymysql.connect(**cfg)
try:
with conn.cursor() as cur:
placeholders = ','.join(['%s'] * len(plates))
cur.execute(
f'''SELECT plate_number, dates, total_mileage
FROM ln_vehicle_g7_mileage
WHERE dates >= %s AND dates <= %s
AND plate_number IN ({placeholders})''',
[date(2026, 7, 1), date(2026, 7, 31), *plates],
)
db_rows = cur.fetchall()
finally:
conn.close()
daily = defaultdict(dict)
for plate, day, raw_mileage in db_rows:
if isinstance(day, datetime):
day = day.date()
daily[norm(plate)][day] = float(raw_mileage or 0) / 100000.0
if not BACKUP.exists():
shutil.copy2(OUTPUT, BACKUP)
filled = []
still_missing = []
for row in range(data_start, ws.max_row + 1):
cell = ws.cell(row, 12)
if cell.value != '无来源':
continue
plate = norm(ws.cell(row, 2).value)
start = ws.cell(row, 4).value
end = ws.cell(row, 5).value
if isinstance(start, datetime):
start = start.date()
if isinstance(end, datetime):
end = end.date()
matched = [value for day, value in daily.get(plate, {}).items() if start <= day <= end]
if matched:
cell.value = round(sum(matched), 2)
cell.number_format = '#,##0.0'
filled.append((plate, start, end, cell.value))
else:
still_missing.append((plate, start, end))
ws['A1'] = (
'1. 纯氢里程(km)、纯电里程(km)来源于广州平台统计,TBOX里程来源于车机上报的仪表盘里程统计,'
'GPS里程优先使用两份7月GPS日明细,缺失车辆由GPS数据库每日里程按考核区间回补;测试车辆以7月最终考核源表已确认GPS为准\n'
'2. 广州平台总里程(km)=纯氢里程(km)+纯电里程(km)\n'
'3. 已确认广州平台暂未接入86辆普货车数据;所有GPS来源均无对应记录时标记为“无来源”'
)
wb.save(OUTPUT)
print({
'database_rows': len(db_rows),
'database_plates': len(daily),
'backfilled_records': len(filled),
'remaining_missing_records': len(still_missing),
'remaining_missing_plates': len({x[0] for x in still_missing}),
})
+216
View File
@@ -0,0 +1,216 @@
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from zipfile import ZipFile
from xml.etree import ElementTree as ET
import re
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
BASE = Path("/Users/kkfluous/Downloads")
ASSESSMENT = BASE / "7月核算结果/租赁任务考核_2026年7月.xlsx"
PLATFORM_DIR = BASE / "广州新能源平台_纯氢纯电里程_2026年7月_按日"
GPS_FILES = [
BASE / "里程统计[天][2026-07-01至2026-07-31].xlsx",
BASE / "里程统计[天][2026-07-01至2026-07-31] (1).xlsx",
]
OUTPUT = BASE / "7月核算结果/7月考核车辆多源里程汇总.xlsx"
NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
def normalize_plate(value):
return re.sub(r"\s+", "", str(value or "")).upper()
def parse_gps_file(path):
result = {}
with ZipFile(path) as zf:
root = ET.fromstring(zf.read("xl/worksheets/sheet1.xml"))
for row in root.findall(f".//{NS}row"):
if int(row.get("r")) < 4:
continue
values = {}
for cell in row.findall(f"{NS}c"):
col = re.match(r"[A-Z]+", cell.get("r")).group()
if cell.get("t") == "inlineStr":
values[col] = "".join(t.text or "" for t in cell.findall(f".//{NS}t"))
else:
node = cell.find(f"{NS}v")
values[col] = node.text if node is not None else None
plate = normalize_plate(values.get("H"))
date_text = str(values.get("O") or "").strip()
if not plate or plate == "合计" or not re.fullmatch(r"2026-07-\d{2}", date_text):
continue
result[(plate, datetime.strptime(date_text, "%Y-%m-%d").date())] = float(values.get("P") or 0)
return result
platform = defaultdict(lambda: [0.0, 0.0])
platform_presence = set()
for path in sorted(PLATFORM_DIR.glob("2026-07-??_纯氢纯电里程.xlsx")):
wb = load_workbook(path, read_only=True, data_only=True)
ws = wb.active
for row in ws.iter_rows(min_row=2, values_only=True):
plate = normalize_plate(row[5])
date_match = re.search(r"2026-07-\d{2}", str(row[6] or ""))
if not plate or not date_match:
continue
date = datetime.strptime(date_match.group(), "%Y-%m-%d").date()
electric = float(row[7] or 0)
hydrogen = float(row[8] or 0)
platform[(plate, date)][0] += hydrogen
platform[(plate, date)][1] += electric
platform_presence.add((plate, date))
wb.close()
gps = {}
for path in GPS_FILES:
incoming = parse_gps_file(path)
overlap = set(gps) & set(incoming)
if overlap:
raise RuntimeError(f"GPS来源存在重复车辆日期:{sorted(overlap)[:5]}")
gps.update(incoming)
source_wb = load_workbook(ASSESSMENT, read_only=True, data_only=True)
source_ws = source_wb["业务考核视图"]
headers = [c.value for c in source_ws[1]]
idx = {name: i for i, name in enumerate(headers)}
records = []
for row in source_ws.iter_rows(min_row=2, values_only=True):
plate = normalize_plate(row[idx["车牌号"]])
if not plate:
continue
start = row[idx["考核开始日期"]].date()
end = row[idx["考核结束日期"]].date()
dates = [(start.fromordinal(start.toordinal() + i)) for i in range((end - start).days + 1)]
p_dates = [d for d in dates if (plate, d) in platform_presence]
if p_dates:
hydrogen = sum(platform[(plate, d)][0] for d in p_dates)
electric = sum(platform[(plate, d)][1] for d in p_dates)
else:
hydrogen = electric = None
test_mileage = row[idx["测试里程(km)"]]
is_test = isinstance(test_mileage, (int, float)) and test_mileage > 0
g_dates = [d for d in dates if (plate, d) in gps]
gps_total = sum(gps[(plate, d)] for d in g_dates) if g_dates else None
# 7月最终考核源表的“实际行驶里程”已按原处理链路写入测试车辆GPS里程:
# 优先智能管车日报,缺失时用两份GPS日明细补充。补充明细未命中时,
# 必须从最终源表回填,不能误标为“无来源”。
if gps_total is None and is_test:
gps_total = row[idx["实际行驶里程(km)"]]
records.append({
"plate": plate,
"customer": row[idx["客户名称"]] or "",
"start": start,
"end": end,
"days": (end - start).days + 1,
"test": "" if is_test else "",
"hydrogen": hydrogen,
"electric": electric,
"tbox": row[idx["原TBOX里程(km)"]] if row[idx["原TBOX里程(km)"]] is not None else None,
"gps": gps_total,
})
source_wb.close()
wb = Workbook()
ws = wb.active
ws.title = "7月考核车辆里程汇总"
columns = [
"序号", "车牌", "客户", "考核开始日期", "考核结束日期", "考核天数", "是否存在测试",
"纯氢里程(km)", "纯电里程(km)", "广州平台总里程(km)", "TBOX里程(km)", "GPS里程(km)",
]
ws.append(columns)
for n, rec in enumerate(records, 1):
row_num = ws.max_row + 1
ws.append([
n, rec["plate"], rec["customer"], rec["start"], rec["end"], rec["days"], rec["test"],
rec["hydrogen"] if rec["hydrogen"] is not None else "无来源",
rec["electric"] if rec["electric"] is not None else "无来源",
round(rec["hydrogen"] + rec["electric"], 3) if rec["hydrogen"] is not None else "无来源",
rec["tbox"] if rec["tbox"] is not None else "无来源",
rec["gps"] if rec["gps"] is not None else "无来源",
])
ws.insert_rows(1)
ws.merge_cells("A1:L1")
ws["A1"] = (
"1. 纯氢里程(km)、纯电里程(km)来源于广州平台统计,TBOX里程来源于车机上报的仪表盘里程统计,"
"GPS里程优先按两份7月GPS日明细汇总;测试车辆未命中时,回填7月最终考核源表中已确认的GPS实际里程\n"
"2. 广州平台总里程(km)=纯氢里程(km)+纯电里程(km)\n"
"3. 已确认广州平台暂未接入86辆普货车数据;无对应记录统一标记为“无来源”"
)
header_fill = PatternFill("solid", fgColor="1F4E78")
platform_fill = PatternFill("solid", fgColor="DDEBF7")
tbox_fill = PatternFill("solid", fgColor="E2F0D9")
gps_fill = PatternFill("solid", fgColor="E4DFEC")
test_fill = PatternFill("solid", fgColor="FFF2CC")
missing_fill = PatternFill("solid", fgColor="F2F2F2")
thin = Side(style="thin", color="B7B7B7")
for cell in ws[2]:
cell.fill = header_fill
cell.font = Font(color="FFFFFF", bold=True)
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = Border(bottom=Side(style="medium", color="FFFFFF"))
for row in ws.iter_rows(min_row=3, max_row=ws.max_row):
is_test = row[6].value == ""
for cell in row:
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
cell.alignment = Alignment(horizontal="center", vertical="center")
row[2].alignment = Alignment(horizontal="left", vertical="center")
if is_test:
for cell in row:
cell.fill = test_fill
else:
for cell in row[7:10]:
cell.fill = platform_fill
row[10].fill = tbox_fill
row[11].fill = gps_fill
for cell in row[7:12]:
if cell.value == "无来源":
cell.fill = missing_fill
cell.font = Font(color="7F7F7F", italic=True)
for row in range(3, ws.max_row + 1):
ws.cell(row, 4).number_format = "yyyy-mm-dd"
ws.cell(row, 5).number_format = "yyyy-mm-dd"
for col in range(8, 13):
ws.cell(row, col).number_format = "#,##0.0"
widths = [8, 14, 34, 15, 15, 11, 14, 17, 17, 23, 17, 17]
for col, width in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(col)].width = width
ws["A1"].fill = PatternFill("solid", fgColor="FFF2CC")
ws["A1"].font = Font(color="7F6000", italic=True)
ws["A1"].alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
ws.row_dimensions[1].height = 58
ws.row_dimensions[2].height = 36
ws.freeze_panes = "A3"
ws.auto_filter.ref = f"A2:L{ws.max_row}"
ws.sheet_view.showGridLines = False
ws.sheet_properties.pageSetUpPr.fitToPage = True
ws.page_setup.orientation = "landscape"
ws.page_setup.fitToWidth = 1
ws.page_setup.fitToHeight = 0
ws.print_title_rows = "1:2"
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
wb.save(OUTPUT)
print({
"output": str(OUTPUT),
"records": len(records),
"test_records": sum(r["test"] == "" for r in records),
"platform_missing": sum(r["hydrogen"] is None for r in records),
"tbox_missing": sum(r["tbox"] is None for r in records),
"gps_missing": sum(r["gps"] is None for r in records),
})
+153 -124
View File
@@ -2,15 +2,22 @@
import openpyxl
from collections import defaultdict
# RULES: 18T用(1000/0/1000/1000), 非18T保持原逻辑(未达标不发, 结转补发=原奖励额)
RULES = {
'交投40辆4.5T普货': {'km': 3000, '': 150},
'交投190辆4.5T冷链车': {'km': 3000, '': 150},
'羚牛136辆4.5T冷链车': {'km': 5000, '': 260},
'恒运50辆4.5T普货': {'km': 5000, '': 260},
'恒运50辆4.5T 普货': {'km': 5000, '': 260},
'羚牛100辆18T': {'km': 6000, '': 1000},
'交投40辆4.5T普货': {'km': 3000, 'good': 150, 'bad': 0, 'carry': 150, 'other': 150},
'交投190辆4.5T冷链车': {'km': 3000, 'good': 150, 'bad': 0, 'carry': 150, 'other': 150},
'羚牛136辆4.5T冷链车': {'km': 5000, 'good': 260, 'bad': 0, 'carry': 260, 'other': 260},
'恒运50辆4.5T普货': {'km': 5000, 'good': 260, 'bad': 0, 'carry': 260, 'other': 260},
'恒运50辆4.5T 普货': {'km': 5000, 'good': 260, 'bad': 0, 'carry': 260, 'other': 260},
'羚牛100辆18T': {'km': 6000, 'good': 1000, 'bad': 0, 'carry': 1000, 'other': 1000},
}
DAYS = {1: 31, 2: 28, 3: 31}
# 2026年7月经业务确认的4辆18T执行下半年新政策:
# 月目标5000公里,达标150元,未达标不发,结转/补发按150元基准。
# 其余18T继续使用上面的原口径。
JULY_SCHEME3_PLATES = {'粤AGR0772', '粤AGQ0178', '粤AGP6675', '粤AGP9757'}
JULY_SCHEME3_RULE = {'km': 5000, 'good': 150, 'bad': 0, 'carry': 150, 'other': 150}
DAYS = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
def read_file(fp, month):
wb = openpyxl.load_workbook(fp, data_only=True)
@@ -23,13 +30,22 @@ def read_file(fp, month):
r['应考核里程(km)'] = float(r.get('应考核里程(km)') or 0)
r['实际行驶里程(km)'] = float(r.get('实际行驶里程(km)') or 0)
r['月份'] = month
rule = RULES.get(r.get('考核目标', ''), {})
if month == 7 and str(r.get('车牌号') or '').strip().upper() in JULY_SCHEME3_PLATES:
rule = JULY_SCHEME3_RULE
else:
rule = RULES.get(r.get('考核目标', ''), {'km': 0, 'good': 0, 'bad': 0, 'carry': 0, 'other': 0})
r['月度目标里程'] = rule.get('km', 0)
r['对应月奖励金额'] = rule.get('', 0)
r['奖励基准_达标'] = rule.get('good', 0)
r['奖励基准_未达标'] = rule.get('bad', 0)
r['奖励基准_结转'] = rule.get('carry', 0)
r['奖励基准_补发'] = rule.get('other', 0)
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
r['奖金'] = r['对应月奖励金额'] * (r['考核天数'] / DAYS[month]) if q else 0
if q:
r['奖金'] = r['奖励基准_达标'] * (r['考核天数'] / DAYS[month])
else:
r['奖金'] = r['奖励基准_未达标'] * (r['考核天数'] / DAYS[month])
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // r['月度目标里程']) if r['月度目标里程'] > 0 and q else 0
recs.append(r)
@@ -38,7 +54,8 @@ def read_file(fp, month):
def grp(recs, month):
gs = defaultdict(lambda: {'recs':[], '应考核':0, '实际':0, '奖金':0, '天数':0,
'有达标':False, '目标km':0, '奖励':0})
'有达标':False, '目标km':0, '奖励达标':0, '奖励未达标':0,
'奖励结转':0, '奖励补发':0})
for r in recs:
k = (r['车牌号'], r['销售经理'])
g = gs[k]
@@ -49,7 +66,10 @@ def grp(recs, month):
g['天数'] += r['考核天数']
if r['是否达标'] == '达标': g['有达标'] = True
g['目标km'] = max(g['目标km'], r['月度目标里程'])
g['奖励'] = max(g['奖励'], r['对应月奖励金额'])
g['奖励达标'] = max(g['奖励达标'], r['奖励基准_达标'])
g['奖励未达标'] = max(g['奖励未达标'], r['奖励基准_未达标'])
g['奖励结转'] = max(g['奖励结转'], r['奖励基准_结转'])
g['奖励补发'] = max(g['奖励补发'], r['奖励基准_补发'])
g['部门'] = r['部门名称']
g['销售'] = r['销售经理']
g['车牌'] = r['车牌号']
@@ -61,136 +81,142 @@ def calc_jan_carryover(G1):
for r in g['recs']:
if r['是否达标'] == '达标': tc += r['可结转']
g['可结转'] = tc
g['累计已发结转'] = 0
g['1月已发'] = g['有达标'] # 当月有达标 → 已发
g['cum_t'] = g['应考核']
g['cum_a'] = g['实际']
g['cum_q'] = g['实际'] >= g['应考核'] and g['应考核'] > 0
def calc_feb(G1, G2):
feb_data = {'结转':[], '补发1月':[], '当月':[], '累计补发2月':[]}
for k, g2 in G2.items():
g1 = G1.get(k)
bf = g2['奖励额']
j_t = g1['应考核'] if g1 else 0; j_a = g1['实际'] if g1 else 0
j_q = g1['达标'] if g1 else False; j_c = g1['可结转'] if g1 else 0
cum_t = j_t + g2['应考核']; cum_a = j_a + g2['实际']
def calc_month(month, G_all, all_data, G_prev, G_cur):
"""通用月计算: 结转/补发/累计补发/当月"""
prev_m = month - 1
days_m = DAYS[month]
data_m = {'结转': [], '当月': []}
# 向后兼容的key: 当月达标/当月未达标 用于18T区分
data_18t = {'当月达标': [], '当月未达标': []}
use_18t = True # 统一使用拆分标签
for k, gm in G_cur.items():
gp = G_prev.get(k)
# 累计
cum_t = (gp['cum_t'] if gp else 0) + gm['应考核']
cum_a = (gp['cum_a'] if gp else 0) + gm['实际']
cum_q = cum_a >= cum_t and cum_t > 0
# 结转: carry × 1 (整月, 不折算)
carry = 0
if g1 and j_q and j_c >= 1:
carry = bf
feb_data['结转'].append({'车牌':k[0],'销售':g1['销售'],'部门':g2['部门'],'':carry})
bp1 = 0
if g1 and not j_q and cum_q:
bp1 = g1['奖励额'] * (g1['天数'] / 31)
feb_data['补发1月'].append({'车牌':k[0],'销售':g1['销售'],'部门':g2['部门'],'':bp1})
bonus2 = 0
if g2['有达标'] and carry == 0:
bonus2 = g2['奖金']
feb_data['当月'].append({'车牌':k[0],'销售':g2['销售'],'部门':g2['部门'],'':bonus2})
cbp2 = 0
if not g2['有达标'] and carry == 0 and cum_q:
cbp2 = g2['奖励额'] * (g2['天数'] / 28)
feb_data['累计补发2月'].append({'车牌':k[0],'销售':g2['销售'],'部门':g2['部门'],'':cbp2})
p_carry = gp['可结转'] if gp else 0
if gp and p_carry >= 1:
carry = gm['奖励结转']
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gm['部门'], '': carry})
g2['cum_t']=cum_t; g2['cum_a']=cum_a; g2['cum_q']=cum_q
g2['结转']=carry; g2['补发1月']=bp1
g2['补发1月对应']=g1['销售'] if g1 and bp1>0 else ''
g2['当月奖金']=bonus2; g2['累计补发2月']=cbp2; g2['结转占位']=carry>0
fc = sum(r['可结转'] for r in g2['recs'] if r['是否达标']=='达标')
jr = max(0, j_c - (1 if carry>0 else 0))
g2['可结转'] = fc + jr
g2['2月已发'] = carry>0 or bonus2>0 or cbp2>0
g2['1月已补发'] = bp1>0
# 当月奖金
is_18t = gm['奖励达标'] == 1000 # 18T特征
bonus = 0
if carry == 0 and gm['奖金'] > 0:
bonus = gm['奖金']
data_m['当月'].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '': bonus})
# 补充:1月有结转但2月无考核记录的车 → 创建虚拟2月group并发放结转
for k, g1 in G1.items():
if k not in G2 and g1['可结转'] >= 1:
bf = g1['奖励额']
carry = bf
feb_data['结转'].append({'车牌':k[0],'销售':g1['销售'],'部门':g1['部门'],'':carry})
# 创建虚拟2月group(考核里程=满月目标,实际=0)
G2[k] = {
'recs':[], '应考核':g1['目标km'], '实际':0, '奖金':0, '天数':DAYS[2],
'有达标':False, '目标km':g1['目标km'], '奖励额':g1['奖励额'],
'部门':g1['部门'], '销售':g1['销售'], '车牌':k[0],
'cum_t':g1['应考核']+g1['目标km'], 'cum_a':g1['实际'],
'cum_q': g1['实际'] >= g1['应考核']+g1['目标km'],
'结转':carry, '补发1月':0, '补发1月对应':'',
'当月奖金':0, '累计补发2':0, '结转占位':True,
'可结转': max(0, g1['可结转'] - 1),
'2月已发':True, '1月已补发':False,
'虚拟':True, # 标记为无考核记录
}
return feb_data
# 补发历史月: other × 折算
for back_m in range(1, month):
label = f'补发{back_m}'
if label not in data_m:
data_m[label] = []
g_bm = G_all[back_m].get(k)
if not g_bm:
continue
bm_paid = g_bm.get(f'{back_m}月已发', g_bm['有达标'])
if bm_paid:
continue
bm_paid_later = False
for later_m in range(back_m + 1, month):
gl = G_all[later_m].get(k)
if gl and gl.get(f'补发{back_m}', 0) > 0:
bm_paid_later = True
break
if bm_paid_later:
continue
if cum_q:
bj = g_bm['奖励补发'] * (g_bm['天数'] / DAYS[back_m])
data_m[label].append({'车牌': k[0], '销售': g_bm['销售'], '部门': gm['部门'], '': bj})
gm[f'补发{back_m}'] = bj
def calc_mar(G1, G2, G3, feb_data):
mar_data = {'结转':[], '补发1月':[], '补发2月':[], '当月':[], '累计补发3月':[]}
for k, g3 in G3.items():
g1 = G1.get(k); g2 = G2.get(k)
bf = g3['奖励额']
j_t=g1['应考核'] if g1 else 0; j_a=g1['实际'] if g1 else 0
j_q=g1['有达标'] if g1 else False; j_paid=(g1['奖金']>0) if g1 else False
f_t=g2['应考核'] if g2 else 0; f_a=g2['实际'] if g2 else 0
f_paid=g2['2月已发'] if g2 else False; f_carry=g2['可结转'] if g2 else 0
if g1 and j_q: j_paid=True
if g2 and g2.get('1月已补发',False): j_paid=True
cum_t=j_t+f_t+g3['应考核']; cum_a=j_a+f_a+g3['实际']
cum_q=cum_a>=cum_t and cum_t>0
# 累计补发当月: other × 折算
label_cbp = f'累计补发{month}'
if label_cbp not in data_m:
data_m[label_cbp] = []
cbp = 0
if not gm['有达标'] and carry == 0 and cum_q:
cbp = gm['奖励补发'] * (gm['天数'] / days_m)
data_m[label_cbp].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '': cbp})
carry=0
if g2 and f_carry>=1:
carry=bf; mar_data['结转'].append({'车牌':k[0],'销售':g2['销售'],'部门':g3['部门'],'':carry})
bj=0
if g1 and not j_paid and cum_q:
bj=g1['奖励额']*(g1['天数']/31); mar_data['补发1月'].append({'车牌':k[0],'销售':g1['销售'],'部门':g3['部门'],'':bj})
bf2=0
if g2 and not f_paid and cum_q:
bf2=g2['奖励额']*(g2['天数']/28); mar_data['补发2月'].append({'车牌':k[0],'销售':g2['销售'],'部门':g3['部门'],'':bf2})
bonus3=0
if g3['有达标'] and carry==0:
bonus3=g3['奖金']; mar_data['当月'].append({'车牌':k[0],'销售':g3['销售'],'部门':g3['部门'],'':bonus3})
cbp3=0
if not g3['有达标'] and carry==0 and cum_q:
cbp3=g3['奖励额']*(g3['天数']/31); mar_data['累计补发3月'].append({'车牌':k[0],'销售':g3['销售'],'部门':g3['部门'],'':cbp3})
gm['cum_t'] = cum_t
gm['cum_a'] = cum_a
gm['cum_q'] = cum_q
gm['结转'] = carry
gm['当月奖金'] = bonus
gm['累计补发当月'] = cbp
gm[f'{month}月已发'] = carry > 0 or gm['奖金'] > 0 or cbp > 0
g3['cum_t']=cum_t; g3['cum_a']=cum_a; g3['cum_q']=cum_q
g3['结转']=carry; g3['补发1月']=bj; g3['补发1月对应']=g1['销售'] if g1 and bj>0 else ''
g3['补发2月']=bf2; g3['补发2月对应']=g2['销售'] if g2 and bf2>0 else ''
g3['当月奖金']=bonus3; g3['累计补发3月']=cbp3; g3['结转占位']=carry>0
# 更新可结转
cum_over = max(0, cum_a - cum_t) if cum_q else 0
earned = int(cum_over // gm['目标km']) if gm['目标km'] > 0 else 0
prev_used = gp['累计已发结转'] if gp else 0
cur_used = prev_used + (1 if carry > 0 else 0)
gm['累计已发结转'] = cur_used
gm['可结转'] = max(0, earned - cur_used)
# 补充2月有结转但3月无考核记录的车
for k, g2 in G2.items():
if k not in G3 and g2.get('可结转', 0) >= 1:
bf = g2['奖励']
carry = bf
mar_data['结转'].append({'车牌':k[0],'销售':g2['销售'],'部门':g2['部门'],'':carry})
G3[k] = {
'recs':[], '应考核':g2['目标km'], '实际':0, '奖金':0, '天数':DAYS[3],
'达标':False, '目标km':g2['目标km'], '奖励':g2['奖励'],
'部门':g2['部门'], '销售':g2['销售'], '车牌':k[0],
'cum_t':(G1.get(k,{}).get('应考核',0))+g2['应考核']+g2['目标km'],
'cum_a':(G1.get(k,{}).get('实际',0))+g2['实际'],
# 补充: 上月有结转但月无考核记录的车
for k, gp in G_prev.items():
if k not in G_cur and gp.get('可结转', 0) >= 1:
carry = gp['奖励结转']
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gp['部门'], '': carry})
G_cur[k] = {
'recs': [], '应考核': gp['目标km'], '实际': 0, '奖金': 0, '天数': days_m,
'有达标': False, '目标km': gp['目标km'],
'奖励达标': gp['奖励达标'], '奖励未达标': gp['奖励未达标'],
'奖励结转': gp['奖励结转'], '奖励补发': gp['奖励补发'],
'部门': gp['部门'], '销售': gp['销售'], '车牌': k[0],
'cum_t': gp.get('cum_t', 0) + gp['目标km'], 'cum_a': gp.get('cum_a', 0),
'cum_q': False,
'结转':carry, '补发1月':0, '补发1月对应':'',
'补发2月':0, '补发2月对应':'',
'当月奖金':0, '累计补发3月':0, '结转占位':True,
'结转': carry, '当月奖金': 0,
f'{month}月已发': True,
'可结转': max(0, gp['结转'] - 1),
'累计已发结转': gp.get('累计已发结转', 0) + 1,
'虚拟': True,
}
return mar_data
def collect_vehicle_payments(G, feb_data, mar_data):
all_data[month] = data_m
return data_m, G_cur
def collect_vehicle_payments(G, all_data):
"""收集所有月的付款记录"""
payments = defaultdict(list)
info = {}
# 1月
for k, g in G[1].items():
plate = k[0]
if not info.get(plate):
first = g['recs'][0]
info[plate] = {'考核目标':first.get('考核目标',''),'月度奖励':g['奖励'],'目标km':g['目标km']}
info[plate] = {'考核目标': first.get('考核目标', ''), '月度奖励': g['奖励达标'], '目标km': g['目标km']}
if g['奖金'] > 0:
payments[plate].append({'结算月':1,'对应考核月':1,'业务员':g['销售'],'金额':g['奖金'],'类型':'当月达标','部门':g['部门']})
payments[plate].append({'结算月': 1, '对应考核月': 1, '业务员': g['销售'],
'金额': g['奖金'], '类型': '当月达标', '部门': g['部门']})
cat_map = {
('结转',2):2, ('补发1月',2):1, ('当月',2):2, ('累计补发2月',2):2,
('结转',3):3, ('补发1月',3):1, ('补发2月',3):2, ('当月',3):3, ('累计补发3月',3):3,
}
for month, mdata in [(2, feb_data), (3, mar_data)]:
# 2月-7月
cat_map = {}
for month in range(2, 8):
cat_map[('结转', month)] = month
for bm in range(1, month):
cat_map[(f'补发{bm}', month)] = bm
cat_map[('当月', month)] = month
cat_map[(f'累计补发{month}', month)] = month
for month in range(2, 8):
mdata = all_data.get(month, {})
for cat, dl in mdata.items():
assess_month = cat_map.get((cat, month), month)
for d in dl:
@@ -200,14 +226,16 @@ def collect_vehicle_payments(G, feb_data, mar_data):
for m in G:
for k, g in G[m].items():
if not info.get(k[0]):
first = g['recs'][0]
info[k[0]] = {'考核目标':first.get('考核目标',''),'月度奖励':g['奖励额'],'目标km':g['目标km']}
first = g['recs'][0] if g['recs'] else None
if first:
info[k[0]] = {'考核目标': first.get('考核目标', ''), '月度奖励': g['奖励达标'], '目标km': g['目标km']}
for plate in payments:
records = sorted(payments[plate], key=lambda x: (x['对应考核月'], x['结算月']))
for i, r in enumerate(records): r['期数'] = i + 1
return payments, info
# (归属公司+车型) → 考核目标 映射,用于补全无考核记录的车辆
VEHICLE_TARGET_MAP = {
('广州开发区交投氢能运营管理有限公司', '4.5吨冷链车'): ('交投190辆4.5T冷链车', 3000, 150),
@@ -218,6 +246,7 @@ VEHICLE_TARGET_MAP = {
('现代氢能科技(广州)有限公司', '4.5吨货车'): ('恒运50辆4.5T普货', 5000, 260),
}
def read_loss_data(month):
"""读取亏损表,返回 {客户名称: ''/''} 字典。无亏损表返回None"""
import os
@@ -229,8 +258,6 @@ def read_loss_data(month):
ws = wb[wb.sheetnames[0]]
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
# 1月: 列名"项目","1月是否亏损"
# 2月: 列名"客户名称","是否亏损"
client_col = None; loss_col = None
for i, col_name in enumerate(h):
if col_name and ('项目' in str(col_name) or '客户' in str(col_name)):
@@ -251,6 +278,7 @@ def read_loss_data(month):
wb.close()
return result
def get_vehicle_client_map(D):
"""从考核源数据构建 {车牌号: 客户名称} 映射(取最新月的客户名)"""
plate_client = {}
@@ -259,6 +287,7 @@ def get_vehicle_client_map(D):
plate_client[r['车牌号']] = r.get('客户名称', '')
return plate_client
def read_master_vehicles(fp='里程任务考核_Q1汇总.xlsx'):
"""从现有Q1汇总文件读取全量车辆台账"""
wb = openpyxl.load_workbook(fp, data_only=True)
@@ -0,0 +1,549 @@
# 初次建设碳平台:同行交流问题清单
> 适用场景:企业第一次建设碳管理 / 碳资产 / 碳核算平台时,用于和同行、供应商、咨询机构、碳管理团队交流。
---
## 一、先明确:碳平台到底服务谁?
1. 你们做碳平台的第一目标是什么?
- 政府监管?
- 集团管理?
- 客户披露?
- 碳交易履约?
- ESG 报告?
2. 平台一期最先做的是哪块?
- 碳盘查
- 碳核算
- 碳资产
- 碳交易
- 产品碳足迹
- 能源管理
3. 你们一开始有没有做“大而全”?后来哪些模块发现其实用不上?
4. 平台最终是给谁每天使用?
- 安环部
- 能源管理部
- 财务部
- 生产部门
- 集团总部
- 第三方核查机构
5. 你们怎么定义平台成功?
- 减少人工填报?
- 提升核算准确性?
- 支撑履约?
- 形成减排决策?
---
## 二、碳核算边界怎么定?
这是最容易前期没想清、后期返工的地方。
1. 组织边界怎么划?
- 按法人?
- 按厂区?
- 按项目?
- 按车队?
- 按站点?
- 按集团层级?
2. 运营边界怎么划?
- 范围一做到什么程度?
- 范围二做到什么程度?
- 范围三是否纳入?
3. 哪些排放源必须纳入?
-
-
-
-
- 蒸汽
- 制冷剂
- 运输
- 外购服务
- 员工通勤
4. 对于多业务混合场景,怎么拆分碳排放?
- 一个站点服务多个客户,怎么分摊?
- 一个车辆服务多个项目,怎么分摊?
- 一个能源账单对应多个业务单元,怎么分摊?
5. 历史数据从哪一年开始补?
- 只做当前年度?
- 补 3 年?
- 补 5 年?
- 是否需要和历史报告口径一致?
---
## 三、数据来源和数据质量问题
碳平台本质上很大程度是一个数据平台,而不是单纯报表系统。
1. 你们碳数据主要来自哪里?
- ERP
- MES
- SCADA
- 电表系统
- 能源系统
- 财务系统
- 人工 Excel
- 第三方平台
2. 哪些数据最难自动获取?
- 电费
- 燃气
- 柴油
- 蒸汽
- 原材料
- 运输里程
- 供应商数据
3. 你们有没有做原始凭证管理?
- 发票
- 账单
- 抄表照片
- 采购单
- 加油记录
- 加氢记录
4. 数据质量怎么校验?
- 缺失值
- 异常值
- 重复数据
- 跨月修正
- 人工调整留痕
5. 人工填报和系统采集冲突时,以谁为准?
6. 有没有“数据质量评分”?
- 自动采集数据是否优于人工填报?
- 带凭证数据是否优于无凭证数据?
7. 月度数据锁定机制怎么做?
- 锁定后还能不能修改?
- 修改是否需要审批?
- 是否保留修改前后的版本?
---
## 四、核算模型和排放因子怎么维护?
1. 你们核算方法是内置固定模板,还是可配置公式?
2. 排放因子怎么管理?
- 国家因子
- 省级因子
- 行业因子
- 企业自定义因子
3. 排放因子优先级怎么定?
4. 排放因子版本如何追溯?
- 2024 年用了 A 因子,2025 年用了 B 因子,历史结果是否重算?
5. 活动数据和排放因子是否分开存?
- 只存最终碳排放结果,还是保留计算过程?
6. 核算公式有没有审批发布流程?
7. 不同行业是否做了不同模板?
- 电力
- 钢铁
- 水泥
-
- 化工
- 交通运输
- 园区
8. 平台是否支持核算结果解释?
- 能不能点开一条排放数据,看到:来源数据 × 排放因子 × 公式 = 结果?
---
## 五、MRV:监测、报告、核查怎么做?
1. 平台是否覆盖完整 MRV 流程?
- Monitoring:监测
- Reporting:报告
- Verification:核查
2. 第三方核查机构怎么使用平台?
- 给账号直接进系统看?
- 导出材料给核查机构?
- 是否有专门的核查视图?
3. 核查问题如何闭环?
- 核查意见
- 整改记录
- 补充材料
- 版本留痕
4. 是否支持自动生成排放报告?
5. 报告模板是否按行业、地区、监管要求区分?
6. 是否有审计轨迹?
- 谁填的?
- 谁改的?
- 谁审的?
- 什么时候提交的?
7. 是否做了填报任务管理?
- 每月提醒
- 逾期提醒
- 部门催办
- 审批流
---
## 六、碳资产和碳交易怎么设计?
如果涉及全国碳市场或地方碳市场,这块要提前问清楚。
1. 平台是否管理配额?
- 初始配额
- 调整配额
- 已用配额
- 剩余配额
- 履约缺口
2. 是否支持 CCER 或其他减排量资产管理?
3. 是否和交易系统打通?
- 直接交易?
- 只做台账?
- 只做履约测算?
4. 碳价怎么维护?
- 手工录入
- 接口获取
- 财务确认
5. 是否做履约预测?
- 当前排放趋势下,年底预计缺多少配额?
- 是否需要提前采购?
6. 是否支持碳资产成本测算?
- 买配额成本
- 减排改造成本
- 不同策略对比
7. 财务上怎么入账?
- 碳配额
- 碳资产
- 履约成本
- 减排收益
---
## 七、减排分析和业务决策怎么落地?
很多碳平台最后容易变成“填报系统”,所以要重点问同行怎么避免。
1. 平台有没有做减排潜力分析?
2. 是否能按不同维度拆分碳排放?
- 组织
- 站点
- 车辆
- 设备
- 产品
- 客户
- 线路
3. 是否能看单位指标?
- 吨产品碳排
- 百公里碳排
- 单车碳排
- 单位营收碳排
- 单位能耗碳排
4. 是否能识别异常排放?
- 某站点本月电耗异常
- 某车辆能耗异常
- 某条线路碳排异常
5. 是否能支撑减排项目管理?
- 项目立项
- 预计减排量
- 实际减排量
- 投资回收期
6. 有没有做情景预测?
- 电价变化
- 绿电比例提升
- 车辆替换
- 工艺改造
- 能源结构调整
7. 平台输出的数据,业务部门真的会用吗?
- 用于经营分析?
- 用于客户报价?
- 用于 ESG 披露?
- 还是只给领导看大屏?
---
## 八、产品碳足迹是否要做?
产品碳足迹一期不一定要上,但要提前了解是否存在外部压力。
1. 你们有没有做产品碳足迹?
2. 如果做,是按什么标准?
- ISO 14067
- PAS 2050
- 客户自定义规则
- 行业规则
3. 生命周期边界做到哪里?
- cradle-to-gate
- cradle-to-grave
- 只做到企业内部生产环节
4. 原材料供应商数据怎么获取?
5. 供应商不给数据时怎么处理?
- 用行业平均值
- 用数据库因子
- 要求补充证明
- 先人工估算
6. 产品碳足迹和企业碳核算是否共用一套数据底座?
7. 有没有客户、出口、CBAM、ESG 披露等实际压力?
---
## 九、平台架构和系统集成问题
可以结合现有业务系统、数据中台、车辆平台、能源账单系统来交流。
1. 平台是独立建设,还是接入现有数据中台?
2. 主数据怎么定?
- 组织
- 厂区
- 站点
- 车辆
- 设备
- 能源品类
- 供应商
- 客户
3. 是否需要和这些系统打通?
- ERP
- 财务系统
- 电表系统
- 能源管理系统
- 车辆 TBox
- 加氢站系统
- 合同系统
- 采购系统
4. 碳平台的数据粒度是什么?
- 实时
- 日级
- 月级
- 年度
5. 对于碳核算来说,实时数据有没有必要?
- 哪些场景需要实时?
- 哪些场景只需要月度?
6. 是否做数据血缘?
- 一个碳排结果能不能追溯到原始业务单据?
7. 权限怎么设计?
- 集团
- 子公司
- 厂区
- 部门
- 第三方核查机构
8. 是否支持多租户?
- 如果未来给多个客户、园区、站点使用,架构能不能撑住?
---
## 十、实施踩坑问题
这些问题最容易问出真实经验。
1. 第一期最不该做的功能是什么?
2. 最大的坑是技术问题、数据问题,还是组织协同问题?
3. 哪些数据一开始以为有,后来发现没有?
4. 哪些数据口径最容易产生争议?
- 财务账单
- 抄表数
- 生产系统数
- 业务系统数
5. 平台上线后,谁负责持续维护?
- 排放因子
- 核算公式
- 组织架构
- 数据质量
- 报告模板
6. 有没有专门的碳管理岗位?
- 安环负责?
- 财务负责?
- 能源部门负责?
- 信息化负责?
- 多部门共同维护?
7. 你们上线用了多久?
- 试点范围多大?
- 先选一个厂区?
- 先选一个园区?
- 还是集团全铺?
8. 系统上线后,人工 Excel 有没有真的减少?
9. 做平台前有没有先做碳盘查咨询?
- 咨询成果怎么转成系统需求?
10. 平台建设是 IT 主导,还是碳管理部门主导?最终谁说了算?
---
## 十一、结合车辆 / 加氢 / 能源业务的重点追问方向
如果企业业务涉及车辆运营、加氢站、能源账单、TBox、物流轨迹,可以重点问以下方向。
### 1. 交通 / 车队碳核算
1. 车辆碳排是按燃料消耗核算,还是按里程和车型因子估算?
2. 新能源 / 氢能车辆怎么计算减排量?
3. 车辆、司机、客户、线路之间的碳排怎么分摊?
4. 空驶、异常行驶、运营行驶是否区分?
5. 有没有把 TBox 里程、加氢订单、ETC、物流单结合起来做碳排核算?
6. 车辆维度是否支持以下指标?
- 单车碳排
- 百公里碳排
- 单趟运输碳排
- 客户维度碳排
- 线路维度碳排
### 2. 加氢站 / 能源侧碳数据
1. 加氢站的电耗、氢源、压缩、储运环节碳排怎么核算?
2. 不同氢源的排放因子怎么维护?
- 灰氢
- 蓝氢
- 绿氢
- 外购氢
3. 站点级碳排是否能分摊到车辆或客户?
4. 是否记录每笔加氢订单对应的碳因子?
5. 氢气采购、运输、站内损耗是否纳入?
6. 站点电耗与加氢量之间是否做能效分析?
### 3. 碳平台与业务系统融合
1. 碳平台是单独填报,还是从业务系统自动取数?
2. 如果业务系统数据不完整,碳平台是反向推动业务系统补数据,还是允许人工补录?
3. 碳数据能不能回流业务?
- 客户账单
- 绿色运力证明
- ESG 报告
- 项目报价
- 客户披露材料
4. 有没有把碳排放作为业务 KPI
5. 碳平台是否能和车辆资产、合同、订单、账单系统联动?
---
## 十二、可直接带去交流的高质量问题清单
下面这组问题适合在同行交流会上直接使用。
1. 你们碳平台一期的最小可用范围是什么?
2. 组织边界、运营边界、核算边界是谁定的?
3. 数据主要来自系统接口还是人工填报?比例大概多少?
4. 活动数据、排放因子、核算公式是否都能版本追溯?
5. 核算结果能否追溯到原始凭证?
6. 月度数据有没有锁定、审批、修改留痕机制?
7. 第三方核查机构怎么使用平台?
8. 平台是否支持全国碳市场履约和配额管理?
9. 是否做减排预测,而不仅是排放统计?
10. 上线后,哪些功能使用频率最高?哪些功能基本没人用?
11. 最大的实施难点是数据、口径、组织协同,还是系统集成?
12. 如果重做一次,你们第一期会砍掉哪些功能?
13. 你们的排放因子、公式、报告模板由谁维护?
14. 碳平台上线后,有没有真正减少人工 Excel?
15. 平台输出的数据是否参与经营决策?还是主要用于报告和展示?
---
## 十三、交流时建议关注的判断标准
和同行交流时,不建议只问“有什么功能”,而要重点判断这些问题:
1. 数据是否可追溯
- 能否从碳排结果追到原始单据、原始读数、原始凭证。
2. 口径是否可解释
- 每个排放结果是否能说明怎么算出来的。
3. 过程是否可核查
- 是否有审批、留痕、版本、附件、核查意见闭环。
4. 结果是否可使用
- 是否真正服务履约、减排、经营、客户披露,而不是只做大屏。
5. 平台是否可持续维护
- 因子、公式、组织、模板、权限、数据源变化后,是否能低成本维护。
---
## 十四、总结建议
第一次做碳平台,不要先问:
> 系统有什么功能?
而要先问:
> 数据从哪里来?口径谁确认?结果给谁用?能不能被核查?上线后谁维护?
碳平台的核心不是大屏,而是形成一套:
> 可追溯、可核查、可解释、可持续维护的数据闭环。
+52 -7
View File
@@ -61,22 +61,32 @@ def write_total(ws, rn, month, all_data):
def write_rules_sheet(wb):
ws = wb.active; ws.title='考核奖励规则'
WH(ws,['考核目标','月度目标里程(km)','奖励金额(元)'])
for i,(n,km,b) in enumerate([('交投40辆4.5T普货',3000,150),('交投190辆4.5T冷链车',3000,150),
('羚牛136辆4.5T冷链车',5000,260),('恒运50辆4.5T普货',5000,260),('羚牛100辆18T',6000,1000)],2):
WR(ws,i,[n,km,b])
WH(ws,['考核目标','月度目标里程(km)','当月达标','当月未达标','结转','补发/累计补发'])
for i,(n,km,g,b,c,o,is18t) in enumerate([
('交投40辆4.5T普货',3000,150,0,150,150,False),
('交投190辆4.5T冷链车',3000,150,0,150,150,False),
('羚牛136辆4.5T冷链车',5000,260,0,260,260,False),
('恒运50辆4.5T普货',5000,260,0,260,260,False),
('羚牛100辆18T',6000,1000,0,1000,1000,True),
('2026年7月指定4辆18T',5000,150,0,150,150,True),
],2):
WR(ws,i,[n,km,
f'{g}元×天数折算',
f'{b}元×天数折算' if is18t else '不发',
f'{c}元整月',
f'{o}元×折算'])
AW(ws)
def write_detail_sheet(wb, records, month):
ws = wb.create_sheet(f'里程明细{month}')
WH(ws,['车牌号','部门名称','销售经理','客户名称','合同编号','考核目标','月度目标里程','对应月奖励金额',
WH(ws,['车牌号','部门名称','销售经理','客户名称','合同编号','考核目标','月度目标里程','月奖励金额',
'考核天数',f'{month}月应考核里程',f'{month}月实际行驶里程','完成率',f'{month}月是否达标',
f'{month}月奖金(天数折算)','多跑里程','可结转月数','考核状态'])
rn=2
for r in sorted(records, key=lambda x: (x['销售经理'],x['车牌号'])):
t,a = r['应考核里程(km)'], r['实际行驶里程(km)']
WR(ws,rn,[r['车牌号'],r['部门名称'],r['销售经理'],r.get('客户名称',''),r.get('合同编号',''),
r.get('考核目标',''),r['月度目标里程'],r['对应月奖励金额'],r['考核天数'],
r.get('考核目标',''),r['月度目标里程'],r['奖励基准_达标'],r['考核天数'],
R(t),R(a),R(a/t,4) if t>0 else 0,r['是否达标'],R(r['奖金']),R(r['多跑']),r['可结转'],
r.get('考核状态','')]); rn+=1
AW(ws)
@@ -162,6 +172,41 @@ def write_calc_process_mar(wb, G1, G2, G3, feb_data):
c5,R(cbp3) if cbp3>0 else 0,R(total) if total>0 else 0]); rn+=1
AW(ws)
def write_calc_process_generic(wb, month, G_all, all_data):
"""通用计算过程sheet,支持1-6月"""
G_cur = G_all[month]
headers = ['车牌号','销售经理','部门',
f'{month}月记录数',f'{month}月总天数',f'{month}月应考核',f'{month}月实际',f'{month}月有达标',f'{month}月奖金',
'累计应考核','累计实际','累计达标','可结转','结转发放']
headers += [f'补发{m}' for m in range(1, month)]
headers += ['当月奖金', f'累计补发{month}', f'{month}月发放合计']
ws = wb.create_sheet(f'{month}月计算过程')
WH(ws, headers)
rn = 2
for k in sorted(G_cur.keys(), key=lambda x: (x[1], x[0])):
g = G_cur[k]
if g.get('虚拟'):
continue
cum_q_str = '达标' if g.get('cum_q') else '未达标'
dq_str = '' if g['有达标'] else ''
carry = g.get('结转', 0)
bonus = g.get('当月奖金', 0)
cbp = g.get('累计补发当月', 0)
back_pays = [g.get(f'补发{m}', 0) for m in range(1, month)]
total = carry + bonus + cbp + sum(back_pays)
row = [k[0], k[1], g['部门'],
len(g['recs']), g['天数'], R(g['应考核']), R(g['实际']), dq_str, R(g['奖金']),
R(g.get('cum_t', 0)), R(g.get('cum_a', 0)), cum_q_str,
g.get('可结转', 0), R(carry)]
row += [R(bp) for bp in back_pays]
row += [R(bonus), R(cbp), R(total)]
WR(ws, rn, row)
rn += 1
AW(ws)
def write_summary_jan(wb, records, loss_data=None, plate_client=None):
ws = wb.create_sheet('1月汇总')
jan_dl=[{'车牌':r['车牌号'],'销售':r['销售经理'],'部门':r['部门名称'],'':r['奖金']} for r in records if r['是否达标']=='达标']
@@ -452,7 +497,7 @@ def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, ve
mkm = g_cur['目标km']
# 第1行:车辆信息 + 各月里程/目标
row = [plate, first.get('考核目标',''), g_cur['奖励']]
row = [plate, first.get('考核目标',''), g_cur['奖励达标']]
cum_t = 0; cum_a = 0
for m in range(1, settle_month+1):
gm = G.get(m, {}).get((plate, person))
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""复刻6月干预说明配色,并将达标分类改为一行一人。"""
from copy import copy
from collections import defaultdict
from pathlib import Path
import openpyxl
BASE = Path('/Users/kkfluous/Downloads')
JUL = BASE / '7月核算结果/租赁任务考核_2026年7月_干预情况说明.xlsx'
JUN = BASE / '6月核算结果/租赁任务考核_2026年6月_干预情况说明.xlsx'
SRC = BASE / '7月核算结果/租赁任务考核_2026年7月.xlsx'
BAK = BASE / '7月核算结果/租赁任务考核_2026年7月_原版备份.xlsx'
TEST = Path('/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-08/4340099467fbf08b4c6583c49adba4ee/7月测试车辆明细.xlsx')
def recs(path):
wb = openpyxl.load_workbook(path, data_only=True)
ws = wb['业务考核视图']
h = [c.value for c in ws[1]]
out = [dict(zip(h, r)) for r in ws.iter_rows(min_row=2, values_only=True) if r[0]]
wb.close()
return out
def clone_style(src, dst):
dst.font = copy(src.font)
dst.fill = copy(src.fill)
dst.border = copy(src.border)
dst.alignment = copy(src.alignment)
dst.protection = copy(src.protection)
dst.number_format = src.number_format
def style_row(ws, row, cols, ref_ws, ref_row, ref_cols=None):
ref_cols = ref_cols or cols
for c, rc in zip(cols, ref_cols):
clone_style(ref_ws.cell(ref_row, rc), ws.cell(row, c))
new = recs(SRC)
old = recs(BAK)
old_map = {}
for r in old:
old_map[r['车牌号']] = r
twb = openpyxl.load_workbook(TEST, data_only=True)
test_plates = {str(r[0]).strip() for r in twb.active.iter_rows(min_row=2, values_only=True) if r[0]}
test_plates.discard('粤AG37316')
twb.close()
intervention = [r for r in new if r['车牌号'] in test_plates]
flip = []
for r in intervention:
old_r = old_map.get(r['车牌号'])
if old_r and old_r.get('是否达标') == '达标' and r.get('是否达标') != '达标':
flip.append(r)
groups = defaultdict(lambda: defaultdict(int))
for r in flip:
groups[r.get('考核目标') or '未知'][(r.get('部门名称') or '', r.get('销售经理') or '')] += 1
wb = openpyxl.load_workbook(JUL)
ref = openpyxl.load_workbook(JUN)
ws = wb['测试与考核情况说明']
rws = ref['测试与考核情况说明']
# 清空原说明页后,按6月视觉规范重建。
for merged in list(ws.merged_cells.ranges):
ws.unmerge_cells(str(merged))
ws.delete_rows(1, ws.max_row)
for col in range(1, 8):
letter = openpyxl.utils.get_column_letter(col)
ws.column_dimensions[letter].width = rws.column_dimensions[letter].width
total_plates = len({r['车牌号'] for r in new})
interv_plates = len({r['车牌号'] for r in intervention})
total_tbox = sum(float(r.get('原TBOX里程(km)') or 0) for r in new)
total_actual = sum(float(r.get('实际行驶里程(km)') or 0) for r in new)
total_test = sum(float(r.get('测试里程(km)') or 0) for r in new)
interv_tbox = sum(float(r.get('原TBOX里程(km)') or 0) for r in intervention)
interv_actual = sum(float(r.get('实际行驶里程(km)') or 0) for r in intervention)
interv_test = sum(float(r.get('测试里程(km)') or 0) for r in intervention)
# 标题及说明:深蓝标题、浅黄说明,与6月一致。
ws.merge_cells('A1:G1'); ws['A1'] = '2026年7月 业务考核 — 测试与考核情况说明'; clone_style(rws['A1'], ws['A1'])
ws.row_dimensions[1].height = rws.row_dimensions[1].height
ws.merge_cells('A2:G3'); ws['A2'] = ('7月业务考核车辆中,测试车辆统一以指定GPS日里程按考核期汇总替换TBOX里程,差额作为测试里程剥离且最低取0。\n'
'粤AG37316因GPS缺失,经业务确认暂按0核算。'); clone_style(rws['A2'], ws['A2'])
ws.row_dimensions[2].height = rws.row_dimensions[2].height
ws.merge_cells('A5:G5'); ws['A5'] = '一、7月业务考核与干预情况'; clone_style(rws['A5'], ws['A5'])
headers = ['项目', '7月业务考核车辆', '涉及测试的车辆', '占比']
for c, v in enumerate(headers, 1): ws.cell(6,c,v); clone_style(rws.cell(6,c), ws.cell(6,c))
stats = [
('车辆数(辆)', total_plates, interv_plates),
('产生考核记录数(条)', len(new), len(intervention)),
('仪表盘里程(km)', round(total_tbox), round(interv_tbox)),
('业务考核里程 GPS(km)', round(total_actual), round(interv_actual)),
('测试里程(km)', round(total_test), round(interv_test)),
]
for rr, (label, allv, partv) in enumerate(stats, 7):
for c, v in enumerate([label, allv, partv, partv/allv if allv else 0], 1):
ws.cell(rr,c,v); clone_style(rws.cell(7 + ((rr-7)%2),c), ws.cell(rr,c))
# “测试里程”比例改为测试车辆剔除率,避免原100%被理解成车辆全部里程均为测试。
ws['D6'] = '占比/剔除率'
ws['D11'] = interv_test / interv_tbox if interv_tbox else 0
negative_floor_adjustment = sum(max(0, float(r.get('实际行驶里程(km)') or 0) - float(r.get('原TBOX里程(km)') or 0)) for r in intervention)
ws.merge_cells('A12:G12')
ws['A12'] = (f'勾稽:测试里程 {round(interv_tbox):,} {round(interv_actual):,} '
f'{round(negative_floor_adjustment):,}(负差归0调整)= {round(interv_test):,} km')
clone_style(rws['A2'], ws['A12'])
ws.row_dimensions[12].height = 22
ws.merge_cells('A13:G13'); ws['A13']='二、干预车辆考核达标情况说明'; clone_style(rws['A13'],ws['A13'])
ws.merge_cells('A14:G14'); ws['A14']=f'{len(intervention)} 条考核记录涉及里程干预;替换为GPS里程后,共 {len(flip)} 条考核车辆记录由“达标”转判为“未达标”。按车型分类如下:'; clone_style(rws['A14'],ws['A14'])
target_order = ['羚牛136辆4.5T冷链车','羚牛100辆18T','交投40辆4.5T普货','交投190辆4.5T冷链车','恒运50辆4.5T普货','恒运50辆4.5T 普货']
rn = 16
for target in target_order:
items = sorted(groups.get(target, {}).items())
if not items: continue
total = sum(v for _, v in items)
ws.merge_cells(start_row=rn,start_column=1,end_row=rn,end_column=3)
ws.cell(rn,1,f'{target}{total} 辆)'); clone_style(rws['A15'],ws.cell(rn,1)); rn += 1
for c,v in enumerate(['部门','销售经理','数量'],1): ws.cell(rn,c,v); clone_style(rws.cell(16,c),ws.cell(rn,c))
rn += 1
for idx, ((dept,sales),cnt) in enumerate(items):
for c,v in enumerate([dept,sales,cnt],1): ws.cell(rn,c,v); clone_style(rws.cell(17 + idx%2,c),ws.cell(rn,c))
rn += 1
rn += 1
# 数据来源说明沿用6月蓝色区域。
ws.merge_cells(start_row=rn,start_column=1,end_row=rn,end_column=7); ws.cell(rn,1,'三、里程三列数据来源说明'); clone_style(rws['A13'],ws.cell(rn,1)); rn += 1
for c,v in enumerate(['','字段名','数据来源','说明'],1): ws.cell(rn,c,v); clone_style(rws.cell(6,c),ws.cell(rn,c))
rn += 1
descs = [
('O','原TBOX里程(km)','车辆 T-Box / 仪表盘读数','业务系统原始TBOX里程,包含运营、测试及调试里程。'),
('P','实际行驶里程(km)','指定GPS日里程文件','按考核日期逐日汇总GPS里程;粤AG37316经确认暂按0核算。'),
('Q','测试里程(km)','O P(最低取0','原TBOX里程与GPS里程的非负差额,不计入考核。'),
]
for idx,row in enumerate(descs):
for c,v in enumerate(row,1): ws.cell(rn,c,v); clone_style(rws.cell(28 + idx,c),ws.cell(rn,c))
ws.row_dimensions[rn].height = rws.row_dimensions[28 + idx].height
rn += 1
ws.sheet_view.showGridLines = rws.sheet_view.showGridLines
ws.freeze_panes = rws.freeze_panes
wb.save(JUL)
wb.close(); ref.close()
print(f'updated={JUL}')
print(f'rows={rn-1} flip={len(flip)} groups={sum(len(v) for v in groups.values())}')
+793
View File
@@ -0,0 +1,793 @@
#!/usr/bin/env python3
"""
全流程:用GPS汇总文件替换测试车辆里程 → 重新核算6月 → 离职转嫁 → 输出
输出目录: 20260713_1500/
"""
import openpyxl, os, shutil, xlrd
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
from datetime import datetime, timedelta
from calc_engine import *
import excel_writer as ew
os.chdir('/Users/kkfluous/Downloads')
GPS_BASE = 'gps里程汇总2026年1-6月 3'
OUT_DIR = '20260713_1500'
os.makedirs(OUT_DIR, exist_ok=True)
# ============================================================
# Step 1: Build GPS daily mileage lookup from all 4 providers
# ============================================================
print("=" * 60)
print("Step 1: 构建GPS日里程映射")
print("=" * 60)
# {plate: {day_number (1-30): mileage_km}}
gps_daily = defaultdict(lambda: defaultdict(float))
# --- G7 ---
print("\n读取 G7...")
fp_g7 = os.path.join(GPS_BASE, 'G7/202606_G7.xlsx')
wb = openpyxl.load_workbook(fp_g7, data_only=True)
ws = wb.active
h_g7 = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
# Find daily columns: 06月01日 ... 06月30日
g7_day_cols = {}
for i, h in enumerate(h_g7):
if h and '' in str(h) and '' in str(h):
# Parse "06月15日" -> day=15
import re
m = re.search(r'(\d+)月(\d+)日', str(h))
if m:
day = int(m.group(2))
g7_day_cols[day] = i
print(f" G7日期列: {len(g7_day_cols)} 天, 共 {ws.max_row-1} 辆车")
g7_count = 0
for row in ws.iter_rows(min_row=2, values_only=True):
plate = str(row[0]).strip() if row[0] else ''
if not plate: continue
for day, col in g7_day_cols.items():
val = float(row[col] or 0)
if val > 0:
gps_daily[plate][day] += val
g7_count += 1
wb.close()
print(f" G7: {g7_count} 辆车")
# --- 东方北斗 ---
print("\n读取 东方北斗...")
fp_df = os.path.join(GPS_BASE, '东方北斗/202606_东方北斗.xlsx')
wb = openpyxl.load_workbook(fp_df, data_only=True)
ws = wb.active
h_df = [c.value for c in next(ws.iter_rows(min_row=3, max_row=3))]
# Daily columns: 1号(Col 11) ... 30号(Col 40)
df_day_cols = {}
for i, h in enumerate(h_df):
if h and '' in str(h) and str(h)[0].isdigit():
try:
day = int(str(h).replace('', ''))
df_day_cols[day] = i
except: pass
print(f" 东方北斗日期列: {len(df_day_cols)}")
df_count = 0
for row in ws.iter_rows(min_row=4, values_only=True):
plate = str(row[5]).strip() if row[5] else ''
if not plate: continue
for day, col in df_day_cols.items():
val = float(row[col] or 0)
if val > 0:
gps_daily[plate][day] += val
df_count += 1
wb.close()
print(f" 东方北斗: {df_count} 辆车")
# --- 信达GPS (daily detail format) ---
print("\n读取 信达GPS...")
fp_xd = os.path.join(GPS_BASE, '信达GPS/202606_信达.xlsx')
wb = openpyxl.load_workbook(fp_xd, data_only=True)
ws = wb.active
xd_count = 0
for row in ws.iter_rows(min_row=4, values_only=True):
plate = str(row[7]).strip() if row[7] else ''
date_str = str(row[14]).strip() if row[14] else ''
mileage = float(row[15] or 0)
if not plate or not date_str or mileage <= 0: continue
try:
# Parse date: "2026-06-01"
dt = datetime.strptime(date_str[:10], '%Y-%m-%d')
day = dt.day
gps_daily[plate][day] += mileage
xd_count += 1
except: pass
wb.close()
print(f" 信达GPS: {xd_count} 条日记录")
# --- 广安北斗 (monthly only - xls) ---
print("\n读取 广安北斗...")
fp_ga = os.path.join(GPS_BASE, '广安北斗/202606_广安北斗.xls')
gdwb = xlrd.open_workbook(fp_ga)
gdws = gdwb.sheet_by_index(0)
ga_count = 0
for r in range(7, gdws.nrows):
plate = str(gdws.cell_value(r, 3)).strip()
mileage = float(gdws.cell_value(r, 11) or 0)
if not plate or mileage <= 0: continue
# Spread across all 30 days proportionally? No - store as a special total
# Better: store at day 0 as "full month total"
if 0 not in gps_daily[plate] or gps_daily[plate][0] < mileage:
gps_daily[plate][0] = mileage # day 0 = full month total marker
ga_count += 1
print(f" 广安北斗: {ga_count} 辆车 (月度汇总)")
total_plates = len(gps_daily)
print(f"\nGPS映射总计: {total_plates} 辆车")
# ============================================================
# Step 2: Read test vehicle plates
# ============================================================
print("\n" + "=" * 60)
print("Step 2: 提取测试车辆清单")
print("=" * 60)
# Extract from干预情况说明
fp_interv = '租赁任务考核_2026年6月_干预情况说明.xlsx'
if os.path.exists(fp_interv):
wb = openpyxl.load_workbook(fp_interv, data_only=True)
ws = wb['涉及测试的业务考核车辆']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
plate_col = h.index('车牌号')
test_plates = set()
for row in ws.iter_rows(min_row=2, values_only=True):
p = row[plate_col]
if p: test_plates.add(str(p).strip())
wb.close()
else:
# Fallback: vehicles with test mileage > 0 in current source
wb = openpyxl.load_workbook('租赁任务考核_2026年6月.xlsx', data_only=True)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
pcol = h.index('车牌号')
tcol = h.index('测试里程(km)')
test_plates = set()
for row in ws.iter_rows(min_row=2, values_only=True):
if float(row[tcol] or 0) > 0:
test_plates.add(str(row[pcol]).strip())
wb.close()
print(f"测试车辆: {len(test_plates)}")
# Check GPS coverage
gps_hit = sum(1 for p in test_plates if p in gps_daily)
gps_miss = test_plates - set(gps_daily.keys())
print(f"GPS命中: {gps_hit}, GPS缺失: {len(gps_miss)}")
if gps_miss:
print(f"缺失车辆(前10): {sorted(gps_miss)[:10]}")
# ============================================================
# Step 3: Replace GPS mileage for test vehicles
# ============================================================
print("\n" + "=" * 60)
print("Step 3: 替换测试车辆实际行驶里程")
print("=" * 60)
# Start from current source file (has correct structure with TBOX/GPS/测试 columns)
src_fp = '租赁任务考核_2026年6月.xlsx'
wb_src = openpyxl.load_workbook(src_fp)
ws_src = wb_src['业务考核视图']
h_src = [c.value for c in next(ws_src.iter_rows(min_row=1, max_row=1))]
# Column indices
col_plate = h_src.index('车牌号')
col_tbox = h_src.index('原TBOX里程(km)')
col_actual = h_src.index('实际行驶里程(km)')
col_test = h_src.index('测试里程(km)')
col_rate = h_src.index('完成率(%)')
col_dabiao = h_src.index('是否达标')
col_start = h_src.index('考核开始日期')
col_end = h_src.index('考核结束日期')
col_target_km = h_src.index('应考核里程(km)')
YELLOW_FILL = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
updated_count = 0
no_gps_count = 0
records = []
for row_idx, row in enumerate(ws_src.iter_rows(min_row=2), start=2):
plate = str(row[col_plate].value or '').strip()
tbox_val = float(row[col_tbox].value or 0)
start_date = row[col_start].value
end_date = row[col_end].value
target_km = float(row[col_target_km].value or 0)
if plate not in test_plates:
records.append((row_idx, plate, False, None, None))
continue
# Calculate GPS mileage within考核日期区间
gps_sum = 0.0
gps_data = gps_daily.get(plate, {})
if 0 in gps_data and len(gps_data) == 1:
# 广安北斗: only has full month total
gps_sum = gps_data[0]
elif start_date and end_date:
# Parse dates and sum daily GPS
if isinstance(start_date, datetime) and isinstance(end_date, datetime):
start_day = start_date.day
end_day = end_date.day
for day in range(start_day, end_day + 1):
gps_sum += gps_data.get(day, 0)
elif isinstance(start_date, str):
try:
sd = datetime.strptime(str(start_date)[:10], '%Y-%m-%d')
ed = datetime.strptime(str(end_date)[:10], '%Y-%m-%d')
for day in range(sd.day, ed.day + 1):
gps_sum += gps_data.get(day, 0)
except: pass
else:
# No dates, use full month
gps_sum = sum(gps_data.values())
else:
gps_sum = sum(gps_data.values())
if gps_sum <= 0 and tbox_val <= 0:
no_gps_count += 1
records.append((row_idx, plate, True, tbox_val, tbox_val))
continue
# Test mileage = max(0, TBOX - GPS)
test_mileage = max(0, tbox_val - gps_sum)
# Completion rate
rate = (gps_sum / target_km * 100) if target_km > 0 else 0
# 达标判断
dabiao = '达标' if gps_sum >= target_km and target_km > 0 else '未达标'
# Update cells
row[col_actual].value = round(gps_sum, 2)
row[col_test].value = round(test_mileage, 2)
row[col_rate].value = round(rate, 2)
row[col_dabiao].value = dabiao
# Yellow highlight
for cell in row:
cell.fill = YELLOW_FILL
updated_count += 1
records.append((row_idx, plate, True, gps_sum, test_mileage))
print(f"测试车辆记录: {updated_count} 条已更新, {no_gps_count} 条无GPS数据")
# Save updated source file
new_src = os.path.join(OUT_DIR, '租赁任务考核_2026年6月.xlsx')
wb_src.save(new_src)
print(f"✅ 新数据源: {new_src}")
# ============================================================
# Step 4: Run 核算
# ============================================================
print("\n" + "=" * 60)
print("Step 4: 运行核算")
print("=" * 60)
# Temporarily copy source to working location
shutil.copy(new_src, '租赁任务考核_2026年6月.xlsx')
# Re-run main.py logic
print("读取源数据...")
D = {}
for m in range(1, 7):
fp = f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
D[m] = read_file(fp, m)
print(f" {m}月: {len(D[m])}")
G = {m: grp(D[m], m) for m in D}
calc_jan_carryover(G[1])
all_data = {}
all_data[1] = {'当月': [{'车牌':k[0],'销售':g['销售'],'部门':g['部门'],'':g['奖金']}
for k,g in G[1].items() if g['奖金'] > 0]}
for m in range(2, 7):
if m not in G: continue
data_m, G[m] = calc_month(m, G, all_data, G[m-1], G[m])
vehicle_payments, vehicle_info = collect_vehicle_payments(G, all_data)
master_vehicles = read_master_vehicles()
# Loss data
loss_data = {}
for m in D:
ld = read_loss_data(m)
if ld:
loss_data[m] = ld
else:
from collections import defaultdict as dd
ld_tmp = dd(lambda: '')
for r in D[m]:
c = r.get('客户名称')
if c: ld_tmp[c] = ''
loss_data[m] = ld_tmp
plate_client = get_vehicle_client_map(D)
all_persons = {}
for m in G:
for k, g in G[m].items():
all_persons[g['销售']] = g['部门']
# Print summary
settle_month = 6
month_data = all_data[settle_month]
total_6 = sum(sum(d[''] for d in v) for v in month_data.values())
print(f"\n6月考核应发: {total_6:.2f}")
# Generate核算 file
print(f"\n生成 {settle_month}月核算文件...")
wb = openpyxl.Workbook()
ew.write_rules_sheet(wb)
ew.write_detail_sheet(wb, D[settle_month], settle_month)
ew.write_calc_process_generic(wb, settle_month, G, all_data)
ew.write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info, loss_data[settle_month], plate_client)
class _AllPass:
def get(self, _k, _d=None): return ''
def __bool__(self): return True
payment_records = ew.build_payment_records(settle_month, month_data, {m: _AllPass() for m in D}, plate_client)
ew.write_payment_record_sheet(wb, settle_month, payment_records)
ew.write_summary_from_records(wb, settle_month, payment_records)
for person in sorted(all_persons.keys()):
ew.write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments)
if 'Sheet' in wb.sheetnames:
del wb['Sheet']
calc_fp = os.path.join(OUT_DIR, '里程任务考核_6月核算.xlsx')
wb.save(calc_fp)
print(f"✅ 核算文件: {calc_fp}")
# ============================================================
# Step 5: 离职转嫁 + 格式重建
# ============================================================
print("\n" + "=" * 60)
print("Step 5: 离职转嫁 + 汇总格式重建")
print("=" * 60)
RESIGNED = {'赵连飞', '伍仲文', '岑彦'}
TRANSFERS = [
('刘念念', '赵连飞', 2260.00),
('钟祥', '伍仲文、岑彦', 180.00),
]
TRANSFER_TOTAL = sum(t[2] for t in TRANSFERS)
TRANSFER_MAP = {'赵连飞': ('刘念念', 2260.00), '伍仲文': ('钟祥', 150.00), '岑彦': ('钟祥', 30.00)}
# Styles
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10, name='宋体')
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
TITLE_FNT = Font(bold=True, size=12, name='宋体')
SECTION_FNT = Font(bold=True, size=11, name='宋体')
NOTE_FNT = Font(size=10, color='C00000', name='宋体')
NORM_FNT = Font(size=10, name='宋体')
BOLD_FNT = Font(bold=True, size=10, name='宋体')
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
def hdr_row(ws, r, headers):
for c, h in enumerate(headers, 1):
cell = ws.cell(row=r, column=c, value=h)
cell.font = HEAD_FONT; cell.fill = HEAD_FILL; cell.alignment = CC; cell.border = BORD
def data_row(ws, r, values, bold=False):
for c, v in enumerate(values, 1):
cell = ws.cell(row=r, column=c, value=v)
cell.font = BOLD_FNT if bold else NORM_FNT; cell.alignment = CC; cell.border = BORD
if isinstance(v, float): cell.number_format = '#,##0.00'
def sub_total(ws, r, values):
for c, v in enumerate(values, 1):
cell = ws.cell(row=r, column=c, value=v)
cell.font = BOLD_FNT; cell.fill = SUB_FILL; cell.alignment = CC; cell.border = BORD
if isinstance(v, float): cell.number_format = '#,##0.00'
# Parse existing summary
ws_old = wb['6月汇总']
person_dept = {}
for r in range(1, ws_old.max_row + 1):
v1 = str(ws_old.cell(row=r, column=1).value or '')
if '6月最终发放(按销售人员)' in v1:
for r2 in range(r+1, ws_old.max_row + 1):
name = str(ws_old.cell(row=r2, column=1).value or '')
dept = str(ws_old.cell(row=r2, column=2).value or '')
if name == '合计': break
if name and name != '销售人员' and dept:
person_dept[name] = dept
break
# Parse考核应发 subsections
subsections = []
current_title = None; current_rows = []
for r in range(1, ws_old.max_row + 1):
v1 = str(ws_old.cell(row=r, column=1).value or '')
if v1.startswith('考核应发-'):
if current_title and current_rows:
subsections.append((current_title, current_rows))
current_title = v1; current_rows = []
continue
if current_title:
name = str(ws_old.cell(row=r, column=1).value or '')
vehicles = ws_old.cell(row=r, column=2).value
amount = ws_old.cell(row=r, column=3).value
if name == '总计':
subsections.append((current_title, current_rows))
current_title = None; current_rows = []
elif name and name != '销售人员':
current_rows.append((name, vehicles, amount))
# Filter out resigned
filtered_subsections = []
grand_total = 0.0
for title, rows in subsections:
filtered = [(n, v, a) for (n, v, a) in rows if n not in RESIGNED]
new_veh = sum(int(v or 0) for _, v, _ in filtered)
new_amt = sum(float(a or 0) for _, _, a in filtered)
grand_total += new_amt
filtered_subsections.append((title, filtered, new_veh, new_amt))
# Aggregate最终发放
person_data = defaultdict(lambda: {'vehicles': 0, 'amount': 0.0})
for title, rows, _, _ in filtered_subsections:
for name, vehicles, amount in rows:
person_data[name]['vehicles'] += int(vehicles or 0)
person_data[name]['amount'] += float(amount or 0)
for resigned, (receiver, amt) in TRANSFER_MAP.items():
person_data[receiver]['amount'] += amt
for name in RESIGNED:
person_data.pop(name, None)
final_total = sum(d['amount'] for d in person_data.values())
dept_order = ['业务二部', '业务三部', '业务五部', '业务六部']
def sort_key(item):
name = item[0]
dept = person_dept.get(name, '')
try: di = dept_order.index(dept)
except: di = 99
return (di, name)
sorted_persons = sorted(person_data.items(), key=sort_key)
dept_totals = defaultdict(float)
for name, data in sorted_persons:
dept = person_dept.get(name, '')
dept_totals[dept] += data['amount']
# Rebuild 6月汇总 sheet
ws = wb['6月汇总']
ws.delete_rows(1, ws.max_row)
rn = 1
# 一、考核应发明细
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value='一、考核应发明细').font = TITLE_FNT
rn += 2
for title, rows, total_veh, total_amt in filtered_subsections:
ws.cell(row=rn, column=1, value=title).font = SECTION_FNT
rn += 1
hdr_row(ws, rn, ['销售人员', '车辆数', '金额'])
rn += 1
sorted_rows = sorted(rows, key=lambda x: sort_key((x[0],)) if x[0] in person_dept else (99, x[0]))
for name, vehicles, amount in sorted_rows:
data_row(ws, rn, [name, vehicles, round(float(amount or 0), 2)])
rn += 1
sub_total(ws, rn, ['总计', total_veh, round(total_amt, 2)])
rn += 2
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=2)
ws.cell(row=rn, column=1, value='考核应发合计').font = BOLD_FNT
ws.cell(row=rn, column=1).alignment = CC
ws.cell(row=rn, column=3, value=round(grand_total, 2)).font = BOLD_FNT
ws.cell(row=rn, column=3).alignment = CC; ws.cell(row=rn, column=3).number_format = '#,##0.00'
for c in range(1, 4):
ws.cell(row=rn, column=c).border = BORD; ws.cell(row=rn, column=c).fill = SUB_FILL
rn += 2
# 二、离职人员转嫁收入
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value='二、离职人员转嫁收入').font = TITLE_FNT
rn += 1
hdr_row(ws, rn, ['接收人', '来源', '转嫁金额'])
rn += 1
for receiver, source, amt in TRANSFERS:
data_row(ws, rn, [receiver, source, amt]); rn += 1
sub_total(ws, rn, ['合计', '', TRANSFER_TOTAL]); rn += 2
# 三、最终发放
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value='三、最终发放').font = TITLE_FNT
rn += 2
ws.cell(row=rn, column=1, value='最终发放明细').font = SECTION_FNT; rn += 1
hdr_row(ws, rn, ['销售人员', '车辆数', '金额']); rn += 1
for name, data in sorted_persons:
data_row(ws, rn, [name, data['vehicles'], round(data['amount'], 2)]); rn += 1
sub_total(ws, rn, ['总计', sum(d['vehicles'] for _, d in sorted_persons), round(final_total, 2)]); rn += 2
ws.cell(row=rn, column=1, value='6月最终发放(按销售人员)').font = SECTION_FNT; rn += 1
hdr_row(ws, rn, ['销售人员', '部门名称', '最终发放']); rn += 1
for name, data in sorted_persons:
dept = person_dept.get(name, '')
data_row(ws, rn, [name, dept, round(data['amount'], 2)]); rn += 1
sub_total(ws, rn, ['合计', '', round(final_total, 2)]); rn += 2
ws.cell(row=rn, column=1, value='6月最终发放(按部门)').font = SECTION_FNT; rn += 1
hdr_row(ws, rn, ['部门名称', '最终发放', '']); rn += 1
for dept in dept_order:
if dept in dept_totals:
data_row(ws, rn, [dept, round(dept_totals[dept], 2), '']); rn += 1
sub_total(ws, rn, ['合计', round(sum(dept_totals.values()), 2), '']); rn += 2
# 总览
ws.cell(row=rn, column=1, value='总览').font = SECTION_FNT; rn += 1
data_row(ws, rn, ['考核应发', round(final_total, 2), '']); rn += 1
data_row(ws, rn, ['亏损拦截', 0, '']); rn += 1
data_row(ws, rn, ['最终发放', round(final_total, 2), '']); rn += 2
# 备注
note = '备注:赵连飞(2026-04-30离职)、伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)\n三人已从上述汇总中剔除,其6月当月达标奖金已转嫁至新销售;其余结转、补发、累计补发等一律不发了。'
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value=note).font = NOTE_FNT
ws.cell(row=rn, column=1).alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
ws.row_dimensions[rn].height = 36
# Column widths
ws.column_dimensions['A'].width = 16
ws.column_dimensions['B'].width = 18
ws.column_dimensions['C'].width = 16
# Rename resigned sheets
for old, new in [('二部-赵连飞','【已离职】二部-赵连飞'),('六部-伍仲文','【已离职】六部-伍仲文'),('六部-岑彦','【已离职】六部-岑彦')]:
if old in wb.sheetnames:
wb[old].title = new
wb.move_sheet(new, offset=len(wb.sheetnames)-1-wb.sheetnames.index(new))
wb.save(calc_fp)
print(f"✅ 核算文件(含转嫁): {calc_fp}")
# ============================================================
# Step 6: 干预情况说明 (copy from existing)
# ============================================================
print("\n" + "=" * 60)
print("Step 6: 干预情况说明")
print("=" * 60)
old_interv = '租赁任务考核_2026年6月_干预情况说明.xlsx'
if os.path.exists(old_interv):
shutil.copy(old_interv, os.path.join(OUT_DIR, '租赁任务考核_2026年6月_干预情况说明.xlsx'))
print(f"✅ 干预说明已复制")
# ============================================================
# Step 7: 离职明细 (regenerate with new amounts)
# ============================================================
print("\n" + "=" * 60)
print("Step 7: 离职人员处理明细")
print("=" * 60)
# Collect resigned data from all_data[6]
detail_records = []
for ptype, items in month_data.items():
for it in items:
if it['销售'] not in RESIGNED: continue
name = it['销售']
amount = it['']
is_dangyue = (ptype == '当月')
transfer_to = TRANSFER_MAP[name][0]
handling = f'{transfer_to}(需确认)' if is_dangyue else '不发了'
transfer_disp = transfer_to if is_dangyue else ''
note_txt = '6月当月达标,转新销售' if is_dangyue else f'{ptype},不发了'
client = ''; target_name = ''
for r in D.get(6, []):
if r['车牌号'] == it['车牌'] and r['销售经理'] == name:
client = r.get('客户名称', ''); target_name = r.get('考核目标', '')
break
g6 = G[6].get((it['车牌'], name))
monthly_bonus = g6['奖励达标'] if g6 else 0
days = g6['天数'] if g6 else ''
if ptype in ('当月', f'累计补发{settle_month}', '结转'):
loss_month = settle_month
elif ptype.startswith('补发'):
try: loss_month = int(ptype.replace('补发','').replace('',''))
except: loss_month = settle_month
else: loss_month = settle_month
detail_records.append({
'name': name, 'plate': it['车牌'], 'pay_type': ptype,
'amount': amount, 'handling': handling, 'transfer': transfer_disp,
'note': note_txt, 'client': client, 'loss_month': loss_month,
'loss_status': '', 'monthly_bonus': monthly_bonus,
'days': days, 'target': target_name,
})
print(f"离职人员发放记录: {len(detail_records)}")
# Generate resignation workbook (simplified)
wb_res = openpyxl.Workbook()
# Sheet 1: 总览
ws1 = wb_res.active; ws1.title = '离职人员发放处理总览'
ws1.merge_cells('A1:G1')
ws1.cell(row=1, column=1, value='2026年6月核算 — 离职人员奖金处理方案').font = Font(bold=True, size=14, name='宋体')
ws1.cell(row=1, column=1).alignment = CC
ws1.row_dimensions[1].height = 30
ws1.merge_cells('A2:G2')
ws1.cell(row=2, column=1, value='处理规则:仅6月当月达标奖金转给新接手的销售(需与其他部门确认是否发放);其余结转、补发、累计补发等一律不发了。').font = NOTE_FNT
ws1.cell(row=2, column=1).alignment = LL
r = 4
for c, h in enumerate(['离职人员', '部门', '业务转嫁至', '不发了', '转新销售(需确认)', '原考核应发合计'], 1):
ws1.cell(row=r, column=c, value=h)
ws1.cell(row=r, column=c).font = HEAD_FONT; ws1.cell(row=r, column=c).fill = HEAD_FILL
ws1.cell(row=r, column=c).alignment = CC; ws1.cell(row=r, column=c).border = BORD
r += 1
dept_map = {'赵连飞': '业务二部', '伍仲文': '业务六部', '岑彦': '业务六部'}
total_not = 0; total_trf = 0
for name in ['赵连飞', '伍仲文', '岑彦']:
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
transfer = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
total_not += not_pay; total_trf += transfer
data_row(ws1, r, [name, dept_map[name], TRANSFER_MAP[name][0], round(not_pay,2), round(transfer,2), round(not_pay+transfer,2)])
r += 1
sub_total(ws1, r, ['合计', '', '', round(total_not,2), round(total_trf,2), round(total_not+total_trf,2)])
r += 2
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
ws1.cell(row=r, column=1, value='转嫁接收方汇总(需与其他部门确认是否发放)').font = Font(bold=True, size=12, color='1F4E78')
ws1.cell(row=r, column=1).alignment = CC; r += 1
for c, h in enumerate(['接收人', '来源离职人员', '转嫁金额', '备注'], 1):
ws1.cell(row=r, column=c, value=h)
ws1.cell(row=r, column=c).font = HEAD_FONT; ws1.cell(row=r, column=c).fill = HEAD_FILL
ws1.cell(row=r, column=c).alignment = CC; ws1.cell(row=r, column=c).border = BORD
r += 1
by_recv = defaultdict(lambda: {'sources':[], 'amt':0})
for rec in detail_records:
if rec['pay_type'] == '当月':
t = rec['transfer']
by_recv[t]['sources'].append(rec['name'])
by_recv[t]['amt'] += rec['amount']
for receiver in ['刘念念', '钟祥']:
d = by_recv[receiver]
data_row(ws1, r, [receiver, ''.join(sorted(set(d['sources']))), round(d['amt'],2), '需与相关部门确认是否发放给新销售'])
r += 1
for i, w in enumerate([14, 22, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(i)].width = w
# Sheet 2: 逐条明细
ws2 = wb_res.create_sheet('发放明细(逐条)')
ws2.merge_cells('A1:M1')
ws2.cell(row=1, column=1, value='离职人员 — 6月考核应发逐条明细').font = Font(bold=True, size=14, name='宋体')
ws2.cell(row=1, column=1).alignment = CC
dh = ['离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏', '处理方式', '转嫁接收人', '6月考核天数', '月度奖励', '考核目标', '备注']
r2 = 3
hdr_row(ws2, r2, dh); r2 += 1
name_order = ['赵连飞', '伍仲文', '岑彦']
detail_records.sort(key=lambda x: (name_order.index(x['name']), 0 if x['pay_type']=='当月' else 1, x['plate']))
for i, rec in enumerate(detail_records):
vals = [rec['name'], rec['plate'], rec['pay_type'], round(rec['amount'],2), f"{rec['loss_month']}",
rec['client'], rec['loss_status'], rec['handling'], rec['transfer'],
rec['days'] if rec['days'] else '', rec['monthly_bonus'] if rec['monthly_bonus'] else '', rec['target'], rec['note']]
data_row(ws2, r2, vals); ws2.row_dimensions[r2].height = 22; r2 += 1
sub_total(ws2, r2, ['', '', '合计', round(sum(r['amount'] for r in detail_records),2), '', '', '', '', '', '', '', '', ''])
for i, w in enumerate([10, 12, 14, 14, 10, 24, 8, 18, 14, 12, 10, 22, 28], 1):
ws2.column_dimensions[get_column_letter(i)].width = w
# Personal sheets (3-5) + Transfer sheets (6-7) - simplified
for name, title_txt, transfer_to in [
('赵连飞', '赵连飞(2026-04-30离职,业务转→刘念念)', '刘念念'),
('伍仲文', '伍仲文(2026-05-09离职,业务转→钟祥)', '钟祥'),
('岑彦', '岑彦(2026-05-09离职,业务转→钟祥)', '钟祥'),
]:
ws = wb_res.create_sheet(name)
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = Font(bold=True, size=14, name='宋体')
ws.cell(row=1, column=1).alignment = CC
ph = ['车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏', '处理方式', '6月考核天数', '月度奖励', '考核目标', '备注']
r3 = 3
hdr_row(ws, r3, ph); r3 += 1
person_recs = [p for p in detail_records if p['name'] == name]
for rec in person_recs:
vals = [rec['plate'], rec['pay_type'], round(rec['amount'],2), f"{rec['loss_month']}",
rec['client'], rec['loss_status'], rec['handling'],
rec['days'] if rec['days'] else '', rec['monthly_bonus'] if rec['monthly_bonus'] else '',
rec['target'], rec['note']]
data_row(ws, r3, vals); ws.row_dimensions[r3].height = 22; r3 += 1
dn = sum(p['amount'] for p in person_recs if p['pay_type']=='当月')
ot = sum(p['amount'] for p in person_recs if p['pay_type']!='当月')
sub_total(ws, r3, ['', '不发了小计', round(ot,2), '', '', '', '', '', '', '', '']); r3 += 1
sub_total(ws, r3, ['', f'{transfer_to}小计', round(dn,2), '', '', '', '', '', '', '', '需与其他部门确认']); r3 += 1
sub_total(ws, r3, ['', '合计', round(dn+ot,2), '', '', '', '', '', '', '', ''])
for i, w in enumerate([12, 14, 14, 10, 26, 8, 18, 12, 10, 22, 30], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# Transfer detail sheets
for title_txt, sources, receiver in [
('转嫁至刘念念的奖金 — 来源:赵连飞(2026-04-30离职)', ['赵连飞'], '刘念念'),
('转嫁至钟祥的奖金 — 来源:伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)', ['伍仲文', '岑彦'], '钟祥'),
]:
ws = wb_res.create_sheet(f'{receiver}明细')
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = Font(bold=True, size=14, name='宋体')
ws.cell(row=1, column=1).alignment = CC
ws.merge_cells('A2:K2')
ws.cell(row=2, column=1, value='⚠ 以下金额需与其他部门确认是否可发放给新销售').font = NOTE_FNT
ws.cell(row=2, column=1).alignment = LL
th = ['来源离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏', '6月考核天数', '月度奖励', '考核目标', '备注']
r4 = 4
hdr_row(ws, r4, th); r4 += 1
trf_recs = [p for p in detail_records if p['name'] in sources and p['pay_type']=='当月']
for rec in trf_recs:
vals = [rec['name'], rec['plate'], '当月', round(rec['amount'],2), f"{rec['loss_month']}",
rec['client'], rec['loss_status'], rec['days'] if rec['days'] else '',
rec['monthly_bonus'] if rec['monthly_bonus'] else '', rec['target'], '需确认是否发给新销售']
data_row(ws, r4, vals); r4 += 1
sub_total(ws, r4, ['', '', '合计', round(sum(r['amount'] for r in trf_recs),2), '', '', '', '', '', '', ''])
for i, w in enumerate([14, 12, 14, 14, 10, 26, 8, 12, 10, 22, 22], 1):
ws.column_dimensions[get_column_letter(i)].width = w
resign_fp = os.path.join(OUT_DIR, '离职人员6月奖金处理明细.xlsx')
wb_res.save(resign_fp)
print(f"✅ 离职明细: {resign_fp}")
# ============================================================
# Step 8: Summary
# ============================================================
print("\n" + "=" * 60)
print("Step 8: 输出汇总")
print("=" * 60)
files = os.listdir(OUT_DIR)
for f in sorted(files):
fp = os.path.join(OUT_DIR, f)
size_kb = os.path.getsize(fp) / 1024
print(f" {f} ({size_kb:.0f} KB)")
print(f"\n{'=' * 60}")
print(f"6月考核应发: {total_6:.2f}")
print(f"最终发放: {final_total:.2f}")
print(f"全部文件已输出到 {OUT_DIR}/")
print(f"{'=' * 60}")
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python3
"""生成6月干预情况说明"""
import openpyxl, os, datetime
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
# ========== Styles ==========
hdr_blue_font = Font(name='宋体', size=11, bold=True, color='FFFFFF')
hdr_blue_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
hdr_green_font = Font(name='宋体', size=11, bold=True, color='FFFFFF')
hdr_green_fill = PatternFill(start_color='548235', end_color='548235', fill_type='solid')
hdr_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
data_font = Font(name='宋体', size=10)
data_align = Alignment(horizontal='center', vertical='center')
title_font = Font(name='宋体', size=14, bold=True)
subtitle_font = Font(name='宋体', size=10, bold=True)
yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
light_yellow_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
light_gray_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
thin_border = Border(left=Side('thin'), right=Side('thin'), top=Side('thin'), bottom=Side('thin'))
def set_cell(ws, row, col, value, font=None, fill=None, align=None, border=None, num_fmt=None):
cell = ws.cell(row=row, column=col, value=value)
if font: cell.font = font
if fill: cell.fill = fill
if align: cell.alignment = align
if border: cell.border = border
if num_fmt: cell.number_format = num_fmt
return cell
# ========== Read Data ==========
src = openpyxl.load_workbook('租赁任务考核_2026年6月.xlsx', data_only=True)
ws_src = src['业务考核视图']
h_src = [c.value for c in next(ws_src.iter_rows(min_row=1, max_row=1))]
all_recs = []
for row in ws_src.iter_rows(min_row=2, values_only=True):
r = dict(zip(h_src, row))
if r['车牌号']:
all_recs.append(r)
src.close()
# Read test vehicle list
fp_test = '/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-07/65b41c10aa83cabb7a365b2b8437c1c7/6月测试车辆.xlsx'
wb_test = openpyxl.load_workbook(fp_test, data_only=True)
ws_test = wb_test.active
test_rows = list(ws_test.iter_rows(min_row=1, values_only=True))
test_plates = set()
for row in test_rows[1:]:
for ci in [0, 3, 6]:
p = str(row[ci]).strip() if row[ci] else ''
if p and p != 'None':
test_plates.add(p)
wb_test.close()
# Identify干预 records (test vehicles in source)
intervention_recs = [r for r in all_recs if r['车牌号'] in test_plates]
intervention_plates = set(r['车牌号'] for r in intervention_recs)
# Stats
total_vehicles = len(set(r['车牌号'] for r in all_recs))
total_records = len(all_recs)
interv_vehicles = len(intervention_plates)
interv_records = len(intervention_recs)
total_actual = sum(float(r['实际行驶里程(km)'] or 0) for r in all_recs)
total_tbox = sum(float(r['原TBOX里程(km)'] or 0) for r in all_recs)
total_test = sum(float(r['测试里程(km)'] or 0) for r in all_recs)
interv_actual = sum(float(r['实际行驶里程(km)'] or 0) for r in intervention_recs)
interv_tbox = sum(float(r['原TBOX里程(km)'] or 0) for r in intervention_recs)
interv_test = sum(float(r['测试里程(km)'] or 0) for r in intervention_recs)
# Flip analysis:达→未达标 (using GPS backup for original)
bak = openpyxl.load_workbook('租赁任务考核_2026年6月_备份.xlsx', data_only=True)
ws_bak = bak['业务考核视图']
h_bak = [c.value for c in next(ws_bak.iter_rows(min_row=1, max_row=1))]
bak_recs = {}
for row in ws_bak.iter_rows(min_row=2, values_only=True):
r = dict(zip(h_bak, row))
if r['车牌号']:
bak_recs[r['车牌号']] = r
bak.close()
# For intervention vehicles: compare old达→new未达标
flip_recs = [] # records that changed from达 to未达标
for r in intervention_recs:
plate = r['车牌号']
old_rec = bak_recs.get(plate)
if old_rec:
old_q = old_rec.get('是否达标') == '达标'
new_q = r['是否达标'] == '达标'
if old_q and not new_q:
flip_recs.append(r)
# Group flip by考核目标+部门+销售
flip_by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for r in flip_recs:
target = r.get('考核目标', '未知')
dept = r.get('部门名称', '')
sales = r.get('销售经理', '')
flip_by_target[target][dept][sales] += 1
# ========== Create Workbook ==========
wb = openpyxl.Workbook()
# ===== Sheet 1: 测试与考核情况说明 =====
ws1 = wb.active
ws1.title = '测试与考核情况说明'
# Column widths
for col, w in enumerate([4, 30, 18, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(col)].width = w
rn = 1
# Title
set_cell(ws1, rn, 1, '2026年6月 业务考核 — 测试与考核情况说明', title_font)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 1
# Subtitle
set_cell(ws1, rn, 1, '6月业务考核车辆中,部分车辆当月存在测试行为(仪表盘里程 > GPS 实际行驶里程),其差额作为测试里程剥离,不参与考核',
Font(name='宋体', size=10), light_yellow_fill)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 2
# Section 1: Stats
set_cell(ws1, rn, 1, '一、6月业务考核与干预情况', subtitle_font); rn += 1
# Header
for ci, h in enumerate(['项目', '6月业务考核车辆', '涉及测试的车辆', '占比'], 1):
set_cell(ws1, rn, ci, h, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
rn += 1
# Data rows
stats = [
('车辆数(辆)', total_vehicles, interv_vehicles),
('产生考核记录数(条)', total_records, interv_records),
('仪表盘里程(km)', round(total_tbox), round(interv_tbox)),
('业务考核里程 GPS(km)', round(total_actual), round(interv_actual)),
('测试里程(km)', round(total_test), round(interv_test)),
]
for label, all_v, interv_v in stats:
ratio = interv_v / all_v if all_v else 0
set_cell(ws1, rn, 1, label, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, all_v, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 3, interv_v, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 4, ratio, data_font, None, data_align, thin_border, '0.00%')
rn += 1
rn += 2
# Section 2: Flip details
set_cell(ws1, rn, 1, '二、干预车辆考核达标情况说明', subtitle_font); rn += 1
flip_total = len(flip_recs)
summary_text = f'{interv_records} 条考核记录涉及里程干预;替换为GPS里程后,共 {flip_total} 条考核车辆记录由"达标"转判为"未达标"。按车型分类如下'
set_cell(ws1, rn, 1, summary_text, Font(name='宋体', size=10))
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 2
# By target
target_order = ['羚牛136辆4.5T冷链车', '羚牛100辆18T', '交投40辆4.5T普货', '交投190辆4.5T冷链车', '恒运50辆4.5T普货']
for target in target_order:
if target not in flip_by_target:
continue
dept_sales = flip_by_target[target]
total_cnt = sum(sum(sales.values()) for sales in dept_sales.values())
set_cell(ws1, rn, 1, f'{target}{total_cnt} 辆)', subtitle_font)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 1
# Header: dept | sales | count | dept | sales | count
set_cell(ws1, rn, 1, '部门', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 2, '销售经理', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 3, '数量', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 4, '部门', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 5, '销售经理', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 6, '数量', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
rn += 1
# Collect all dept-sales-count tuples
items = []
for dept in sorted(dept_sales.keys()):
for sales in sorted(dept_sales[dept].keys()):
items.append((dept, sales, dept_sales[dept][sales]))
# Write in two columns
mid = (len(items) + 1) // 2
max_rows = max(mid, len(items) - mid)
for i in range(max_rows):
if i < mid:
d1, s1, c1 = items[i]
set_cell(ws1, rn, 1, d1, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, s1, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 3, c1, data_font, None, data_align, thin_border)
else:
for c in [1,2,3]:
set_cell(ws1, rn, c, '', data_font, None, data_align, thin_border)
if i + mid < len(items):
d2, s2, c2 = items[i + mid]
set_cell(ws1, rn, 4, d2, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 5, s2, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 6, c2, data_font, None, data_align, thin_border)
else:
for c in [4,5,6]:
set_cell(ws1, rn, c, '', data_font, None, data_align, thin_border)
rn += 1
rn += 1
rn += 1
# Section 3: Column source explanation
set_cell(ws1, rn, 1, '三、里程三列数据来源说明', subtitle_font); rn += 1
set_cell(ws1, rn, 1, '', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 2, '字段名', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 3, '数据来源', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 4, '说明', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
rn += 1
col_descs = [
('P', '实际行驶里程(km)', 'GPS 轨迹还原', '基于 G7 GPS 定位轨迹按考核期逐日汇总计算的运营里程(total_mileage/100000=km),本月里程考核以此列为准'),
('O', '原TBOX里程(km)', '车辆 T-Box / 仪表盘读数', '当月仪表总里程(业务系统原始数据),含运营+测试+调试里程。仅干预车辆填充,非干预车辆为空'),
('Q', '测试里程(km)', 'O P(原TBOX GPS,差额)', '仪表盘跑了但 GPS 未记录到的部分,判定为厂内测试/调试/路试等非运营里程,不计入考核。仅干预车辆填充,非干预车辆为0'),
]
for col_letter, name, source, desc in col_descs:
set_cell(ws1, rn, 1, col_letter, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, name, data_font, None, Alignment(horizontal='left', vertical='center'), thin_border)
set_cell(ws1, rn, 3, source, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 4, desc, data_font, None, Alignment(horizontal='left', vertical='center', wrap_text=True), thin_border)
ws1.row_dimensions[rn].height = 40
rn += 1
# ===== Sheet 2: 业务考核视图 (full data,干预标黄) =====
ws2 = wb.create_sheet('业务考核视图')
target_order = ['车牌号','考核年份','考核月份','部门名称','销售经理','客户名称','合同编号','项目名称',
'考核目标ID','考核目标','考核开始日期','考核结束日期','考核天数','应考核里程(km)',
'原TBOX里程(km)','实际行驶里程(km)','测试里程(km)','完成率(%)','是否达标','考核状态',
'交车日期','预计还车日期','实际还车日期']
for ci, name in enumerate(target_order, 1):
set_cell(ws2, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(all_recs, 2):
is_interv = r['车牌号'] in intervention_plates
for ci, name in enumerate(target_order, 1):
val = r.get(name)
set_cell(ws2, ri, ci, val, data_font, yellow_fill if is_interv else None, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws2.column_dimensions[get_column_letter(col)].width = w
# ===== Sheet 3: 6月业务考核车辆总览 =====
ws3 = wb.create_sheet('6月业务考核车辆总览')
for ci, name in enumerate(target_order, 1):
set_cell(ws3, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(all_recs, 2):
for ci, name in enumerate(target_order, 1):
set_cell(ws3, ri, ci, r.get(name), data_font, None, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws3.column_dimensions[get_column_letter(col)].width = w
# ===== Sheet 4: 涉及测试的业务考核车辆 =====
ws4 = wb.create_sheet('涉及测试的业务考核车辆')
for ci, name in enumerate(target_order, 1):
set_cell(ws4, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(intervention_recs, 2):
for ci, name in enumerate(target_order, 1):
set_cell(ws4, ri, ci, r.get(name), data_font, yellow_fill, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws4.column_dimensions[get_column_letter(col)].width = w
# ========== Save ==========
out = '租赁任务考核_2026年6月_干预情况说明.xlsx'
wb.save(out)
print(f'{out}')
print(f'\n概况:')
print(f' 全量: {total_vehicles}辆, {total_records}')
print(f' 干预: {interv_vehicles}辆, {interv_records}')
print(f' 达→未达标: {flip_total}')
print(f' 仪表盘里程: {total_tbox:.0f} (干预{interv_tbox:.0f})')
print(f' GPS里程: {total_actual:.0f} (干预{interv_actual:.0f})')
print(f' 测试里程: {total_test:.0f} (干预{interv_test:.0f})')
+393
View File
@@ -0,0 +1,393 @@
#!/usr/bin/env python3
"""生成 离职人员7月奖金处理明细 — 当月转嫁,其他不发了(18T: 1000/0/1000/1000"""
import os, openpyxl, warnings
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
from calc_engine import *
warnings.filterwarnings('ignore')
os.chdir('/Users/kkfluous/Downloads')
JULY_SOURCE = os.environ.get('JULY_SOURCE', '7月核算结果/租赁任务考核_2026年7月.xlsx')
JULY_OUTPUT_DIR = os.environ.get('JULY_OUTPUT_DIR', '7月核算结果')
# ── Full calc (same as main.py) ──
print("读取源数据...")
D = {}
for m in range(1, 8):
fp = JULY_SOURCE if m == 7 else f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
D[m] = read_file(fp, m)
print(f" {m}月: {len(D[m])}")
G = {m: grp(D[m], m) for m in D}
calc_jan_carryover(G[1])
all_data = {}
all_data[1] = {'当月': [{'车牌': k[0], '销售': g['销售'], '部门': g['部门'], '': g['奖金']}
for k, g in G[1].items() if g['奖金'] > 0]}
for m in range(2, 8):
if m not in G:
continue
data_m, G[m] = calc_month(m, G, all_data, G[m-1], G[m])
# ── Resignation config ──
RESIGNATIONS = {
'赵连飞': {'dept': '业务二部', 'transfer_to': '刘念念'},
'伍仲文': {'dept': '业务六部', 'transfer_to': '钟祥'},
'岑彦': {'dept': '业务六部', 'transfer_to': '钟祥'},
'钱婧': {'dept': '业务三部', 'transfer_to': ''},
}
resigned = set(RESIGNATIONS.keys())
settle_month = 7
month_data = all_data[settle_month]
# ── Collect resigned payments from all_data[6] ──
detail_records = []
for ptype, items in month_data.items():
for it in items:
if it['销售'] not in resigned:
continue
name = it['销售']
amount = it['']
is_dangyue = (ptype == '当月')
transfer_to = RESIGNATIONS[name]['transfer_to']
handling = f'{transfer_to}(需确认)' if is_dangyue else '不发了'
transfer_disp = transfer_to if is_dangyue else ''
note = '7月当月奖金,转新销售' if is_dangyue else f'{ptype},不发了'
# Get client info from D[6]
client = ''
target_name = ''
for r in D.get(settle_month, []):
if r['车牌号'] == it['车牌'] and r['销售经理'] == name:
client = r.get('客户名称', '')
target_name = r.get('考核目标', '')
break
# Get group info
g6 = G[settle_month].get((it['车牌'], name))
monthly_bonus = g6['奖励达标'] if g6 else 0
days = g6['天数'] if g6 else ''
# 盈亏查询月
if ptype == '当月' or ptype == f'累计补发{settle_month}':
loss_month = settle_month
elif ptype == '结转':
loss_month = settle_month
elif ptype.startswith('补发'):
try:
loss_month = int(ptype.replace('补发', '').replace('', ''))
except:
loss_month = settle_month
else:
loss_month = settle_month
detail_records.append({
'name': name, 'plate': it['车牌'], 'pay_type': ptype,
'amount': amount, 'handling': handling, 'transfer': transfer_disp,
'note': note, 'client': client, 'loss_month': loss_month,
'loss_status': '', 'monthly_bonus': monthly_bonus,
'days': days, 'target': target_name,
})
print(f"离职人员发放记录: {len(detail_records)}")
# ── Styles ──
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10)
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
ALT_FILL = PatternFill('solid', fgColor='F2F2F2')
TITLE_FNT = Font(bold=True, size=14)
NOTE_FNT = Font(size=10, color='C00000')
NORM_FNT = Font(size=10)
BOLD_FNT = Font(bold=True, size=10)
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
def hdr(ws, r, nc):
for c in range(1, nc+1):
cell = ws.cell(row=r, column=c)
cell.fill = HEAD_FILL; cell.font = HEAD_FONT; cell.alignment = CC; cell.border = BORD
def drw(ws, r, nc, alt=False):
for c in range(1, nc+1):
cell = ws.cell(row=r, column=c)
cell.font = NORM_FNT; cell.alignment = CC; cell.border = BORD
if alt: cell.fill = ALT_FILL
wb = openpyxl.Workbook()
# ═══════════════════════════════════════
# Sheet 1: 离职人员发放处理总览
# ═══════════════════════════════════════
ws1 = wb.active
ws1.title = '离职人员发放处理总览'
ws1.merge_cells('A1:G1')
ws1.cell(row=1, column=1, value='2026年7月核算 — 离职人员奖金处理方案').font = TITLE_FNT
ws1.cell(row=1, column=1).alignment = CC
ws1.row_dimensions[1].height = 30
ws1.merge_cells('A2:G2')
ws1.cell(row=2, column=1,
value='处理规则:仅7月当月奖金转给新接手的销售(需与其他部门确认是否发放);其余结转、补发、累计补发等一律不发了。').font = NOTE_FNT
ws1.cell(row=2, column=1).alignment = LL
ws1.row_dimensions[2].height = 22
r = 4
oh = ['离职人员', '部门', '业务转嫁至', '不发了', '转新销售(需确认)', '原考核应发合计']
for c, h in enumerate(oh, 1):
ws1.cell(row=r, column=c, value=h)
hdr(ws1, r, 6)
r += 1
total_not = 0; total_trf = 0
for name in ['赵连飞', '伍仲文', '岑彦', '钱婧']:
info = RESIGNATIONS[name]
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
transfer = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
total_not += not_pay; total_trf += transfer
ws1.cell(row=r, column=1, value=name)
ws1.cell(row=r, column=2, value=info['dept'])
ws1.cell(row=r, column=3, value=info['transfer_to'])
ws1.cell(row=r, column=4, value=round(not_pay, 2))
ws1.cell(row=r, column=5, value=round(transfer, 2))
ws1.cell(row=r, column=6, value=round(not_pay+transfer, 2))
drw(ws1, r, 6)
ws1.row_dimensions[r].height = 22
r += 1
# 合计
for c in range(1, 7):
ws1.cell(row=r, column=c).fill = SUB_FILL
ws1.cell(row=r, column=c).border = BORD
ws1.cell(row=r, column=c).font = BOLD_FNT
ws1.cell(row=r, column=c).alignment = CC
ws1.cell(row=r, column=1, value='合计')
ws1.cell(row=r, column=4, value=round(total_not, 2))
ws1.cell(row=r, column=5, value=round(total_trf, 2))
ws1.cell(row=r, column=6, value=round(total_not+total_trf, 2))
r += 2
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
ws1.cell(row=r, column=1, value='转嫁接收方汇总(需与其他部门确认是否发放)').font = Font(bold=True, size=12, color='1F4E78')
ws1.cell(row=r, column=1).alignment = CC
r += 1
for c, hh in enumerate(['接收人', '来源离职人员', '转嫁金额', '备注'], 1):
ws1.cell(row=r, column=c, value=hh)
hdr(ws1, r, 4)
r += 1
by_recv = defaultdict(lambda: {'sources':[], 'amt':0})
for rec in detail_records:
if rec['pay_type'] == '当月':
t = rec['transfer']
by_recv[t]['sources'].append(rec['name'])
by_recv[t]['amt'] += rec['amount']
for receiver in ['刘念念', '钟祥']:
d = by_recv[receiver]
ws1.cell(row=r, column=1, value=receiver)
ws1.cell(row=r, column=2, value=''.join(sorted(set(d['sources']))))
ws1.cell(row=r, column=3, value=round(d['amt'], 2))
ws1.cell(row=r, column=4, value='需与相关部门确认是否发放给新销售')
drw(ws1, r, 4)
r += 1
for i, w in enumerate([14, 22, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheet 2: 发放明细(逐条)
# ═══════════════════════════════════════
ws2 = wb.create_sheet('发放明细(逐条)')
ws2.merge_cells('A1:M1')
ws2.cell(row=1, column=1, value='离职人员 — 7月考核应发逐条明细').font = TITLE_FNT
ws2.cell(row=1, column=1).alignment = CC
ws2.row_dimensions[1].height = 28
dh = ['离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称',
'客户盈亏', '处理方式', '转嫁接收人', '7月考核天数', '月度奖励', '考核目标', '备注']
r2 = 3
for c, h in enumerate(dh, 1):
ws2.cell(row=r2, column=c, value=h)
hdr(ws2, r2, 13)
r2 += 1
name_order = ['赵连飞', '伍仲文', '岑彦', '钱婧']
detail_records.sort(key=lambda x: (name_order.index(x['name']), 0 if x['pay_type']=='当月' else 1, x['plate']))
for i, rec in enumerate(detail_records):
ws2.cell(row=r2, column=1, value=rec['name'])
ws2.cell(row=r2, column=2, value=rec['plate'])
ws2.cell(row=r2, column=3, value=rec['pay_type'])
ws2.cell(row=r2, column=4, value=round(rec['amount'], 2))
ws2.cell(row=r2, column=5, value=f"{rec['loss_month']}")
ws2.cell(row=r2, column=6, value=rec['client'])
ws2.cell(row=r2, column=7, value=rec['loss_status'])
ws2.cell(row=r2, column=8, value=rec['handling'])
ws2.cell(row=r2, column=9, value=rec['transfer'])
ws2.cell(row=r2, column=10, value=rec['days'] if rec['days'] else '')
ws2.cell(row=r2, column=11, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws2.cell(row=r2, column=12, value=rec['target'])
ws2.cell(row=r2, column=13, value=rec['note'])
drw(ws2, r2, 13, alt=(i%2==1))
ws2.row_dimensions[r2].height = 22
r2 += 1
# 合计行
ws2.cell(row=r2, column=3, value='合计')
ws2.cell(row=r2, column=4, value=round(sum(r['amount'] for r in detail_records), 2))
for c in range(1, 14):
ws2.cell(row=r2, column=c).fill = SUB_FILL
ws2.cell(row=r2, column=c).border = BORD
ws2.cell(row=r2, column=c).font = BOLD_FNT
ws2.cell(row=r2, column=c).alignment = CC
for i, w in enumerate([10, 12, 14, 14, 10, 24, 8, 18, 14, 12, 10, 22, 28], 1):
ws2.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheets 3-5: 个人明细
# ═══════════════════════════════════════
for name, title_txt, transfer_to in [
('赵连飞', '赵连飞(2026-04-30离职,业务转→刘念念)', '刘念念'),
('伍仲文', '伍仲文(2026-05-09离职,业务转→钟祥)', '钟祥'),
('岑彦', '岑彦(2026-05-09离职,业务转→钟祥)', '钟祥'),
('钱婧', '钱婧(已离职,所有奖金不再发放)', ''),
]:
ws = wb.create_sheet(name)
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = TITLE_FNT
ws.cell(row=1, column=1).alignment = CC
ws.row_dimensions[1].height = 28
ph = ['车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏',
'处理方式', '7月考核天数', '月度奖励', '考核目标', '备注']
r3 = 3
for c, h in enumerate(ph, 1):
ws.cell(row=r3, column=c, value=h)
hdr(ws, r3, 11)
r3 += 1
person_recs = [p for p in detail_records if p['name'] == name]
dangyue_recs = [p for p in person_recs if p['pay_type'] == '当月']
other_recs = [p for p in person_recs if p['pay_type'] != '当月']
for i, rec in enumerate(person_recs):
h_disp = rec['handling']
ws.cell(row=r3, column=1, value=rec['plate'])
ws.cell(row=r3, column=2, value=rec['pay_type'])
ws.cell(row=r3, column=3, value=round(rec['amount'], 2))
ws.cell(row=r3, column=4, value=f"{rec['loss_month']}")
ws.cell(row=r3, column=5, value=rec['client'])
ws.cell(row=r3, column=6, value=rec['loss_status'])
ws.cell(row=r3, column=7, value=h_disp)
ws.cell(row=r3, column=8, value=rec['days'] if rec['days'] else '')
ws.cell(row=r3, column=9, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws.cell(row=r3, column=10, value=rec['target'])
ws.cell(row=r3, column=11, value=rec['note'])
drw(ws, r3, 11, alt=(i%2==1))
ws.row_dimensions[r3].height = 22
r3 += 1
dn = sum(p['amount'] for p in dangyue_recs)
ot = sum(p['amount'] for p in other_recs)
gt = dn + ot
# 不发了小计
ws.cell(row=r3, column=2, value='不发了小计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(ot, 2)).font = BOLD_FNT
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
r3 += 1
# 转嫁小计
ws.cell(row=r3, column=2, value=f'{transfer_to}小计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(dn, 2)).font = BOLD_FNT
ws.cell(row=r3, column=11, value='需与其他部门确认').font = Font(size=9, color='C00000')
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
r3 += 1
# 合计
ws.cell(row=r3, column=2, value='合计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(gt, 2)).font = BOLD_FNT
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
for i, w in enumerate([12, 14, 14, 10, 26, 8, 18, 12, 10, 22, 30], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheets 6-7: 转嫁明细
# ═══════════════════════════════════════
for title_txt, sources, receiver in [
('转嫁至刘念念的奖金 — 来源:赵连飞(2026-04-30离职)', ['赵连飞'], '刘念念'),
('转嫁至钟祥的奖金 — 来源:伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)', ['伍仲文', '岑彦'], '钟祥'),
]:
ws = wb.create_sheet(f'{receiver}明细')
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = TITLE_FNT
ws.cell(row=1, column=1).alignment = CC
ws.row_dimensions[1].height = 28
ws.merge_cells('A2:K2')
ws.cell(row=2, column=1, value='⚠ 以下金额需与其他部门确认是否可发放给新销售').font = NOTE_FNT
ws.cell(row=2, column=1).alignment = LL
th = ['来源离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称',
'客户盈亏', '7月考核天数', '月度奖励', '考核目标', '备注']
r4 = 4
for c, h in enumerate(th, 1):
ws.cell(row=r4, column=c, value=h)
hdr(ws, r4, 11)
r4 += 1
trf_recs = [p for p in detail_records if p['name'] in sources and p['pay_type'] == '当月']
for i, rec in enumerate(trf_recs):
ws.cell(row=r4, column=1, value=rec['name'])
ws.cell(row=r4, column=2, value=rec['plate'])
ws.cell(row=r4, column=3, value='当月')
ws.cell(row=r4, column=4, value=round(rec['amount'], 2))
ws.cell(row=r4, column=5, value=f"{rec['loss_month']}")
ws.cell(row=r4, column=6, value=rec['client'])
ws.cell(row=r4, column=7, value=rec['loss_status'])
ws.cell(row=r4, column=8, value=rec['days'] if rec['days'] else '')
ws.cell(row=r4, column=9, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws.cell(row=r4, column=10, value=rec['target'])
ws.cell(row=r4, column=11, value='需确认是否发给新销售')
drw(ws, r4, 11, alt=(i%2==1))
ws.row_dimensions[r4].height = 22
r4 += 1
ws.cell(row=r4, column=3, value='合计')
ws.cell(row=r4, column=4, value=round(sum(r['amount'] for r in trf_recs), 2))
for c in range(1, 12):
ws.cell(row=r4, column=c).fill = SUB_FILL; ws.cell(row=r4, column=c).border = BORD
ws.cell(row=r4, column=c).font = BOLD_FNT; ws.cell(row=r4, column=c).alignment = CC
for i, w in enumerate([14, 12, 14, 14, 10, 26, 8, 12, 10, 22, 22], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ── Save ──
os.makedirs(JULY_OUTPUT_DIR, exist_ok=True)
out = os.path.join(JULY_OUTPUT_DIR, '离职人员7月奖金处理明细.xlsx')
wb.save(out)
print(f'\n{out} ({len(wb.sheetnames)} sheets)')
print(f'\n汇总:')
for name in ['赵连飞', '伍仲文', '岑彦', '钱婧']:
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
trf = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
print(f' {name}: 不发了={not_pay:.2f}{RESIGNATIONS[name]["transfer_to"]}={trf:.2f} 合计={not_pay+trf:.2f}')
print(f' 总计: 不发了={total_not:.2f} 转新销售={total_trf:.2f}')
+388
View File
@@ -0,0 +1,388 @@
#!/usr/bin/env python3
"""生成 离职人员6月奖金处理明细 — 当月转嫁,其他不发了(18T: 1000/0/1000/1000"""
import os, openpyxl, warnings
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict
from calc_engine import *
warnings.filterwarnings('ignore')
os.chdir('/Users/kkfluous/Downloads')
# ── Full calc (same as main.py) ──
print("读取源数据...")
D = {}
for m in range(1, 7):
fp = f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
D[m] = read_file(fp, m)
print(f" {m}月: {len(D[m])}")
G = {m: grp(D[m], m) for m in D}
calc_jan_carryover(G[1])
all_data = {}
all_data[1] = {'当月': [{'车牌': k[0], '销售': g['销售'], '部门': g['部门'], '': g['奖金']}
for k, g in G[1].items() if g['奖金'] > 0]}
for m in range(2, 7):
if m not in G:
continue
data_m, G[m] = calc_month(m, G, all_data, G[m-1], G[m])
# ── Resignation config ──
RESIGNATIONS = {
'赵连飞': {'dept': '业务二部', 'transfer_to': '刘念念'},
'伍仲文': {'dept': '业务六部', 'transfer_to': '钟祥'},
'岑彦': {'dept': '业务六部', 'transfer_to': '钟祥'},
}
resigned = set(RESIGNATIONS.keys())
settle_month = 6
month_data = all_data[settle_month]
# ── Collect resigned payments from all_data[6] ──
detail_records = []
for ptype, items in month_data.items():
for it in items:
if it['销售'] not in resigned:
continue
name = it['销售']
amount = it['']
is_dangyue = (ptype == '当月')
transfer_to = RESIGNATIONS[name]['transfer_to']
handling = f'{transfer_to}(需确认)' if is_dangyue else '不发了'
transfer_disp = transfer_to if is_dangyue else ''
note = '6月当月达标,转新销售' if is_dangyue else f'{ptype},不发了'
# Get client info from D[6]
client = ''
target_name = ''
for r in D.get(settle_month, []):
if r['车牌号'] == it['车牌'] and r['销售经理'] == name:
client = r.get('客户名称', '')
target_name = r.get('考核目标', '')
break
# Get group info
g6 = G[settle_month].get((it['车牌'], name))
monthly_bonus = g6['奖励达标'] if g6 else 0
days = g6['天数'] if g6 else ''
# 盈亏查询月
if ptype == '当月' or ptype == f'累计补发{settle_month}':
loss_month = settle_month
elif ptype == '结转':
loss_month = settle_month
elif ptype.startswith('补发'):
try:
loss_month = int(ptype.replace('补发', '').replace('', ''))
except:
loss_month = settle_month
else:
loss_month = settle_month
detail_records.append({
'name': name, 'plate': it['车牌'], 'pay_type': ptype,
'amount': amount, 'handling': handling, 'transfer': transfer_disp,
'note': note, 'client': client, 'loss_month': loss_month,
'loss_status': '', 'monthly_bonus': monthly_bonus,
'days': days, 'target': target_name,
})
print(f"离职人员发放记录: {len(detail_records)}")
# ── Styles ──
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10)
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
ALT_FILL = PatternFill('solid', fgColor='F2F2F2')
TITLE_FNT = Font(bold=True, size=14)
NOTE_FNT = Font(size=10, color='C00000')
NORM_FNT = Font(size=10)
BOLD_FNT = Font(bold=True, size=10)
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
def hdr(ws, r, nc):
for c in range(1, nc+1):
cell = ws.cell(row=r, column=c)
cell.fill = HEAD_FILL; cell.font = HEAD_FONT; cell.alignment = CC; cell.border = BORD
def drw(ws, r, nc, alt=False):
for c in range(1, nc+1):
cell = ws.cell(row=r, column=c)
cell.font = NORM_FNT; cell.alignment = CC; cell.border = BORD
if alt: cell.fill = ALT_FILL
wb = openpyxl.Workbook()
# ═══════════════════════════════════════
# Sheet 1: 离职人员发放处理总览
# ═══════════════════════════════════════
ws1 = wb.active
ws1.title = '离职人员发放处理总览'
ws1.merge_cells('A1:G1')
ws1.cell(row=1, column=1, value='2026年6月核算 — 离职人员奖金处理方案').font = TITLE_FNT
ws1.cell(row=1, column=1).alignment = CC
ws1.row_dimensions[1].height = 30
ws1.merge_cells('A2:G2')
ws1.cell(row=2, column=1,
value='处理规则:仅6月当月达标奖金转给新接手的销售(需与其他部门确认是否发放);其余结转、补发、累计补发等一律不发了。').font = NOTE_FNT
ws1.cell(row=2, column=1).alignment = LL
ws1.row_dimensions[2].height = 22
r = 4
oh = ['离职人员', '部门', '业务转嫁至', '不发了', '转新销售(需确认)', '原考核应发合计']
for c, h in enumerate(oh, 1):
ws1.cell(row=r, column=c, value=h)
hdr(ws1, r, 6)
r += 1
total_not = 0; total_trf = 0
for name in ['赵连飞', '伍仲文', '岑彦']:
info = RESIGNATIONS[name]
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
transfer = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
total_not += not_pay; total_trf += transfer
ws1.cell(row=r, column=1, value=name)
ws1.cell(row=r, column=2, value=info['dept'])
ws1.cell(row=r, column=3, value=info['transfer_to'])
ws1.cell(row=r, column=4, value=round(not_pay, 2))
ws1.cell(row=r, column=5, value=round(transfer, 2))
ws1.cell(row=r, column=6, value=round(not_pay+transfer, 2))
drw(ws1, r, 6)
ws1.row_dimensions[r].height = 22
r += 1
# 合计
for c in range(1, 7):
ws1.cell(row=r, column=c).fill = SUB_FILL
ws1.cell(row=r, column=c).border = BORD
ws1.cell(row=r, column=c).font = BOLD_FNT
ws1.cell(row=r, column=c).alignment = CC
ws1.cell(row=r, column=1, value='合计')
ws1.cell(row=r, column=4, value=round(total_not, 2))
ws1.cell(row=r, column=5, value=round(total_trf, 2))
ws1.cell(row=r, column=6, value=round(total_not+total_trf, 2))
r += 2
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
ws1.cell(row=r, column=1, value='转嫁接收方汇总(需与其他部门确认是否发放)').font = Font(bold=True, size=12, color='1F4E78')
ws1.cell(row=r, column=1).alignment = CC
r += 1
for c, hh in enumerate(['接收人', '来源离职人员', '转嫁金额', '备注'], 1):
ws1.cell(row=r, column=c, value=hh)
hdr(ws1, r, 4)
r += 1
by_recv = defaultdict(lambda: {'sources':[], 'amt':0})
for rec in detail_records:
if rec['pay_type'] == '当月':
t = rec['transfer']
by_recv[t]['sources'].append(rec['name'])
by_recv[t]['amt'] += rec['amount']
for receiver in ['刘念念', '钟祥']:
d = by_recv[receiver]
ws1.cell(row=r, column=1, value=receiver)
ws1.cell(row=r, column=2, value=''.join(sorted(set(d['sources']))))
ws1.cell(row=r, column=3, value=round(d['amt'], 2))
ws1.cell(row=r, column=4, value='需与相关部门确认是否发放给新销售')
drw(ws1, r, 4)
r += 1
for i, w in enumerate([14, 22, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheet 2: 发放明细(逐条)
# ═══════════════════════════════════════
ws2 = wb.create_sheet('发放明细(逐条)')
ws2.merge_cells('A1:M1')
ws2.cell(row=1, column=1, value='离职人员 — 6月考核应发逐条明细').font = TITLE_FNT
ws2.cell(row=1, column=1).alignment = CC
ws2.row_dimensions[1].height = 28
dh = ['离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称',
'客户盈亏', '处理方式', '转嫁接收人', '6月考核天数', '月度奖励', '考核目标', '备注']
r2 = 3
for c, h in enumerate(dh, 1):
ws2.cell(row=r2, column=c, value=h)
hdr(ws2, r2, 13)
r2 += 1
name_order = ['赵连飞', '伍仲文', '岑彦']
detail_records.sort(key=lambda x: (name_order.index(x['name']), 0 if x['pay_type']=='当月' else 1, x['plate']))
for i, rec in enumerate(detail_records):
ws2.cell(row=r2, column=1, value=rec['name'])
ws2.cell(row=r2, column=2, value=rec['plate'])
ws2.cell(row=r2, column=3, value=rec['pay_type'])
ws2.cell(row=r2, column=4, value=round(rec['amount'], 2))
ws2.cell(row=r2, column=5, value=f"{rec['loss_month']}")
ws2.cell(row=r2, column=6, value=rec['client'])
ws2.cell(row=r2, column=7, value=rec['loss_status'])
ws2.cell(row=r2, column=8, value=rec['handling'])
ws2.cell(row=r2, column=9, value=rec['transfer'])
ws2.cell(row=r2, column=10, value=rec['days'] if rec['days'] else '')
ws2.cell(row=r2, column=11, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws2.cell(row=r2, column=12, value=rec['target'])
ws2.cell(row=r2, column=13, value=rec['note'])
drw(ws2, r2, 13, alt=(i%2==1))
ws2.row_dimensions[r2].height = 22
r2 += 1
# 合计行
ws2.cell(row=r2, column=3, value='合计')
ws2.cell(row=r2, column=4, value=round(sum(r['amount'] for r in detail_records), 2))
for c in range(1, 14):
ws2.cell(row=r2, column=c).fill = SUB_FILL
ws2.cell(row=r2, column=c).border = BORD
ws2.cell(row=r2, column=c).font = BOLD_FNT
ws2.cell(row=r2, column=c).alignment = CC
for i, w in enumerate([10, 12, 14, 14, 10, 24, 8, 18, 14, 12, 10, 22, 28], 1):
ws2.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheets 3-5: 个人明细
# ═══════════════════════════════════════
for name, title_txt, transfer_to in [
('赵连飞', '赵连飞(2026-04-30离职,业务转→刘念念)', '刘念念'),
('伍仲文', '伍仲文(2026-05-09离职,业务转→钟祥)', '钟祥'),
('岑彦', '岑彦(2026-05-09离职,业务转→钟祥)', '钟祥'),
]:
ws = wb.create_sheet(name)
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = TITLE_FNT
ws.cell(row=1, column=1).alignment = CC
ws.row_dimensions[1].height = 28
ph = ['车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏',
'处理方式', '6月考核天数', '月度奖励', '考核目标', '备注']
r3 = 3
for c, h in enumerate(ph, 1):
ws.cell(row=r3, column=c, value=h)
hdr(ws, r3, 11)
r3 += 1
person_recs = [p for p in detail_records if p['name'] == name]
dangyue_recs = [p for p in person_recs if p['pay_type'] == '当月']
other_recs = [p for p in person_recs if p['pay_type'] != '当月']
for i, rec in enumerate(person_recs):
h_disp = rec['handling']
ws.cell(row=r3, column=1, value=rec['plate'])
ws.cell(row=r3, column=2, value=rec['pay_type'])
ws.cell(row=r3, column=3, value=round(rec['amount'], 2))
ws.cell(row=r3, column=4, value=f"{rec['loss_month']}")
ws.cell(row=r3, column=5, value=rec['client'])
ws.cell(row=r3, column=6, value=rec['loss_status'])
ws.cell(row=r3, column=7, value=h_disp)
ws.cell(row=r3, column=8, value=rec['days'] if rec['days'] else '')
ws.cell(row=r3, column=9, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws.cell(row=r3, column=10, value=rec['target'])
ws.cell(row=r3, column=11, value=rec['note'])
drw(ws, r3, 11, alt=(i%2==1))
ws.row_dimensions[r3].height = 22
r3 += 1
dn = sum(p['amount'] for p in dangyue_recs)
ot = sum(p['amount'] for p in other_recs)
gt = dn + ot
# 不发了小计
ws.cell(row=r3, column=2, value='不发了小计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(ot, 2)).font = BOLD_FNT
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
r3 += 1
# 转嫁小计
ws.cell(row=r3, column=2, value=f'{transfer_to}小计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(dn, 2)).font = BOLD_FNT
ws.cell(row=r3, column=11, value='需与其他部门确认').font = Font(size=9, color='C00000')
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
r3 += 1
# 合计
ws.cell(row=r3, column=2, value='合计').font = BOLD_FNT
ws.cell(row=r3, column=3, value=round(gt, 2)).font = BOLD_FNT
for c in range(1, 12):
ws.cell(row=r3, column=c).fill = SUB_FILL; ws.cell(row=r3, column=c).border = BORD
ws.cell(row=r3, column=2).alignment = CC; ws.cell(row=r3, column=3).alignment = CC
for i, w in enumerate([12, 14, 14, 10, 26, 8, 18, 12, 10, 22, 30], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ═══════════════════════════════════════
# Sheets 6-7: 转嫁明细
# ═══════════════════════════════════════
for title_txt, sources, receiver in [
('转嫁至刘念念的奖金 — 来源:赵连飞(2026-04-30离职)', ['赵连飞'], '刘念念'),
('转嫁至钟祥的奖金 — 来源:伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)', ['伍仲文', '岑彦'], '钟祥'),
]:
ws = wb.create_sheet(f'{receiver}明细')
ws.merge_cells('A1:K1')
ws.cell(row=1, column=1, value=title_txt).font = TITLE_FNT
ws.cell(row=1, column=1).alignment = CC
ws.row_dimensions[1].height = 28
ws.merge_cells('A2:K2')
ws.cell(row=2, column=1, value='⚠ 以下金额需与其他部门确认是否可发放给新销售').font = NOTE_FNT
ws.cell(row=2, column=1).alignment = LL
th = ['来源离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称',
'客户盈亏', '6月考核天数', '月度奖励', '考核目标', '备注']
r4 = 4
for c, h in enumerate(th, 1):
ws.cell(row=r4, column=c, value=h)
hdr(ws, r4, 11)
r4 += 1
trf_recs = [p for p in detail_records if p['name'] in sources and p['pay_type'] == '当月']
for i, rec in enumerate(trf_recs):
ws.cell(row=r4, column=1, value=rec['name'])
ws.cell(row=r4, column=2, value=rec['plate'])
ws.cell(row=r4, column=3, value='当月')
ws.cell(row=r4, column=4, value=round(rec['amount'], 2))
ws.cell(row=r4, column=5, value=f"{rec['loss_month']}")
ws.cell(row=r4, column=6, value=rec['client'])
ws.cell(row=r4, column=7, value=rec['loss_status'])
ws.cell(row=r4, column=8, value=rec['days'] if rec['days'] else '')
ws.cell(row=r4, column=9, value=rec['monthly_bonus'] if rec['monthly_bonus'] else '')
ws.cell(row=r4, column=10, value=rec['target'])
ws.cell(row=r4, column=11, value='需确认是否发给新销售')
drw(ws, r4, 11, alt=(i%2==1))
ws.row_dimensions[r4].height = 22
r4 += 1
ws.cell(row=r4, column=3, value='合计')
ws.cell(row=r4, column=4, value=round(sum(r['amount'] for r in trf_recs), 2))
for c in range(1, 12):
ws.cell(row=r4, column=c).fill = SUB_FILL; ws.cell(row=r4, column=c).border = BORD
ws.cell(row=r4, column=c).font = BOLD_FNT; ws.cell(row=r4, column=c).alignment = CC
for i, w in enumerate([14, 12, 14, 14, 10, 26, 8, 12, 10, 22, 22], 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ── Save ──
out = '离职人员6月奖金处理明细.xlsx'
wb.save(out)
print(f'\n{out} ({len(wb.sheetnames)} sheets)')
print(f'\n汇总:')
for name in ['赵连飞', '伍仲文', '岑彦']:
recs = [p for p in detail_records if p['name'] == name]
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
trf = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
print(f' {name}: 不发了={not_pay:.2f}{RESIGNATIONS[name]["transfer_to"]}={trf:.2f} 合计={not_pay+trf:.2f}')
print(f' 总计: 不发了={total_not:.2f} 转新销售={total_trf:.2f}')
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
用本地GPS数据(G7日里程 + 东方北斗月里程)替换125辆测试车辆的实际行驶里程。
测试里程 = TBOX - GPS(负数为0)。
"""
import openpyxl, os, shutil, xlrd
from datetime import datetime, date, timedelta
from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年6月 (1).xlsx'
DST = '租赁任务考核_2026年6月.xlsx'
GPS_DIR = 'gps里程汇总2026年1-6月 3'
# ========== 读取125辆测试车辆清单 ==========
TEST_FILE = '/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-07/65b41c10aa83cabb7a365b2b8437c1c7/6月测试车辆.xlsx'
wb_test = openpyxl.load_workbook(TEST_FILE, data_only=True)
ws_test = wb_test.active
TEST_PLATES = set()
for row in ws_test.iter_rows(min_row=2, values_only=True):
for ci in range(len(row)):
p = str(row[ci]).strip() if row[ci] else ''
if p and p != 'None' and len(p) >= 7:
TEST_PLATES.add(p)
wb_test.close()
print(f"测试车辆清单: {len(TEST_PLATES)}")
# ========== Step 1: 加载G7 GPS日里程 ==========
print("\n加载G7 GPS日里程...")
g7_wb = openpyxl.load_workbook(f'{GPS_DIR}/G7/202606_G7.xlsx')
g7_ws = g7_wb.active
gps_daily = {} # plate -> [day1, day2, ..., day30]
for row in g7_ws.iter_rows(min_row=2, values_only=True):
plate = str(row[0]).strip() if row[0] else ''
if not plate or plate not in TEST_PLATES:
continue
daily = [float(row[c]) if row[c] else 0 for c in range(3, 33)]
gps_daily[plate] = daily
g7_wb.close()
print(f"G7覆盖: {len(gps_daily)}辆测试车")
# ========== Step 2: 加载东方北斗GPS月里程 ==========
print("加载东方北斗GPS月里程...")
df_wb = openpyxl.load_workbook(f'{GPS_DIR}/东方北斗/202606_东方北斗.xlsx')
df_ws = df_wb.active
gps_monthly = {} # plate -> monthly_total_km
for row in df_ws.iter_rows(min_row=4, values_only=True):
plate = str(row[5]).strip() if row[5] else ''
if not plate or plate not in TEST_PLATES:
continue
monthly = float(row[10]) if row[10] else 0
gps_monthly[plate] = monthly
df_wb.close()
print(f"东方北斗覆盖: {len(gps_monthly)}辆测试车")
# ========== Step 3: 读取源文件,执行GPS替换 ==========
print("\n执行GPS替换...")
# 备份
BAK = '租赁任务考核_2026年6月_备份_0715.xlsx'
if os.path.exists(DST):
shutil.copy2(DST, BAK)
shutil.copy2(SRC, DST)
wb = openpyxl.load_workbook(DST)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
print(f"源文件: {ws.max_row}行, {len(h)}")
# 读取所有记录
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
r['row_num'] = len(recs) + 2
recs.append(r)
test_recs = [r for r in recs if r['车牌号'] in TEST_PLATES]
print(f"测试车辆记录: {len(test_recs)}条 ({len(set(r['车牌号'] for r in test_recs))}辆)")
# 添加列
if '原TBOX里程(km)' not in h:
tbox_new_col = ws.max_column + 1
test_new_col = ws.max_column + 2
ws.cell(row=1, column=tbox_new_col, value='原TBOX里程(km)')
ws.cell(row=1, column=test_new_col, value='测试里程(km)')
h_new = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
else:
h_new = h
actual_idx = h_new.index('实际行驶里程(km)') + 1
tbox_idx = h_new.index('原TBOX里程(km)') + 1
test_idx = h_new.index('测试里程(km)') + 1
rate_idx = h_new.index('完成率(%)') + 1
qualified_idx = h_new.index('是否达标') + 1
yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
replaced_g7 = 0
replaced_df = 0
no_gps = 0
total_test_km = 0
total_gps_km = 0
total_tbox_km = 0
for r in test_recs:
plate = r['车牌号']
rn = r['row_num']
# 获取考核区间
start_date = r.get('考核开始日期')
end_date = r.get('考核结束日期')
gps_total = None
source = None
# 优先用G7日数据
if plate in gps_daily:
daily = gps_daily[plate]
try:
sd = start_date.date() if hasattr(start_date, 'date') else datetime.strptime(str(start_date)[:10], '%Y-%m-%d').date()
ed = end_date.date() if hasattr(end_date, 'date') else datetime.strptime(str(end_date)[:10], '%Y-%m-%d').date()
except:
sd = date(2026, 6, 1)
ed = date(2026, 6, 30)
gps_total = 0
for day_offset in range(30):
day = date(2026, 6, 1) + timedelta(days=day_offset)
if sd <= day <= ed:
gps_total += daily[day_offset]
gps_total = round(gps_total, 1)
source = 'G7'
replaced_g7 += 1
# 其次用东方北斗月里程
elif plate in gps_monthly:
# 东方北斗只有月度合计,考核区间一般都是整月
gps_total = gps_monthly[plate]
source = '东方北斗'
replaced_df += 1
if gps_total is None:
no_gps += 1
continue
# 原TBOX里程 = 当前实际行驶里程
tbox_val = float(ws.cell(row=rn, column=actual_idx).value or 0)
# 测试里程 = TBOX - GPS(负数为0
test_km = max(0, round(tbox_val - gps_total, 1))
# 写入
ws.cell(row=rn, column=tbox_idx, value=tbox_val)
ws.cell(row=rn, column=test_idx, value=test_km)
ws.cell(row=rn, column=actual_idx, value=gps_total)
# 标记黄色
for c in range(1, ws.max_column + 1):
ws.cell(row=rn, column=c).fill = yellow_fill
# 重算完成率
target = float(r.get('应考核里程(km)') or 0)
pct = round(gps_total / target * 100, 2) if target > 0 else 0
qualified = '达标' if gps_total >= target and target > 0 else '未达标'
ws.cell(row=rn, column=rate_idx, value=pct)
ws.cell(row=rn, column=qualified_idx, value=qualified)
total_test_km += test_km
total_gps_km += gps_total
total_tbox_km += tbox_val
print(f"\n替换统计:")
print(f" G7日数据: {replaced_g7}")
print(f" 东方北斗月数据: {replaced_df}")
print(f" 无GPS数据: {no_gps}")
print(f" TBOX总里程: {total_tbox_km:.1f}")
print(f" GPS总里程: {total_gps_km:.1f}")
print(f" 测试总里程: {total_test_km:.1f}")
# ========== Step 4: TBOX回填(非测试车辆) ==========
print("\nTBOX回填(非测试车辆)...")
filled = 0
for row_idx in range(2, ws.max_row + 1):
tbox_val = ws.cell(row=row_idx, column=tbox_idx).value
actual_val = ws.cell(row=row_idx, column=actual_idx).value
test_val = ws.cell(row=row_idx, column=test_idx).value
if (tbox_val is None or float(tbox_val) == 0):
if test_val is None or float(test_val) == 0:
ws.cell(row=row_idx, column=tbox_idx, value=float(actual_val) if actual_val else 0)
filled += 1
print(f"TBOX回填: {filled}")
# ========== Step 5: 列序排版 ==========
print("\n列序排版...")
target_order = [
'车牌号', '考核年份', '考核月份', '部门名称', '销售经理', '客户名称',
'合同编号', '项目名称', '考核目标ID', '考核目标', '考核开始日期', '考核结束日期',
'考核天数', '应考核里程(km)', '原TBOX里程(km)', '实际行驶里程(km)', '测试里程(km)',
'完成率(%)', '是否达标', '考核状态', '交车日期', '预计还车日期', '实际还车日期'
]
h_cur = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
all_rows = []
for row_idx in range(1, ws.max_row + 1):
row_data = {}
for c in range(1, ws.max_column + 1):
row_data[h_cur[c-1]] = ws.cell(row=row_idx, column=c).value
all_rows.append(row_data)
# Clear
for r in range(1, ws.max_row + 1):
for c in range(1, ws.max_column + 1):
ws.cell(row=r, column=c, value=None)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(name='宋体', size=10, bold=True, color='FFFFFF')
data_font = Font(name='宋体', size=10)
for i, col_name in enumerate(target_order):
c = i + 1
cell = ws.cell(row=1, column=c, value=col_name)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center', vertical='center')
cell.border = thin_border
for row_idx, row_data in enumerate(all_rows[1:], start=2):
for i, col_name in enumerate(target_order):
c = i + 1
val = row_data.get(col_name)
cell = ws.cell(row=row_idx, column=c, value=val)
cell.font = data_font
cell.alignment = Alignment(vertical='center')
cell.border = thin_border
if col_name in ['考核开始日期', '考核结束日期', '交车日期', '预计还车日期', '实际还车日期']:
cell.number_format = 'yyyy-mm-dd h:mm:ss'
if row_data.get('测试里程(km)') and float(row_data.get('测试里程(km)', 0) or 0) != 0:
cell.fill = yellow_fill
for i in range(len(target_order)):
ws.column_dimensions[get_column_letter(i+1)].width = 15
wb.save(DST)
wb.close()
print(f"列序排版: {len(target_order)}")
print(f"\n{'='*60}")
print(f"GPS替换完成!")
print(f"测试车辆: {len(TEST_PLATES)}辆 -> 源数据{len(test_recs)}条记录")
print(f"GPS来源: G7({replaced_g7}条) + 东方北斗({replaced_df}条)")
print(f"测试总里程: {total_test_km:.1f} km")
print(f"已保存: {DST}")
print(f"{'='*60}")
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""
从本地GPS数据(G7)整合6月GPS里程,替换测试车辆实际行驶里程
测试里程 = TBOX总里程 - GPS总里程(负数为0)
重新核算6月里程考核,输出到 ./20260713_1500/
"""
import openpyxl, os, shutil
from datetime import datetime, date
from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
os.chdir('/Users/kkfluous/Downloads')
SRC = '租赁任务考核_2026年6月 (1).xlsx'
DST = '租赁任务考核_2026年6月.xlsx'
GPS_DIR = 'gps里程汇总2026年1-6月 3'
# 36辆测试车辆
TEST_PLATES = {
'粤AGE5480','粤AGF4535','粤AGG3490','粤AGG4135','粤AGG4191',
'粤AGP2033','粤AGP3080','粤AGP3663','粤AGP3676','粤AGP5139',
'粤AGP5156','粤AGP5163','粤AGP5168','粤AGP5636','粤AGP5767',
'粤AGP5768','粤AGP5796','粤AGP9733','粤AGP9786','粤AGR0298',
'粤AGR1586','粤AGR3288','粤AGR5056','粤AGR5068','粤AGR5088',
'粤AGR6869','粤AGR6877','粤AGR6879','粤AGR6881','粤AGR8506',
'粤AGR8551','粤AGR8558','粤AGR8799','粤AGR9866','粤AGR9893',
'粤AGR9899',
}
# ========== Step 1: 加载G7 GPS日里程数据 ==========
print("=" * 60)
print("Step 1: 加载G7 GPS日里程数据")
print("=" * 60)
g7_wb = openpyxl.load_workbook(f'{GPS_DIR}/G7/202606_G7.xlsx')
g7_ws = g7_wb.active
# G7格式: 车牌号(0), 机构(1), 行驶里程(2), 06月01日(3)...06月30日(32), 运行时长(33), 数据完整度说明(34)
gps_daily = {} # plate -> [day1_km, day2_km, ..., day30_km]
gps_monthly = {} # plate -> monthly_total_km
no_data_plates = set()
for row in g7_ws.iter_rows(min_row=2, values_only=True):
plate = str(row[0]).strip() if row[0] else ''
if not plate:
continue
monthly_km = float(row[2]) if row[2] else 0
daily = []
for c in range(3, 33): # 06月01日 to 06月30日
daily.append(float(row[c]) if row[c] else 0)
gps_daily[plate] = daily
gps_monthly[plate] = monthly_km
g7_wb.close()
print(f"G7加载完成: {len(gps_daily)}辆车")
# ========== Step 2: 读取源文件,执行GPS替换 ==========
print("\n" + "=" * 60)
print("Step 2: GPS里程替换")
print("=" * 60)
# 备份现有文件
BAK = '租赁任务考核_2026年6月_备份_0715.xlsx'
if os.path.exists(DST):
shutil.copy2(DST, BAK)
print(f'已备份: {BAK}')
# 从原始新文件开始
shutil.copy2(SRC, DST)
print(f'源文件: {SRC}{DST}')
wb = openpyxl.load_workbook(DST)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
print(f'列数: {len(h)}')
# 读取所有记录
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
r['row_num'] = len(recs) + 2
recs.append(r)
# 找测试车辆记录
test_recs = [r for r in recs if r['车牌号'] in TEST_PLATES]
print(f'测试车辆记录: {len(test_recs)}条 ({len(set(r["车牌号"] for r in test_recs))}辆)')
# 添加 原TBOX里程(km) 和 测试里程(km) 列
if '原TBOX里程(km)' not in h:
tbox_new_col = ws.max_column + 1
test_new_col = ws.max_column + 2
ws.cell(row=1, column=tbox_new_col, value='原TBOX里程(km)')
ws.cell(row=1, column=test_new_col, value='测试里程(km)')
h_new = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
else:
h_new = h
actual_idx = h_new.index('实际行驶里程(km)') + 1
tbox_idx = h_new.index('原TBOX里程(km)') + 1
test_idx = h_new.index('测试里程(km)') + 1
rate_idx = h_new.index('完成率(%)') + 1
qualified_idx = h_new.index('是否达标') + 1
yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
DAYSM = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30}
replaced = 0
no_gps = 0
no_gps_plates = set()
for r in test_recs:
plate = r['车牌号']
rn = r['row_num']
# 获取考核起止日期
start_date = r.get('考核开始日期')
end_date = r.get('考核结束日期')
if plate not in gps_daily:
no_gps += 1
no_gps_plates.add(plate)
continue
daily = gps_daily[plate]
# 计算考核区间内的GPS总里程
if start_date and end_date:
try:
sd = start_date.date() if hasattr(start_date, 'date') else datetime.strptime(str(start_date)[:10], '%Y-%m-%d').date()
ed = end_date.date() if hasattr(end_date, 'date') else datetime.strptime(str(end_date)[:10], '%Y-%m-%d').date()
except:
sd = date(2026, 6, 1)
ed = date(2026, 6, 30)
else:
sd = date(2026, 6, 1)
ed = date(2026, 6, 30)
# 汇总考核区间内的GPS日里程(G7日数据索引: 0=6/1, 29=6/30
gps_total = 0
for day_offset in range(30):
day = date(2026, 6, 1) + date.resolution * day_offset # timedelta
from datetime import timedelta
day = date(2026, 6, 1) + timedelta(days=day_offset)
if sd <= day <= ed:
gps_total += daily[day_offset]
gps_total = round(gps_total, 1)
# 原TBOX里程 = 当前实际行驶里程
tbox_val = float(ws.cell(row=rn, column=actual_idx).value or 0)
# 测试里程 = TBOX - GPS(负数为0
test_km = max(0, round(tbox_val - gps_total, 1))
# 写入
ws.cell(row=rn, column=tbox_idx, value=tbox_val)
ws.cell(row=rn, column=test_idx, value=test_km)
ws.cell(row=rn, column=actual_idx, value=gps_total)
# 标记黄色
for c in range(1, ws.max_column + 1):
ws.cell(row=rn, column=c).fill = yellow_fill
# 重算完成率
target = float(r.get('应考核里程(km)') or 0)
pct = round(gps_total / target * 100, 2) if target > 0 else 0
qualified = '达标' if gps_total >= target and target > 0 else '未达标'
ws.cell(row=rn, column=rate_idx, value=pct)
ws.cell(row=rn, column=qualified_idx, value=qualified)
replaced += 1
print(f" {plate}: TBOX={tbox_val}, GPS={gps_total}, 测试={test_km}, 达标={qualified}")
print(f'\nGPS替换: 成功{replaced}条, 无GPS数据{no_gps}')
if no_gps_plates:
print(f'无GPS数据车牌: {sorted(no_gps_plates)}')
# ========== Step 3: TBOX回填(非测试车辆) ==========
print("\n" + "=" * 60)
print("Step 3: TBOX回填(非测试车辆)")
print("=" * 60)
filled = 0
for row_idx in range(2, ws.max_row + 1):
tbox_val = ws.cell(row=row_idx, column=tbox_idx).value
actual_val = ws.cell(row=row_idx, column=actual_idx).value
test_val = ws.cell(row=row_idx, column=test_idx).value
if (tbox_val is None or float(tbox_val) == 0):
if test_val is None or float(test_val) == 0:
ws.cell(row=row_idx, column=tbox_idx, value=float(actual_val) if actual_val else 0)
filled += 1
print(f'TBOX回填: {filled}')
# ========== Step 4: 列序排版 ==========
print("\n" + "=" * 60)
print("Step 4: 列序排版")
print("=" * 60)
target_order = [
'车牌号', '考核年份', '考核月份', '部门名称', '销售经理', '客户名称',
'合同编号', '项目名称', '考核目标ID', '考核目标', '考核开始日期', '考核结束日期',
'考核天数', '应考核里程(km)', '原TBOX里程(km)', '实际行驶里程(km)', '测试里程(km)',
'完成率(%)', '是否达标', '考核状态', '交车日期', '预计还车日期', '实际还车日期'
]
# Read all data with styles
h_cur = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
all_rows = []
for row_idx in range(1, ws.max_row + 1):
row_data = {}
for c in range(1, ws.max_column + 1):
row_data[h_cur[c-1]] = ws.cell(row=row_idx, column=c).value
all_rows.append(row_data)
# Clear sheet
for r in range(1, ws.max_row + 1):
for c in range(1, ws.max_column + 1):
ws.cell(row=r, column=c, value=None)
# Apply styles
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(name='宋体', size=10, bold=True, color='FFFFFF')
data_font = Font(name='宋体', size=10)
# Write headers
for i, col_name in enumerate(target_order):
c = i + 1
cell = ws.cell(row=1, column=c, value=col_name)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center', vertical='center')
cell.border = thin_border
# Write data
for row_idx, row_data in enumerate(all_rows[1:], start=2):
for i, col_name in enumerate(target_order):
c = i + 1
val = row_data.get(col_name)
cell = ws.cell(row=row_idx, column=c, value=val)
cell.font = data_font
cell.alignment = Alignment(vertical='center')
cell.border = thin_border
if col_name in ['考核开始日期', '考核结束日期', '交车日期', '预计还车日期', '实际还车日期']:
cell.number_format = 'yyyy-mm-dd h:mm:ss'
# Restore yellow fill for test vehicles
if row_data.get('测试里程(km)') and float(row_data.get('测试里程(km)', 0) or 0) != 0:
cell.fill = yellow_fill
for i in range(len(target_order)):
ws.column_dimensions[get_column_letter(i+1)].width = 15
wb.save(DST)
wb.close()
print(f'列序排版完成 ({len(target_order)}列)')
print(f'已保存: {DST}')
print("\n" + "=" * 60)
print("GPS替换完成!接下来运行 main.py...")
print("=" * 60)
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>小羚羚小程序</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html, body {
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
margin-left: auto;
margin-right: auto;
height: 100%;
min-height: 100vh;
overflow: visible;
}
body.is-element-page #root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<script>
(function () {
var isComponent = false;
if (document.body && (isComponent || window.location.pathname.includes('/components/'))) {
document.body.classList.add('is-element-page');
}
})();
</script>
<div id="root"></div>
<script>window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};</script>
<script src="./assets/react.production.min.js"></script>
<script src="./assets/react-dom.production.min.js"></script>
<script src="./assets/export-html-bootstrap.js"></script>
<script>
function waitForBootstrap(timeoutMs) {
timeoutMs = timeoutMs || 10000;
return new Promise(function (resolve, reject) {
var startedAt = Date.now();
function check() {
if (window.HtmlTemplateBootstrap) {
resolve(window.HtmlTemplateBootstrap);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
reject(new Error('[Html Template] Bootstrap 初始化超时'));
return;
}
setTimeout(check, 10);
}
check();
});
}
function loadEntryScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.async = false;
script.onload = function () { resolve(); };
script.onerror = function () { reject(new Error('[Html Template] 入口脚本加载失败: ' + src)); };
document.body.appendChild(script);
});
}
async function bootstrapAndRender() {
try {
var bootstrap = await waitForBootstrap();
var renderComponent = bootstrap.renderComponent;
window.React = bootstrap.React;
window.ReactDOM = bootstrap.ReactDOM;
await loadEntryScript('./index.js');
window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};
var Component = window.UserComponent && (window.UserComponent.Component || window.UserComponent.default || window.UserComponent);
if (!Component) {
throw new Error('[Html Template] 入口脚本未暴露 UserComponent');
}
renderComponent(Component);
} catch (error) {
console.error('[Html Template] 页面初始化失败:', error);
}
}
bootstrapAndRender();
</script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>小羚羚小程序</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html, body {
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
margin-left: auto;
margin-right: auto;
height: 100%;
min-height: 100vh;
overflow: visible;
}
body.is-element-page #root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<script>
(function () {
var isComponent = false;
if (document.body && (isComponent || window.location.pathname.includes('/components/'))) {
document.body.classList.add('is-element-page');
}
})();
</script>
<div id="root"></div>
<script>window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};</script>
<script src="./assets/react.production.min.js"></script>
<script src="./assets/react-dom.production.min.js"></script>
<script src="./assets/export-html-bootstrap.js"></script>
<script>
function waitForBootstrap(timeoutMs) {
timeoutMs = timeoutMs || 10000;
return new Promise(function (resolve, reject) {
var startedAt = Date.now();
function check() {
if (window.HtmlTemplateBootstrap) {
resolve(window.HtmlTemplateBootstrap);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
reject(new Error('[Html Template] Bootstrap 初始化超时'));
return;
}
setTimeout(check, 10);
}
check();
});
}
function loadEntryScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.async = false;
script.onload = function () { resolve(); };
script.onerror = function () { reject(new Error('[Html Template] 入口脚本加载失败: ' + src)); };
document.body.appendChild(script);
});
}
async function bootstrapAndRender() {
try {
var bootstrap = await waitForBootstrap();
var renderComponent = bootstrap.renderComponent;
window.React = bootstrap.React;
window.ReactDOM = bootstrap.ReactDOM;
await loadEntryScript('./index.js');
window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};
var Component = window.UserComponent && (window.UserComponent.Component || window.UserComponent.default || window.UserComponent);
if (!Component) {
throw new Error('[Html Template] 入口脚本未暴露 UserComponent');
}
renderComponent(Component);
} catch (error) {
console.error('[Html Template] 页面初始化失败:', error);
}
}
bootstrapAndRender();
</script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>小羚羚小程序</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html, body {
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
margin-left: auto;
margin-right: auto;
height: 100%;
min-height: 100vh;
overflow: visible;
}
body.is-element-page #root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<script>
(function () {
var isComponent = false;
if (document.body && (isComponent || window.location.pathname.includes('/components/'))) {
document.body.classList.add('is-element-page');
}
})();
</script>
<div id="root"></div>
<script>window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};</script>
<script src="./assets/react.production.min.js"></script>
<script src="./assets/react-dom.production.min.js"></script>
<script src="./assets/export-html-bootstrap.js"></script>
<script>
function waitForBootstrap(timeoutMs) {
timeoutMs = timeoutMs || 10000;
return new Promise(function (resolve, reject) {
var startedAt = Date.now();
function check() {
if (window.HtmlTemplateBootstrap) {
resolve(window.HtmlTemplateBootstrap);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
reject(new Error('[Html Template] Bootstrap 初始化超时'));
return;
}
setTimeout(check, 10);
}
check();
});
}
function loadEntryScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.async = false;
script.onload = function () { resolve(); };
script.onerror = function () { reject(new Error('[Html Template] 入口脚本加载失败: ' + src)); };
document.body.appendChild(script);
});
}
async function bootstrapAndRender() {
try {
var bootstrap = await waitForBootstrap();
var renderComponent = bootstrap.renderComponent;
window.React = bootstrap.React;
window.ReactDOM = bootstrap.ReactDOM;
await loadEntryScript('./index.js');
window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};
var Component = window.UserComponent && (window.UserComponent.Component || window.UserComponent.default || window.UserComponent);
if (!Component) {
throw new Error('[Html Template] 入口脚本未暴露 UserComponent');
}
renderComponent(Component);
} catch (error) {
console.error('[Html Template] 页面初始化失败:', error);
}
}
bootstrapAndRender();
</script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>小羚羚小程序</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html, body {
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
margin-left: auto;
margin-right: auto;
height: 100%;
min-height: 100vh;
overflow: visible;
}
body.is-element-page #root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<script>
(function () {
var isComponent = false;
if (document.body && (isComponent || window.location.pathname.includes('/components/'))) {
document.body.classList.add('is-element-page');
}
})();
</script>
<div id="root"></div>
<script>window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};</script>
<script src="./assets/react.production.min.js"></script>
<script src="./assets/react-dom.production.min.js"></script>
<script src="./assets/export-html-bootstrap.js"></script>
<script>
function waitForBootstrap(timeoutMs) {
timeoutMs = timeoutMs || 10000;
return new Promise(function (resolve, reject) {
var startedAt = Date.now();
function check() {
if (window.HtmlTemplateBootstrap) {
resolve(window.HtmlTemplateBootstrap);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
reject(new Error('[Html Template] Bootstrap 初始化超时'));
return;
}
setTimeout(check, 10);
}
check();
});
}
function loadEntryScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.async = false;
script.onload = function () { resolve(); };
script.onerror = function () { reject(new Error('[Html Template] 入口脚本加载失败: ' + src)); };
document.body.appendChild(script);
});
}
async function bootstrapAndRender() {
try {
var bootstrap = await waitForBootstrap();
var renderComponent = bootstrap.renderComponent;
window.React = bootstrap.React;
window.ReactDOM = bootstrap.ReactDOM;
await loadEntryScript('./index.js');
window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};
var Component = window.UserComponent && (window.UserComponent.Component || window.UserComponent.default || window.UserComponent);
if (!Component) {
throw new Error('[Html Template] 入口脚本未暴露 UserComponent');
}
renderComponent(Component);
} catch (error) {
console.error('[Html Template] 页面初始化失败:', error);
}
}
bootstrapAndRender();
</script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>小羚羚小程序</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html, body {
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
margin-left: auto;
margin-right: auto;
height: 100%;
min-height: 100vh;
overflow: visible;
}
body.is-element-page #root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<script>
(function () {
var isComponent = false;
if (document.body && (isComponent || window.location.pathname.includes('/components/'))) {
document.body.classList.add('is-element-page');
}
})();
</script>
<div id="root"></div>
<script>window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};</script>
<script src="./assets/react.production.min.js"></script>
<script src="./assets/react-dom.production.min.js"></script>
<script src="./assets/export-html-bootstrap.js"></script>
<script>
function waitForBootstrap(timeoutMs) {
timeoutMs = timeoutMs || 10000;
return new Promise(function (resolve, reject) {
var startedAt = Date.now();
function check() {
if (window.HtmlTemplateBootstrap) {
resolve(window.HtmlTemplateBootstrap);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
reject(new Error('[Html Template] Bootstrap 初始化超时'));
return;
}
setTimeout(check, 10);
}
check();
});
}
function loadEntryScript(src) {
return new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.async = false;
script.onload = function () { resolve(); };
script.onerror = function () { reject(new Error('[Html Template] 入口脚本加载失败: ' + src)); };
document.body.appendChild(script);
});
}
async function bootstrapAndRender() {
try {
var bootstrap = await waitForBootstrap();
var renderComponent = bootstrap.renderComponent;
window.React = bootstrap.React;
window.ReactDOM = bootstrap.ReactDOM;
await loadEntryScript('./index.js');
window.__AXHUB_ANNOTATION_SOURCE_REFERENCE__={root:"source",manifest:"source/manifest.json"};
var Component = window.UserComponent && (window.UserComponent.Component || window.UserComponent.default || window.UserComponent);
if (!Component) {
throw new Error('[Html Template] 入口脚本未暴露 UserComponent');
}
renderComponent(Component);
} catch (error) {
console.error('[Html Template] 页面初始化失败:', error);
}
}
bootstrapAndRender();
</script>
</body>
</html>
+369
View File
@@ -0,0 +1,369 @@
#!/usr/bin/env python3
"""
6月 18T 绩效考核 — 方案3 (800/300 当月, 800结转, 500补发/累计)
"""
import os, openpyxl, unicodedata
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
DAYS = {1:31, 2:28, 3:31, 4:30, 5:31, 6:30}
TARGET_KM = 6000
BASE_GOOD = 800 # 当月达标
BASE_BAD = 300 # 当月未达标
BASE_CARRY = 800 # 结转
BASE_OTHER = 500 # 补发/累计补发
OUTPUT_DIR = '/Users/kkfluous/Downloads/6月18T测算'
# ========== 数据读取 ==========
def read_18t(filepath, month):
wb = openpyxl.load_workbook(filepath, data_only=True)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
if r.get('考核目标') != '羚牛100辆18T':
continue
r['考核天数'] = float(r['考核天数'] or 0)
r['应考核里程(km)'] = float(r['应考核里程(km)'] or 0)
r['实际行驶里程(km)'] = float(r['实际行驶里程(km)'] or 0)
r['月份'] = month
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
recs.append(r)
wb.close()
return recs
def grp(recs):
gs = defaultdict(lambda: {
'recs': [], '应考核': 0, '实际': 0, '奖金': 0, '天数': 0,
'有达标': False, '部门': '', '销售': '', '车牌': ''
})
for r in recs:
k = (r['车牌号'], r['销售经理'])
g = gs[k]
g['recs'].append(r)
g['应考核'] += r['应考核里程(km)']
g['实际'] += r['实际行驶里程(km)']
g['奖金'] += r.get('奖金', 0)
g['天数'] += r['考核天数']
if r['是否达标'] == '达标':
g['有达标'] = True
g['部门'] = r['部门名称']
g['销售'] = r['销售经理']
g['车牌'] = r['车牌号']
return dict(gs)
# ========== 计算 ==========
def calc():
D = {}
for m in range(1, 7):
fp = f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
D[m] = read_18t(fp, m)
# 给每条记录算奖金
for m in D:
for r in D[m]:
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
if q:
r['奖金'] = BASE_GOOD * (r['考核天数'] / DAYS[m])
else:
r['奖金'] = BASE_BAD * (r['考核天数'] / DAYS[m])
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
G = {m: grp(D[m]) for m in D}
# 1月: 初始化
for k, g in G[1].items():
cum_q = g['实际'] >= g['应考核'] and g['应考核'] > 0
cum_over = max(0, g['实际'] - g['应考核']) if cum_q else 0
g['可结转'] = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
g['累计已发结转'] = 0
g['1月已发'] = g['有达标']
g['cum_t'] = g['应考核']
g['cum_a'] = g['实际']
g['cum_q'] = cum_q
# 逐月计算 2→6
for month in range(2, 7):
if month not in G:
continue
prev_m = month - 1
days_m = DAYS[month]
for k, gm in G[month].items():
gp = G[prev_m].get(k)
cum_t = (gp['cum_t'] if gp else 0) + gm['应考核']
cum_a = (gp['cum_a'] if gp else 0) + gm['实际']
cum_q = cum_a >= cum_t and cum_t > 0
# 结转: 800
carry = 0
p_carry = gp['可结转'] if gp else 0
if gp and p_carry >= 1:
carry = BASE_CARRY
# 当月奖金
gm['当月奖金'] = gm['奖金'] if carry == 0 else 0
# 补发历史月: 500
for back_m in range(1, month):
g_bm = G[back_m].get(k)
if not g_bm:
continue
bm_paid = g_bm.get(f'{back_m}月已发', g_bm['有达标'])
if bm_paid:
continue
bm_paid_later = False
for later_m in range(back_m + 1, month):
gl = G[later_m].get(k)
if gl and gl.get(f'补发{back_m}', 0) > 0:
bm_paid_later = True
break
if bm_paid_later:
continue
if cum_q:
bj = BASE_OTHER * (g_bm['天数'] / DAYS[back_m])
gm[f'补发{back_m}'] = bj
# 累计补发当月: 500
cbp = 0
if not gm['有达标'] and carry == 0 and cum_q:
cbp = BASE_OTHER * (gm['天数'] / days_m)
gm['累计补发当月'] = cbp
gm['cum_t'] = cum_t
gm['cum_a'] = cum_a
gm['cum_q'] = cum_q
gm['结转'] = carry
gm[f'{month}月已发'] = carry > 0 or gm['有达标'] or cbp > 0
# 更新可结转
cum_over = max(0, cum_a - cum_t) if cum_q else 0
earned = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
prev_used = gp['累计已发结转'] if gp else 0
cur_used = prev_used + (1 if carry > 0 else 0)
gm['累计已发结转'] = cur_used
gm['可结转'] = max(0, earned - cur_used)
# 补充: 上月有结转但本月无考核记录的车
for k, gp in G[prev_m].items():
if k not in G[month] and gp.get('可结转', 0) >= 1:
G[month][k] = {
'recs': [], '应考核': TARGET_KM, '实际': 0, '奖金': 0, '天数': days_m,
'有达标': False, '部门': gp['部门'], '销售': gp['销售'], '车牌': k[0],
'cum_t': gp.get('cum_t', 0) + TARGET_KM, 'cum_a': gp.get('cum_a', 0),
'cum_q': False,
'结转': BASE_CARRY, '当月奖金': 0,
f'{month}月已发': True,
'可结转': max(0, gp['可结转'] - 1),
'累计已发结转': gp.get('累计已发结转', 0) + 1,
'虚拟': True,
}
# 收集6月发放记录
pay_records = collect_payments(G, 6)
return D[6], G[6], pay_records
def collect_payments(G, month):
G_m = G[month]
records = []
for k, g in sorted(G_m.items()):
if g.get('虚拟'):
continue
carry = g.get('结转', 0)
if carry > 0:
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
'金额': carry, '类型': '结转(800)'})
bonus = g.get('当月奖金', 0)
if bonus > 0:
ptype = '当月达标(800)' if g['有达标'] else '当月未达标(300)'
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
'金额': bonus, '类型': ptype})
cbp = g.get('累计补发当月', 0)
if cbp > 0:
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
'金额': cbp, '类型': f'累计补发{month}月(500)'})
for bm in range(1, month):
bv = g.get(f'补发{bm}', 0)
if bv > 0:
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
'金额': bv, '类型': f'补发{bm}月(500)'})
records.sort(key=lambda x: (x['车牌'], x['类型']))
return records
# ========== Excel输出 ==========
def set_widths(ws, widths):
for i, w in enumerate(widths, 1):
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = w
def write_xlsx(recs_6, G6, pay_records, filepath):
wb = openpyxl.Workbook()
# Sheet 1: 规则
ws = wb.active
ws.title = '考核规则'
ws.append(['6月 18T 绩效考核 — 方案3'])
ws.append(['考核目标', '羚牛100辆18T'])
ws.append(['月度目标里程', '6000km'])
ws.append(['当月达标', '800元 × (考核天数/30天)'])
ws.append(['当月未达标', '300元 × (考核天数/30天)'])
ws.append(['结转奖励', '800元/次 (消耗1个可结转月)'])
ws.append(['补发历史月', '500元 × (历史月天数/历史总天数)'])
ws.append(['累计补发当月', '500元 × (当月天数/30天)'])
ws.append(['规则', '当月达标800/未达标300 → 结转800 → 补发500 → 累计补发500'])
ws.append(['亏损筛选', '6月无盈亏表,不管亏损全发'])
set_widths(ws, [30, 60])
# Sheet 2: 里程明细
ws2 = wb.create_sheet('6月里程明细')
ws2.append(['车牌号', '销售经理', '部门', '客户', '考核天数', '应考核里程', '实际里程', '是否达标', '月度目标'])
for r in recs_6:
ws2.append([r['车牌号'], r['销售经理'], r.get('部门名称', ''),
r.get('客户名称', ''), r['考核天数'], r['应考核里程(km)'],
r['实际行驶里程(km)'], r['是否达标'], TARGET_KM])
set_widths(ws2, [14, 10, 30, 35, 10, 12, 12, 10, 10])
# Sheet 3: 计算过程
ws3 = wb.create_sheet('6月计算过程')
ws3.append(['车牌号', '销售经理', '部门', '6月应考核', '6月实际', '6月达标',
'累计应考核', '累计实际', '累计达标', '可结转', '结转发放(800)',
'当月奖金', '累计补发6月(500)', '补发1月', '补发2月', '补发3月', '补发4月', '补发5月',
'6月总发放'])
for k, g in sorted(G6.items()):
if g.get('虚拟'):
continue
cum_q_str = '' if g.get('cum_q') else ''
dq_str = '' if g['有达标'] else ''
total = (g.get('结转', 0) + g.get('当月奖金', 0) + g.get('累计补发当月', 0) +
sum(g.get(f'补发{m}', 0) for m in range(1, 6)))
ws3.append([k[0], k[1], g['部门'], g['应考核'], g['实际'], dq_str,
g.get('cum_t', ''), g.get('cum_a', ''), cum_q_str,
g.get('可结转', 0), g.get('结转', 0),
g.get('当月奖金', 0), g.get('累计补发当月', 0),
g.get('补发1月', 0), g.get('补发2月', 0), g.get('补发3月', 0),
g.get('补发4月', 0), g.get('补发5月', 0), total])
set_widths(ws3, [14, 10, 30, 12, 12, 10, 12, 12, 10, 10, 14, 12, 16, 10, 10, 10, 10, 10, 14])
# Sheet 4: 发放明细
ws4 = wb.create_sheet('6月发放明细')
ws4.append(['车牌号', '销售经理', '部门', '金额', '发放类型'])
for r in pay_records:
ws4.append([r['车牌'], r['销售'], r['部门'], round(r['金额'], 2), r['类型']])
total = sum(r['金额'] for r in pay_records)
ws4.append([])
ws4.append(['', '', '合计', round(total, 2), ''])
set_widths(ws4, [14, 10, 30, 12, 22])
# Sheet 5: 汇总
ws5 = wb.create_sheet('汇总')
ws5.append(['6月18T 方案3 汇总 (800/300, 结转800, 补发500)'])
ws5.append(['发放类型', '辆数', '金额合计'])
cat_sum, cat_cnt = defaultdict(float), defaultdict(int)
for r in pay_records:
cat_sum[r['类型']] += r['金额']
cat_cnt[r['类型']] += 1
all_cats = ['当月达标(800)', '当月未达标(300)', '结转(800)', '累计补发6月(500)',
'补发1月(500)', '补发2月(500)', '补发3月(500)', '补发4月(500)', '补发5月(500)']
for cat in all_cats:
if cat in cat_sum:
ws5.append([cat, cat_cnt[cat], round(cat_sum[cat], 2)])
ws5.append(['合计', sum(cat_cnt.values()), round(sum(cat_sum.values()), 2)])
set_widths(ws5, [22, 10, 15])
wb.save(filepath)
print(f'{filepath}')
# ========== 主流程 ==========
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
print('计算6月18T方案3...')
recs_6, G6, pay_records = calc()
# 统计
total_groups = sum(1 for g in G6.values() if not g.get('虚拟'))
good_groups = sum(1 for g in G6.values() if g['有达标'] and not g.get('虚拟'))
bad_groups = sum(1 for g in G6.values() if not g['有达标'] and not g.get('虚拟'))
carry_groups = sum(1 for g in G6.values() if g.get('结转', 0) > 0)
print(f' 6月18T组数: {total_groups}, 达标: {good_groups}, 未达标: {bad_groups}, 结转: {carry_groups}')
# 分类汇总
def cats(dl, *keys):
cnt = sum(sum(1 for r in dl if r['类型']==k) for k in keys)
amt = sum(sum(r['金额'] for r in dl if r['类型']==k) for k in keys)
return cnt, amt
def cell(amt, cnt):
if cnt == 0: return ''
return f'{amt:,.2f} ({cnt}辆)'
rows = [
('当月达标(800)', cats(pay_records, '当月达标(800)')),
('当月未达标(300)', cats(pay_records, '当月未达标(300)')),
('结转(800)', cats(pay_records, '结转(800)')),
('累计补发6月(500)', cats(pay_records, '累计补发6月(500)')),
('补发1月(500)', cats(pay_records, '补发1月(500)')),
('补发2月(500)', cats(pay_records, '补发2月(500)')),
('补发3月(500)', cats(pay_records, '补发3月(500)')),
('补发4月(500)', cats(pay_records, '补发4月(500)')),
('补发5月(500)', cats(pay_records, '补发5月(500)')),
]
grand_total = sum(r['金额'] for r in pay_records)
def vw(s):
return sum(2 if unicodedata.east_asian_width(c) in ('F','W') else 1 for c in str(s))
def padr(s, w):
return ' ' * (w - vw(s)) + str(s)
def padl(s, w):
return str(s) + ' ' * (w - vw(s))
W0, W1 = 17, 26
print()
print(' 6月 18T 绩效考核 — 方案3 (800/300, 结转800, 补发500)')
print(f"{''*W0}{''*W1}")
for label, (cnt, amt) in rows:
if amt > 0 or ('累计补发' in label) or ('补发' in label):
c = cell(amt, cnt)
else:
c = cell(amt, cnt)
if amt > 0 or cnt > 0:
print(f"{padl(label,W0)}{padr(c,W1)}")
# separator
print(f"{''*W0}{''*W1}")
print(f"{padl('合计',W0)}{padr(f'{grand_total:,.2f}',W1)}")
print(f"{''*W0}{''*W1}")
# 写Excel
write_xlsx(recs_6, G6, pay_records, os.path.join(OUTPUT_DIR, '6月18T_方案3.xlsx'))
print(f"\n文件输出到: {OUTPUT_DIR}/")
if __name__ == '__main__':
main()
+84 -40
View File
@@ -1,40 +1,69 @@
#!/usr/bin/env python3
"""按月生成独立Excel文件 - 里程考核绩效(含亏损筛选)"""
"""按月生成独立Excel文件 - 里程考核绩效,支持1-7月"""
import os, openpyxl
from calc_engine import *
from excel_writer import *
os.chdir('/Users/kkfluous/Downloads')
JULY_SOURCE = os.environ.get('JULY_SOURCE', os.path.join('7月核算结果', '租赁任务考核_2026年7月.xlsx'))
JULY_OUTPUT_DIR = os.environ.get('JULY_OUTPUT_DIR', '7月核算结果')
# 读取
# 读取1-7月;7月优先读取核算结果目录内已完成GPS干预的源表
print("读取源数据...")
D = {1: read_file('租赁任务考核_2026年1月.xlsx', 1),
2: read_file('租赁任务考核_2026年2月.xlsx', 2),
3: read_file('租赁任务考核_2026年3月.xlsx', 3)}
for m in [1,2,3]: print(f" {m}月: {len(D[m])}")
months = []
for m in range(1, 8):
fp = JULY_SOURCE if m == 7 else f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
months.append(m)
D = {}
for m in months:
fp = JULY_SOURCE if m == 7 else f'租赁任务考核_2026年{m}月.xlsx'
D[m] = read_file(fp, m)
print(f" {m}月: {len(D[m])}")
# 分组+计算
G = {m: grp(D[m], m) for m in [1,2,3]}
# 分组
G = {m: grp(D[m], m) for m in months}
# 1月初始化
calc_jan_carryover(G[1])
feb_data = calc_feb(G[1], G[2])
mar_data = calc_mar(G[1], G[2], G[3], feb_data)
all_data = {}
all_data[1] = {'当月': [{'车牌':k[0],'销售':g['销售'],'部门':g['部门'],'':g['奖金']}
for k,g in G[1].items() if g['奖金'] > 0]}
# 逐月计算: 2→7
for m in range(2, 8):
if m not in G:
continue
data_m, G[m] = calc_month(m, G, all_data, G[m-1], G[m])
# 钱婧已离职:从2026年7月起所有结转、当月、补发及累计补发均不再发放。
all_data[7] = {
cat: [item for item in items if item.get('销售') != '钱婧']
for cat, items in all_data[7].items()
}
# 发放追踪
vehicle_payments, vehicle_info = collect_vehicle_payments(G, feb_data, mar_data)
vehicle_payments, vehicle_info = collect_vehicle_payments(G, all_data)
master_vehicles = read_master_vehicles()
# 亏损表
print("\n读取亏损表...")
loss_data = {}
for m in [1,2,3]:
for m in months:
ld = read_loss_data(m)
if ld:
loss_clients = sum(1 for v in ld.values() if v == '')
print(f" {m}月: {len(ld)}个客户, 其中亏损{loss_clients}")
loss_data[m] = ld
else:
print(f" {m}月: 无亏损表")
loss_data[m] = None
# 无亏损表的月份不管亏损全发 → 构造全'否'的代理
from collections import defaultdict as dd
ld_tmp = dd(lambda: '')
for r in D[m]:
c = r.get('客户名称')
if c: ld_tmp[c] = ''
print(f" {m}月: 无亏损表,按全部不亏损处理(全发)")
loss_data[m] = ld_tmp
# 车牌→客户名称映射
plate_client = get_vehicle_client_map(D)
@@ -46,27 +75,23 @@ for m in G:
all_persons[g['销售']] = g['部门']
# 打印汇总
jan_total = sum(r['奖金'] for r in D[1] if r['是否达标']=='达标')
feb_total = sum(sum(d[''] for d in v) for v in feb_data.values())
mar_total = sum(sum(d[''] for d in v) for v in mar_data.values())
print(f"\n考核应发: 1月{jan_total:.2f}, 2月{feb_total:.2f}, 3月{mar_total:.2f}")
print(f"Q1考核应发合计: {jan_total + feb_total + mar_total:.2f}")
print("\n考核应发汇总:")
grand = 0
for m in months:
total_m = sum(sum(d[''] for d in v) for v in all_data[m].values())
grand += total_m
print(f" {m}月: {total_m:.2f}")
print(f" 合计: {grand:.2f}")
# ============================================================
# 按月生成
# 按月生成(只生成7月)
# ============================================================
for settle_month in [1, 2, 3]:
for settle_month in [7]:
print(f"\n生成 {settle_month}月核算文件...")
wb = openpyxl.Workbook()
# 当月数据
if settle_month == 1:
month_data = {'当月': [{'车牌':r['车牌号'],'销售':r['销售经理'],'部门':r['部门名称'],'':r['奖金']}
for r in D[1] if r['是否达标']=='达标']}
elif settle_month == 2:
month_data = feb_data
else:
month_data = mar_data
month_data = all_data[settle_month]
# Sheet 1: 考核奖励规则
write_rules_sheet(wb)
@@ -75,31 +100,50 @@ for settle_month in [1, 2, 3]:
write_detail_sheet(wb, D[settle_month], settle_month)
# Sheet 3: 计算过程
if settle_month == 1:
write_calc_process_jan(wb, G[1])
elif settle_month == 2:
write_calc_process_feb(wb, G[1], G[2])
else:
write_calc_process_mar(wb, G[1], G[2], G[3], feb_data)
write_calc_process_generic(wb, settle_month, G, all_data)
# Sheet 5: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info, loss_data[settle_month], plate_client)
# Sheet 6: 奖金发放记录(叠加亏损筛选,补发查对应月盈亏)
payment_records = build_payment_records(settle_month, month_data, loss_data, plate_client)
# 不做任何亏损拦截,全发
class _AllPass:
def get(self, _k, _d=None): return ''
def __bool__(self): return True
payment_records = build_payment_records(settle_month, month_data, {m: _AllPass() for m in months}, plate_client)
write_payment_record_sheet(wb, settle_month, payment_records)
# Sheet 7: 月汇总(从发放记录生成)
write_summary_from_records(wb, settle_month, payment_records)
# Sheet 7: 月汇总。离职人员仅当月奖金转给现销售,其余结转/补发不发;
# 发放记录和个人明细仍保留原归属,方便审计。
resignation_transfer = {'赵连飞': '刘念念', '伍仲文': '钟祥', '岑彦': '钟祥'}
summary_records = []
for rec in payment_records:
item = dict(rec)
sales = item.get('业务员') or item.get('销售人员') or item.get('销售')
ptype = item.get('发放类型') or item.get('类型')
if sales in resignation_transfer:
if ptype != '当月':
continue
new_sales = resignation_transfer[sales]
if '业务员' in item: item['业务员'] = new_sales
if '销售人员' in item: item['销售人员'] = new_sales
if '销售' in item: item['销售'] = new_sales
summary_records.append(item)
write_summary_from_records(wb, settle_month, summary_records)
# Sheet 6-17: 业务员
# 业务员sheets
for person in sorted(all_persons.keys()):
write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments)
# 删除默认空sheet
if 'Sheet' in wb.sheetnames:
del wb['Sheet']
fname = f'里程任务考核_{settle_month}月核算.xlsx'
for old in ['二部-赵连飞', '六部-伍仲文', '六部-岑彦', '三部-钱婧']:
if old in wb.sheetnames:
wb[old].title = f'【已离职】{old}'
wb.move_sheet(wb[f'【已离职】{old}'], offset=len(wb.sheetnames))
os.makedirs(JULY_OUTPUT_DIR, exist_ok=True)
fname = os.path.join(JULY_OUTPUT_DIR, f'里程任务考核_{settle_month}月核算.xlsx')
wb.save(fname)
print(f"{fname} ({len(wb.sheetnames)} sheets)")
+741
View File
@@ -0,0 +1,741 @@
#!/usr/bin/env python3
"""
5月 18T 绩效三种测算方案
方案1: 保留原逻辑 (1000元基准, 含结转/补发/累计)
方案2: 新测算简化版 (800/300, 仅当月, 无结转/补发/累计)
方案3: 新测算完整版 (800/300/500, 含结转/补发/累计)
"""
import os, openpyxl
from collections import defaultdict
from copy import deepcopy
os.chdir('/Users/kkfluous/Downloads')
DAYS = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31}
TARGET_KM = 6000
OUTPUT_DIR = '/Users/kkfluous/Downloads/5月18T测算'
# ============================================================
# 数据读取
# ============================================================
def read_18t(filepath, month):
"""只读羚牛100辆18T记录"""
wb = openpyxl.load_workbook(filepath, data_only=True)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
if r.get('考核目标') != '羚牛100辆18T':
continue
r['考核天数'] = float(r.get('考核天数') or 0)
r['应考核里程(km)'] = float(r.get('应考核里程(km)') or 0)
r['实际行驶里程(km)'] = float(r.get('实际行驶里程(km)') or 0)
r['月份'] = month
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
recs.append(r)
wb.close()
return recs
def grp(recs):
"""按(车牌号, 销售经理)分组"""
gs = defaultdict(lambda: {
'recs': [], '应考核': 0, '实际': 0, '奖金': 0, '天数': 0,
'有达标': False, '目标km': TARGET_KM,
'部门': '', '销售': '', '车牌': ''
})
for r in recs:
k = (r['车牌号'], r['销售经理'])
g = gs[k]
g['recs'].append(r)
g['应考核'] += r['应考核里程(km)']
g['实际'] += r['实际行驶里程(km)']
g['奖金'] += r.get('奖金', 0)
g['天数'] += r['考核天数']
if r['是否达标'] == '达标':
g['有达标'] = True
g['部门'] = r['部门名称']
g['销售'] = r['销售经理']
g['车牌'] = r['车牌号']
return dict(gs)
# ============================================================
# 方案1: 原逻辑 (base=1000)
# ============================================================
def calc_s1():
"""方案1: 保留原逻辑,base=1000。只算18T"""
BASE = 1000
D = {}
for m in range(1, 6):
fp = f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
D[m] = read_18t(fp, m)
# 给每条记录加上奖励额并计算奖金
for m in D:
for r in D[m]:
r['奖励额'] = BASE
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
r['奖金'] = BASE * (r['考核天数'] / DAYS[m]) if q else 0
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
G = {m: grp(D[m]) for m in D}
# 1月: 计算可结转 + 初始化累计
for k, g in G[1].items():
cum_q = g['实际'] >= g['应考核'] and g['应考核'] > 0
cum_over = max(0, g['实际'] - g['应考核']) if cum_q else 0
g['可结转'] = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
g['累计已发结转'] = 0
g['1月已发'] = g['奖金'] > 0
g['奖励额'] = BASE
g['cum_t'] = g['应考核']
g['cum_a'] = g['实际']
g['cum_q'] = cum_q
# 用于追踪每个月的发放
all_data = {1: {'结转': [], '当月': []}}
# 用于追踪每个月的发放
all_data = {1: {'结转': [], '当月': []}}
for k, g in G[1].items():
if g['奖金'] > 0:
all_data[1]['当月'].append({'车牌': k[0], '销售': g['销售'], '部门': g['部门'], '': g['奖金']})
# 逐月计算: 2→5
for month in range(2, 6):
if month not in G:
continue
data_m = {'结转': [], '当月': []}
prev_m = month - 1
days_m = DAYS[month]
for k, gm in G[month].items():
gp = G[prev_m].get(k)
bf = BASE
# 累计
cum_t = (gp['cum_t'] if gp else 0) + gm['应考核']
cum_a = (gp['cum_a'] if gp else 0) + gm['实际']
cum_q = cum_a >= cum_t and cum_t > 0
# 结转
carry = 0
p_carry = gp['可结转'] if gp else 0
if gp and p_carry >= 1:
carry = bf
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gm['部门'], '': carry})
# 当月达标
bonus = 0
if gm['有达标'] and carry == 0:
bonus = gm['奖金']
data_m['当月'].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '': bonus})
# 补发历史月
for back_m in range(1, month):
label = f'补发{back_m}'
if label not in data_m:
data_m[label] = []
for back_m in range(1, month):
label = f'补发{back_m}'
g_bm = G[back_m].get(k)
if not g_bm:
continue
# 检查该月是否已发
bm_paid = g_bm.get(f'{back_m}月已发', g_bm['奖金'] > 0)
if bm_paid:
continue
# 检查后续月是否已补发
bm_paid_later = False
for later_m in range(back_m + 1, month):
gl = G[later_m].get(k)
if gl and gl.get(f'补发{back_m}', 0) > 0:
bm_paid_later = True
break
if bm_paid_later:
continue
if cum_q:
bj = g_bm['奖励额'] * (g_bm['天数'] / DAYS[back_m])
data_m[label].append({'车牌': k[0], '销售': g_bm['销售'], '部门': gm['部门'], '': bj})
gm[f'补发{back_m}'] = bj
# 累计补发当月
label_cbp = f'累计补发{month}'
if label_cbp not in data_m:
data_m[label_cbp] = []
cbp = 0
if not gm['有达标'] and carry == 0 and cum_q:
cbp = bf * (gm['天数'] / days_m)
data_m[label_cbp].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '': cbp})
gm['cum_t'] = cum_t
gm['cum_a'] = cum_a
gm['cum_q'] = cum_q
gm['结转'] = carry
gm['当月奖金'] = bonus
gm['累计补发当月'] = cbp
gm['奖励额'] = BASE
gm[f'{month}月已发'] = carry > 0 or bonus > 0 or cbp > 0
# 更新可结转
cum_over = max(0, cum_a - cum_t) if cum_q else 0
earned = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
prev_used = gp['累计已发结转'] if gp else 0
cur_used = prev_used + (1 if carry > 0 else 0)
gm['累计已发结转'] = cur_used
gm['可结转'] = max(0, earned - cur_used)
# 补充: 上个月有结转但本月无考核记录的车
for k, gp in G[prev_m].items():
if k not in G[month] and gp.get('可结转', 0) >= 1:
carry = BASE
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gp['部门'], '': carry})
G[month][k] = {
'recs': [], '应考核': TARGET_KM, '实际': 0, '奖金': 0, '天数': days_m,
'有达标': False, '目标km': TARGET_KM, '部门': gp['部门'], '销售': gp['销售'],
'车牌': k[0], '奖励额': BASE,
'cum_t': gp.get('cum_t', 0) + TARGET_KM, 'cum_a': gp.get('cum_a', 0),
'cum_q': False,
'结转': carry, '当月奖金': 0,
f'{month}月已发': True,
'可结转': max(0, gp['可结转'] - 1),
'累计已发结转': gp.get('累计已发结转', 0) + 1,
'虚拟': True,
}
for bm in range(1, month):
G[month][k][f'补发{bm}'] = 0
all_data[month] = data_m
# 收集5月发放明细
pay_records = collect_payments_s1(G, all_data)
return D[5], G[5], all_data.get(5, {}), pay_records
def collect_payments_s1(G, all_data):
"""收集5月结算的所有发放记录"""
records = []
m5 = all_data.get(5, {})
for cat, items in m5.items():
for d in items:
records.append({
'车牌': d['车牌'], '销售': d['销售'], '部门': d.get('部门', ''),
'金额': d[''], '类型': cat, '结算月': 5
})
records.sort(key=lambda x: (x['车牌'], x['类型']))
return records
# ============================================================
# 方案2: 简化版 (800/300, 仅当月)
# ============================================================
def calc_s2():
"""方案2: 仅看5月, 达标800, 未达标300, 无结转/补发/累计。按组统计(同方案1)"""
BASE_GOOD = 800
BASE_BAD = 300
fp = '租赁任务考核_2026年5月.xlsx'
recs = read_18t(fp, 5)
# 给每条记录算好奖金
for r in recs:
if r['是否达标'] == '达标':
r['奖金'] = BASE_GOOD * (r['考核天数'] / DAYS[5])
else:
r['奖金'] = BASE_BAD * (r['考核天数'] / DAYS[5])
# 按(车牌+销售)分组, 每组一笔 (同方案1)
G5 = grp(recs)
pay_records = []
for k, g in sorted(G5.items()):
total = g['奖金'] # 组内达标+未达标奖金合计
ptype = '当月达标(800)' if g['有达标'] else '当月未达标(300)'
pay_records.append({
'车牌': k[0], '销售': k[1], '部门': g['部门'],
'金额': total, '类型': ptype
})
pay_records.sort(key=lambda x: (x['车牌'], x['销售']))
return recs, pay_records
# ============================================================
# 方案3: 完整版 (800/300, 结转800, 补发/累计500)
# ============================================================
def calc_s3():
"""方案3: 当月达标800, 当月未达标300, 结转800, 补发/累计补发500"""
BASE_GOOD = 800
BASE_BAD = 300
BASE_CARRY = 800
BASE_OTHER = 500
D = {}
for m in range(1, 6):
fp = f'租赁任务考核_2026年{m}月.xlsx'
if os.path.exists(fp):
D[m] = read_18t(fp, m)
# 给每条记录加上奖励额和当月奖金(按新标准)
for m in D:
for r in D[m]:
r['奖励额'] = BASE_GOOD # 当月达标基准
r['奖励额_未达标'] = BASE_BAD # 当月未达标基准
r['奖励额_其他'] = BASE_OTHER # 结转/补发/累计基准
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
if q:
r['奖金'] = BASE_GOOD * (r['考核天数'] / DAYS[m])
else:
r['奖金'] = BASE_BAD * (r['考核天数'] / DAYS[m])
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
G = {m: grp(D[m]) for m in D}
# 1月: 计算可结转 + 初始化累计
for k, g in G[1].items():
g['奖励额'] = BASE_GOOD
g['奖励额_其他'] = BASE_OTHER
cum_q = g['实际'] >= g['应考核'] and g['应考核'] > 0
cum_over = max(0, g['实际'] - g['应考核']) if cum_q else 0
g['可结转'] = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
g['累计已发结转'] = 0
# 方案3: 1月已发 = 有达标 (仅300未达标不算)
g['1月已发'] = g['有达标']
g['cum_t'] = g['应考核']
g['cum_a'] = g['实际']
g['cum_q'] = cum_q
all_data = {1: {'结转': [], '当月达标(800)': [], '当月未达标(300)': []}}
for k, g in G[1].items():
# 按组统计: 有达标→当月达标(合并金额), 全未达标→当月未达标
if g['有达标']:
all_data[1]['当月达标(800)'].append({'车牌': k[0], '销售': g['销售'], '部门': g['部门'], '': g['奖金']})
elif g['奖金'] > 0:
all_data[1]['当月未达标(300)'].append({'车牌': k[0], '销售': g['销售'], '部门': g['部门'], '': g['奖金']})
# 逐月计算: 2→5
for month in range(2, 6):
if month not in G:
continue
data_m = {'结转': [], '当月达标(800)': [], '当月未达标(300)': []}
prev_m = month - 1
days_m = DAYS[month]
for k, gm in G[month].items():
gp = G[prev_m].get(k)
# 累计
cum_t = (gp['cum_t'] if gp else 0) + gm['应考核']
cum_a = (gp['cum_a'] if gp else 0) + gm['实际']
cum_q = cum_a >= cum_t and cum_t > 0
# 结转: 800 (整月)
carry = 0
p_carry = gp['可结转'] if gp else 0
if gp and p_carry >= 1:
carry = BASE_CARRY
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gm['部门'], '': carry})
# 当月奖金: 按组统计(同方案1), 有达标→当月达标, 全未达标→当月未达标
if carry == 0 and gm['奖金'] > 0:
ptype = '当月达标(800)' if gm['有达标'] else '当月未达标(300)'
data_m[ptype].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '': gm['奖金']})
# 补发历史月: 500基准
for back_m in range(1, month):
label = f'补发{back_m}'
if label not in data_m:
data_m[label] = []
g_bm = G[back_m].get(k)
if not g_bm:
continue
# 方案3: 历史月已发 = 有达标/已结转/已补发/已累计补发. 仅拿300未达标不算已发.
bm_paid = g_bm.get(f'{back_m}月已发', g_bm['有达标'])
if bm_paid:
continue
bm_paid_later = False
for later_m in range(back_m + 1, month):
gl = G[later_m].get(k)
if gl and gl.get(f'补发{back_m}', 0) > 0:
bm_paid_later = True
break
if bm_paid_later:
continue
if cum_q:
bj = BASE_OTHER * (g_bm['天数'] / DAYS[back_m])
data_m[label].append({'车牌': k[0], '销售': g_bm['销售'], '部门': gm['部门'], '': bj})
gm[f'补发{back_m}'] = bj
# 累计补发当月: 500基准
label_cbp = f'累计补发{month}'
if label_cbp not in data_m:
data_m[label_cbp] = []
cbp = 0
if not gm['有达标'] and carry == 0 and cum_q:
cbp = BASE_OTHER * (gm['天数'] / days_m)
data_m[label_cbp].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '': cbp})
gm['cum_t'] = cum_t
gm['cum_a'] = cum_a
gm['cum_q'] = cum_q
gm['结转'] = carry
gm['当月奖金'] = gm['奖金'] if carry == 0 else 0
gm['累计补发当月'] = cbp
gm['奖励额'] = BASE_GOOD
gm['奖励额_其他'] = BASE_OTHER
# 方案3: 已发 = 结转/当月有达标/累计补发 (仅300未达标不算已发,可后续补发)
gm[f'{month}月已发'] = carry > 0 or gm['有达标'] or cbp > 0
# 更新可结转
cum_over = max(0, cum_a - cum_t) if cum_q else 0
earned = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
prev_used = gp['累计已发结转'] if gp else 0
cur_used = prev_used + (1 if carry > 0 else 0)
gm['累计已发结转'] = cur_used
gm['可结转'] = max(0, earned - cur_used)
# 补充: 上个月有结转但本月无考核记录的车
for k, gp in G[prev_m].items():
if k not in G[month] and gp.get('可结转', 0) >= 1:
carry = BASE_CARRY
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gp['部门'], '': carry})
G[month][k] = {
'recs': [], '应考核': TARGET_KM, '实际': 0, '奖金': 0, '天数': days_m,
'有达标': False, '目标km': TARGET_KM, '部门': gp['部门'], '销售': gp['销售'],
'车牌': k[0], '奖励额': BASE_GOOD, '奖励额_其他': BASE_OTHER,
'cum_t': gp.get('cum_t', 0) + TARGET_KM, 'cum_a': gp.get('cum_a', 0),
'cum_q': False,
'结转': carry, '当月奖金': 0,
f'{month}月已发': True,
'可结转': max(0, gp['可结转'] - 1),
'累计已发结转': gp.get('累计已发结转', 0) + 1,
'虚拟': True,
}
for bm in range(1, month):
G[month][k][f'补发{bm}'] = 0
all_data[month] = data_m
pay_records = collect_payments_s3(G, all_data)
return D[5], G[5], all_data.get(5, {}), pay_records
def collect_payments_s3(G, all_data):
"""收集方案3的5月发放记录"""
records = []
m5 = all_data.get(5, {})
for cat, items in m5.items():
for d in items:
records.append({
'车牌': d['车牌'], '销售': d['销售'], '部门': d.get('部门', ''),
'金额': d[''], '类型': cat, '结算月': 5
})
records.sort(key=lambda x: (x['车牌'], x['类型']))
return records
# ============================================================
# Excel 输出
# ============================================================
def write_xlsx_s1(recs_5, G5, data5, pay_records, filepath):
"""方案1输出"""
wb = openpyxl.Workbook()
# Sheet 1: 规则
ws = wb.active
ws.title = '考核规则'
ws.append(['方案1: 保留原逻辑'])
ws.append(['考核目标', '羚牛100辆18T'])
ws.append(['月度目标里程', '6000km'])
ws.append(['奖励金额(达标)', '1000元/月 (按考核天数/当月天数折算)'])
ws.append(['结转奖励', '1000元/次 (消耗1个月目标)'])
ws.append(['补发/累计补发', '1000元/月 (按考核天数/当月天数折算)'])
ws.append(['规则', '达标→1000, 结转→1000, 补发→1000×折算, 累计补发当月→1000×折算'])
set_col_widths(ws, [30, 60])
# Sheet 2: 5月里程明细
write_mileage_detail(wb, recs_5, '5月里程明细')
# Sheet 3: 计算过程
ws3 = wb.create_sheet('5月计算过程')
ws3.append(['车牌号', '销售经理', '部门', '5月应考核', '5月实际', '5月达标',
'累计应考核', '累计实际', '累计达标', '可结转', '结转发放',
'当月奖金', '累计补发5月', '补发1月', '补发2月', '补发3月', '补发4月',
'5月总发放'])
for k, g in sorted(G5.items()):
if g.get('虚拟'):
continue
cum_q_str = '' if g.get('cum_q') else ''
dq_str = '' if g['有达标'] else ''
total = (g.get('结转', 0) + g.get('当月奖金', 0) + g.get('累计补发当月', 0) +
g.get('补发1月', 0) + g.get('补发2月', 0) + g.get('补发3月', 0) +
g.get('补发4月', 0))
ws3.append([k[0], k[1], g['部门'], g['应考核'], g['实际'], dq_str,
g.get('cum_t', ''), g.get('cum_a', ''), cum_q_str,
g.get('可结转', 0), g.get('结转', 0),
g.get('当月奖金', 0), g.get('累计补发当月', 0),
g.get('补发1月', 0), g.get('补发2月', 0), g.get('补发3月', 0),
g.get('补发4月', 0), total])
set_col_widths(ws3, [14, 10, 30, 12, 12, 10, 12, 12, 10, 10, 12, 12, 14, 12, 12, 12, 12, 14])
# Sheet 4: 发放明细
ws4 = wb.create_sheet('5月发放明细')
ws4.append(['车牌号', '销售经理', '部门', '金额', '发放类型'])
for r in pay_records:
ws4.append([r['车牌'], r['销售'], r['部门'], round(r['金额'], 2), r['类型']])
# 汇总行
total = sum(r['金额'] for r in pay_records)
ws4.append([])
ws4.append(['', '', '合计', round(total, 2), ''])
set_col_widths(ws4, [14, 10, 30, 12, 20])
# Sheet 5: 汇总
ws5 = wb.create_sheet('汇总')
ws5.append(['方案1: 原逻辑 (1000元基准)'])
ws5.append(['发放类型', '辆数', '金额合计'])
from collections import defaultdict
cat_sum = defaultdict(float)
cat_cnt = defaultdict(int)
for r in pay_records:
cat_sum[r['类型']] += r['金额']
cat_cnt[r['类型']] += 1
for cat in ['当月', '结转', '累计补发5月', '补发1月', '补发2月', '补发3月', '补发4月']:
if cat in cat_sum:
ws5.append([cat, cat_cnt[cat], round(cat_sum[cat], 2)])
ws5.append(['合计', sum(cat_cnt.values()), round(sum(cat_sum.values()), 2)])
set_col_widths(ws5, [20, 10, 15])
wb.save(filepath)
print(f'{filepath}')
def write_xlsx_s2(recs_5, pay_records, filepath):
"""方案2输出"""
wb = openpyxl.Workbook()
# Sheet 1: 规则
ws = wb.active
ws.title = '考核规则'
ws.append(['方案2: 新测算简化版 (仅当月)'])
ws.append(['考核目标', '羚牛100辆18T'])
ws.append(['月度目标里程', '6000km'])
ws.append(['当月达标', '800元 × (考核天数/31天)'])
ws.append(['当月未达标', '300元 × (考核天数/31天)'])
ws.append(['结转/补发/累计', '不计算'])
ws.append(['规则', '只看5月当月,按(车牌+销售)分组统计'])
set_col_widths(ws, [30, 60])
# Sheet 2: 5月里程明细
write_mileage_detail(wb, recs_5, '5月里程明细')
# Sheet 3: 发放明细
ws3 = wb.create_sheet('5月发放明细')
ws3.append(['车牌号', '销售经理', '部门', '金额', '发放类型'])
for r in pay_records:
ws3.append([r['车牌'], r['销售'], r['部门'], round(r['金额'], 2), r['类型']])
total = sum(r['金额'] for r in pay_records)
ws3.append([])
ws3.append(['', '', '合计', round(total, 2), ''])
set_col_widths(ws3, [14, 10, 30, 12, 20])
# Sheet 4: 汇总
ws4 = wb.create_sheet('汇总')
ws4.append(['方案2: 简化版 (800/300, 仅当月)'])
ws4.append(['发放类型', '辆数', '金额合计'])
cat_sum = defaultdict(float)
cat_cnt = defaultdict(int)
for r in pay_records:
cat_sum[r['类型']] += r['金额']
cat_cnt[r['类型']] += 1
for cat in sorted(cat_sum.keys()):
ws4.append([cat, cat_cnt[cat], round(cat_sum[cat], 2)])
ws4.append(['合计', sum(cat_cnt.values()), round(sum(cat_sum.values()), 2)])
set_col_widths(ws4, [25, 10, 15])
wb.save(filepath)
print(f'{filepath}')
def write_xlsx_s3(recs_5, G5, data5, pay_records, filepath):
"""方案3输出"""
wb = openpyxl.Workbook()
# Sheet 1: 规则
ws = wb.active
ws.title = '考核规则'
ws.append(['方案3: 新测算完整版 (800/300/500, 结转800 补发500)'])
ws.append(['考核目标', '羚牛100辆18T'])
ws.append(['月度目标里程', '6000km'])
ws.append(['当月达标', '800元 × (考核天数/31天)'])
ws.append(['当月未达标', '300元 × (考核天数/31天)'])
ws.append(['结转奖励', '800元/次 (消耗1个月目标)'])
ws.append(['补发历史月', '500元 × (历史月考核天数/历史月天数)'])
ws.append(['累计补发当月', '500元 × (当月考核天数/31天)'])
ws.append(['规则', '当月达标800/未达标300 → 结转800 → 补发500 → 累计补发500'])
set_col_widths(ws, [30, 60])
# Sheet 2: 5月里程明细
write_mileage_detail(wb, recs_5, '5月里程明细')
# Sheet 3: 计算过程
ws3 = wb.create_sheet('5月计算过程')
ws3.append(['车牌号', '销售经理', '部门', '5月应考核', '5月实际', '5月达标',
'累计应考核', '累计实际', '累计达标', '可结转', '结转发放(800)',
'当月奖金', '累计补发5月(500)', '补发1月', '补发2月', '补发3月', '补发4月',
'5月总发放'])
for k, g in sorted(G5.items()):
if g.get('虚拟'):
continue
cum_q_str = '' if g.get('cum_q') else ''
dq_str = '' if g['有达标'] else ''
total = (g.get('结转', 0) + g.get('当月奖金', 0) + g.get('累计补发当月', 0) +
g.get('补发1月', 0) + g.get('补发2月', 0) + g.get('补发3月', 0) +
g.get('补发4月', 0))
ws3.append([k[0], k[1], g['部门'], g['应考核'], g['实际'], dq_str,
g.get('cum_t', ''), g.get('cum_a', ''), cum_q_str,
g.get('可结转', 0), g.get('结转', 0),
g.get('当月奖金', 0), g.get('累计补发当月', 0),
g.get('补发1月', 0), g.get('补发2月', 0), g.get('补发3月', 0),
g.get('补发4月', 0), total])
set_col_widths(ws3, [14, 10, 30, 12, 12, 10, 12, 12, 10, 10, 14, 12, 16, 12, 12, 12, 12, 14])
# Sheet 4: 发放明细
ws4 = wb.create_sheet('5月发放明细')
ws4.append(['车牌号', '销售经理', '部门', '金额', '发放类型'])
for r in pay_records:
ws4.append([r['车牌'], r['销售'], r['部门'], round(r['金额'], 2), r['类型']])
total = sum(r['金额'] for r in pay_records)
ws4.append([])
ws4.append(['', '', '合计', round(total, 2), ''])
set_col_widths(ws4, [14, 10, 30, 12, 20])
# Sheet 5: 汇总
ws5 = wb.create_sheet('汇总')
ws5.append(['方案3: 完整版 (800/300/500)'])
ws5.append(['发放类型', '辆数', '金额合计'])
cat_sum = defaultdict(float)
cat_cnt = defaultdict(int)
for r in pay_records:
cat_sum[r['类型']] += r['金额']
cat_cnt[r['类型']] += 1
for cat in ['当月达标(800)', '当月未达标(300)', '结转', '累计补发5月', '补发1月', '补发2月', '补发3月', '补发4月']:
if cat in cat_sum:
ws5.append([cat, cat_cnt[cat], round(cat_sum[cat], 2)])
ws5.append(['合计', sum(cat_cnt.values()), round(sum(cat_sum.values()), 2)])
set_col_widths(ws5, [20, 10, 15])
wb.save(filepath)
print(f'{filepath}')
def write_mileage_detail(wb, recs, sheet_name):
"""里程明细sheet"""
ws = wb.create_sheet(sheet_name)
ws.append(['车牌号', '销售经理', '部门', '客户', '考核天数', '应考核里程',
'实际里程', '是否达标', '月度目标'])
for r in recs:
ws.append([r['车牌号'], r['销售经理'], r.get('部门名称', ''),
r.get('客户名称', ''), r['考核天数'], r['应考核里程(km)'],
r['实际行驶里程(km)'], r['是否达标'], TARGET_KM])
set_col_widths(ws, [14, 10, 30, 35, 10, 12, 12, 10, 10])
def set_col_widths(ws, widths):
for i, w in enumerate(widths, 1):
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = w
# ============================================================
# 主流程
# ============================================================
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
# 所有计算
recs_5, G5_s1, data5_s1, pay_s1 = calc_s1()
recs_5_s2, pay_s2 = calc_s2()
recs_5_s3, G5_s3, data5_s3, pay_s3 = calc_s3()
# 汇总各方案
import unicodedata
def vw(s):
return sum(2 if unicodedata.east_asian_width(c) in ('F','W') else 1 for c in str(s))
def padr(s, w):
return ' ' * (w - vw(s)) + str(s)
def padl(s, w):
return str(s) + ' ' * (w - vw(s))
def cats(dl, *keys):
cnt = sum(sum(1 for r in dl if r['类型']==k) for k in keys)
amt = sum(sum(r['金额'] for r in dl if r['类型']==k) for k in keys)
return cnt, amt
def cell(amt, cnt):
if cnt == 0: return ''
return f'{amt:,.2f} ({cnt}辆)'
rows = [
('当月达标', cats(pay_s1, '当月'), cats(pay_s2, '当月达标(800)'), cats(pay_s3, '当月达标(800)')),
('当月未达标', cats(pay_s1), cats(pay_s2, '当月未达标(300)'), cats(pay_s3, '当月未达标(300)')),
('结转', cats(pay_s1, '结转'), cats(pay_s2), cats(pay_s3, '结转')),
('累计补发当月', cats(pay_s1, '累计补发5月'), cats(pay_s2), cats(pay_s3, '累计补发5月')),
('补发2月', cats(pay_s1, '补发2月'), cats(pay_s2), cats(pay_s3, '补发2月')),
('补发3月', cats(pay_s1, '补发3月'), cats(pay_s2), cats(pay_s3, '补发3月')),
('补发4月', cats(pay_s1, '补发4月'), cats(pay_s2), cats(pay_s3, '补发4月')),
]
t1 = sum(r['金额'] for r in pay_s1)
t2 = sum(r['金额'] for r in pay_s2)
t3 = sum(r['金额'] for r in pay_s3)
H1, H2, H3 = '方案1: 原逻辑 (1000)', '方案2: 简化 (800/300)', '方案3: 完整 (结转800 补发500)'
W0, W1, W2, W3 = 15, 24, 24, 27
print()
print(' 三方案对比')
print(f"{''*W0}{''*W1}{''*W2}{''*W3}")
print(f"{padl('',W0)}{padl(H1,W1)}{padl(H2,W2)}{padl(H3,W3)}")
for i, (label, vs1, vs2, vs3) in enumerate(rows):
if i == 2:
print(f"{''*W0}{''*W1}{''*W2}{''*W3}")
c1 = cell(vs1[1], vs1[0]); c2 = cell(vs2[1], vs2[0]); c3 = cell(vs3[1], vs3[0])
print(f"{padl(label,W0)}{padr(c1,W1)}{padr(c2,W2)}{padr(c3,W3)}")
print(f"{''*W0}{''*W1}{''*W2}{''*W3}")
print(f"{padl('合计',W0)}{padr(f'{t1:,.2f}',W1)}{padr(f'{t2:,.2f}',W2)}{padr(f'{t3:,.2f}',W3)}")
print(f"{''*W0}{''*W1}{''*W2}{''*W3}")
# 写Excel
print("\n生成Excel文件...")
write_xlsx_s1(recs_5, G5_s1, data5_s1, pay_s1,
os.path.join(OUTPUT_DIR, '方案1_原逻辑_1000基准.xlsx'))
write_xlsx_s2(recs_5_s2, pay_s2,
os.path.join(OUTPUT_DIR, '方案2_简化版_800_300仅当月.xlsx'))
write_xlsx_s3(recs_5_s3, G5_s3, data5_s3, pay_s3,
os.path.join(OUTPUT_DIR, '方案3_完整版_800_300_500.xlsx'))
print(f"文件输出到: {OUTPUT_DIR}/")
if __name__ == '__main__':
main()
+553
View File
@@ -0,0 +1,553 @@
openapi: 3.0.3
info:
title: 车辆数据开放平台 API
version: 1.1.0
license:
name: Proprietary
description: |
向授权合作方开放车辆单日用氢量、单日里程和指定时刻总里程。
appKey 和逐车授权必须完整覆盖查询自然日。
servers:
- url: /
description: 当前服务
tags:
- name: 合作方数据接口
description: 使用 appKey 查询已授权车辆的日统计数据
- name: 开放平台管理
description: 仅车辆数据平台管理员可调用的应用和车辆授权管理接口
paths:
/api/v1/vehicles/hydrogen-consumption/query:
post:
tags: [合作方数据接口]
summary: 查询车辆单日用氢量
operationId: queryDailyHydrogenConsumption
security:
- AppKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VehicleDailyQuery'
example:
plateNumbers: [粤A12345, 粤B67890]
date: '2026-07-01'
responses:
'200':
description: 查询成功;无数据车辆仍保留在结果中
content:
application/json:
schema:
$ref: '#/components/schemas/HydrogenQueryResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/api/v1/vehicles/mileage/query:
post:
tags: [合作方数据接口]
summary: 查询车辆单日里程
operationId: queryDailyMileage
security:
- AppKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VehicleDailyQuery'
example:
plateNumbers: [粤A12345]
date: '2026-07-01'
responses:
'200':
description: 查询成功;无数据车辆仍保留在结果中
content:
application/json:
schema:
$ref: '#/components/schemas/MileageQueryResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/api/v1/vehicles/total-mileage/query:
post:
tags: [合作方数据接口]
summary: 查询指定时刻的车辆总里程
description: |
返回不晚于请求时间的最近一条有效总里程记录、实际记录时间和时间差秒数。
protocol 不传时严格按 GB32960 > YUTONG_MQTT > JT808 选择首个有数据的协议。
GB32960 和 YUTONG_MQTT 为车辆仪表盘或车端累计里程;JT808 为定位终端/GPS侧累计里程。
operationId: queryTotalMileageAtTime
security:
- AppKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TotalMileageQuery'
example:
vin: LA9GG68L2PBAF4790
time: '2026-07-21 09:30:00'
protocol: '32960'
responses:
'200':
description: 查询成功;没有可用记录时返回 NO_DATA
content:
application/json:
schema:
$ref: '#/components/schemas/TotalMileageQueryResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/api/v2/open-platform/apps:
get:
tags: [开放平台管理]
summary: 查询开放平台应用
operationId: listOpenPlatformApps
security:
- AdminBearer: []
responses:
'200':
description: 应用列表
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAppListResponse'
'401':
description: 未登录
'403':
description: 非管理员
post:
tags: [开放平台管理]
summary: 创建应用和 appKey
description: appKey 明文仅在本次响应中返回。
operationId: createOpenPlatformApp
security:
- AdminBearer: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AppInput'
responses:
'200':
description: 创建成功
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAppCreatedResponse'
'400':
description: 参数错误
'401':
description: 未登录
'403':
description: 非管理员
/api/v2/open-platform/apps/{id}:
put:
tags: [开放平台管理]
summary: 更新应用状态和有效期
operationId: updateOpenPlatformApp
security:
- AdminBearer: []
parameters:
- $ref: '#/components/parameters/AppId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AppInput'
responses:
'200':
description: 更新成功
'400':
description: 参数错误
'404':
description: 应用不存在
/api/v2/open-platform/apps/{id}/rotate-key:
post:
tags: [开放平台管理]
summary: 轮换 appKey
description: 旧 Key 立即失效,新 Key 明文仅在本次响应中返回。
operationId: rotateOpenPlatformAppKey
security:
- AdminBearer: []
parameters:
- $ref: '#/components/parameters/AppId'
responses:
'200':
description: 轮换成功
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAppCreatedResponse'
'404':
description: 应用不存在
/api/v2/open-platform/apps/{id}/vehicles:
get:
tags: [开放平台管理]
summary: 查询应用的车辆授权
operationId: listOpenPlatformVehicleGrants
security:
- AdminBearer: []
parameters:
- $ref: '#/components/parameters/AppId'
responses:
'200':
description: 授权列表
'404':
description: 应用不存在
put:
tags: [开放平台管理]
summary: 完整替换车辆授权
operationId: replaceOpenPlatformVehicleGrants
security:
- AdminBearer: []
parameters:
- $ref: '#/components/parameters/AppId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VehicleGrantRequest'
responses:
'200':
description: 替换成功
'400':
description: VIN 或有效期不正确
'404':
description: 应用不存在
components:
securitySchemes:
AppKeyAuth:
type: http
scheme: bearer
bearerFormat: 32-character appKey
description: 32 位无连字符 UUID appKey
AdminBearer:
type: http
scheme: bearer
bearerFormat: Platform session token
description: 车辆数据平台管理员令牌
parameters:
AppId:
name: id
in: path
required: true
schema:
type: integer
format: int64
minimum: 1
responses:
BadRequest:
description: 请求参数不正确
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
invalidDate:
value:
code: INVALID_DATE_FORMAT
message: date格式必须为yyyy-MM-dd
traceId: 4ccf63c4e51d4d4ab9107d931783a53e
Unauthorized:
description: appKey 不存在、停用或过期
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Forbidden:
description: Key 或车辆授权未完整覆盖查询自然日
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
InternalError:
description: 服务内部异常
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
VehicleDailyQuery:
type: object
additionalProperties: false
required: [plateNumbers, date]
properties:
plateNumbers:
type: array
minItems: 1
maxItems: 200
uniqueItems: true
items:
type: string
minLength: 1
maxLength: 32
description: 需要查询的车牌号
date:
type: string
format: date
description: 查询自然日,yyyy-MM-dd
TotalMileageQuery:
type: object
additionalProperties: false
required: [vin, time]
properties:
vin:
type: string
pattern: '^[A-HJ-NPR-Z0-9]{17}$'
description: 已授权车辆 VIN
time:
type: string
pattern: '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$'
description: 请求时刻,北京时间,固定格式 yyyy-MM-dd HH:mm:ss
protocol:
type: string
enum: ['32960', mqtt, '808', GB32960, YUTONG_MQTT, JT808]
description: 可选;不传时按 GB32960 > YUTONG_MQTT > JT808
HydrogenResult:
type: object
required: [plateNumber, date, hydrogenConsumptionKg, status]
properties:
plateNumber:
type: string
date:
type: string
format: date
hydrogenConsumptionKg:
type: number
format: double
nullable: true
description: 单日用氢量,kg;无数据时为 null
status:
$ref: '#/components/schemas/DataStatus'
MileageResult:
type: object
required: [plateNumber, date, dailyMileageKm, status]
properties:
plateNumber:
type: string
date:
type: string
format: date
dailyMileageKm:
type: number
format: double
nullable: true
description: 单日里程,km;无数据时为 null
status:
$ref: '#/components/schemas/DataStatus'
DataStatus:
type: string
enum: [NORMAL, NO_DATA]
HydrogenQueryResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
required: [data]
properties:
data:
type: array
items:
$ref: '#/components/schemas/HydrogenResult'
MileageQueryResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
required: [data]
properties:
data:
type: array
items:
$ref: '#/components/schemas/MileageResult'
TotalMileageResult:
type: object
required: [vin, queryTime, totalMileageKm, selectionPolicy, status]
properties:
vin:
type: string
queryTime:
type: string
description: 请求时刻,北京时间
totalMileageKm:
type: number
format: double
nullable: true
description: 总里程,km;无数据时为 null
protocol:
type: string
enum: [GB32960, YUTONG_MQTT, JT808]
description: 实际采用的采集协议
protocolInput:
type: string
description: 请求中指定的协议原值;未指定时省略
mileageMeaning:
type: string
description: 当前协议总里程的业务含义
recordTime:
type: string
description: 命中的实际采集记录时间,北京时间
timeDifferenceSeconds:
type: integer
format: int64
minimum: 0
description: 请求时间减实际记录时间,单位秒
selectionPolicy:
type: string
example: GB32960 > YUTONG_MQTT > JT808
status:
$ref: '#/components/schemas/DataStatus'
TotalMileageQueryResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
required: [data]
properties:
data:
$ref: '#/components/schemas/TotalMileageResult'
SuccessEnvelope:
type: object
required: [code, message, traceId]
properties:
code:
type: string
enum: [SUCCESS]
message:
type: string
example: success
traceId:
type: string
example: 4ccf63c4e51d4d4ab9107d931783a53e
ErrorResponse:
type: object
required: [code, message, traceId]
properties:
code:
type: string
enum:
- INVALID_REQUEST
- PLATE_NUMBERS_EMPTY
- INVALID_DATE_FORMAT
- UNAUTHORIZED
- FORBIDDEN
- INTERNAL_ERROR
message:
type: string
traceId:
type: string
AppInput:
type: object
additionalProperties: false
required: [name, status, validFrom]
properties:
name:
type: string
maxLength: 96
example: 示例合作方
status:
type: string
enum: [enabled, disabled]
validFrom:
type: string
format: date-time
example: '2026-07-01T00:00:00+08:00'
validTo:
type: string
format: date-time
nullable: true
example: '2027-07-01T00:00:00+08:00'
App:
type: object
required: [id, name, appKeyPrefix, status, validFrom]
properties:
id:
type: integer
format: int64
name:
type: string
appKeyPrefix:
type: string
minLength: 8
maxLength: 8
status:
type: string
enum: [enabled, disabled]
validFrom:
type: string
format: date-time
validTo:
type: string
format: date-time
nullable: true
AppCreated:
allOf:
- $ref: '#/components/schemas/App'
- type: object
required: [appKey]
properties:
appKey:
type: string
minLength: 32
maxLength: 32
pattern: '^[0-9a-f]{32}$'
description: 仅本次响应返回
VehicleGrantRequest:
type: object
additionalProperties: false
required: [vehicles]
properties:
vehicles:
type: array
maxItems: 500
items:
$ref: '#/components/schemas/VehicleGrantInput'
VehicleGrantInput:
type: object
additionalProperties: false
required: [vin, validFrom]
properties:
vin:
type: string
minLength: 17
maxLength: 17
validFrom:
type: string
format: date-time
validTo:
type: string
format: date-time
nullable: true
AdminAppListResponse:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/App'
traceId:
type: string
AdminAppCreatedResponse:
type: object
properties:
data:
$ref: '#/components/schemas/AppCreated'
traceId:
type: string
+235
View File
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""后处理:在核算文件中体现离职转嫁 — 当月转新销售,其他不发了"""
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from copy import copy
import os
os.chdir('/Users/kkfluous/Downloads/20260713')
fp = '里程任务考核_6月核算.xlsx'
wb = openpyxl.load_workbook(fp)
ws = wb['6月汇总']
# ── Styles ──
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10, name='宋体')
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
TITLE_FNT = Font(bold=True, size=12, name='宋体')
NOTE_FNT = Font(size=10, color='C00000', name='宋体')
NORM_FNT = Font(size=10, name='宋体')
BOLD_FNT = Font(bold=True, size=10, name='宋体')
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
YELLOW_FILL = PatternFill('solid', fgColor='FFF2CC')
# ── Find key rows ──
row_考核应发合计 = None
row_最终发放_title = None
row_最终发放明细 = None
row_按销售人员 = None
row_按部门 = None
row_总览 = None
for r in range(1, ws.max_row + 1):
v = str(ws.cell(row=r, column=1).value or '')
if '考核应发合计' in v and r > 50:
row_考核应发合计 = r
elif '三、最终发放' in v:
row_最终发放_title = r
elif v == '最终发放明细':
row_最终发放明细 = r
elif '6月最终发放(按销售人员)' in v:
row_按销售人员 = r
elif '6月最终发放(按部门)' in v:
row_按部门 = r
elif v == '总览':
row_总览 = r
print(f"考核应发合计: row {row_考核应发合计}")
print(f"三、最终发放: row {row_最终发放_title}")
print(f"最终发放明细: row {row_最终发放明细}")
print(f"按销售人员: row {row_按销售人员}")
print(f"按部门: row {row_按部门}")
print(f"总览: row {row_总览}")
# ── Transfer config ──
# (离职人员, 接收人, 当月转嫁金额, 不发了金额)
TRANSFERS = {
'赵连飞': {'to': '刘念念', 'transfer': 2260.00, 'dept': '业务二部'},
'伍仲文': {'to': '钟祥', 'transfer': 150.00, 'dept': '业务六部'},
'岑彦': {'to': '钟祥', 'transfer': 30.00, 'dept': '业务六部'},
}
# ── Step 1: Insert "二、离职人员当月奖金转嫁" section ──
# Insert 9 rows after 考核应发合计 row
insert_at = row_考核应发合计 + 1
ws.insert_rows(insert_at, 9)
# Write transfer section
r = insert_at
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=3)
ws.cell(row=r, column=1, value='二、离职人员当月奖金转嫁').font = TITLE_FNT
r += 2
for c, h in enumerate(['离职人员', '转嫁至', '转嫁金额', '说明'], 1):
cell = ws.cell(row=r, column=c, value=h)
cell.font = HEAD_FONT; cell.fill = HEAD_FILL; cell.alignment = CC; cell.border = BORD
r += 1
total_transfer = 0
for name, info in TRANSFERS.items():
ws.cell(row=r, column=1, value=name).font = NORM_FNT
ws.cell(row=r, column=1).alignment = CC; ws.cell(row=r, column=1).border = BORD
ws.cell(row=r, column=2, value=info['to']).font = NORM_FNT
ws.cell(row=r, column=2).alignment = CC; ws.cell(row=r, column=2).border = BORD
ws.cell(row=r, column=3, value=info['transfer']).font = NORM_FNT
ws.cell(row=r, column=3).alignment = CC; ws.cell(row=r, column=3).border = BORD
ws.cell(row=r, column=3).number_format = '#,##0.00'
ws.cell(row=r, column=4, value='当月达标奖金转新销售(需确认是否发放)').font = NORM_FNT
ws.cell(row=r, column=4).alignment = LL; ws.cell(row=r, column=4).border = BORD
total_transfer += info['transfer']
r += 1
# 合计行
for c in range(1, 5):
ws.cell(row=r, column=c).fill = SUB_FILL; ws.cell(row=r, column=c).border = BORD
ws.cell(row=r, column=c).font = BOLD_FNT; ws.cell(row=r, column=c).alignment = CC
ws.cell(row=r, column=1, value='合计')
ws.cell(row=r, column=3, value=total_transfer)
ws.cell(row=r, column=3).number_format = '#,##0.00'
# ── Step 2: Update 最终发放明细 (shifted down by 9 rows) ──
# Find the shifted positions
shift = 9
row_detail_header = row_最终发放明细 + shift # 最终发放明细 header
row_detail_data_start = row_detail_header + 2 # first data row (after "销售人员|车辆数|金额")
print(f"\n最终发放明细 shifted to header row {row_detail_header}, data starts at {row_detail_data_start}")
# Parse and update the data rows
# Data rows are from row_detail_data_start to the 总计 row
resigned_names = set(TRANSFERS.keys())
receiver_additions = {'刘念念': 2260.0, '钟祥': 180.0}
for r in range(row_detail_data_start, ws.max_row + 1):
name_cell = ws.cell(row=r, column=1)
amt_cell = ws.cell(row=r, column=3)
name = str(name_cell.value or '').strip()
if name == '总计':
# Update total (should remain same: 113408.28)
break
if not name or name == '':
break
# Update receiver amounts
if name in receiver_additions:
old_val = float(amt_cell.value or 0)
new_val = old_val + receiver_additions[name]
amt_cell.value = new_val
print(f" {name}: {old_val:.2f}{new_val:.2f} (+{receiver_additions[name]:.2f})")
# Update resigned amounts (keep only不发了)
if name in TRANSFERS:
old_val = float(amt_cell.value or 0)
new_val = old_val - TRANSFERS[name]['transfer']
amt_cell.value = new_val
# Add note
note_cell = ws.cell(row=r, column=4)
note_cell.value = '不发了(仅当月转嫁)'
note_cell.font = NOTE_FNT
note_cell.alignment = CC
print(f" {name}: {old_val:.2f}{new_val:.2f} (不发了)")
# ── Step 3: Update 按销售人员 section ──
row_sales_header = row_按销售人员 + shift
print(f"\n按销售人员 shifted to header row {row_sales_header}")
for r in range(row_sales_header + 1, ws.max_row + 1):
name_cell = ws.cell(row=r, column=1)
amt_cell = ws.cell(row=r, column=3)
name = str(name_cell.value or '').strip()
if name == '合计':
break
if not name:
break
if name in receiver_additions:
old_val = float(amt_cell.value or 0)
amt_cell.value = old_val + receiver_additions[name]
if name in TRANSFERS:
old_val = float(amt_cell.value or 0)
amt_cell.value = old_val - TRANSFERS[name]['transfer']
# ── Step 4: Update 总览 section (add notes) ──
row_overview = row_总览 + shift
print(f"\n总览 shifted to row {row_overview}")
# Add notes below总览
note_row = row_overview + 5 # after "最终发放" row
ws.merge_cells(start_row=note_row, start_column=1, end_row=note_row, end_column=3)
ws.cell(row=note_row, column=1, value='').font = NORM_FNT
note_row += 1
ws.merge_cells(start_row=note_row, start_column=1, end_row=note_row, end_column=3)
ws.cell(row=note_row, column=1, value='备注:').font = BOLD_FNT
note_row += 1
notes = [
'1. 离职人员(赵连飞、伍仲文、岑彦)当月达标奖金转嫁至接收人;结转、补发、累计补发等一律不发放。',
'2. 转嫁金额需与相关部门确认是否发放给新销售。',
'3. 赵连飞(2026-04-30离职)→ 刘念念;伍仲文(2026-05-09离职)→ 钟祥;岑彦(2026-05-09离职)→ 钟祥。',
]
for note in notes:
ws.merge_cells(start_row=note_row, start_column=1, end_row=note_row, end_column=3)
ws.cell(row=note_row, column=1, value=note).font = NOTE_FNT
ws.cell(row=note_row, column=1).alignment = LL
note_row += 1
# ── Step 5: Rename resigned sheets with 【已离职】prefix and move to end ──
resigned_sheet_names = {
'二部-赵连飞': '【已离职】二部-赵连飞',
'六部-伍仲文': '【已离职】六部-伍仲文',
'六部-岑彦': '【已离职】六部-岑彦',
}
for old_name, new_name in resigned_sheet_names.items():
if old_name in wb.sheetnames:
idx = wb.sheetnames.index(old_name)
wb[old_name].title = new_name
# Move to end
wb.move_sheet(new_name, offset=len(wb.sheetnames) - 1 - idx)
print(f" Sheet renamed: {old_name}{new_name} (moved to end)")
# ── Save ──
wb.save(fp)
print(f"\n{fp} 已更新")
# ── Verify ──
wb2 = openpyxl.load_workbook(fp, data_only=True)
ws2 = wb2['6月汇总']
print("\n验证 - 最终发放明细:")
in_detail = False
for r in range(1, ws2.max_row + 1):
v = str(ws2.cell(row=r, column=1).value or '')
if v == '最终发放明细':
in_detail = True
continue
if in_detail:
name = ws2.cell(row=r, column=1).value
amt = ws2.cell(row=r, column=3).value
note = ws2.cell(row=r, column=4).value
if name and name != '销售人员':
print(f" {name}: {amt}" + (f" [{note}]" if note else ""))
if name == '总计':
break
print("\nSheets:", wb2.sheetnames)
wb2.close()
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""按三份GPS日里程文件处理2026年7月测试车辆考核源表。"""
from copy import copy
from collections import defaultdict
from datetime import datetime
from pathlib import Path
import re
import shutil
import openpyxl
from openpyxl.styles import PatternFill
BASE = Path('/Users/kkfluous/Downloads')
OUT = BASE / '7月核算结果'
SRC = BASE / '租赁任务考核_2026年7月 (1).xlsx'
TEST = Path('/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-08/4340099467fbf08b4c6583c49adba4ee/7月测试车辆明细.xlsx')
GPS_DAILY = BASE / '智能管车_车辆里程日报-140106360.xlsx'
GPS_DETAIL = [
BASE / '里程统计[天][2026-07-01至2026-07-31].xlsx',
BASE / '里程统计[天][2026-07-01至2026-07-31] (1).xlsx',
]
EXCLUDED_TEST_PLATES = {'粤AG37316'}
ZERO_OVERRIDE = set()
DEPARTMENT_OVERRIDE = {'粤AGP6675': '业务六部', '粤AGP9757': '业务六部'}
def norm(v):
return re.sub(r'\s+', '', str(v or '')).upper()
def number(v):
try:
return float(v or 0)
except (TypeError, ValueError):
return 0.0
def read_test_plates():
wb = openpyxl.load_workbook(TEST, data_only=True)
ws = wb.active
plates = {norm(r[0]) for r in ws.iter_rows(min_row=2, values_only=True) if norm(r[0])}
wb.close()
return plates - EXCLUDED_TEST_PLATES
def read_daily_matrix():
wb = openpyxl.load_workbook(GPS_DAILY, data_only=True)
ws = wb.active
headers = [c.value for c in ws[1]]
day_cols = {}
for i, v in enumerate(headers):
m = re.fullmatch(r'07月(\d{2})日', str(v or ''))
if m:
day_cols[int(m.group(1))] = i
data = {}
for row in ws.iter_rows(min_row=2, values_only=True):
plate = norm(row[0])
if plate:
data[plate] = {d: number(row[i]) for d, i in day_cols.items()}
wb.close()
return data
def read_detail_files():
data = defaultdict(lambda: defaultdict(float))
for path in GPS_DETAIL:
wb = openpyxl.load_workbook(path, data_only=True)
ws = wb.active
headers = [c.value for c in ws[3]]
pcol = next(i for i, v in enumerate(headers) if v and '车牌' in str(v))
dcol = next(i for i, v in enumerate(headers) if str(v or '').strip() == '日期')
mcol = next(i for i, v in enumerate(headers) if v and '里程' in str(v) and 'km' in str(v).lower())
for row in ws.iter_rows(min_row=4, values_only=True):
plate = norm(row[pcol])
if not plate:
continue
try:
dt = row[dcol] if isinstance(row[dcol], datetime) else datetime.strptime(str(row[dcol])[:10], '%Y-%m-%d')
except (TypeError, ValueError):
continue
# 同一车辆同一天在两份明细中如重复,取较大值,避免重复累计。
data[plate][dt.day] = max(data[plate][dt.day], number(row[mcol]))
wb.close()
return {p: dict(days) for p, days in data.items()}
def copy_cell_style(src, dst):
dst._style = copy(src._style)
if src.has_style:
dst.number_format = src.number_format
dst.protection = copy(src.protection)
dst.alignment = copy(src.alignment)
def main():
OUT.mkdir(parents=True, exist_ok=True)
shutil.copy2(SRC, OUT / '租赁任务考核_2026年7月_原版备份.xlsx')
tests = read_test_plates()
matrix = read_daily_matrix()
details = read_detail_files()
wb = openpyxl.load_workbook(SRC)
ws = wb['业务考核视图']
# 原表O列是实际里程。插入后统一为 O原TBOX、P实际、Q测试、R完成率、S是否达标。
ws.insert_cols(15, 1)
ws.insert_cols(17, 1)
ws['O1'] = '原TBOX里程(km)'
ws['Q1'] = '测试里程(km)'
copy_cell_style(ws['P1'], ws['O1'])
copy_cell_style(ws['P1'], ws['Q1'])
yellow = PatternFill(fill_type='solid', fgColor='FFFF00')
records = []
missing = []
for ri in range(2, ws.max_row + 1):
plate = norm(ws.cell(ri, 1).value)
if plate in DEPARTMENT_OVERRIDE:
ws.cell(ri, 4).value = DEPARTMENT_OVERRIDE[plate]
original = number(ws.cell(ri, 16).value)
ws.cell(ri, 15).value = round(original, 2)
copy_cell_style(ws.cell(ri, 16), ws.cell(ri, 15))
copy_cell_style(ws.cell(ri, 16), ws.cell(ri, 17))
if plate not in tests:
ws.cell(ri, 17).value = 0
continue
start = ws.cell(ri, 11).value
end = ws.cell(ri, 12).value
start_day = start.day if isinstance(start, datetime) else 1
end_day = end.day if isinstance(end, datetime) else 31
if plate in ZERO_OVERRIDE:
actual = 0.0
source = 'GPS缺失,业务确认暂按0'
elif plate in matrix:
actual = sum(matrix[plate].get(d, 0) for d in range(start_day, end_day + 1))
source = GPS_DAILY.name
elif plate in details:
actual = sum(details[plate].get(d, 0) for d in range(start_day, end_day + 1))
source = '里程统计日明细'
else:
missing.append(plate)
continue
target = number(ws.cell(ri, 14).value)
test_mileage = max(0.0, original - actual)
rate = actual / target * 100 if target > 0 else 0
ws.cell(ri, 16).value = round(actual, 2)
ws.cell(ri, 17).value = round(test_mileage, 2)
ws.cell(ri, 18).value = round(rate, 2)
ws.cell(ri, 19).value = '达标' if target > 0 and actual >= target else '未达标'
for cell in ws[ri]:
cell.fill = copy(yellow)
records.append((plate, ri, original, actual, test_mileage, source))
if missing:
raise RuntimeError('仍缺GPS车牌: ' + ','.join(sorted(set(missing))))
# 对齐5/6月的宽度、筛选和冻结窗格;保留原表其余样式。
ref = openpyxl.load_workbook(BASE / '租赁任务考核_2026年6月.xlsx', read_only=False)
rws = ref['业务考核视图']
for i in range(1, 24):
letter = openpyxl.utils.get_column_letter(i)
if rws.column_dimensions[letter].width:
ws.column_dimensions[letter].width = rws.column_dimensions[letter].width
ws.freeze_panes = rws.freeze_panes
ws.auto_filter.ref = f'A1:W{ws.max_row}'
ref.close()
out = OUT / '租赁任务考核_2026年7月.xlsx'
wb.save(out)
wb.close()
print(f'output={out}')
print(f'test_list={len(tests)} intervention_records={len(records)} intervention_plates={len(set(r[0] for r in records))}')
print(f'zero_override={sorted(ZERO_OVERRIDE)}')
print(f'department_override={DEPARTMENT_OVERRIDE}')
print(f'total_original={sum(r[2] for r in records):.2f} total_gps={sum(r[3] for r in records):.2f} total_test={sum(r[4] for r in records):.2f}')
if __name__ == '__main__':
main()
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""将用户确认的多源核算里程按车牌+考核区间回填到7月考核源表。"""
import os
import re
import shutil
from datetime import datetime
from pathlib import Path
import openpyxl
BASE = Path('/Users/kkfluous/Downloads')
MILEAGE = BASE / '7月考核车辆多源里程汇总_用户确认版.xlsx'
SOURCE = BASE / '7月核算结果/租赁任务考核_2026年7月.xlsx'
OUTPUT_DIR = Path(os.environ.get('JULY_OUTPUT_DIR', BASE / '7月绩效重算_多源考核里程_20260817'))
OUTPUT = OUTPUT_DIR / '租赁任务考核_2026年7月.xlsx'
def norm(v):
return re.sub(r'\s+', '', str(v or '')).upper()
def day(v):
return v.date() if isinstance(v, datetime) else v
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
shutil.copy2(SOURCE, OUTPUT)
mw = openpyxl.load_workbook(MILEAGE, data_only=True)
ms = mw['7月考核车辆里程汇总']
mh = {ms.cell(2, c).value: c for c in range(1, ms.max_column + 1)}
mileage = {}
for r in range(3, ms.max_row + 1):
key = (norm(ms.cell(r, mh['车牌']).value), day(ms.cell(r, mh['考核开始日期']).value),
day(ms.cell(r, mh['考核结束日期']).value))
value = ms.cell(r, mh['核算里程(km)']).value
if key in mileage:
raise RuntimeError(f'多源汇总存在重复键: {key}')
if not isinstance(value, (int, float)):
raise RuntimeError(f'核算里程不是数值: {key}={value}')
mileage[key] = float(value)
mw.close()
wb = openpyxl.load_workbook(OUTPUT)
ws = wb['业务考核视图']
h = {ws.cell(1, c).value: c for c in range(1, ws.max_column + 1)}
used = set()
changed = 0
for r in range(2, ws.max_row + 1):
key = (norm(ws.cell(r, h['车牌号']).value), day(ws.cell(r, h['考核开始日期']).value),
day(ws.cell(r, h['考核结束日期']).value))
if key not in mileage:
raise RuntimeError(f'7月源表记录在多源汇总中缺失: {key}')
used.add(key)
actual = round(mileage[key], 2)
original = float(ws.cell(r, h['原TBOX里程(km)']).value or 0)
target = float(ws.cell(r, h['应考核里程(km)']).value or 0)
old_actual = float(ws.cell(r, h['实际行驶里程(km)']).value or 0)
if abs(old_actual - actual) > 0.005:
changed += 1
ws.cell(r, h['实际行驶里程(km)']).value = actual
ws.cell(r, h['测试里程(km)']).value = round(max(0, original - actual), 2)
rate = round(actual / target * 100, 2) if target > 0 else 0
ws.cell(r, h['完成率(%)']).value = rate
ws.cell(r, h['是否达标']).value = '达标' if actual >= target and target > 0 else '未达标'
for name in ['原TBOX里程(km)', '实际行驶里程(km)', '测试里程(km)']:
ws.cell(r, h[name]).number_format = '#,##0.00'
ws.cell(r, h['完成率(%)']).number_format = '0.00'
extra = set(mileage) - used
if extra:
raise RuntimeError(f'多源汇总存在源表未使用记录: {list(extra)[:5]}')
ws.freeze_panes = 'A2'
wb.save(OUTPUT)
wb.close()
print({'output': str(OUTPUT), 'rows': len(used), 'changed_rows': changed})
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""按参考格式重建 6月汇总 sheet:剔除离职,转嫁用接收人格式"""
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from collections import defaultdict, OrderedDict
import os, copy
os.chdir('/Users/kkfluous/Downloads')
fp = '里程任务考核_6月核算.xlsx'
# ── Config ──
RESIGNED = {'赵连飞', '伍仲文', '岑彦'}
TRANSFERS = [
('刘念念', '赵连飞', 2260.00),
('钟祥', '伍仲文、岑彦', 180.00),
]
TRANSFER_TOTAL = sum(t[2] for t in TRANSFERS)
# Map resigned -> receiver for aggregation
TRANSFER_MAP = {'赵连飞': ('刘念念', 2260.00), '伍仲文': ('钟祥', 150.00), '岑彦': ('钟祥', 30.00)}
# ── Styles ──
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10, name='宋体')
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
TITLE_FNT = Font(bold=True, size=12, name='宋体')
SECTION_FNT = Font(bold=True, size=11, name='宋体')
NOTE_FNT = Font(size=10, color='C00000', name='宋体')
NORM_FNT = Font(size=10, name='宋体')
BOLD_FNT = Font(bold=True, size=10, name='宋体')
THIN = Side('thin', color='B4B4B4')
BORD = Border(THIN, THIN, THIN, THIN)
CC = Alignment(horizontal='center', vertical='center', wrap_text=True)
LL = Alignment(horizontal='left', vertical='center', wrap_text=True)
def hdr_row(ws, r, headers):
for c, h in enumerate(headers, 1):
cell = ws.cell(row=r, column=c, value=h)
cell.font = HEAD_FONT; cell.fill = HEAD_FILL; cell.alignment = CC; cell.border = BORD
def data_row(ws, r, values, bold=False):
for c, v in enumerate(values, 1):
cell = ws.cell(row=r, column=c, value=v)
cell.font = BOLD_FNT if bold else NORM_FNT
cell.alignment = CC; cell.border = BORD
if isinstance(v, float):
cell.number_format = '#,##0.00'
def sub_total(ws, r, values):
for c, v in enumerate(values, 1):
cell = ws.cell(row=r, column=c, value=v)
cell.font = BOLD_FNT; cell.fill = SUB_FILL
cell.alignment = CC; cell.border = BORD
if isinstance(v, float):
cell.number_format = '#,##0.00'
# ── Step 1: Parse existing data ──
wb = openpyxl.load_workbook(fp)
ws_old = wb['6月汇总']
# Parse all考核应发 subsections
subsections = [] # [(title, [(name, vehicles, amount), ...], total_row)]
current_title = None
current_rows = []
in_detail = False
# Map person->dept from按销售人员 section
person_dept = {}
in_sales = False
for r in range(1, ws_old.max_row + 1):
v1 = str(ws_old.cell(row=r, column=1).value or '')
v2 = str(ws_old.cell(row=r, column=2).value or '')
if '6月最终发放(按销售人员)' in v1:
in_sales = True
continue
if in_sales:
name = str(ws_old.cell(row=r, column=1).value or '')
dept = str(ws_old.cell(row=r, column=2).value or '')
if name == '合计':
break
if name and name != '销售人员' and dept:
person_dept[name] = dept
print(f"人员部门映射: {person_dept}")
# Parse考核应发 subsections
for r in range(1, ws_old.max_row + 1):
v1 = str(ws_old.cell(row=r, column=1).value or '')
if v1.startswith('考核应发-'):
if current_title and current_rows:
subsections.append((current_title, current_rows))
current_title = v1
current_rows = []
in_detail = True
continue
if current_title and in_detail:
name = str(ws_old.cell(row=r, column=1).value or '')
vehicles = ws_old.cell(row=r, column=2).value
amount = ws_old.cell(row=r, column=3).value
if name == '总计':
subsections.append((current_title, current_rows))
current_title = None
current_rows = []
in_detail = False
elif name and name != '销售人员':
current_rows.append((name, vehicles, amount))
print(f"\n解析到 {len(subsections)} 个考核应发 subsection:")
# ── Step 2: Filter out resigned, recalculate ──
filtered_subsections = []
grand_total = 0.0
for title, rows in subsections:
filtered = [(n, v, a) for (n, v, a) in rows if n not in RESIGNED]
# Recalculate vehicles and amounts
new_vehicles = sum(int(v or 0) for _, v, _ in filtered)
new_amount = sum(float(a or 0) for _, _, a in filtered)
grand_total += new_amount
filtered_subsections.append((title, filtered, new_vehicles, new_amount))
removed = [(n, a) for (n, _, a) in rows if n in RESIGNED]
if removed:
print(f" {title}: 剔除 {removed}, 新合计={new_amount:.2f}")
print(f"\n考核应发合计: {grand_total:.2f}")
# ── Step 3: Build最终发放 (aggregate by person + transfers) ──
person_data = defaultdict(lambda: {'vehicles': 0, 'amount': 0.0})
for title, rows, _, _ in filtered_subsections:
for name, vehicles, amount in rows:
person_data[name]['vehicles'] += int(vehicles or 0)
person_data[name]['amount'] += float(amount or 0)
# Add transfers to receivers
for resigned, (receiver, amt) in TRANSFER_MAP.items():
person_data[receiver]['amount'] += amt
# Count vehicles? The reference doesn't seem to add vehicle count for transfers
# Remove resigned (should already be filtered, but just in case)
for name in list(person_data.keys()):
if name in RESIGNED:
del person_data[name]
final_total = sum(d['amount'] for d in person_data.values())
print(f"最终发放合计: {final_total:.2f}")
# Sort by department then name (matching reference order)
dept_order = ['业务二部', '业务三部', '业务五部', '业务六部']
def sort_key(item):
name = item[0]
dept = person_dept.get(name, '')
try:
di = dept_order.index(dept)
except ValueError:
di = 99
return (di, name)
sorted_persons = sorted(person_data.items(), key=sort_key)
# ── Step 4: Department totals ──
dept_totals = defaultdict(float)
for name, data in sorted_persons:
dept = person_dept.get(name, '')
dept_totals[dept] += data['amount']
# ── Step 5: Rebuild the sheet ──
# Delete old sheet and create new
ws = wb['6月汇总']
ws.delete_rows(1, ws.max_row)
rn = 1
# === 一、考核应发明细 ===
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value='一、考核应发明细').font = TITLE_FNT
rn += 2
for title, rows, total_veh, total_amt in filtered_subsections:
ws.cell(row=rn, column=1, value=title).font = SECTION_FNT
rn += 1
hdr_row(ws, rn, ['销售人员', '车辆数', '金额'])
rn += 1
# Sort rows by dept then name
sorted_rows = sorted(rows, key=lambda x: sort_key((x[0],)) if x[0] in person_dept else (99, x[0]))
for name, vehicles, amount in sorted_rows:
data_row(ws, rn, [name, vehicles, round(float(amount or 0), 2)])
rn += 1
sub_total(ws, rn, ['总计', total_veh, round(total_amt, 2)])
rn += 2
# 考核应发合计
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=2)
ws.cell(row=rn, column=1, value='考核应发合计').font = BOLD_FNT
ws.cell(row=rn, column=1).alignment = CC
ws.cell(row=rn, column=3, value=round(grand_total, 2)).font = BOLD_FNT
ws.cell(row=rn, column=3).alignment = CC
ws.cell(row=rn, column=3).number_format = '#,##0.00'
for c in range(1, 4):
ws.cell(row=rn, column=c).border = BORD
ws.cell(row=rn, column=c).fill = SUB_FILL
rn += 2
# === 二、离职人员转嫁收入 ===
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value='二、离职人员转嫁收入').font = TITLE_FNT
rn += 1
hdr_row(ws, rn, ['接收人', '来源', '转嫁金额'])
rn += 1
for receiver, source, amt in TRANSFERS:
data_row(ws, rn, [receiver, source, amt])
rn += 1
sub_total(ws, rn, ['合计', '', TRANSFER_TOTAL])
rn += 2
# === 三、最终发放 ===
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value='三、最终发放').font = TITLE_FNT
rn += 2
# 最终发放明细
ws.cell(row=rn, column=1, value='最终发放明细').font = SECTION_FNT
rn += 1
hdr_row(ws, rn, ['销售人员', '车辆数', '金额'])
rn += 1
for name, data in sorted_persons:
data_row(ws, rn, [name, data['vehicles'], round(data['amount'], 2)])
rn += 1
sub_total(ws, rn, ['总计', sum(d['vehicles'] for _, d in sorted_persons), round(final_total, 2)])
rn += 2
# 6月最终发放(按销售人员)
ws.cell(row=rn, column=1, value='6月最终发放(按销售人员)').font = SECTION_FNT
rn += 1
hdr_row(ws, rn, ['销售人员', '部门名称', '最终发放'])
rn += 1
for name, data in sorted_persons:
dept = person_dept.get(name, '')
data_row(ws, rn, [name, dept, round(data['amount'], 2)])
rn += 1
sub_total(ws, rn, ['合计', '', round(final_total, 2)])
rn += 2
# 6月最终发放(按部门)
ws.cell(row=rn, column=1, value='6月最终发放(按部门)').font = SECTION_FNT
rn += 1
hdr_row(ws, rn, ['部门名称', '最终发放', ''])
rn += 1
for dept in dept_order:
if dept in dept_totals:
data_row(ws, rn, [dept, round(dept_totals[dept], 2), ''])
rn += 1
sub_total(ws, rn, ['合计', round(sum(dept_totals.values()), 2), ''])
rn += 2
# === 总览 ===
ws.cell(row=rn, column=1, value='总览').font = SECTION_FNT
rn += 1
hdr_row(ws, rn, ['', '', ''])
# Override header style for总览
data_row(ws, rn, ['考核应发', round(final_total, 2), ''])
rn += 1
data_row(ws, rn, ['亏损拦截', 0, ''])
rn += 1
data_row(ws, rn, ['最终发放', round(final_total, 2), ''])
rn += 2
# === 备注 ===
note_text = '备注:赵连飞(2026-04-30离职)、伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)\n三人已从上述汇总中剔除,其6月当月达标奖金已转嫁至新销售;其余结转、补发、累计补发等一律不发了。'
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
ws.cell(row=rn, column=1, value=note_text).font = NOTE_FNT
ws.cell(row=rn, column=1).alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
ws.row_dimensions[rn].height = 36
# ── Column widths ──
ws.column_dimensions['A'].width = 16
ws.column_dimensions['B'].width = 18
ws.column_dimensions['C'].width = 16
# ── Rename resigned sheets ──
for old_name, new_name in [('二部-赵连飞','【已离职】二部-赵连飞'),('六部-伍仲文','【已离职】六部-伍仲文'),('六部-岑彦','【已离职】六部-岑彦')]:
if old_name in wb.sheetnames:
wb[old_name].title = new_name
wb.move_sheet(new_name, offset=len(wb.sheetnames)-1-wb.sheetnames.index(new_name))
print(f" Sheet: {old_name}{new_name}")
# ── Copy to output ──
import shutil
out = '/Users/kkfluous/Downloads/20260713/里程任务考核_6月核算.xlsx'
wb.save(out)
print(f"\n{out} 已重建")
# ── Verify ──
wb2 = openpyxl.load_workbook(out, data_only=True)
ws2 = wb2['6月汇总']
print(f"\n验证 ({ws2.max_row} rows):")
for row in ws2.iter_rows(min_row=1, max_row=ws2.max_row):
vals = [str(c.value)[:50] if c.value is not None else '' for c in row]
line = ' | '.join(vals)
if line.replace(' | ', '').strip():
print(f" {line}")
wb2.close()
@@ -0,0 +1,203 @@
from ast import parse, walk, Call, Constant, keyword
from collections import defaultdict
from copy import copy
from datetime import datetime, date
from pathlib import Path
from zipfile import ZipFile
from xml.etree import ElementTree as ET
import re
import shutil
import openpyxl
import pymysql
from openpyxl.styles import Alignment, Font, PatternFill
BASE = Path('/Users/kkfluous/Downloads')
OUTPUT = BASE / '7月核算结果/7月考核车辆多源里程汇总_含车型.xlsx'
MAIN_OUTPUT = BASE / '7月核算结果/7月考核车辆多源里程汇总.xlsx'
BACKUP = BASE / '7月核算结果/7月考核车辆多源里程汇总_新日规则前.xlsx'
ASSESSMENT = BASE / '7月核算结果/租赁任务考核_2026年7月.xlsx'
TBOX_FILE = BASE / '车辆里程查询_20260701-20260731_1024辆.xlsx'
PLATFORM_DIR = BASE / '广州新能源平台_纯氢纯电里程_2026年7月_按日'
DETAILS = [BASE / '里程统计[天][2026-07-01至2026-07-31].xlsx', BASE / '里程统计[天][2026-07-01至2026-07-31] (1).xlsx']
NS = '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'
def norm(v): return re.sub(r'\s+', '', str(v or '')).upper()
def db_config():
tree = parse((BASE / 'replace_gps_jun.py').read_text())
for node in walk(tree):
if isinstance(node, Call) and getattr(node.func, 'attr', None) == 'connect':
values = {x.arg: x.value.value for x in node.keywords if isinstance(x.value, Constant)}
if {'host', 'user', 'password', 'database'} <= values.keys(): return values
raise RuntimeError('未找到既有GPS数据库配置')
def read_detail_gps():
out = defaultdict(dict)
for path in DETAILS:
with ZipFile(path) as zf:
root = ET.fromstring(zf.read('xl/worksheets/sheet1.xml'))
for row in root.findall(f'.//{NS}row'):
if int(row.get('r')) < 4: continue
values = {}
for cell in row.findall(f'{NS}c'):
col = re.match(r'[A-Z]+', cell.get('r')).group()
if cell.get('t') == 'inlineStr':
values[col] = ''.join(t.text or '' for t in cell.findall(f'.//{NS}t'))
else:
node = cell.find(f'{NS}v'); values[col] = node.text if node is not None else None
p = norm(values.get('H')); ds = str(values.get('O') or '').strip()
if p and p != '合计' and re.fullmatch(r'2026-07-\d{2}', ds):
out[p][datetime.strptime(ds, '%Y-%m-%d').date()] = float(values.get('P') or 0)
return out
# 测试候选车辆以7月最终考核源表中黄色干预行为准。
awb = openpyxl.load_workbook(ASSESSMENT, data_only=True)
aws = awb['业务考核视图']
candidates = set()
for row in range(2, aws.max_row + 1):
fills = {aws.cell(row, c).fill.fgColor.rgb for c in range(1, aws.max_column + 1) if aws.cell(row, c).fill.fill_type == 'solid'}
if any(str(x).upper().endswith('FFFF00') for x in fills if x):
candidates.add(norm(aws.cell(row, 1).value))
awb.close()
# 每日TBOX。文件中的空白日按0处理,不会触发测试条件。
twb = openpyxl.load_workbook(TBOX_FILE, read_only=True, data_only=True)
tws = twb['里程查询']
header = next(tws.iter_rows(min_row=6, max_row=6, values_only=True))
day_cols = {i: value.date() for i, value in enumerate(header) if isinstance(value, datetime)}
tbox = defaultdict(dict)
for row in tws.iter_rows(min_row=7, values_only=True):
p = norm(row[0])
if p:
for i, day in day_cols.items(): tbox[p][day] = float(row[i] or 0)
twb.close()
# 每日GPS:两份日明细优先,数据库补充缺失日期。
gps = read_detail_gps()
cfg = db_config(); cfg.update(charset='utf8mb4', connect_timeout=8, read_timeout=30, write_timeout=30)
conn = pymysql.connect(**cfg)
try:
with conn.cursor() as cur:
placeholders = ','.join(['%s'] * len(candidates))
cur.execute(
f'SELECT plate_number,dates,total_mileage FROM ln_vehicle_g7_mileage WHERE dates >= %s AND dates <= %s AND plate_number IN ({placeholders})',
[date(2026, 7, 1), date(2026, 7, 31), *sorted(candidates)],
)
db_rows = cur.fetchall()
finally: conn.close()
for p, day, raw in db_rows:
if isinstance(day, datetime): day = day.date()
gps[norm(p)].setdefault(day, float(raw or 0) / 100000.0)
# 广州平台每日纯氢里程。
hydrogen = defaultdict(dict)
for path in sorted(PLATFORM_DIR.glob('2026-07-??_纯氢纯电里程.xlsx')):
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
ws = wb.active
for row in ws.iter_rows(min_row=2, values_only=True):
p = norm(row[5]); match = re.search(r'2026-07-\d{2}', str(row[6] or ''))
if p and match:
day = datetime.strptime(match.group(), '%Y-%m-%d').date()
hydrogen[p][day] = hydrogen[p].get(day, 0) + float(row[8] or 0)
wb.close()
if not BACKUP.exists(): shutil.copy2(OUTPUT, BACKUP)
wb = openpyxl.load_workbook(OUTPUT)
ws = wb.active
header_row = next(r for r in range(1, 8) if ws.cell(r, 2).value == '车牌')
headers = [ws.cell(header_row, c).value for c in range(1, ws.max_column + 1)]
if '考核里程(km)' not in headers:
for merged in list(ws.merged_cells.ranges):
if merged.min_row == 1 and merged.max_row == 1:
ws.unmerge_cells(str(merged))
total_col = headers.index('广州平台总里程(km)') + 1
ws.insert_cols(total_col + 1, 1)
assessment_col = total_col + 1
else:
assessment_col = headers.index('考核里程(km)') + 1
ws.cell(header_row, assessment_col).value = '考核里程(km)'
ws.cell(header_row, assessment_col)._style = copy(ws.cell(header_row, assessment_col - 1)._style)
ws.column_dimensions[openpyxl.utils.get_column_letter(assessment_col)].width = 18
headers = [ws.cell(header_row, c).value for c in range(1, ws.max_column + 1)]
col = {v: i + 1 for i, v in enumerate(headers)}
yes_records = 0; test_days_total = 0; excluded_hydrogen_total = 0.0; no_hydrogen = 0
fallback_tbox = 0; fallback_gps = 0; fallback_missing = 0
white = PatternFill('solid', fgColor='FFFFFF')
platform_fill = PatternFill('solid', fgColor='DDEBF7')
assessment_fill = PatternFill('solid', fgColor='FCE4D6')
tbox_fill = PatternFill('solid', fgColor='E2F0D9')
gps_fill = PatternFill('solid', fgColor='E4DFEC')
yellow = PatternFill('solid', fgColor='FFF2CC')
grey = PatternFill('solid', fgColor='F2F2F2')
for row in range(header_row + 1, ws.max_row + 1):
p = norm(ws.cell(row, col['车牌']).value)
start = ws.cell(row, col['考核开始日期']).value.date()
end = ws.cell(row, col['考核结束日期']).value.date()
days = [start.fromordinal(start.toordinal() + n) for n in range((end - start).days + 1)]
test_days = []
if p in candidates:
for day in days:
if day in gps.get(p, {}) and tbox[p].get(day, 0) - gps[p][day] > 50:
test_days.append(day)
has_test = bool(test_days)
ws.cell(row, col['是否存在测试']).value = '' if has_test else ''
if has_test: yes_records += 1
test_days_total += len(test_days)
daily_h = hydrogen.get(p, {})
present = [day for day in days if day in daily_h]
if not present:
no_hydrogen += 1
fallback_name = 'GPS里程(km)' if p in candidates else 'TBOX仪表里程(km)'
fallback_value = ws.cell(row, col[fallback_name]).value
if isinstance(fallback_value, (int, float)):
ws.cell(row, assessment_col).value = round(float(fallback_value), 2)
ws.cell(row, assessment_col).number_format = '#,##0.0'
if p in candidates:
fallback_gps += 1
else:
fallback_tbox += 1
else:
ws.cell(row, assessment_col).value = '无来源'
fallback_missing += 1
else:
excluded_hydrogen_total += sum(daily_h.get(day, 0) for day in test_days)
ws.cell(row, assessment_col).value = round(sum(daily_h.get(day, 0) for day in days if day not in test_days), 2)
ws.cell(row, assessment_col).number_format = '#,##0.0'
# 重建行配色:新规则判定存在测试时整行黄色,否则恢复各数据组颜色。
if has_test:
for c in range(1, ws.max_column + 1): ws.cell(row, c).fill = copy(yellow)
else:
for c in range(1, ws.max_column + 1): ws.cell(row, c).fill = copy(white)
for name in ['纯氢里程(km)', '纯电里程(km)', '广州平台总里程(km)']:
ws.cell(row, col[name]).fill = copy(platform_fill)
ws.cell(row, assessment_col).fill = copy(assessment_fill)
ws.cell(row, col['TBOX仪表里程(km)']).fill = copy(tbox_fill)
ws.cell(row, col['GPS里程(km)']).fill = copy(gps_fill)
for name in ['纯氢里程(km)', '纯电里程(km)', '广州平台总里程(km)', '考核里程(km)', 'TBOX仪表里程(km)', 'GPS里程(km)']:
cell = ws.cell(row, col[name])
if cell.value == '无来源':
cell.fill = copy(grey); cell.font = Font(color='7F7F7F', italic=True)
for merged in list(ws.merged_cells.ranges):
if merged.min_row == 1 and merged.max_row == 1:
ws.unmerge_cells(str(merged))
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=ws.max_column)
ws['A1'] = (
'1. 考核里程按纯氢里程计算:普通车辆取考核区间纯氢里程;测试候选车辆逐日比较,'
'当日TBOX里程-当日GPS里程>50km时判为测试日,并剔除该日纯氢里程;'
'无纯氢里程的4.5T普货车辆,测试候选车辆取GPS里程,其他车辆取TBOX仪表里程\n'
'2. 每日TBOX来源:车辆里程查询_20260701-20260731_1024辆.xlsx;空白日按0处理。每日GPS优先两份7月日明细,数据库补充\n'
'3. 4.5T普货的TBOX或GPS备选来源也缺失时,考核里程标记“无来源”\n'
'4. 纯电里程及广州平台总里程仅作对照,不参与考核里程'
)
ws['A1'].alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
ws.auto_filter.ref = f'A{header_row}:{openpyxl.utils.get_column_letter(ws.max_column)}{ws.max_row}'
ws.row_dimensions[1].height = 58
wb.save(OUTPUT)
shutil.copy2(OUTPUT, MAIN_OUTPUT)
print({'candidate_plates': len(candidates), 'test_records_new_rule': yes_records, 'test_days': test_days_total, 'excluded_hydrogen_km': round(excluded_hydrogen_total, 2), 'pure_hydrogen_missing': no_hydrogen, 'fallback_tbox_records': fallback_tbox, 'fallback_gps_records': fallback_gps, 'assessment_mileage_still_missing': fallback_missing, 'columns': ws.max_column})
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""用新GPS数据重新生成6月干预情况说明 — 测试里程/实际里程/翻转分析全部更新"""
import openpyxl, os
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
# ── Styles ──
hdr_blue_font = Font(name='宋体', size=11, bold=True, color='FFFFFF')
hdr_blue_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
hdr_green_font = Font(name='宋体', size=11, bold=True, color='FFFFFF')
hdr_green_fill = PatternFill(start_color='548235', end_color='548235', fill_type='solid')
hdr_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
data_font = Font(name='宋体', size=10)
data_align = Alignment(horizontal='center', vertical='center')
title_font = Font(name='宋体', size=14, bold=True)
subtitle_font = Font(name='宋体', size=10, bold=True)
yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
light_yellow_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
thin_border = Border(left=Side('thin'), right=Side('thin'), top=Side('thin'), bottom=Side('thin'))
def set_cell(ws, row, col, value, font=None, fill=None, align=None, border=None, num_fmt=None):
cell = ws.cell(row=row, column=col, value=value)
if font: cell.font = font
if fill: cell.fill = fill
if align: cell.alignment = align
if border: cell.border = border
if num_fmt: cell.number_format = num_fmt
return cell
# ── Step 1: Extract test plates from OLD intervention report ──
print("提取测试车辆清单...")
old_interv = '租赁任务考核_2026年6月_干预情况说明.xlsx'
wb_old = openpyxl.load_workbook(old_interv, data_only=True)
ws_old = wb_old['涉及测试的业务考核车辆']
h_old = [c.value for c in next(ws_old.iter_rows(min_row=1, max_row=1))]
plate_col_old = h_old.index('车牌号')
test_plates = set()
for row in ws_old.iter_rows(min_row=2, values_only=True):
p = row[plate_col_old]
if p: test_plates.add(str(p).strip())
wb_old.close()
print(f" 测试车辆: {len(test_plates)}")
# ── Step 2: Read NEW source data ──
print("读取新数据源...")
src_fp = '20260713_1500/租赁任务考核_2026年6月.xlsx'
src = openpyxl.load_workbook(src_fp, data_only=True)
ws_src = src['业务考核视图']
h_src = [c.value for c in next(ws_src.iter_rows(min_row=1, max_row=1))]
all_recs = []
for row in ws_src.iter_rows(min_row=2, values_only=True):
r = dict(zip(h_src, row))
if r['车牌号']:
all_recs.append(r)
src.close()
# ── Step 3: Read BACKUP for original comparison ──
print("读取备份(原始仪表盘)...")
bak = openpyxl.load_workbook('租赁任务考核_2026年6月_备份.xlsx', data_only=True)
ws_bak = bak['业务考核视图']
h_bak = [c.value for c in next(ws_bak.iter_rows(min_row=1, max_row=1))]
bak_recs = {}
for row in ws_bak.iter_rows(min_row=2, values_only=True):
r = dict(zip(h_bak, row))
if r['车牌号']:
bak_recs[str(r['车牌号']).strip()] = r
bak.close()
# ── Step 4: Analyze ──
print("分析干预数据...")
intervention_recs = [r for r in all_recs if r['车牌号'] in test_plates]
intervention_plates = set(r['车牌号'] for r in intervention_recs)
# Stats
total_vehicles = len(set(r['车牌号'] for r in all_recs))
total_records = len(all_recs)
interv_vehicles = len(intervention_plates)
interv_records = len(intervention_recs)
total_actual = sum(float(r['实际行驶里程(km)'] or 0) for r in all_recs)
total_tbox = sum(float(r['原TBOX里程(km)'] or 0) for r in all_recs)
total_test = sum(float(r['测试里程(km)'] or 0) for r in all_recs)
interv_actual = sum(float(r['实际行驶里程(km)'] or 0) for r in intervention_recs)
interv_tbox = sum(float(r['原TBOX里程(km)'] or 0) for r in intervention_recs)
interv_test = sum(float(r['测试里程(km)'] or 0) for r in intervention_recs)
# Flip analysis: old达→new未达标
flip_recs = []
for r in intervention_recs:
plate = r['车牌号']
old_rec = bak_recs.get(plate)
if old_rec:
old_q = str(old_rec.get('是否达标', '')).strip() == '达标'
new_q = str(r.get('是否达标', '')).strip() == '达标'
if old_q and not new_q:
flip_recs.append(r)
# Group flip by target
flip_by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for r in flip_recs:
target = r.get('考核目标', '未知')
dept = r.get('部门名称', '')
sales = r.get('销售经理', '')
flip_by_target[target][dept][sales] += 1
print(f" 全量: {total_vehicles}辆/{total_records}")
print(f" 干预: {interv_vehicles}辆/{interv_records}")
print(f" 达→未达标: {len(flip_recs)}")
print(f" TBOX总计: {total_tbox:.0f}, GPS总计: {total_actual:.0f}, 测试总计: {total_test:.0f}")
# ── Step 5: Build workbook ──
wb = openpyxl.Workbook()
# ===== Sheet 1: 测试与考核情况说明 =====
ws1 = wb.active; ws1.title = '测试与考核情况说明'
for col, w in enumerate([4, 30, 18, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(col)].width = w
rn = 1
set_cell(ws1, rn, 1, '2026年6月 业务考核 — 测试与考核情况说明', title_font)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 1
set_cell(ws1, rn, 1, '6月业务考核车辆中,部分车辆当月存在测试行为(仪表盘里程 > GPS 实际行驶里程),其差额作为测试里程剥离,不参与考核。GPS数据来源:G7/东方北斗/信达GPS/广安北斗 四家供应商6月日里程汇总文件。',
Font(name='宋体', size=10), light_yellow_fill)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 2
# Section 1: Stats
set_cell(ws1, rn, 1, '一、6月业务考核与干预情况', subtitle_font); rn += 1
for ci, h in enumerate(['项目', '6月业务考核车辆', '涉及测试的车辆', '占比'], 1):
set_cell(ws1, rn, ci, h, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
rn += 1
stats = [
('车辆数(辆)', total_vehicles, interv_vehicles),
('产生考核记录数(条)', total_records, interv_records),
('仪表盘里程(km)', round(total_tbox), round(interv_tbox)),
('业务考核里程 GPS(km)', round(total_actual), round(interv_actual)),
('测试里程(km)', round(total_test), round(interv_test)),
]
for label, all_v, interv_v in stats:
ratio = interv_v / all_v if all_v else 0
set_cell(ws1, rn, 1, label, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, all_v, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 3, interv_v, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 4, ratio, data_font, None, data_align, thin_border, '0.00%')
rn += 1
rn += 2
# Section 2: Flip details
set_cell(ws1, rn, 1, '二、干预车辆考核达标情况说明', subtitle_font); rn += 1
flip_total = len(flip_recs)
summary_text = f'{interv_records} 条考核记录涉及里程干预;替换为GPS里程后,共 {flip_total} 条考核车辆记录由"达标"转判为"未达标"。按车型分类如下:'
set_cell(ws1, rn, 1, summary_text, Font(name='宋体', size=10))
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 2
target_order = ['羚牛136辆4.5T冷链车', '羚牛100辆18T', '交投40辆4.5T普货', '交投190辆4.5T冷链车', '恒运50辆4.5T普货']
for target in target_order:
if target not in flip_by_target: continue
dept_sales = flip_by_target[target]
total_cnt = sum(sum(sales.values()) for sales in dept_sales.values())
set_cell(ws1, rn, 1, f'{target}{total_cnt} 辆)', subtitle_font)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 1
set_cell(ws1, rn, 1, '部门', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 2, '销售经理', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 3, '数量', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 4, '部门', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 5, '销售经理', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 6, '数量', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
rn += 1
items = []
for dept in sorted(dept_sales.keys()):
for sales in sorted(dept_sales[dept].keys()):
items.append((dept, sales, dept_sales[dept][sales]))
mid = (len(items) + 1) // 2
max_rows = max(mid, len(items) - mid)
for i in range(max_rows):
if i < mid:
d1, s1, c1 = items[i]
set_cell(ws1, rn, 1, d1, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, s1, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 3, c1, data_font, None, data_align, thin_border)
else:
for c in [1,2,3]: set_cell(ws1, rn, c, '', data_font, None, data_align, thin_border)
if i + mid < len(items):
d2, s2, c2 = items[i + mid]
set_cell(ws1, rn, 4, d2, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 5, s2, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 6, c2, data_font, None, data_align, thin_border)
else:
for c in [4,5,6]: set_cell(ws1, rn, c, '', data_font, None, data_align, thin_border)
rn += 1
rn += 1
rn += 1
# Section 3: Column source explanation
set_cell(ws1, rn, 1, '三、里程三列数据来源说明', subtitle_font); rn += 1
set_cell(ws1, rn, 1, '', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 2, '字段名', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 3, '数据来源', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 4, '说明', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
rn += 1
col_descs = [
('O', '原TBOX里程(km)', '车辆 T-Box / 仪表盘读数', '当月仪表总里程(业务系统原始数据),含运营+测试+调试里程。仅干预车辆填充,非干预车辆为空'),
('P', '实际行驶里程(km)', 'GPS 轨迹还原', '基于 G7/东方北斗/信达GPS/广安北斗 四家供应商6月GPS日里程汇总文件,按考核期逐日汇总计算的运营里程,本月里程考核以此列为准'),
('Q', '测试里程(km)', 'O P(原TBOX GPS,差额)', '仪表盘跑了但 GPS 未记录到的部分,判定为厂内测试/调试/路试等非运营里程,不计入考核。仅干预车辆填充,非干预车辆为0。若差额为负则取0'),
]
for col_letter, name, source, desc in col_descs:
set_cell(ws1, rn, 1, col_letter, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, name, data_font, None, Alignment(horizontal='left', vertical='center'), thin_border)
set_cell(ws1, rn, 3, source, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 4, desc, data_font, None, Alignment(horizontal='left', vertical='center', wrap_text=True), thin_border)
ws1.row_dimensions[rn].height = 40
rn += 1
# ===== Sheet 2: 业务考核视图 (full data,干预标黄) =====
ws2 = wb.create_sheet('业务考核视图')
target_order_full = ['车牌号','考核年份','考核月份','部门名称','销售经理','客户名称','合同编号','项目名称',
'考核目标ID','考核目标','考核开始日期','考核结束日期','考核天数','应考核里程(km)',
'原TBOX里程(km)','实际行驶里程(km)','测试里程(km)','完成率(%)','是否达标','考核状态',
'交车日期','预计还车日期','实际还车日期']
for ci, name in enumerate(target_order_full, 1):
set_cell(ws2, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(all_recs, 2):
is_interv = r['车牌号'] in intervention_plates
for ci, name in enumerate(target_order_full, 1):
set_cell(ws2, ri, ci, r.get(name), data_font, yellow_fill if is_interv else None, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws2.column_dimensions[get_column_letter(col)].width = w
# ===== Sheet 3: 6月业务考核车辆总览 =====
ws3 = wb.create_sheet('6月业务考核车辆总览')
for ci, name in enumerate(target_order_full, 1):
set_cell(ws3, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(all_recs, 2):
for ci, name in enumerate(target_order_full, 1):
set_cell(ws3, ri, ci, r.get(name), data_font, None, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws3.column_dimensions[get_column_letter(col)].width = w
# ===== Sheet 4: 涉及测试的业务考核车辆 =====
ws4 = wb.create_sheet('涉及测试的业务考核车辆')
for ci, name in enumerate(target_order_full, 1):
set_cell(ws4, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(intervention_recs, 2):
for ci, name in enumerate(target_order_full, 1):
set_cell(ws4, ri, ci, r.get(name), data_font, yellow_fill, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws4.column_dimensions[get_column_letter(col)].width = w
# ── Save ──
out = '20260713_1500/租赁任务考核_2026年6月_干预情况说明.xlsx'
wb.save(out)
print(f'\n{out}')
print(f'\n概况:')
print(f' 全量: {total_vehicles}辆, {total_records}')
print(f' 干预: {interv_vehicles}辆, {interv_records}')
print(f' 达→未达标: {flip_total}')
print(f' 仪表盘里程: {total_tbox:.0f} (干预{interv_tbox:.0f})')
print(f' GPS里程: {total_actual:.0f} (干预{interv_actual:.0f})')
print(f' 测试里程: {total_test:.0f} (干预{interv_test:.0f})')
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""用新GPS数据重新生成7月干预情况说明 — 测试里程/实际里程/翻转分析全部更新"""
import openpyxl, os
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
# ── Styles ──
hdr_blue_font = Font(name='宋体', size=11, bold=True, color='FFFFFF')
hdr_blue_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
hdr_green_font = Font(name='宋体', size=11, bold=True, color='FFFFFF')
hdr_green_fill = PatternFill(start_color='548235', end_color='548235', fill_type='solid')
hdr_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
data_font = Font(name='宋体', size=10)
data_align = Alignment(horizontal='center', vertical='center')
title_font = Font(name='宋体', size=14, bold=True)
subtitle_font = Font(name='宋体', size=10, bold=True)
yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
light_yellow_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
thin_border = Border(left=Side('thin'), right=Side('thin'), top=Side('thin'), bottom=Side('thin'))
def set_cell(ws, row, col, value, font=None, fill=None, align=None, border=None, num_fmt=None):
cell = ws.cell(row=row, column=col, value=value)
if font: cell.font = font
if fill: cell.fill = fill
if align: cell.alignment = align
if border: cell.border = border
if num_fmt: cell.number_format = num_fmt
return cell
# ── Step 1: Extract test plates from July test list ──
print("提取测试车辆清单...")
test_file = '/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-08/4340099467fbf08b4c6583c49adba4ee/7月测试车辆明细.xlsx'
wb_old = openpyxl.load_workbook(test_file, data_only=True)
ws_old = wb_old.active
test_plates = set()
for row in ws_old.iter_rows(min_row=2, values_only=True):
p = row[0]
if p: test_plates.add(str(p).strip())
test_plates.discard('粤AG37316')
wb_old.close()
print(f" 测试车辆: {len(test_plates)}")
# ── Step 2: Read NEW source data ──
print("读取新数据源...")
src_fp = '7月核算结果/租赁任务考核_2026年7月.xlsx'
src = openpyxl.load_workbook(src_fp, data_only=True)
ws_src = src['业务考核视图']
h_src = [c.value for c in next(ws_src.iter_rows(min_row=1, max_row=1))]
all_recs = []
for row in ws_src.iter_rows(min_row=2, values_only=True):
r = dict(zip(h_src, row))
if r['车牌号']:
all_recs.append(r)
src.close()
# ── Step 3: Read BACKUP for original comparison ──
print("读取备份(原始仪表盘)...")
bak = openpyxl.load_workbook('7月核算结果/租赁任务考核_2026年7月_原版备份.xlsx', data_only=True)
ws_bak = bak['业务考核视图']
h_bak = [c.value for c in next(ws_bak.iter_rows(min_row=1, max_row=1))]
bak_recs = {}
for row in ws_bak.iter_rows(min_row=2, values_only=True):
r = dict(zip(h_bak, row))
if r['车牌号']:
bak_recs[str(r['车牌号']).strip()] = r
bak.close()
# ── Step 4: Analyze ──
print("分析干预数据...")
intervention_recs = [r for r in all_recs if r['车牌号'] in test_plates]
intervention_plates = set(r['车牌号'] for r in intervention_recs)
# Stats
total_vehicles = len(set(r['车牌号'] for r in all_recs))
total_records = len(all_recs)
interv_vehicles = len(intervention_plates)
interv_records = len(intervention_recs)
total_actual = sum(float(r['实际行驶里程(km)'] or 0) for r in all_recs)
total_tbox = sum(float(r['原TBOX里程(km)'] or 0) for r in all_recs)
total_test = sum(float(r['测试里程(km)'] or 0) for r in all_recs)
interv_actual = sum(float(r['实际行驶里程(km)'] or 0) for r in intervention_recs)
interv_tbox = sum(float(r['原TBOX里程(km)'] or 0) for r in intervention_recs)
interv_test = sum(float(r['测试里程(km)'] or 0) for r in intervention_recs)
# Flip analysis: old达→new未达标
flip_recs = []
for r in intervention_recs:
plate = r['车牌号']
old_rec = bak_recs.get(plate)
if old_rec:
old_q = str(old_rec.get('是否达标', '')).strip() == '达标'
new_q = str(r.get('是否达标', '')).strip() == '达标'
if old_q and not new_q:
flip_recs.append(r)
# Group flip by target
flip_by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
for r in flip_recs:
target = r.get('考核目标', '未知')
dept = r.get('部门名称', '')
sales = r.get('销售经理', '')
flip_by_target[target][dept][sales] += 1
print(f" 全量: {total_vehicles}辆/{total_records}")
print(f" 干预: {interv_vehicles}辆/{interv_records}")
print(f" 达→未达标: {len(flip_recs)}")
print(f" TBOX总计: {total_tbox:.0f}, GPS总计: {total_actual:.0f}, 测试总计: {total_test:.0f}")
# ── Step 5: Build workbook ──
wb = openpyxl.Workbook()
# ===== Sheet 1: 测试与考核情况说明 =====
ws1 = wb.active; ws1.title = '测试与考核情况说明'
for col, w in enumerate([4, 30, 18, 18, 18, 18, 18], 1):
ws1.column_dimensions[get_column_letter(col)].width = w
rn = 1
set_cell(ws1, rn, 1, '2026年7月 业务考核 — 测试与考核情况说明', title_font)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 1
set_cell(ws1, rn, 1, '7月业务考核车辆中,测试车辆统一以指定GPS日里程按考核期汇总替换TBOX里程,差额作为测试里程剥离且最低取0。粤AG37316因GPS缺失,经业务确认暂按0核算。',
Font(name='宋体', size=10), light_yellow_fill)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 2
# Section 1: Stats
set_cell(ws1, rn, 1, '一、7月业务考核与干预情况', subtitle_font); rn += 1
for ci, h in enumerate(['项目', '7月业务考核车辆', '涉及测试的车辆', '占比'], 1):
set_cell(ws1, rn, ci, h, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
rn += 1
stats = [
('车辆数(辆)', total_vehicles, interv_vehicles),
('产生考核记录数(条)', total_records, interv_records),
('仪表盘里程(km)', round(total_tbox), round(interv_tbox)),
('业务考核里程 GPS(km)', round(total_actual), round(interv_actual)),
('测试里程(km)', round(total_test), round(interv_test)),
]
for label, all_v, interv_v in stats:
ratio = interv_v / all_v if all_v else 0
set_cell(ws1, rn, 1, label, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, all_v, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 3, interv_v, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 4, ratio, data_font, None, data_align, thin_border, '0.00%')
rn += 1
rn += 2
# Section 2: Flip details
set_cell(ws1, rn, 1, '二、干预车辆考核达标情况说明', subtitle_font); rn += 1
flip_total = len(flip_recs)
summary_text = f'{interv_records} 条考核记录涉及里程干预;替换为GPS里程后,共 {flip_total} 条考核车辆记录由"达标"转判为"未达标"。按车型分类如下:'
set_cell(ws1, rn, 1, summary_text, Font(name='宋体', size=10))
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 2
target_order = ['羚牛136辆4.5T冷链车', '羚牛100辆18T', '交投40辆4.5T普货', '交投190辆4.5T冷链车', '恒运50辆4.5T普货']
for target in target_order:
if target not in flip_by_target: continue
dept_sales = flip_by_target[target]
total_cnt = sum(sum(sales.values()) for sales in dept_sales.values())
set_cell(ws1, rn, 1, f'{target}{total_cnt} 辆)', subtitle_font)
ws1.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=5); rn += 1
set_cell(ws1, rn, 1, '部门', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 2, '销售经理', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 3, '数量', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 4, '部门', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 5, '销售经理', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
set_cell(ws1, rn, 6, '数量', hdr_green_font, hdr_green_fill, hdr_align, thin_border)
rn += 1
items = []
for dept in sorted(dept_sales.keys()):
for sales in sorted(dept_sales[dept].keys()):
items.append((dept, sales, dept_sales[dept][sales]))
mid = (len(items) + 1) // 2
max_rows = max(mid, len(items) - mid)
for i in range(max_rows):
if i < mid:
d1, s1, c1 = items[i]
set_cell(ws1, rn, 1, d1, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, s1, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 3, c1, data_font, None, data_align, thin_border)
else:
for c in [1,2,3]: set_cell(ws1, rn, c, '', data_font, None, data_align, thin_border)
if i + mid < len(items):
d2, s2, c2 = items[i + mid]
set_cell(ws1, rn, 4, d2, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 5, s2, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 6, c2, data_font, None, data_align, thin_border)
else:
for c in [4,5,6]: set_cell(ws1, rn, c, '', data_font, None, data_align, thin_border)
rn += 1
rn += 1
rn += 1
# Section 3: Column source explanation
set_cell(ws1, rn, 1, '三、里程三列数据来源说明', subtitle_font); rn += 1
set_cell(ws1, rn, 1, '', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 2, '字段名', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 3, '数据来源', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
set_cell(ws1, rn, 4, '说明', hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
rn += 1
col_descs = [
('O', '原TBOX里程(km)', '车辆 T-Box / 仪表盘读数', '当月仪表总里程(业务系统原始数据),含运营+测试+调试里程。仅干预车辆填充,非干预车辆为空'),
('P', '实际行驶里程(km)', '指定GPS日里程文件', '优先使用智能管车车辆里程日报;缺失车辆由两份里程统计日明细补充,并按考核期逐日汇总。粤AG37316经确认暂按0核算'),
('Q', '测试里程(km)', 'O P(原TBOX GPS,差额)', '仪表盘跑了但 GPS 未记录到的部分,判定为厂内测试/调试/路试等非运营里程,不计入考核。仅干预车辆填充,非干预车辆为0。若差额为负则取0'),
]
for col_letter, name, source, desc in col_descs:
set_cell(ws1, rn, 1, col_letter, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 2, name, data_font, None, Alignment(horizontal='left', vertical='center'), thin_border)
set_cell(ws1, rn, 3, source, data_font, None, data_align, thin_border)
set_cell(ws1, rn, 4, desc, data_font, None, Alignment(horizontal='left', vertical='center', wrap_text=True), thin_border)
ws1.row_dimensions[rn].height = 40
rn += 1
# ===== Sheet 2: 业务考核视图 (full data,干预标黄) =====
ws2 = wb.create_sheet('业务考核视图')
target_order_full = ['车牌号','考核年份','考核月份','部门名称','销售经理','客户名称','合同编号','项目名称',
'考核目标ID','考核目标','考核开始日期','考核结束日期','考核天数','应考核里程(km)',
'原TBOX里程(km)','实际行驶里程(km)','测试里程(km)','完成率(%)','是否达标','考核状态',
'交车日期','预计还车日期','实际还车日期']
for ci, name in enumerate(target_order_full, 1):
set_cell(ws2, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(all_recs, 2):
is_interv = r['车牌号'] in intervention_plates
for ci, name in enumerate(target_order_full, 1):
set_cell(ws2, ri, ci, r.get(name), data_font, yellow_fill if is_interv else None, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws2.column_dimensions[get_column_letter(col)].width = w
# ===== Sheet 3: 7月业务考核车辆总览 =====
ws3 = wb.create_sheet('7月业务考核车辆总览')
for ci, name in enumerate(target_order_full, 1):
set_cell(ws3, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(all_recs, 2):
for ci, name in enumerate(target_order_full, 1):
set_cell(ws3, ri, ci, r.get(name), data_font, None, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws3.column_dimensions[get_column_letter(col)].width = w
# ===== Sheet 4: 涉及测试的业务考核车辆 =====
ws4 = wb.create_sheet('涉及测试的业务考核车辆')
for ci, name in enumerate(target_order_full, 1):
set_cell(ws4, 1, ci, name, hdr_blue_font, hdr_blue_fill, hdr_align, thin_border)
for ri, r in enumerate(intervention_recs, 2):
for ci, name in enumerate(target_order_full, 1):
set_cell(ws4, ri, ci, r.get(name), data_font, yellow_fill, data_align, thin_border)
for col, w in enumerate([12,8,8,12,10,26,18,22,10,22,13,12,10,14,16,14,14,12,10,8,12,12,12], 1):
ws4.column_dimensions[get_column_letter(col)].width = w
# ── Save ──
out = '7月核算结果/租赁任务考核_2026年7月_干预情况说明.xlsx'
wb.save(out)
print(f'\n{out}')
print(f'\n概况:')
print(f' 全量: {total_vehicles}辆, {total_records}')
print(f' 干预: {interv_vehicles}辆, {interv_records}')
print(f' 达→未达标: {flip_total}')
print(f' 仪表盘里程: {total_tbox:.0f} (干预{interv_tbox:.0f})')
print(f' GPS里程: {total_actual:.0f} (干预{interv_actual:.0f})')
print(f' 测试里程: {total_test:.0f} (干预{interv_test:.0f})')
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""6月测试车辆:用GPS日里程替换仪表盘里程,写入源数据文件"""
import openpyxl, pymysql, os
from datetime import datetime, date
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
# 1. 读取测试车辆清单
fp = '/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-07/65b41c10aa83cabb7a365b2b8437c1c7/6月测试车辆.xlsx'
wb = openpyxl.load_workbook(fp, data_only=True)
ws = wb.active
rows = list(ws.iter_rows(min_row=1, values_only=True))
test_plates = set()
for row in rows[1:]:
for ci in [0, 3, 6]:
p = str(row[ci]).strip() if row[ci] else ''
if p and p != 'None':
test_plates.add(p)
wb.close()
print(f'测试车辆: {len(test_plates)}')
# 2. 备份
src = '租赁任务考核_2026年6月.xlsx'
bak = '租赁任务考核_2026年6月_备份.xlsx'
if not os.path.exists(bak):
import shutil
shutil.copy2(src, bak)
print(f'已备份: {bak}')
# 3. 读源文件
wb = openpyxl.load_workbook(src)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
r['row_num'] = len(recs) + 2
recs.append(r)
# 测试车辆在源数据中的记录
test_recs = [r for r in recs if r['车牌号'] in test_plates]
print(f'测试车辆在6月源数据中的记录: {len(test_recs)}条 ({len(set(r["车牌号"] for r in test_recs))}辆)')
# 4. 连数据库取GPS
conn = pymysql.connect(host='101.133.130.65', port=3306, user='root', password='ln123!@#',
database='hydrogen_energy', connect_timeout=30)
cur = conn.cursor()
def get_gps_km(plate, start_str, end_str):
"""按考核起止日期汇总GPS日里程(km)"""
if not start_str or not end_str:
return None
try:
sd = datetime.strptime(str(start_str)[:10], '%Y-%m-%d').date()
ed = datetime.strptime(str(end_str)[:10], '%Y-%m-%d').date()
except:
return None
cur.execute("""
SELECT dates, total_mileage FROM ln_vehicle_g7_mileage
WHERE plate_number = %s AND dates >= %s AND dates <= %s
ORDER BY dates
""", (plate, sd, ed))
rows = cur.fetchall()
if not rows:
return None
# total_mileage is daily cumulative (meters). Get daily delta then sum.
# Actually check: is it daily km or cumulative? From last time: total_mileage/100000 = km
# Let's check if values are ascending (cumulative) or fluctuating (daily)
vals = [r[1] for r in rows]
is_cumulative = all(vals[i] <= vals[i+1] for i in range(len(vals)-1)) if len(vals) > 1 else False
if is_cumulative:
# Daily delta: today - yesterday
deltas = []
prev = None
for v in vals:
if prev is not None and v > prev:
deltas.append(v - prev)
elif prev is None:
deltas.append(v) # first day
prev = v
total_km = sum(deltas) / 100000
else:
total_km = sum(vals) / 100000
return round(total_km, 1)
# 批量查询: build plate→[(start,end,row_num)] map
queries = defaultdict(list)
for r in test_recs:
plate = r['车牌号']
start = r.get('考核开始日期')
end = r.get('考核结束日期')
queries[plate].append((start, end, r['row_num']))
# Process each unique plate
gps_cache = {}
replaced = 0
no_data = 0
no_data_plates = set()
for plate in sorted(queries.keys()):
# Get all GPS data for this plate in June for caching
cur.execute("""
SELECT dates, total_mileage FROM ln_vehicle_g7_mileage
WHERE plate_number = %s AND dates >= '2026-06-01' AND dates <= '2026-06-30'
ORDER BY dates
""", (plate,))
all_rows = cur.fetchall()
if not all_rows:
no_data += len(queries[plate])
no_data_plates.add(plate)
continue
for start, end, row_num in queries[plate]:
km = get_gps_km(plate, start, end)
if km is None:
no_data += 1
no_data_plates.add(plate)
else:
gps_cache[row_num] = km
conn.close()
# 5. 更新源文件
# Need to add '原TBOX里程' and '测试里程' columns if not exist
# Check existing columns
last_col_letter = openpyxl.utils.get_column_letter(ws.max_column)
need_new_cols = '原TBOX里程(km)' not in h and '测试里程(km)' not in h
if need_new_cols:
# Add two new columns
tbox_col = ws.max_column + 1
test_col = ws.max_column + 2
ws.cell(row=1, column=tbox_col, value='原TBOX里程(km)')
ws.cell(row=1, column=test_col, value='测试里程(km)')
# Find the actual行驶里程 column index
actual_idx = h.index('实际行驶里程(km)') + 1 # 1-indexed
else:
tbox_idx = h.index('原TBOX里程(km)') + 1 if '原TBOX里程(km)' in h else None
test_idx = h.index('测试里程(km)') + 1 if '测试里程(km)' in h else None
# Apply replacements
for row_num, gps_km in gps_cache.items():
# Find column indices in current sheet
row_data = [c.value for c in ws[row_num]]
h_current = [c.value for c in ws[1]]
actual_col = h_current.index('实际行驶里程(km)') + 1
old_val = ws.cell(row=row_num, column=actual_col).value
if need_new_cols:
tbox_col = h_current.index('原TBOX里程(km)') + 1 if '原TBOX里程(km)' in h_current else ws.max_column - 1
test_col = h_current.index('测试里程(km)') + 1 if '测试里程(km)' in h_current else ws.max_column
else:
tbox_col = h_current.index('原TBOX里程(km)') + 1
test_col = h_current.index('测试里程(km)') + 1
# Write original TBOX mileage
ws.cell(row=row_num, column=tbox_col, value=float(old_val) if old_val else 0)
# Write test mileage (TBOX - GPS)
tbox_val = float(old_val or 0)
test_km = round(tbox_val - gps_km, 1)
ws.cell(row=row_num, column=test_col, value=test_km)
# Replace actual mileage with GPS
ws.cell(row=row_num, column=actual_col, value=gps_km)
replaced += 1
wb.save(src)
wb.close()
print(f'\n替换结果:')
print(f' 成功替换: {replaced}')
print(f' 无GPS数据: {no_data}条 ({len(no_data_plates)}辆)')
if no_data_plates:
print(f' 无数据车牌: {sorted(no_data_plates)}')
print(f'\n已保存: {src}')
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Step 1: GPS里程替换 - 针对 租赁任务考核_2026年6月 (1).xlsx"""
import openpyxl, pymysql, os, shutil
from datetime import datetime
from collections import defaultdict
os.chdir('/Users/kkfluous/Downloads')
# 36辆测试车辆(从上一次GPS替换留底提取)
TEST_PLATES = {
'粤AGE5480','粤AGF4535','粤AGG3490','粤AGG4135','粤AGG4191',
'粤AGP2033','粤AGP3080','粤AGP3663','粤AGP3676','粤AGP5139',
'粤AGP5156','粤AGP5163','粤AGP5168','粤AGP5636','粤AGP5767',
'粤AGP5768','粤AGP5796','粤AGP9733','粤AGP9786','粤AGR0298',
'粤AGR1586','粤AGR3288','粤AGR5056','粤AGR5068','粤AGR5088',
'粤AGR6869','粤AGR6877','粤AGR6879','粤AGR6881','粤AGR8506',
'粤AGR8551','粤AGR8558','粤AGR8799','粤AGR9866','粤AGR9893',
'粤AGR9899',
}
SRC = '租赁任务考核_2026年6月 (1).xlsx'
DST = '租赁任务考核_2026年6月.xlsx'
BAK = '租赁任务考核_2026年6月_备份_0714.xlsx'
# 0. 备份现有的6月文件
if os.path.exists(DST):
shutil.copy2(DST, BAK)
print(f'已备份原6月文件 → {BAK}')
# 1. 复制新源文件为目标文件
shutil.copy2(SRC, DST)
print(f'源文件: {SRC}')
print(f'目标文件: {DST}')
# 2. 读取目标文件
wb = openpyxl.load_workbook(DST)
ws = wb['业务考核视图']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
print(f'列数: {len(h)}, 表头: {h}')
recs = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
r['row_num'] = len(recs) + 2
recs.append(r)
test_recs = [r for r in recs if r['车牌号'] in TEST_PLATES]
print(f'测试车辆记录: {len(test_recs)}条 ({len(set(r["车牌号"] for r in test_recs))}辆)')
# 3. 连接数据库取GPS日里程
conn = pymysql.connect(host='101.133.130.65', port=3306, user='root', password='ln123!@#',
database='hydrogen_energy', connect_timeout=30)
cur = conn.cursor()
def get_gps_km(plate, start_str, end_str):
"""汇总GPS日里程(km)"""
if not start_str or not end_str:
return None
try:
sd = datetime.strptime(str(start_str)[:10], '%Y-%m-%d').date()
ed = datetime.strptime(str(end_str)[:10], '%Y-%m-%d').date()
except:
return None
cur.execute("""
SELECT dates, total_mileage FROM ln_vehicle_g7_mileage
WHERE plate_number = %s AND dates >= %s AND dates <= %s
ORDER BY dates
""", (plate, sd, ed))
rows = cur.fetchall()
if not rows:
return None
vals = [r[1] for r in rows]
is_cumulative = all(vals[i] <= vals[i+1] for i in range(len(vals)-1)) if len(vals) > 1 else False
if is_cumulative:
deltas = []
prev = None
for v in vals:
if prev is not None and v > prev:
deltas.append(v - prev)
elif prev is None:
deltas.append(v)
prev = v
total_km = sum(deltas) / 100000
else:
total_km = sum(vals) / 100000
return round(total_km, 1)
# 批量查询
gps_cache = {}
replaced = 0
no_data = 0
no_data_plates = set()
for r in test_recs:
plate = r['车牌号']
km = get_gps_km(plate, r.get('考核开始日期'), r.get('考核结束日期'))
if km is None:
no_data += 1
no_data_plates.add(plate)
else:
gps_cache[r['row_num']] = km
conn.close()
print(f'GPS查询: 成功{len(gps_cache)}条, 无数据{no_data}')
# 4. 添加 原TBOX里程(km) 和 测试里程(km) 列
if '原TBOX里程(km)' not in h:
tbox_col = ws.max_column + 1
test_col = ws.max_column + 2
ws.cell(row=1, column=tbox_col, value='原TBOX里程(km)')
ws.cell(row=1, column=test_col, value='测试里程(km)')
# 刷新表头
h_new = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
else:
h_new = h
# 找列索引
actual_idx = h_new.index('实际行驶里程(km)') + 1
tbox_idx = h_new.index('原TBOX里程(km)') + 1
test_idx = h_new.index('测试里程(km)') + 1
# 5. 写入GPS数据
for row_num, gps_km in gps_cache.items():
old_val = ws.cell(row=row_num, column=actual_idx).value
# 写入原TBOX里程
ws.cell(row=row_num, column=tbox_idx, value=float(old_val) if old_val else 0)
# 写入测试里程
tbox_val = float(old_val or 0)
test_km = round(tbox_val - gps_km, 1)
ws.cell(row=row_num, column=test_idx, value=test_km)
# 替换实际行驶里程为GPS
ws.cell(row=row_num, column=actual_idx, value=gps_km)
replaced += 1
# 6. 测试车辆标记黄色背景
from openpyxl.styles import PatternFill
yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
for row_num in gps_cache.keys():
for c in range(1, ws.max_column + 1):
ws.cell(row=row_num, column=c).fill = yellow_fill
# 7. 重算完成率和是否达标
DAYSM = {1:31,2:28,3:31,4:30,5:31,6:30,7:31,8:31,9:30,10:31,11:30,12:31}
for r in recs:
rn = r['row_num']
actual = ws.cell(row=rn, column=actual_idx).value or 0
target = float(r.get('应考核里程(km)') or 0)
pct = round(actual / target * 100, 2) if target > 0 else 0
q = '达标' if actual >= target and target > 0 else '未达标'
# 找完成率列和是否达标列
rate_idx = h_new.index('完成率(%)') + 1
qualified_idx = h_new.index('是否达标') + 1
ws.cell(row=rn, column=rate_idx, value=pct)
ws.cell(row=rn, column=qualified_idx, value=q)
wb.save(DST)
wb.close()
print(f'\n===== GPS替换完成 =====')
print(f'替换成功: {replaced}')
print(f'无GPS数据: {no_data}条 ({len(no_data_plates)}辆)')
if no_data_plates:
print(f'无数据车牌: {sorted(no_data_plates)}')
print(f'已保存: {DST}')
+211
View File
@@ -0,0 +1,211 @@
# 养馋记 · 物流台账闭环 · 全量对照总表
> **Make 打开本页:** http://localhost:53817/?projectId=make&doc=yangchanji-plan-ab-compare.md
> **主库:** `knowledge/产品线1-项目1/04-产品方案/方案AB对照摘要.md`
| 文档 | Make 打开 |
|------|-----------|
| **本页 · A/B 对照** | http://localhost:53817/?projectId=make&doc=yangchanji-plan-ab-compare.md |
| 方案 A · 可对接 | http://localhost:53817/?projectId=make&doc=yangchanji-plan-a.md |
| 方案 B · 不可对接 | http://localhost:53817/?projectId=make&doc=yangchanji-plan-b.md |
| 数智中心评估汇总 | http://localhost:53817/?projectId=make&doc=yangchanji-ledger-eval.md |
| 文档 | 用途 |
|------|------|
| 本页 | A/B 与共性一表看清 |
| 方案 A | 可对接 API 的完整评估(第五、六点) |
| 方案 B | 不可对接时的完整评估(第五、六点) |
| 数智中心评估汇总 | 双路径合一稿 |
---
## 一、方案基本信息对照
| 对照项 | 方案 A · 可对接 | 方案 B · 不可对接 |
|--------|-----------------|-------------------|
| 前提 | 养馋记 TMS 提供可用 API + 沙箱 | 无可用 API / 短期对不通 |
| 数据进 OneOS | 对方 API 同步(Excel 兜底) | 导入模板 / 手工建单 + 主数据改造 |
| 状态回传对方 | 可做(P1) | 本期不做(可导出) |
| 特有开发 | 对接适配层 | 导入中心 + 字段改造 |
| 可行性 | 有条件可行 | 有条件可行 |
| 推荐场景 | 接口确认可用时默认 | 对不通,或先通闭环再切 A |
| 版本 | V1.2(表格化) | V1.2(表格化) |
---
## 二、第五点八问 · A/B 对照
| # | 评估问题 | 方案 A | 方案 B |
|---|----------|--------|--------|
| 1 | 技术上能否实现 | 有条件可行(依赖 API) | 有条件可行(不依赖 API) |
| 2 | 数据/接口怎么来 | API:订单/路线/门店等;回传 P1 | 无接口;导入/录入同套业务对象 |
| 3 | 我方开发什么 | 对接层 + 待派派单小程序计费对账 | 导入改造 + 待派派单小程序计费对账 |
| 4 | 多长时间 | 约 **1825 周 / 56.5 月** | 约 **2027 周 / 5.57 月** |
| 5 | 投入人员 | 产1研2测1 + 对方接口人 | 产1研2测1 + **运营导单强依赖** |
| 6 | 额外费用 | 联调+小程序等;专线视情况 | 现金更低;**导单隐性成本中** |
| 7 | 前置与风险 | API/沙箱/映射;字段不一致、审核延误 | 导单责任/缺口表;导单延误、数据不一致 |
| 8 | 分期 | 调研→V0→V1→V2→利润后置 | 同左(V1 多 2~4 周) |
---
## 三、周期对照(已压缩调研+V0
| 阶段 | 方案 A | 方案 B | 备注 |
|------|--------|--------|------|
| 业务调研 | 12 周 | 1~2 周 | 与 V0 可并行收口 |
| V0 门禁 | 约 1 周 | 约 1 周 | |
| **调研+V0** | **约 23 周** | **约 23 周** | 原 36 周减半 |
| V1 | 1216 周 | 1418 周 | B 含导入改造 |
| V2 | 46 周 | 46 周 | |
| 利润(后置) | 另 2~4 周 | 另 2~4 周 | 不进首批退出 |
| **首批合计** | **约 56.5 个月** | **约 5.57 个月** | 含调研 |
---
## 四、业务时序与已拍板规则(A/B 相同)
| 步骤 | 规则 |
|------|------|
| 1 | 数据进入 OneOSA=API / B=导入或录入) |
| 2 | 自动生成待派任务 |
| 3 | 人工选车、选司机并派单 |
| 4 | **派单后**司机小程序才可见任务 |
| 5 | 正常情况:**当前运单未完成,不可接下一单** |
| 6 | 办结运单后 → **才触发正式计费/出账** |
| 7 | V2:成本归集 + 流水 + 合作方只读确认 |
| 8 | 利润抽点在流水与应收应付之后 |
| 已拍板项 | 结论 |
|----------|------|
| 首批范围 | V0+V1+V2;V3(用友等)不计入 |
| 计费 | 多模型引擎;每客户只绑一种 |
| 对账深度 | 只读 + 确认(无驳回/补凭证) |
| 司机端 | 微信小程序,V1 必达 |
| 氢/ETC | 可先运费;V2 必须做完 |
| 编制 | 产品1 + 研发2 + 测试1 |
| 待确认项 | 说明 |
|----------|------|
| 走 A / B / 先 B 后 A | 调研结束书面选定 |
| 车司机是否固定绑定 | 运营确认 |
| 异常是否允许代办结 | 运营确认 |
| 规则包书面试算样例 | 业务+财务 |
| 利润具体参数 | 后置再锁 |
---
## 五、数据对象对照(业务要什么 × 怎么拿到)
| 数据对象 | 用途 | 方案 A 获取 | 方案 B 获取 | 优先级 |
|----------|------|-------------|-------------|--------|
| 订单 | 生成待派 | 对方 API | 导入/手工建单 | P0 |
| 路线/经停 | 派单与司机点序 | 对方 API | 导入/录入 | P0 |
| 门店主数据 | 主数据与对账 | API 或首导+增量 | 批量导入+日常维护 | P0 |
| 货物/物料 | 温层货量/计价 | API(汇总或明细) | 随单导入或汇总填 | P0/按需 |
| 运费要素 | 办结后计费 | 订单内或规则侧 | 导入列或规则侧 | P0 |
| 车/司机 | 人工派单 | 我方主数据为主 | 同左 | P0 |
| 状态回传 | 告知对方 | OneOS→对方 API | 不做接口(可导出) | A:P1 / B:— |
| 氢费/ETC | V2 成本 | 我方内部系统 | 同左 | V2 |
---
## 六、我方功能对照
| 功能模块 | 方案 A | 方案 B | 阶段 |
|----------|--------|--------|------|
| 对接适配(鉴权/同步/映射/幂等/重试/差异日志) | ✅ | ❌(可不建) | V0~V1 |
| 主数据/单据字段改造 | 按需 | ✅ 重点 | V0~V1 |
| 导入中心(模板/校验/重导) | 仅兜底 | ✅ 重点 | V1 |
| 手工建单/维护 | 辅 | ✅ | V1 |
| 待派任务池 | ✅ | ✅ | V1 |
| 人工调度台 | ✅ | ✅ | V1 |
| 司机微信小程序 | ✅ | ✅ | V1 |
| 办结后计费出账 | ✅ | ✅ | V1 |
| 成本归集 | ✅ | ✅ | V2 |
| 流水/应收应付/只读确认 | ✅ | ✅ | V2 |
| 利润抽点 | 后置 | 后置 | V2 后 |
| 用友 | 不计入 | 不计入 | V3 |
---
## 七、人员与费用对照
| 角色/费用项 | 方案 A | 方案 B |
|-------------|--------|--------|
| 产品 | 1 | 1 |
| 研发 | 2 | 2 |
| 测试 | 1 | 1 |
| 养馋记配合 | 接口人强 | 台账/导出样例弱 |
| 运营配合 | 调研+双跑 | **导单责任人+日常导单(强)** |
| 对方 API 联调费 | 可能有 | 无 |
| 小程序认证 | 必需 | 必需 |
| 短信 | 可选 | 可选 |
| 氢/ETC 源 | V2 必需 | V2 必需 |
| 独立服务器 | 通常否 | 通常否 |
| 隐性成本 | 联调等待 | **持续导单与主数据质量** |
---
## 八、第六点确认事项 · A/B 对照
| # | 原文件确认项 | 方案 A 状态 | 方案 B 状态 |
|---|--------------|-------------|-------------|
| 1 | 标准接口还是 Excel | 假定 **API**Excel 兜底 | 假定 **无 API**Excel/模板为主 |
| 2 | 能提供哪些字段 | 待映射表收口 | 按业务 P0 定导入列 |
| 3 | 状态能否实时回传 | 目标 P1;调研确认是否准实时 | **本期不做**系统回传 |
| 4 | 氢费/ETC 从哪来 | 我方内部;V2 | 同左 |
| 5 | 司机端形态 | **已定小程序** | **已定小程序** |
| 6 | 车司机是否绑定 | 待运营确认 | 待运营确认 |
| 7 | 利润怎么配 | 后置;按客户配 | 同左 |
| 8 | 对账深度 | **已定只读+确认** | **已定只读+确认** |
| 9 | 是否接用友 | **本期不做** | **本期不做** |
---
## 九、分期退出标准对照
| 期次 | 方案 A 退出标准 | 方案 B 退出标准 |
|------|-----------------|-----------------|
| 调研 | 确认可对接;字段草案+原型范围 | 确认走 B(或先 B);缺口表+导单责任+原型范围 |
| V0 | 映射确认、规则样例、小程序账号、沙箱就绪 | 导入模板、字段缺口确认、规则样例、小程序账号就绪 |
| V1 | 双跑运费可解释;小程序可用;办结出账;未完成不可接下一单 | 导入可生成待派;其余同 A |
| V2 | 成本进项目;双方月确认一版 | 同左(对内或合作方账号) |
| 利润 | 后置 | 后置 |
| 名词 | 含义(白话) |
|------|----------------|
| 退出标准 | 本期做到什么才算结束、可进下一期 |
| 双跑出账可解释(V1) | 运费应收侧能讲清;完整应付成本在 V2 |
| 映射确认 | A=对方字段↔OneOSB=导入列↔OneOS |
---
## 十、决策日志 · 养馋记台账相关(已拍板对照)
| 日期 | 议题 | 结论 |
|------|------|------|
| 2026-08-10 | 首批范围 | 批准 V0+V1+V2V3 不计入 |
| 2026-08-10 | 计费模型 | 多模型引擎;每客户只绑一种 |
| 2026-08-10 | 对账深度 | 只读+确认 |
| 2026-08-10 | 司机端 | 微信小程序;V1 必达 |
| 2026-08-10 | 氢/ETC | 可先运费;V2 必做完 |
| 2026-08-10 | 利润 | 流水与应收应付之后 |
| 2026-08-10 | 编制 | 产1研2测1 |
| 2026-08-10 | 时序 | 派单后可见;办结后计费 |
| 2026-08-10 | 接单约束 | 未完成不可接下一单 |
| 2026-08-11 | 数据路径 | A API / B 导入改造 |
| 2026-08-11 | 周期 | 调研+V0≈23周;A≈56.5月;B≈5.57月 |
---
## 十一、评估结果一览(并排)
| 评估结果项 | 方案 A | 方案 B |
|------------|--------|--------|
| 可行性 | 有条件可行 | 有条件可行 |
| 数据策略 | 对方 API 主路径 | OneOS 改造+导入/维护 |
| 周期 | 约 56.5 个月 | 约 5.57 个月 |
| 人员 | 产1研2测1 | 产1研2测1+运营强依赖 |
| 费用感 | 低~中 | 现金低、运营成本中 |
| 首批 | 调研→V0→V1→V2 | 同左 |
| 利润/用友 | 后置 / 不计入 | 同左 |
@@ -0,0 +1,97 @@
# 养馋记物流派单与对账模块建设评估
## 管理层汇报版
> 本次仅进行方案与工作量评估,暂不进入开发。
> 目标:养馋记订单及路线数据接入 **OneOS**,形成“派单—费用归集—利润核算—月度对账”闭环。
---
## 一、结论与请决
### 结论
- **项目可做,但以 API、费用口径和成本数据可用为前提。**
- **首期采用“系统辅助 + 人工派单”**,不做全自动排班。
- 按“三阶段”推进:先跑通业务与 T+1 暂算盈亏,再完成对账闭环,最后建设 AI 智能调度。
### 请决事项
1. 同意第一阶段按 **1 个月 MVP** 推进;
2. 同意以养馋记 API 为主、Excel/人工建单为兜底;
3. 同意第一阶段配置产品 1 人、研发 2 人、测试 1 人;
4. 同意 API 门禁、字段清单、费用试算样例和 OneOS 成本匹配规则确认后,正式启动开发。
---
## 二、分阶段投入总表
| 阶段 | 核心交付 | 周期 | 人员配置 | 阶段人力成本 | 累计人力成本 |
| --- | --- | ---: | --- | ---: | ---: |
| **启动门禁** | API/字段确认、业务调研、计费试算、OneOS 成本数据匹配、原型与测试环境 | 约 1 周 | 产品 1、研发 2;测试按需 | 纳入第一阶段 | — |
| **第一阶段:MVP** | 订单接入、物流台账、待派任务、人工派车/派司机、司机端基础执行、OneOS 氢费/ETC 归集、逐单及 T+1 暂算盈亏 | **约 1 个月** | 产品 1、研发 2、测试 1 | **96,600 元** | **96,600 元** |
| **第二阶段:业务与对账闭环** | 改单取消、异常办结、完整费用与车辆成本、计费规则、利润精算、明细账/汇总账、月度对账、合作方在线对账、GPS 异常管控 | **2—3 个月** | 产品 1、研发 2、测试 1 | **206,400—322,200 元** | **303,000—418,800 元** |
| **第三阶段:AI 智能调度试点** | 车辆/司机推荐、串单串点、复跑优化、车型装载匹配、空驶/绕行/无效 ETC 优化 | 数据积累 3—6 个月;试点开发 3—4 个月 | 产品 1、研发/算法 2—3、测试 1 | **475,200—633,600 元** | **778,200—1,052,400 元** |
### 成本口径
| 成本项 | 口径 |
| --- | --- |
| 产品、研发 | 1,500 元/人天 |
| 测试 | 1,200 元/人天 |
| 第一、二阶段满负荷日成本 | 5,700 元/工作日 |
| 当前建议审批范围 | **第一阶段 + 第二阶段:303,000—418,800 元** |
| 含 AI 试点的远期总投入 | **778,200—1,052,400 元**,待数据准备完成后单独立项 |
> 第二、三阶段为评估值;正式报价按 API 成熟度、费用规则数量、对账权限范围及实际人天核定。
---
## 三、各阶段能做什么
| 能力 | 第一阶段 | 第二阶段 | 第三阶段 |
| --- | --- | --- | --- |
| 养馋记订单、路线接入 | ✔ | 完善异常与状态回传 | — |
| 待派任务、人工派单 | ✔ | 完善拆单、换车换人、异常流程 | 智能推荐 |
| 司机端执行与凭证 | ✔ | 完善异常与闭环 | — |
| OneOS 氢费、ETC 归集 | ✔(基础归集) | 完整费用、车辆成本和精算 | 成本优化建议 |
| 逐单利润 | ✔(暂算) | ✔(精算、可对账) | 优化决策 |
| 月度账单、合作方在线对账 | — | ✔ | — |
| AI 智能调度 | — | 数据积累 | ✔(试点) |
### 第一阶段验收标准
> **养馋记订单接入 → 待派任务 → 人工派车/派司机 → 司机执行与办结 → OneOS 氢费/ETC 归集 → 逐单暂算 → T+1 盈亏查看**
首期不包含:完整月度对账、合作方确认/驳回、用友接口、全自动派单及 AI 调度。
---
## 四、成本事项
| 类别 | 具体事项 | 是否纳入上述人力成本 |
| --- | --- | --- |
| 人力成本 | 产品、研发、测试投入 | **已纳入** |
| API 与联调 | 养馋记接口服务、联调支持、可能的信息服务费 | 未纳入,待确认 |
| 平台服务 | 小程序认证、短信/消息、地图、GPS 服务 | 未纳入,按实际使用核定 |
| 基础设施 | 云服务器、数据库、对象存储、独立部署及运维 | 未纳入,按部署方案核定 |
| 财务/第三方系统 | 用友等系统后续接口 | 未纳入,本期不做 |
---
## 五、启动前门禁与主要风险
| 门禁事项 | 不确认的影响 |
| --- | --- |
| 养馋记 API 文档、鉴权、沙箱、字段和同步时效 | 首期范围和周期无法锁定 |
| 新单、改单、取消单、拆单及状态回传规则 | 任务与派单逻辑不完整 |
| 运费、抽点、杂费、车辆成本的试算样例 | 利润和对账结果可能有争议 |
| OneOS 氢费、ETC 的来源、时效和运单匹配规则 | 只能形成暂算,无法精算 |
| 合作方对账权限:只读、确认、驳回、补凭证 | 第二阶段在线对账无法闭环 |
---
## 六、建议决策
1. **本次先批准第一阶段 MVP,预算 96,600 元,周期约 1 个月。**
2. 第一阶段验收后,再按实际业务量和对账规则确认第二阶段 2—3 个月投入。
3. AI 智能调度不纳入当前开发承诺;待业务闭环稳定、累计 3—6 个月真实数据后,以试点方式单独立项。
@@ -0,0 +1,26 @@
# 养馋记物流派单与对账模块建设评估
## 一、结论与请决
- **项目可做**:以养馋记 API、费用规则及 OneOS 成本数据可用为前提。
- **首期策略**:系统辅助、人工派单;暂不做全自动与 AI 调度。
- **本次请决**:同意先启动第一阶段 MVP,接口与试算样例确认后正式开发。
## 二、分阶段投入
| 阶段 | 主要交付 | 周期 | 人力成本 |
| --- | --- | --- | --- |
| 第一阶段:MVP | 订单接入、人工派单、司机执行、氢费/ETC 归集、逐单及 T+1 暂算盈亏 | 1 个月 | **114,000 元** |
| 第二阶段:对账闭环 | 异常处理、费用精算、月度账单、合作方在线对账 | 1—2 个月 | **114,000—228,000 元**;累计 **228,000—342,000 元** |
| 第三阶段:AI 试点 | 串单/串点、复跑、车型匹配及成本优化建议 | 数据积累 2—3 个月;试点 1—2 个月 | **114,000—288,000 元**;远期累计 **342,000—630,000 元** |
## 三、成本口径
- 产品、研发:**1,500 元/人天**;测试:**1,200 元/人天**。
- 第一、二阶段配置:产品 1 人、研发 2 人、测试 1 人;按 20 个工作日/月计算,日成本 **5,700 元**
- AI 试点配置:产品 1 人、研发/算法 2—3 人、测试 1 人;数据积累期随前两阶段业务运行完成,不单独计入开发人力。
- 上述为人力成本;API 联调、小程序认证、短信/地图/GPS、云资源及独立部署按实际方案另计。
## 四、启动门禁
确认 API 与字段、订单状态规则、费用试算样例及 OneOS 氢费/ETC 匹配规则后启动。
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More