diff --git a/.gitignore b/.gitignore index 6105287..da062e8 100644 --- a/.gitignore +++ b/.gitignore @@ -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* diff --git a/01-可行性与实施计划书.md b/01-可行性与实施计划书.md new file mode 100644 index 0000000..56c20ed --- /dev/null +++ b/01-可行性与实施计划书.md @@ -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 | +| S1–S2 系统 | C | C | C | C | **A/R** | I | +| S3–S4 立项 | 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 | 首版:计划书 + 流程图 + 实施计划成套 | diff --git a/0813.md b/0813.md new file mode 100644 index 0000000..c3c9fdf --- /dev/null +++ b/0813.md @@ -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 个月后单独评估,避免“先投入、后找场景”。 diff --git a/20260713/离职人员6月奖金处理明细.xlsx b/20260713/离职人员6月奖金处理明细.xlsx new file mode 100644 index 0000000..3721314 Binary files /dev/null and b/20260713/离职人员6月奖金处理明细.xlsx differ diff --git a/20260713/租赁任务考核_2026年6月.xlsx b/20260713/租赁任务考核_2026年6月.xlsx new file mode 100644 index 0000000..8470b1f Binary files /dev/null and b/20260713/租赁任务考核_2026年6月.xlsx differ diff --git a/20260713/租赁任务考核_2026年6月_干预情况说明.xlsx b/20260713/租赁任务考核_2026年6月_干预情况说明.xlsx new file mode 100644 index 0000000..d652542 Binary files /dev/null and b/20260713/租赁任务考核_2026年6月_干预情况说明.xlsx differ diff --git a/20260713/里程任务考核_6月核算_2.xlsx b/20260713/里程任务考核_6月核算_2.xlsx new file mode 100644 index 0000000..e4ca7d5 Binary files /dev/null and b/20260713/里程任务考核_6月核算_2.xlsx differ diff --git a/里程考核绩效核算/数据来源/1月.xlsx b/20260713_1500/1月.xlsx similarity index 100% rename from 里程考核绩效核算/数据来源/1月.xlsx rename to 20260713_1500/1月.xlsx diff --git a/里程考核绩效核算/数据来源/2月.xlsx b/20260713_1500/2月.xlsx similarity index 100% rename from 里程考核绩效核算/数据来源/2月.xlsx rename to 20260713_1500/2月.xlsx diff --git a/里程考核绩效核算/数据来源/3月.xlsx b/20260713_1500/3月.xlsx similarity index 100% rename from 里程考核绩效核算/数据来源/3月.xlsx rename to 20260713_1500/3月.xlsx diff --git a/20260713_1500/6月考核数据去重说明.md b/20260713_1500/6月考核数据去重说明.md new file mode 100644 index 0000000..6e4c620 --- /dev/null +++ b/20260713_1500/6月考核数据去重说明.md @@ -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人发放金额完全不受影响 diff --git a/20260713_1500/6月考核数据去重说明.xlsx b/20260713_1500/6月考核数据去重说明.xlsx new file mode 100644 index 0000000..bd521d4 Binary files /dev/null and b/20260713_1500/6月考核数据去重说明.xlsx differ diff --git a/20260713_1500/离职人员6月奖金处理明细.xlsx b/20260713_1500/离职人员6月奖金处理明细.xlsx new file mode 100644 index 0000000..e671ec5 Binary files /dev/null and b/20260713_1500/离职人员6月奖金处理明细.xlsx differ diff --git a/里程考核绩效核算/数据来源/租赁任务考核_2026年1月.xlsx b/20260713_1500/租赁任务考核_2026年1月.xlsx similarity index 100% rename from 里程考核绩效核算/数据来源/租赁任务考核_2026年1月.xlsx rename to 20260713_1500/租赁任务考核_2026年1月.xlsx diff --git a/里程考核绩效核算/数据来源/租赁任务考核_2026年2月.xlsx b/20260713_1500/租赁任务考核_2026年2月.xlsx similarity index 100% rename from 里程考核绩效核算/数据来源/租赁任务考核_2026年2月.xlsx rename to 20260713_1500/租赁任务考核_2026年2月.xlsx diff --git a/里程考核绩效核算/数据来源/租赁任务考核_2026年3月.xlsx b/20260713_1500/租赁任务考核_2026年3月.xlsx similarity index 100% rename from 里程考核绩效核算/数据来源/租赁任务考核_2026年3月.xlsx rename to 20260713_1500/租赁任务考核_2026年3月.xlsx diff --git a/20260713_1500/租赁任务考核_2026年4月.xlsx b/20260713_1500/租赁任务考核_2026年4月.xlsx new file mode 100644 index 0000000..64e0308 Binary files /dev/null and b/20260713_1500/租赁任务考核_2026年4月.xlsx differ diff --git a/20260713_1500/租赁任务考核_2026年5月.xlsx b/20260713_1500/租赁任务考核_2026年5月.xlsx new file mode 100644 index 0000000..d38f9fc Binary files /dev/null and b/20260713_1500/租赁任务考核_2026年5月.xlsx differ diff --git a/20260713_1500/租赁任务考核_2026年6月 (1).xlsx b/20260713_1500/租赁任务考核_2026年6月 (1).xlsx new file mode 100644 index 0000000..11cfe00 Binary files /dev/null and b/20260713_1500/租赁任务考核_2026年6月 (1).xlsx differ diff --git a/20260713_1500/租赁任务考核_2026年6月.xlsx b/20260713_1500/租赁任务考核_2026年6月.xlsx new file mode 100644 index 0000000..aa6d92c Binary files /dev/null and b/20260713_1500/租赁任务考核_2026年6月.xlsx differ diff --git a/20260713_1500/租赁任务考核_2026年6月_干预情况说明.xlsx b/20260713_1500/租赁任务考核_2026年6月_干预情况说明.xlsx new file mode 100644 index 0000000..e6b722f Binary files /dev/null and b/20260713_1500/租赁任务考核_2026年6月_干预情况说明.xlsx differ diff --git a/20260713_1500/里程任务考核_6月核算_2.xlsx b/20260713_1500/里程任务考核_6月核算_2.xlsx new file mode 100644 index 0000000..92b5428 Binary files /dev/null and b/20260713_1500/里程任务考核_6月核算_2.xlsx differ diff --git a/20260713_1500/里程任务考核_6月核算_(发放记录已删除离职人员补发和转接).xlsx b/20260713_1500/里程任务考核_6月核算_(发放记录已删除离职人员补发和转接).xlsx new file mode 100644 index 0000000..a5c02af Binary files /dev/null and b/20260713_1500/里程任务考核_6月核算_(发放记录已删除离职人员补发和转接).xlsx differ diff --git a/5月18T测算/方案1_原逻辑_1000基准.xlsx b/5月18T测算/方案1_原逻辑_1000基准.xlsx new file mode 100644 index 0000000..f2d9c9a Binary files /dev/null and b/5月18T测算/方案1_原逻辑_1000基准.xlsx differ diff --git a/5月18T测算/方案1_原逻辑_1000基准_0701.xlsx b/5月18T测算/方案1_原逻辑_1000基准_0701.xlsx new file mode 100644 index 0000000..9fe0fd6 Binary files /dev/null and b/5月18T测算/方案1_原逻辑_1000基准_0701.xlsx differ diff --git a/5月18T测算/方案2_简化版_800_300仅当月.xlsx b/5月18T测算/方案2_简化版_800_300仅当月.xlsx new file mode 100644 index 0000000..f760217 Binary files /dev/null and b/5月18T测算/方案2_简化版_800_300仅当月.xlsx differ diff --git a/5月18T测算/方案2_简化版_800_300仅当月_0701.xlsx b/5月18T测算/方案2_简化版_800_300仅当月_0701.xlsx new file mode 100644 index 0000000..1f061c6 Binary files /dev/null and b/5月18T测算/方案2_简化版_800_300仅当月_0701.xlsx differ diff --git a/5月18T测算/方案3_完整版_800_300_500.xlsx b/5月18T测算/方案3_完整版_800_300_500.xlsx new file mode 100644 index 0000000..20322fc Binary files /dev/null and b/5月18T测算/方案3_完整版_800_300_500.xlsx differ diff --git a/5月18T测算/方案3_完整版_800_300_500_0701.xlsx b/5月18T测算/方案3_完整版_800_300_500_0701.xlsx new file mode 100644 index 0000000..967f2bc Binary files /dev/null and b/5月18T测算/方案3_完整版_800_300_500_0701.xlsx differ diff --git a/6月18T测算/6月18T_方案3.xlsx b/6月18T测算/6月18T_方案3.xlsx new file mode 100644 index 0000000..d5fb3e7 Binary files /dev/null and b/6月18T测算/6月18T_方案3.xlsx differ diff --git a/6月核算结果/离职人员6月奖金处理明细.xlsx b/6月核算结果/离职人员6月奖金处理明细.xlsx new file mode 100644 index 0000000..6c454ab Binary files /dev/null and b/6月核算结果/离职人员6月奖金处理明细.xlsx differ diff --git a/6月核算结果/租赁任务考核_2026年6月.xlsx b/6月核算结果/租赁任务考核_2026年6月.xlsx new file mode 100644 index 0000000..8470b1f Binary files /dev/null and b/6月核算结果/租赁任务考核_2026年6月.xlsx differ diff --git a/6月核算结果/租赁任务考核_2026年6月_干预情况说明.xlsx b/6月核算结果/租赁任务考核_2026年6月_干预情况说明.xlsx new file mode 100644 index 0000000..d652542 Binary files /dev/null and b/6月核算结果/租赁任务考核_2026年6月_干预情况说明.xlsx differ diff --git a/6月核算结果/里程任务考核_6月核算.xlsx b/6月核算结果/里程任务考核_6月核算.xlsx new file mode 100644 index 0000000..eeae3cf Binary files /dev/null and b/6月核算结果/里程任务考核_6月核算.xlsx differ diff --git a/7月核算结果/7月考核车辆多源里程汇总.xlsx b/7月核算结果/7月考核车辆多源里程汇总.xlsx new file mode 100644 index 0000000..93f7715 Binary files /dev/null and b/7月核算结果/7月考核车辆多源里程汇总.xlsx differ diff --git a/7月核算结果/7月考核车辆多源里程汇总_GPS全量回补前.xlsx b/7月核算结果/7月考核车辆多源里程汇总_GPS全量回补前.xlsx new file mode 100644 index 0000000..778bb23 Binary files /dev/null and b/7月核算结果/7月考核车辆多源里程汇总_GPS全量回补前.xlsx differ diff --git a/7月核算结果/7月考核车辆多源里程汇总_含车型.xlsx b/7月核算结果/7月考核车辆多源里程汇总_含车型.xlsx new file mode 100644 index 0000000..cb84b84 Binary files /dev/null and b/7月核算结果/7月考核车辆多源里程汇总_含车型.xlsx differ diff --git a/7月核算结果/7月考核车辆多源里程汇总_增加车型前.xlsx b/7月核算结果/7月考核车辆多源里程汇总_增加车型前.xlsx new file mode 100644 index 0000000..7dffd8b Binary files /dev/null and b/7月核算结果/7月考核车辆多源里程汇总_增加车型前.xlsx differ diff --git a/7月核算结果/7月考核车辆多源里程汇总_新日规则前.xlsx b/7月核算结果/7月考核车辆多源里程汇总_新日规则前.xlsx new file mode 100644 index 0000000..d7b3916 Binary files /dev/null and b/7月核算结果/7月考核车辆多源里程汇总_新日规则前.xlsx differ diff --git a/7月核算结果/离职人员7月奖金处理明细.xlsx b/7月核算结果/离职人员7月奖金处理明细.xlsx new file mode 100644 index 0000000..f053a9f Binary files /dev/null and b/7月核算结果/离职人员7月奖金处理明细.xlsx differ diff --git a/7月核算结果/租赁任务考核_2026年7月.xlsx b/7月核算结果/租赁任务考核_2026年7月.xlsx new file mode 100644 index 0000000..f937718 Binary files /dev/null and b/7月核算结果/租赁任务考核_2026年7月.xlsx differ diff --git a/7月核算结果/租赁任务考核_2026年7月_原版备份.xlsx b/7月核算结果/租赁任务考核_2026年7月_原版备份.xlsx new file mode 100644 index 0000000..a692a4c Binary files /dev/null and b/7月核算结果/租赁任务考核_2026年7月_原版备份.xlsx differ diff --git a/7月核算结果/租赁任务考核_2026年7月_干预情况说明.xlsx b/7月核算结果/租赁任务考核_2026年7月_干预情况说明.xlsx new file mode 100644 index 0000000..7ddd48d Binary files /dev/null and b/7月核算结果/租赁任务考核_2026年7月_干预情况说明.xlsx differ diff --git a/7月核算结果/里程任务考核_7月核算.xlsx b/7月核算结果/里程任务考核_7月核算.xlsx new file mode 100644 index 0000000..6780d7b Binary files /dev/null and b/7月核算结果/里程任务考核_7月核算.xlsx differ diff --git a/7月核算结果/里程任务考核_7月核算_旧规则备份_20260812_183423.xlsx b/7月核算结果/里程任务考核_7月核算_旧规则备份_20260812_183423.xlsx new file mode 100644 index 0000000..6c3d52a Binary files /dev/null and b/7月核算结果/里程任务考核_7月核算_旧规则备份_20260812_183423.xlsx differ diff --git a/7月绩效重算_多源考核里程_20260817/离职人员7月奖金处理明细.xlsx b/7月绩效重算_多源考核里程_20260817/离职人员7月奖金处理明细.xlsx new file mode 100644 index 0000000..10367a4 Binary files /dev/null and b/7月绩效重算_多源考核里程_20260817/离职人员7月奖金处理明细.xlsx differ diff --git a/7月绩效重算_多源考核里程_20260817/租赁任务考核_2026年7月.xlsx b/7月绩效重算_多源考核里程_20260817/租赁任务考核_2026年7月.xlsx new file mode 100644 index 0000000..7feb3bc Binary files /dev/null and b/7月绩效重算_多源考核里程_20260817/租赁任务考核_2026年7月.xlsx differ diff --git a/7月绩效重算_多源考核里程_20260817/里程任务考核_7月核算.xlsx b/7月绩效重算_多源考核里程_20260817/里程任务考核_7月核算.xlsx new file mode 100644 index 0000000..f458c8d Binary files /dev/null and b/7月绩效重算_多源考核里程_20260817/里程任务考核_7月核算.xlsx differ diff --git a/_fix_may_flip.py b/_fix_may_flip.py new file mode 100644 index 0000000..69037db --- /dev/null +++ b/_fix_may_flip.py @@ -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'✅ 更新完成') diff --git a/_fix_source_may.py b/_fix_source_may.py new file mode 100644 index 0000000..dc4a0a7 --- /dev/null +++ b/_fix_source_may.py @@ -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}") diff --git a/_gen_intervention_may.py b/_gen_intervention_may.py new file mode 100644 index 0000000..60054a6 --- /dev/null +++ b/_gen_intervention_may.py @@ -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)') diff --git a/_gen_intervention_v2.py b/_gen_intervention_v2.py new file mode 100644 index 0000000..8438de1 --- /dev/null +++ b/_gen_intervention_v2.py @@ -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_mileage(101.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)') diff --git a/_gen_intervention_v3.py b/_gen_intervention_v3.py new file mode 100644 index 0000000..c6c1e67 --- /dev/null +++ b/_gen_intervention_v3.py @@ -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)') diff --git a/_gen_resignation_may.py b/_gen_resignation_may.py new file mode 100644 index 0000000..467a143 --- /dev/null +++ b/_gen_resignation_may.py @@ -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}') diff --git a/_process_may_intervention.py b/_process_may_intervention.py new file mode 100644 index 0000000..65b5964 --- /dev/null +++ b/_process_may_intervention.py @@ -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}") diff --git a/add_vehicle_type_to_july_summary.py b/add_vehicle_type_to_july_summary.py new file mode 100644 index 0000000..280e6df --- /dev/null +++ b/add_vehicle_type_to_july_summary.py @@ -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}) diff --git a/audit_july_daily_test_rule.py b/audit_july_daily_test_rule.py new file mode 100644 index 0000000..e079919 --- /dev/null +++ b/audit_july_daily_test_rule.py @@ -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]}) diff --git a/audit_july_gps_database.py b/audit_july_gps_database.py new file mode 100644 index 0000000..1097ebd --- /dev/null +++ b/audit_july_gps_database.py @@ -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), +}) diff --git a/audit_recalculate_july_from_multi_source.py b/audit_recalculate_july_from_multi_source.py new file mode 100644 index 0000000..f1a1775 --- /dev/null +++ b/audit_recalculate_july_from_multi_source.py @@ -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) + diff --git a/backfill_july_all_gps.py b/backfill_july_all_gps.py new file mode 100644 index 0000000..c2ba204 --- /dev/null +++ b/backfill_july_all_gps.py @@ -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}), +}) diff --git a/build_july_multi_source_summary.py b/build_july_multi_source_summary.py new file mode 100644 index 0000000..4482f93 --- /dev/null +++ b/build_july_multi_source_summary.py @@ -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), +}) diff --git a/calc_engine.py b/calc_engine.py index c4c9b2d..1dfcab7 100644 --- a/calc_engine.py +++ b/calc_engine.py @@ -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,153 +81,161 @@ 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, # 标记为无考核记录 + # 补发历史月: 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 + + # 累计补发当月: 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}) + + 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 + + # 更新可结转 + 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) + + # 补充: 上月有结转但本月无考核记录的车 + 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, '当月奖金': 0, + f'{month}月已发': True, + '可结转': max(0, gp['可结转'] - 1), + '累计已发结转': gp.get('累计已发结转', 0) + 1, + '虚拟': True, } - return feb_data -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 + all_data[month] = data_m + return data_m, G_cur - 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}) - 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 - - # 补充: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['实际'], - 'cum_q':False, - '结转':carry, '补发1月':0, '补发1月对应':'', - '补发2月':0, '补发2月对应':'', - '当月奖金':0, '累计补发3月':0, '结转占位':True, - '虚拟':True, - } - return mar_data - -def collect_vehicle_payments(G, feb_data, mar_data): +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: - payments[d['车牌']].append({'结算月':month,'对应考核月':assess_month, - '业务员':d['销售'],'金额':d['额'],'类型':cat,'部门':d['部门']}) + payments[d['车牌']].append({'结算月': month, '对应考核月': assess_month, + '业务员': d['销售'], '金额': d['额'], '类型': cat, '部门': d['部门']}) 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) @@ -268,7 +297,7 @@ def read_master_vehicles(fp='里程任务考核_Q1汇总.xlsx'): for row in ws.iter_rows(min_row=2, values_only=True): r = dict(zip(h, row)) if r.get('车牌号'): - vehicles.append({'车牌号': r['车牌号'], '车架号': r.get('车架号',''), - '归属公司': r.get('归属公司',''), '车型确定': r.get('车型','')}) + vehicles.append({'车牌号': r['车牌号'], '车架号': r.get('车架号', ''), + '归属公司': r.get('归属公司', ''), '车型确定': r.get('车型', '')}) wb.close() return vehicles diff --git a/carbon_platform_peer_discussion_questions.md b/carbon_platform_peer_discussion_questions.md new file mode 100644 index 0000000..59dd533 --- /dev/null +++ b/carbon_platform_peer_discussion_questions.md @@ -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. 平台是否可持续维护 + - 因子、公式、组织、模板、权限、数据源变化后,是否能低成本维护。 + +--- + +## 十四、总结建议 + +第一次做碳平台,不要先问: + +> 系统有什么功能? + +而要先问: + +> 数据从哪里来?口径谁确认?结果给谁用?能不能被核查?上线后谁维护? + +碳平台的核心不是大屏,而是形成一套: + +> 可追溯、可核查、可解释、可持续维护的数据闭环。 + diff --git a/excel_writer.py b/excel_writer.py index a830949..12fa856 100644 --- a/excel_writer.py +++ b/excel_writer.py @@ -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)) diff --git a/fix_july_intervention_style.py b/fix_july_intervention_style.py new file mode 100644 index 0000000..bf59daf --- /dev/null +++ b/fix_july_intervention_style.py @@ -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())}') diff --git a/full_pipeline_jun.py b/full_pipeline_jun.py new file mode 100644 index 0000000..3701629 --- /dev/null +++ b/full_pipeline_jun.py @@ -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}") diff --git a/gen_intervention_jun.py b/gen_intervention_jun.py new file mode 100644 index 0000000..c4f67a4 --- /dev/null +++ b/gen_intervention_jun.py @@ -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})') diff --git a/gen_resignation_jul.py b/gen_resignation_jul.py new file mode 100644 index 0000000..66d88f2 --- /dev/null +++ b/gen_resignation_jul.py @@ -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}') diff --git a/gen_resignation_jun.py b/gen_resignation_jun.py new file mode 100644 index 0000000..af90d67 --- /dev/null +++ b/gen_resignation_jun.py @@ -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}') diff --git a/gps_replace_full.py b/gps_replace_full.py new file mode 100644 index 0000000..3d99b52 --- /dev/null +++ b/gps_replace_full.py @@ -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}") diff --git a/gps_replace_local.py b/gps_replace_local.py new file mode 100644 index 0000000..0a5efcb --- /dev/null +++ b/gps_replace_local.py @@ -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) diff --git a/index (1).html b/index (1).html new file mode 100644 index 0000000..5ef77db --- /dev/null +++ b/index (1).html @@ -0,0 +1,107 @@ + + + + + 小羚羚小程序 + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/index (2).html b/index (2).html new file mode 100644 index 0000000..5ef77db --- /dev/null +++ b/index (2).html @@ -0,0 +1,107 @@ + + + + + 小羚羚小程序 + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/index (3).html b/index (3).html new file mode 100644 index 0000000..5ef77db --- /dev/null +++ b/index (3).html @@ -0,0 +1,107 @@ + + + + + 小羚羚小程序 + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/index (4).html b/index (4).html new file mode 100644 index 0000000..5ef77db --- /dev/null +++ b/index (4).html @@ -0,0 +1,107 @@ + + + + + 小羚羚小程序 + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..5ef77db --- /dev/null +++ b/index.html @@ -0,0 +1,107 @@ + + + + + 小羚羚小程序 + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/jun_18t_s3.py b/jun_18t_s3.py new file mode 100644 index 0000000..7cb2d92 --- /dev/null +++ b/jun_18t_s3.py @@ -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() diff --git a/main.py b/main.py index f555cd7..a9417ab 100644 --- a/main.py +++ b/main.py @@ -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)") diff --git a/may_18t_recalc.py b/may_18t_recalc.py new file mode 100644 index 0000000..3c02ab3 --- /dev/null +++ b/may_18t_recalc.py @@ -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() diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..aa42405 --- /dev/null +++ b/openapi.yaml @@ -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 diff --git a/post_process_transfer.py b/post_process_transfer.py new file mode 100644 index 0000000..8fb8835 --- /dev/null +++ b/post_process_transfer.py @@ -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() diff --git a/prepare_july_source.py b/prepare_july_source.py new file mode 100644 index 0000000..3b77e4f --- /dev/null +++ b/prepare_july_source.py @@ -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() diff --git a/rebuild_july_source_from_multi_source.py b/rebuild_july_source_from_multi_source.py new file mode 100644 index 0000000..14ec26d --- /dev/null +++ b/rebuild_july_source_from_multi_source.py @@ -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}) + diff --git a/rebuild_summary.py b/rebuild_summary.py new file mode 100644 index 0000000..89a109d --- /dev/null +++ b/rebuild_summary.py @@ -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() diff --git a/recalculate_july_assessment_mileage_daily.py b/recalculate_july_assessment_mileage_daily.py new file mode 100644 index 0000000..d7d3f8a --- /dev/null +++ b/recalculate_july_assessment_mileage_daily.py @@ -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}) diff --git a/regenerate_intervention.py b/regenerate_intervention.py new file mode 100644 index 0000000..6096e87 --- /dev/null +++ b/regenerate_intervention.py @@ -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})') diff --git a/regenerate_intervention_jul.py b/regenerate_intervention_jul.py new file mode 100644 index 0000000..87894f8 --- /dev/null +++ b/regenerate_intervention_jul.py @@ -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})') diff --git a/replace_gps_jun.py b/replace_gps_jun.py new file mode 100644 index 0000000..02b1ac6 --- /dev/null +++ b/replace_gps_jun.py @@ -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}') diff --git a/replace_gps_jun_new.py b/replace_gps_jun_new.py new file mode 100644 index 0000000..14b1f8e --- /dev/null +++ b/replace_gps_jun_new.py @@ -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}') diff --git a/yangchanji-plan-ab-compare.md b/yangchanji-plan-ab-compare.md new file mode 100644 index 0000000..0c7c4af --- /dev/null +++ b/yangchanji-plan-ab-compare.md @@ -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 | 多长时间 | 约 **18~25 周 / 5~6.5 月** | 约 **20~27 周 / 5.5~7 月** | +| 5 | 投入人员 | 产1研2测1 + 对方接口人 | 产1研2测1 + **运营导单强依赖** | +| 6 | 额外费用 | 联调+小程序等;专线视情况 | 现金更低;**导单隐性成本中** | +| 7 | 前置与风险 | API/沙箱/映射;字段不一致、审核延误 | 导单责任/缺口表;导单延误、数据不一致 | +| 8 | 分期 | 调研→V0→V1→V2→利润后置 | 同左(V1 多 2~4 周) | + +--- + +## 三、周期对照(已压缩调研+V0) + +| 阶段 | 方案 A | 方案 B | 备注 | +|------|--------|--------|------| +| 业务调研 | 1~2 周 | 1~2 周 | 与 V0 可并行收口 | +| V0 门禁 | 约 1 周 | 约 1 周 | | +| **调研+V0** | **约 2~3 周** | **约 2~3 周** | 原 3~6 周减半 | +| V1 | 12~16 周 | 14~18 周 | B 含导入改造 | +| V2 | 4~6 周 | 4~6 周 | | +| 利润(后置) | 另 2~4 周 | 另 2~4 周 | 不进首批退出 | +| **首批合计** | **约 5~6.5 个月** | **约 5.5~7 个月** | 含调研 | + +--- + +## 四、业务时序与已拍板规则(A/B 相同) + +| 步骤 | 规则 | +|------|------| +| 1 | 数据进入 OneOS(A=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=对方字段↔OneOS;B=导入列↔OneOS | + +--- + +## 十、决策日志 · 养馋记台账相关(已拍板对照) + +| 日期 | 议题 | 结论 | +|------|------|------| +| 2026-08-10 | 首批范围 | 批准 V0+V1+V2;V3 不计入 | +| 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≈2~3周;A≈5~6.5月;B≈5.5~7月 | + +--- + +## 十一、评估结果一览(并排) + +| 评估结果项 | 方案 A | 方案 B | +|------------|--------|--------| +| 可行性 | 有条件可行 | 有条件可行 | +| 数据策略 | 对方 API 主路径 | OneOS 改造+导入/维护 | +| 周期 | 约 5~6.5 个月 | 约 5.5~7 个月 | +| 人员 | 产1研2测1 | 产1研2测1+运营强依赖 | +| 费用感 | 低~中 | 现金低、运营成本中 | +| 首批 | 调研→V0→V1→V2 | 同左 | +| 利润/用友 | 后置 / 不计入 | 同左 | diff --git a/养馋记物流派单与对账模块_管理层汇报.md b/养馋记物流派单与对账模块_管理层汇报.md new file mode 100644 index 0000000..debbcd8 --- /dev/null +++ b/养馋记物流派单与对账模块_管理层汇报.md @@ -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 个月真实数据后,以试点方式单独立项。 diff --git a/养馋记物流派单与对账模块_管理层汇报_精简版.md b/养馋记物流派单与对账模块_管理层汇报_精简版.md new file mode 100644 index 0000000..e719ae2 --- /dev/null +++ b/养馋记物流派单与对账模块_管理层汇报_精简版.md @@ -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 匹配规则后启动。 diff --git a/租赁任务考核_2026年4月.xlsx b/租赁任务考核_2026年4月.xlsx new file mode 100644 index 0000000..64e0308 Binary files /dev/null and b/租赁任务考核_2026年4月.xlsx differ diff --git a/租赁任务考核_2026年4月_干预情况说明.xlsx.xlsx b/租赁任务考核_2026年4月_干预情况说明.xlsx.xlsx new file mode 100644 index 0000000..efee37f Binary files /dev/null and b/租赁任务考核_2026年4月_干预情况说明.xlsx.xlsx differ diff --git a/租赁任务考核_2026年5月.xlsx b/租赁任务考核_2026年5月.xlsx new file mode 100644 index 0000000..d38f9fc Binary files /dev/null and b/租赁任务考核_2026年5月.xlsx differ diff --git a/租赁任务考核_2026年5月_原版备份.xlsx b/租赁任务考核_2026年5月_原版备份.xlsx new file mode 100644 index 0000000..2471c9e Binary files /dev/null and b/租赁任务考核_2026年5月_原版备份.xlsx differ diff --git a/租赁任务考核_2026年6月 (1).xlsx b/租赁任务考核_2026年6月 (1).xlsx new file mode 100644 index 0000000..11cfe00 Binary files /dev/null and b/租赁任务考核_2026年6月 (1).xlsx differ diff --git a/租赁任务考核_2026年6月.xlsx b/租赁任务考核_2026年6月.xlsx new file mode 100644 index 0000000..aa6d92c Binary files /dev/null and b/租赁任务考核_2026年6月.xlsx differ diff --git a/租赁任务考核_2026年6月_GPS替换后留底.xlsx b/租赁任务考核_2026年6月_GPS替换后留底.xlsx new file mode 100644 index 0000000..499ec2f Binary files /dev/null and b/租赁任务考核_2026年6月_GPS替换后留底.xlsx differ diff --git a/租赁任务考核_2026年6月_备份.xlsx b/租赁任务考核_2026年6月_备份.xlsx new file mode 100644 index 0000000..5a8edd2 Binary files /dev/null and b/租赁任务考核_2026年6月_备份.xlsx differ diff --git a/租赁任务考核_2026年6月_备份_0714.xlsx b/租赁任务考核_2026年6月_备份_0714.xlsx new file mode 100644 index 0000000..ba7fa61 Binary files /dev/null and b/租赁任务考核_2026年6月_备份_0714.xlsx differ diff --git a/租赁任务考核_2026年6月_备份_0715.xlsx b/租赁任务考核_2026年6月_备份_0715.xlsx new file mode 100644 index 0000000..f8cf674 Binary files /dev/null and b/租赁任务考核_2026年6月_备份_0715.xlsx differ diff --git a/租赁任务考核_2026年6月_干预情况说明.xlsx b/租赁任务考核_2026年6月_干预情况说明.xlsx new file mode 100644 index 0000000..d652542 Binary files /dev/null and b/租赁任务考核_2026年6月_干预情况说明.xlsx differ diff --git a/租赁任务考核_2026年6月_干预情况说明_0714.xlsx b/租赁任务考核_2026年6月_干预情况说明_0714.xlsx new file mode 100644 index 0000000..f94c972 Binary files /dev/null and b/租赁任务考核_2026年6月_干预情况说明_0714.xlsx differ diff --git a/租赁任务考核_2026年7月 (1).xlsx b/租赁任务考核_2026年7月 (1).xlsx new file mode 100644 index 0000000..a692a4c Binary files /dev/null and b/租赁任务考核_2026年7月 (1).xlsx differ diff --git a/租赁任务考核_2026年7月.xlsx b/租赁任务考核_2026年7月.xlsx new file mode 100644 index 0000000..8daa45e Binary files /dev/null and b/租赁任务考核_2026年7月.xlsx differ diff --git a/里程任务考核_1月核算.xlsx b/里程任务考核_1月核算.xlsx index 0f5f87f..56c080a 100644 Binary files a/里程任务考核_1月核算.xlsx and b/里程任务考核_1月核算.xlsx differ diff --git a/里程任务考核_2月核算.xlsx b/里程任务考核_2月核算.xlsx index ac7f4a6..68e7f59 100644 Binary files a/里程任务考核_2月核算.xlsx and b/里程任务考核_2月核算.xlsx differ diff --git a/里程任务考核_3月核算.xlsx b/里程任务考核_3月核算.xlsx index f75e7b0..e55c2b1 100644 Binary files a/里程任务考核_3月核算.xlsx and b/里程任务考核_3月核算.xlsx differ diff --git a/里程任务考核_4月核算.xlsx b/里程任务考核_4月核算.xlsx new file mode 100644 index 0000000..f96ccc6 Binary files /dev/null and b/里程任务考核_4月核算.xlsx differ diff --git a/里程任务考核_5月核算.xlsx b/里程任务考核_5月核算.xlsx new file mode 100644 index 0000000..674b837 Binary files /dev/null and b/里程任务考核_5月核算.xlsx differ diff --git a/里程任务考核_6月核算.xlsx b/里程任务考核_6月核算.xlsx new file mode 100644 index 0000000..a5c02af Binary files /dev/null and b/里程任务考核_6月核算.xlsx differ diff --git a/里程考核绩效核算/核算结果/里程任务考核_1月核算.xlsx b/里程考核绩效核算/核算结果/里程任务考核_1月核算.xlsx deleted file mode 100644 index 0f5f87f..0000000 Binary files a/里程考核绩效核算/核算结果/里程任务考核_1月核算.xlsx and /dev/null differ diff --git a/里程考核绩效核算/核算结果/里程任务考核_2月核算.xlsx b/里程考核绩效核算/核算结果/里程任务考核_2月核算.xlsx deleted file mode 100644 index ac7f4a6..0000000 Binary files a/里程考核绩效核算/核算结果/里程任务考核_2月核算.xlsx and /dev/null differ diff --git a/里程考核绩效核算/核算结果/里程任务考核_3月核算.xlsx b/里程考核绩效核算/核算结果/里程任务考核_3月核算.xlsx deleted file mode 100644 index f75e7b0..0000000 Binary files a/里程考核绩效核算/核算结果/里程任务考核_3月核算.xlsx and /dev/null differ diff --git a/里程考核绩效核算/考核依据/车辆里程考核与奖金发放规则(V.1.2).docx b/里程考核绩效核算/考核依据/车辆里程考核与奖金发放规则(V.1.2).docx deleted file mode 100644 index 0855d43..0000000 Binary files a/里程考核绩效核算/考核依据/车辆里程考核与奖金发放规则(V.1.2).docx and /dev/null differ diff --git a/里程考核绩效核算/里程考核核算.zip b/里程考核绩效核算/里程考核核算.zip deleted file mode 100644 index 861b105..0000000 Binary files a/里程考核绩效核算/里程考核核算.zip and /dev/null differ