14 Commits

Author SHA1 Message Date
kkfluous
5a69ef2993 V2.5.0 车辆追踪:多业务员拆行+车牌合并单元格
- 每个(车牌+业务员)独立一行,各自有独立的月度里程和累计
- 同车多人时车牌/车架号/归属公司等信息列合并单元格
- 奖金池(已发期数/金额/剩余)也合并(整车共享)
- 月度数据拆为应考核/实际/达标三列,不再挤在一个单元格
- 同人多条记录在达标列内换行显示各条

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:13:00 +08:00
kkfluous
04b6035d52 fix: 车辆追踪同人多条记录逐条显示,不合并里程
之前同人多条记录合并为一个总里程/总目标,但达标标记取"任一达标",
导致总里程<总目标却显示✓(如粤AGE4080: 1824/1839 ✓)。
改为每条记录独立显示里程/目标/达标,避免误导。
月度颜色也改为"全部达标才绿,否则红"。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:09:21 +08:00
kkfluous
4c43c00e73 feat: 车辆考核追踪sheet美化
- 达标月份绿底✓,未达标红底✗
- 累计达标/未达标加粗+颜色
- 有发放的行金底高亮
- 奖金池已发期数蓝底加粗,显示为"N/12"格式
- 车辆基本信息列浅灰底色区分
- 冻结首行+首列,支持滚动查看
- 开启自动筛选
- 优化列宽

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:02:50 +08:00
kkfluous
f2de5d5500 feat: 车辆考核追踪sheet移到业务员sheet前面
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:01:13 +08:00
kkfluous
54ecbc352f feat: 车辆考核追踪sheet业务员名加部门前缀
考核明细和发放明细中的业务员统一显示为"X部-姓名"格式。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:00:27 +08:00
kkfluous
cdc4cec2ff feat: 业务员sheet命名加部门前缀(如"二部-刘念念")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:53:03 +08:00
kkfluous
8c2f4e73dd fix: 车辆追踪补全所有492辆车的考核目标和月度奖励
通过(归属公司+车型)→考核目标映射,补全109辆无考核记录车辆的考核目标。
含全角/半角括号兼容(现代氢能科技)。
现在492辆车全部有考核目标和月度奖励金额。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:47:41 +08:00
kkfluous
e47cc00b3d V2.3.0 车辆考核追踪:多人多条用单元格内换行显示
同车多个销售经理在同一单元格内换行展示:
- X月考核明细: "刘念念: 4834/2903 达标\n董剑煜: 1294/2710 未达标"
- 本月发放明细: "赵连飞: 150(结转)\n董剑煜: 260(当月达标)"
设置wrap_text自动换行,一车一行不拆分。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:51:16 +08:00
kkfluous
4273592160 V2.2.0 业务员sheet重写为对账单风格
每辆车展示:
- 第1行:车辆信息 + 各月里程/目标 + 累计 + 达标状态
- 下方:历史已发(哪月发的、金额、来源)+ 本月发放明细 + 奖金池
- "历史已发"列清楚展示防重复证据
- 发放说明包含关键数字(累计里程≥目标等)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:33:04 +08:00
kkfluous
1b7d25c821 fix: 业务员sheet奖金池期数按当前核算月过滤
之前vehicle_payments是Q1全量,导致1月文件显示了3个月的累计期数。
改为只统计截至settle_month的发放记录。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:41:34 +08:00
kkfluous
95f1685612 V2.1.0 业务员sheet增加"发放说明"列
将技术化的"发放类型"改为动态生成的"发放说明",用一句话解释为什么发/不发:
- 当月达标 → "当月达标"
- 结转 → "1月多跑9578≥3000,结转(完整月奖金)"
- 累计补发 → "1-2月累计10620≥10000,累计达标补发"
- 未达标 → "未达标(实际1486<目标5000)"
- 补发过去月 → "1-3月累计14427≥10161,补发1月"

包含关键数字,非技术人员一眼看懂计算依据。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:37:13 +08:00
kkfluous
818aacb875 V2.0.0 按月拆分Excel + 业务员/车辆维度展示
重构为3文件架构:
- calc_engine.py: 计算引擎(规则/读取/分组/结转/补发/累计)
- excel_writer.py: Excel输出(所有sheet生成函数)
- main.py: 入口(按月循环生成独立文件)

输出3个独立Excel文件(1-3月各一个),每个17个sheet:
- 考核奖励规则 + 里程明细 + 计算过程 + 汇总(原有)
- 业务员_XXX × 12个(新增,按车分组展示每月考核+累计+发放+奖金池)
- 车辆考核追踪(新增,全量492辆,含每月业务员/里程/发放/奖金池状态)

计算逻辑不变,金额与V1.1.0一致:
1月21212.26, 2月21152.14, 3月56607.10, Q1总计98971.50

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:18:27 +08:00
kkfluous
46fa8aea7a docs: 按月拆分Excel实现计划
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:09:51 +08:00
kkfluous
0baf470d2e docs: 按月拆分Excel + 业务员/车辆维度展示设计文档
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:05:31 +08:00
5 changed files with 1627 additions and 0 deletions

194
calc_engine.py Normal file
View File

@@ -0,0 +1,194 @@
"""计算引擎:规则、读取、分组、结转/补发/累计逻辑"""
import openpyxl
from collections import defaultdict
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},
}
DAYS = {1: 31, 2: 28, 3: 31}
def read_file(fp, month):
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):
r = dict(zip(h, row))
r['考核天数'] = float(r.get('考核天数') or 0)
r['应考核里程(km)'] = float(r.get('应考核里程(km)') or 0)
r['实际行驶里程(km)'] = float(r.get('实际行驶里程(km)') or 0)
r['月份'] = month
rule = RULES.get(r.get('考核目标', ''), {})
r['月度目标里程'] = rule.get('km', 0)
r['对应月奖励金额'] = rule.get('', 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
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // r['月度目标里程']) if r['月度目标里程'] > 0 and q else 0
recs.append(r)
wb.close()
return recs
def grp(recs, month):
gs = defaultdict(lambda: {'recs':[], '应考核':0, '实际':0, '奖金':0, '天数':0,
'有达标':False, '目标km':0, '奖励额':0})
for r in recs:
k = (r['车牌号'], r['销售经理'])
g = gs[k]
g['recs'].append(r)
g['应考核'] += r['应考核里程(km)']
g['实际'] += r['实际行驶里程(km)']
g['奖金'] += r['奖金']
g['天数'] += r['考核天数']
if r['是否达标'] == '达标': g['有达标'] = True
g['目标km'] = max(g['目标km'], r['月度目标里程'])
g['奖励额'] = max(g['奖励额'], r['对应月奖励金额'])
g['部门'] = r['部门名称']
g['销售'] = r['销售经理']
g['车牌'] = r['车牌号']
return dict(gs)
def calc_jan_carryover(G1):
for k, g in G1.items():
tc = 0
for r in g['recs']:
if r['是否达标'] == '达标': tc += r['可结转']
g['可结转'] = tc
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['实际']
cum_q = cum_a >= cum_t and cum_t > 0
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})
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
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
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
return mar_data
def collect_vehicle_payments(G, feb_data, mar_data):
payments = defaultdict(list)
info = {}
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']}
if g['奖金'] > 0:
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)]:
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['部门']})
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']}
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),
('广州开发区交投氢能运营管理有限公司', '4.5吨货车'): ('交投40辆4.5T普货', 3000, 150),
('羚牛氢能科技(广东)有限公司', '4.5吨冷链车'): ('羚牛136辆4.5T冷链车', 5000, 260),
('羚牛氢能科技(广东)有限公司', '18吨双飞翼货车'): ('羚牛100辆18T', 6000, 1000),
('现代氢能科技(广州)有限公司', '4.5吨货车'): ('恒运50辆4.5T普货', 5000, 260),
('现代氢能科技(广州)有限公司', '4.5吨货车'): ('恒运50辆4.5T普货', 5000, 260),
}
def read_master_vehicles(fp='里程任务考核_Q1汇总.xlsx'):
"""从现有Q1汇总文件读取全量车辆台账"""
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))]
vehicles = []
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('车型','')})
wb.close()
return vehicles

View File

@@ -0,0 +1,706 @@
# 按月拆分Excel + 业务员/车辆维度 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将单文件Q1汇总重构为3个月度独立Excel每个文件新增12个业务员sheet和1个车辆考核追踪sheet。
**Architecture:** 将现有 `generate_q1_summary.py` 拆分为 `calc_engine.py`(计算引擎,不变)+ `excel_writer.py`Excel输出含新sheet+ `main.py`入口按月循环生成。计算引擎从原脚本提取Excel输出按月独立调用。
**Tech Stack:** Python 3, openpyxl
---
### Task 1: 提取计算引擎到 calc_engine.py
**Files:**
- Create: `calc_engine.py`
- Modify: `generate_q1_summary.py`后续Task删除
- [ ] **Step 1: 创建 calc_engine.py提取规则/读取/分组/计算逻辑**
从现有 `generate_q1_summary.py` 第1-222行提取为独立模块封装为函数
```python
# calc_engine.py
"""计算引擎:规则、读取、分组、结转/补发/累计逻辑"""
import openpyxl
from collections import defaultdict
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},
}
DAYS = {1: 31, 2: 28, 3: 31}
def read_file(fp, month):
"""读取月度源数据,返回记录列表"""
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):
r = dict(zip(h, row))
r['考核天数'] = float(r.get('考核天数') or 0)
r['应考核里程(km)'] = float(r.get('应考核里程(km)') or 0)
r['实际行驶里程(km)'] = float(r.get('实际行驶里程(km)') or 0)
r['月份'] = month
rule = RULES.get(r.get('考核目标', ''), {})
r['月度目标里程'] = rule.get('km', 0)
r['对应月奖励金额'] = rule.get('', 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
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // r['月度目标里程']) if r['月度目标里程'] > 0 and q else 0
recs.append(r)
wb.close()
return recs
def grp(recs, month):
"""按(车牌号,销售经理)分组"""
gs = defaultdict(lambda: {'recs':[], '应考核':0, '实际':0, '奖金':0, '天数':0,
'有达标':False, '目标km':0, '奖励额':0})
for r in recs:
k = (r['车牌号'], r['销售经理'])
g = gs[k]
g['recs'].append(r)
g['应考核'] += r['应考核里程(km)']
g['实际'] += r['实际行驶里程(km)']
g['奖金'] += r['奖金']
g['天数'] += r['考核天数']
if r['是否达标'] == '达标': g['有达标'] = True
g['目标km'] = max(g['目标km'], r['月度目标里程'])
g['奖励额'] = max(g['奖励额'], r['对应月奖励金额'])
g['部门'] = r['部门名称']
g['销售'] = r['销售经理']
g['车牌'] = r['车牌号']
return dict(gs)
def calc_jan_carryover(G1):
"""计算1月结转"""
for k, g in G1.items():
tc = 0
for r in g['recs']:
if r['是否达标'] == '达标':
tc += r['可结转']
g['可结转'] = tc
def calc_feb(G1, G2):
"""计算2月各类发放返回feb_data字典"""
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['实际']
cum_q = cum_a >= cum_t and cum_t > 0
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})
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
return feb_data
def calc_mar(G1, G2, G3, feb_data):
"""计算3月各类发放返回mar_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_q = g2['有达标'] if g2 else False
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
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
return mar_data
def collect_vehicle_payments(G, month_data_map):
"""收集车辆发放记录,返回 {车牌: [发放记录]}"""
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']}
if g['奖金'] > 0:
payments[plate].append({'结算月':1,'对应考核月':1,'业务员':g['销售'],'金额':g['奖金'],'类型':'当月达标','部门':g['部门']})
type_map = {
'结转': lambda m: (m, m),
'补发1月': lambda m: (m, 1),
'补发2月': lambda m: (m, 2),
'当月': lambda m: (m, m),
'累计补发2月': lambda m: (m, 2),
'累计补发3月': lambda m: (m, 3),
}
for month, mdata in month_data_map.items():
for cat, dl in mdata.items():
settle, assess_fn = month, type_map.get(cat, lambda m: (m, m))
for d in dl:
s, a = assess_fn(month)
payments[d['车牌']].append({'结算月':s,'对应考核月':a,'业务员':d['销售'],'金额':d[''],
'类型':cat.replace(f'{month}','').replace('累计补发','累计补发'),'部门':d['部门']})
if not info.get(d['车牌']):
info[d['车牌']] = {'考核目标':'','月度奖励':0,'目标km':0}
# 补充info
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']}
# 编期数
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
def read_master_vehicles(filepath='里程任务考核_2 月核算.xlsx'):
"""读取全量车辆台账"""
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))]
vehicles = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
if r.get('车牌号'):
vehicles.append(r)
wb.close()
return vehicles
```
- [ ] **Step 2: 验证 calc_engine.py 可导入**
Run: `cd /Users/kkfluous/Downloads && python3 -c "from calc_engine import *; print('OK')"`
Expected: `OK`
- [ ] **Step 3: Commit**
```bash
git add calc_engine.py
git commit -m "refactor: extract calc_engine.py from generate_q1_summary.py"
```
---
### Task 2: 创建 excel_writer.py现有sheet输出函数
**Files:**
- Create: `excel_writer.py`
- [ ] **Step 1: 创建 excel_writer.py提取Excel工具函数和现有sheet写入逻辑**
```python
# excel_writer.py
"""Excel输出工具函数 + 各类sheet生成"""
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from collections import defaultdict
hf = Font(bold=True)
hfl = PatternFill(start_color='D9E1F2', end_color='D9E1F2', fill_type='solid')
bd = Border(left=Side(style='thin'),right=Side(style='thin'),top=Side(style='thin'),bottom=Side(style='thin'))
def WH(ws, headers, row=1):
for c, h in enumerate(headers, 1):
cl = ws.cell(row=row, column=c, value=h)
cl.font = hf; cl.fill = hfl; cl.border = bd
cl.alignment = Alignment(horizontal='center', wrap_text=True)
def WR(ws, rn, vals):
for c, v in enumerate(vals, 1):
cl = ws.cell(row=rn, column=c, value=v); cl.border = bd
def AW(ws):
for col in ws.columns:
ml = max((len(str(c.value or '')) for c in col), default=0)
ws.column_dimensions[col[0].column_letter].width = min(ml+3, 22)
def R(v, n=2):
return round(v, n) if isinstance(v, (int, float)) and v else v
def agg(dl):
bp = defaultdict(lambda: {'n':0, 'a':0})
for d in dl: bp[d['销售']]['n'] += 1; bp[d['销售']]['a'] += d['']
return dict(bp)
def write_sec(ws, rn, title, dl):
"""写入汇总section返回下一行行号"""
ws.cell(row=rn, column=1, value=title).font = Font(bold=True, size=11); rn+=1
WH(ws, ['销售人员','车辆数','金额'], rn); rn+=1
bp = agg(dl); tv=ta=0
for p in sorted(bp.keys()):
WR(ws, rn, [p, bp[p]['n'], R(bp[p]['a'])]); tv+=bp[p]['n']; ta+=bp[p]['a']; rn+=1
WR(ws, rn, ['总计', tv, R(ta)]); ws.cell(row=rn,column=1).font=hf; return rn+2
def write_total(ws, rn, month, all_data):
"""写入合计(按销售/按部门)"""
total_bp = defaultdict(lambda: {'部门':'', '':0})
for cat, dl in all_data.items():
for d in dl: total_bp[d['销售']][''] += d['']; total_bp[d['销售']]['部门'] = d['部门']
ws.cell(row=rn, column=1, value=f'{month}月合计应发奖励(按销售人员)').font=Font(bold=True, size=11); rn+=1
WH(ws, ['销售人员','部门名称','合计应发奖励'], rn); rn+=1
gt=0
for p in sorted(total_bp.keys()):
WR(ws, rn, [p, total_bp[p]['部门'], R(total_bp[p][''])]); gt+=total_bp[p]['']; rn+=1
WR(ws, rn, ['合计','', R(gt)]); ws.cell(row=rn,column=1).font=hf; rn+=2
by_dept = defaultdict(float)
for p,d in total_bp.items(): by_dept[d['部门']] += d['']
ws.cell(row=rn, column=1, value=f'{month}月合计应发奖励(按部门)').font=Font(bold=True, size=11); rn+=1
WH(ws, ['部门名称','合计应发奖励'], rn); rn+=1
for dept in sorted(by_dept.keys()): WR(ws, rn, [dept, R(by_dept[dept])]); rn+=1
WR(ws, rn, ['合计', R(gt)]); ws.cell(row=rn,column=1).font=hf
# --- 各sheet生成函数 ---
# write_rules_sheet(wb) - 考核奖励规则
# write_detail_sheet(wb, records, month) - 里程明细
# write_calc_process_sheet(wb, G, month, ...) - 计算过程
# write_summary_sheet(wb, month, month_data) - 月汇总
# write_salesperson_sheet(wb, person, ...) - 业务员sheet新增
# write_vehicle_tracking_sheet(wb, ...) - 车辆考核追踪(新增)
# 每个函数的完整代码从现有脚本对应部分提取+扩展,此处省略重复
```
注意各write函数的完整实现直接从现有 `generate_q1_summary.py` 的Excel输出部分第224-677行提取重构。每个函数接收 `wb`Workbook和所需数据创建对应sheet。
- [ ] **Step 2: Commit**
```bash
git add excel_writer.py
git commit -m "refactor: create excel_writer.py with sheet generation functions"
```
---
### Task 3: 新增业务员sheet生成函数
**Files:**
- Modify: `excel_writer.py`
- [ ] **Step 1: 在 excel_writer.py 中实现 write_salesperson_sheet**
```python
def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments):
"""
为指定业务员生成sheet展示其所有车辆在当前核算月的考核和发放详情。
person: 销售经理姓名
settle_month: 当前核算月(1/2/3)
D: {月份: [记录列表]}
G: {月份: {(车牌,销售): group}}
month_data: 当月发放数据字典
vehicle_payments: {车牌: [发放记录]}
"""
ws = wb.create_sheet(f'业务员_{person}')
# 头部
ws.cell(row=1, column=1, value=f'{person} | {dept} | {settle_month}月考核').font = Font(bold=True, size=14)
# 收集该业务员在1-settle_month中有记录的所有车牌
plates = set()
for m in range(1, settle_month + 1):
for k in G.get(m, {}):
if k[1] == person:
plates.add(k[0])
# 计算该人本月发放总额
person_total = 0
for cat, dl in month_data.items():
for d in dl:
if d['销售'] == person:
person_total += d['']
ws.cell(row=2, column=1, value=f'本月考核车辆: {len(plates)}辆 | 本月发放合计: {R(person_total)}').font = Font(bold=True, size=11)
rn = 4
for plate in sorted(plates):
# 车辆信息行
# 取最新month的group信息
g_cur = None
for m in range(settle_month, 0, -1):
g_cur = G.get(m, {}).get((plate, person))
if g_cur: break
if not g_cur: continue
first_rec = g_cur['recs'][0]
ws.cell(row=rn, column=1, value=plate).font = Font(bold=True)
ws.cell(row=rn, column=2, value=first_rec.get('合同编号',''))
ws.cell(row=rn, column=3, value=first_rec.get('客户名称',''))
ws.cell(row=rn, column=4, value=first_rec.get('考核目标',''))
ws.cell(row=rn, column=5, value=f'月度目标{g_cur["目标km"]}km')
ws.cell(row=rn, column=6, value=f'月奖励{g_cur["奖励额"]}')
rn += 1
# 表头
headers = ['月份','考核天数','应考核里程','实际里程','完成率','达标','奖金','发放类型']
if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标']
WH(ws, headers, rn); rn += 1
# 每月一行
cum_target = 0; cum_actual = 0
for m in range(1, settle_month + 1):
gm = G.get(m, {}).get((plate, person))
if not gm:
WR(ws, rn, [f'{m}', '', '', '', '', '无记录']); rn += 1; continue
t = gm['应考核']; a = gm['实际']
cum_target += t; cum_actual += a
qual = '达标' if gm['有达标'] else '未达标'
rate = R(a/t, 2) if t > 0 else 0
# 确定发放类型和金额
pay_type = ''; pay_amt = 0
if m == settle_month:
# 当前核算月的发放
carry = gm.get('结转', 0)
bonus = gm.get('当月奖金', 0)
cum_bp = gm.get(f'累计补发{m}', 0)
if carry > 0: pay_type = '结转'; pay_amt = carry
elif bonus > 0: pay_type = '当月达标'; pay_amt = bonus
elif cum_bp > 0: pay_type = f'累计补发{m}'; pay_amt = cum_bp
# 补发之前月份的金额不在这行显示
elif m < settle_month:
pay_type = '(历史)'; pay_amt = gm.get('奖金', 0) if gm['有达标'] else 0
row_data = [f'{m}', gm['天数'], R(t), R(a), f'{rate*100:.0f}%' if rate else '0%', qual,
R(pay_amt) if pay_amt > 0 else 0, pay_type]
if settle_month >= 2:
cum_q = '达标' if (cum_actual >= cum_target and cum_target > 0) else '未达标'
row_data += [R(cum_target), R(cum_actual), cum_q]
WR(ws, rn, row_data); rn += 1
# 补发行(当前核算月补发之前月份)
if settle_month >= 2:
g_settle = G.get(settle_month, {}).get((plate, person), {})
for prev_m in range(1, settle_month):
bp_key = f'补发{prev_m}' if prev_m < settle_month else ''
bp_amt = g_settle.get(bp_key, 0) if bp_key else 0
if bp_amt > 0:
WR(ws, rn, [f'补发{prev_m}', '', '', '', '', '', R(bp_amt), f'补发{prev_m}'])
rn += 1
# 车辆小计 + 奖金池
pays = vehicle_payments.get(plate, [])
total_periods = len(pays)
plate_total_this_month = sum(p['金额'] for p in pays if p['结算月'] == settle_month and p['业务员'] == person)
ws.cell(row=rn, column=1, value=f'小计: {R(plate_total_this_month)}').font = Font(italic=True)
ws.cell(row=rn, column=4, value=f'奖金池: 已发{total_periods}期/共12期, 剩余{12-total_periods}').font = Font(italic=True)
rn += 2 # 空行分隔
# 尾部汇总
ws.cell(row=rn, column=1, value=f'{person} {settle_month}月合计: {len(plates)}辆车, 发放 {R(person_total)}').font = Font(bold=True, size=12)
AW(ws)
```
- [ ] **Step 2: Commit**
```bash
git add excel_writer.py
git commit -m "feat: add write_salesperson_sheet function"
```
---
### Task 4: 新增车辆考核追踪sheet生成函数
**Files:**
- Modify: `excel_writer.py`
- [ ] **Step 1: 在 excel_writer.py 中实现 write_vehicle_tracking_sheet**
```python
def write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info):
"""
车辆考核追踪sheet全量492辆每车一行。
settle_month: 当前核算月
master_vehicles: 全量车辆台账列表
"""
ws = wb.create_sheet('车辆考核追踪')
# 构建表头
headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励金额']
for m in range(1, settle_month + 1):
headers += [f'{m}月业务员', f'{m}月应考核', f'{m}月实际', f'{m}月达标']
if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标']
headers += ['本月发放金额','发放给谁','发放类型','累计已发期数','累计已发金额','剩余可发期数']
WH(ws, headers)
rn = 2
for mv in master_vehicles:
plate = mv['车牌号']
info = vehicle_info.get(plate, {})
pays = vehicle_payments.get(plate, [])
row = [plate, mv.get('车架号',''), mv.get('归属公司',''), mv.get('车型确定',''),
info.get('考核目标',''), info.get('月度奖励',0) or '']
cum_t = 0; cum_a = 0
for m in range(1, settle_month + 1):
# 找该车在m月的所有group可能有多个销售
month_groups = [(k,g) for k,g in G.get(m, {}).items() if k[0] == plate]
if month_groups:
persons = ', '.join(sorted(set(g['销售'] for _,g in month_groups)))
t_sum = sum(g['应考核'] for _,g in month_groups)
a_sum = sum(g['实际'] for _,g in month_groups)
q = '达标' if any(g['有达标'] for _,g in month_groups) else '未达标'
cum_t += t_sum; cum_a += a_sum
row += [persons, R(t_sum), R(a_sum), q]
else:
row += ['', '', '', '']
if settle_month >= 2:
cum_q = '达标' if (cum_a >= cum_t and cum_t > 0) else '未达标'
row += [R(cum_t), R(cum_a), cum_q]
# 本月发放
this_month_pays = [p for p in pays if p['结算月'] == settle_month]
if this_month_pays:
amt = sum(p['金额'] for p in this_month_pays)
persons = ', '.join(sorted(set(p['业务员'] for p in this_month_pays)))
types = ', '.join(sorted(set(p['类型'] for p in this_month_pays)))
row += [R(amt), persons, types]
else:
row += [0, '', '']
# 奖金池
total_periods = len(pays)
total_amt = sum(p['金额'] for p in pays)
row += [total_periods, R(total_amt) if total_amt > 0 else 0, 12 - total_periods]
WR(ws, rn, row); rn += 1
AW(ws)
```
- [ ] **Step 2: Commit**
```bash
git add excel_writer.py
git commit -m "feat: add write_vehicle_tracking_sheet function"
```
---
### Task 5: 创建 main.py 入口,按月循环生成
**Files:**
- Create: `main.py`
- [ ] **Step 1: 创建 main.py**
```python
#!/usr/bin/env python3
"""按月生成独立Excel文件"""
import os
from calc_engine import *
from excel_writer import *
os.chdir('/Users/kkfluous/Downloads')
# 读取数据
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])}")
# 分组
G = {m: grp(D[m], m) for m in [1,2,3]}
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)
# 发放追踪
month_data_map = {2: feb_data, 3: mar_data}
vehicle_payments, vehicle_info = collect_vehicle_payments(G, month_data_map)
master_vehicles = read_master_vehicles()
# 收集所有业务员
all_persons = {} # {name: dept}
for m in G:
for k, g in G[m].items():
all_persons[g['销售']] = g['部门']
# 按月生成
for settle_month in [1, 2, 3]:
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
# Sheet 1: 考核奖励规则
write_rules_sheet(wb)
# Sheet 2: 里程明细
write_detail_sheet(wb, D[settle_month], settle_month)
# Sheet 3: 计算过程
write_calc_process_sheet(wb, settle_month, G, feb_data if settle_month >= 2 else None)
# Sheet 4: 汇总
write_summary_sheet(wb, settle_month, month_data)
# Sheet 5-16: 业务员sheets
for person in sorted(all_persons.keys()):
dept = all_persons[person]
write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments)
# Sheet 17: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info)
# 删除默认空sheet
if 'Sheet' in wb.sheetnames:
del wb['Sheet']
fname = f'里程任务考核_{settle_month}月核算.xlsx'
wb.save(fname)
print(f"{fname} ({len(wb.sheetnames)} sheets)")
# 汇总打印
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"\n1月: {jan_total:.2f}, 2月: {feb_total:.2f}, 3月: {mar_total:.2f}")
print(f"Q1总计: {jan_total + feb_total + mar_total:.2f}")
```
- [ ] **Step 2: 运行验证**
Run: `python3 main.py`
Expected:
```
生成 1月核算文件...
✅ 里程任务考核_1月核算.xlsx (17 sheets)
生成 2月核算文件...
✅ 里程任务考核_2月核算.xlsx (17 sheets)
生成 3月核算文件...
✅ 里程任务考核_3月核算.xlsx (17 sheets)
1月: 21212.26, 2月: 21152.14, 3月: 56607.10
Q1总计: 98971.50
```
- [ ] **Step 3: Commit**
```bash
git add main.py
git commit -m "feat: main.py generates monthly Excel files with all sheets"
```
---
### Task 6: 验证与清理
**Files:**
- Verify: 3个输出xlsx文件
- Delete: `generate_q1_summary.py`(旧脚本,功能已迁移)
- [ ] **Step 1: 验证金额一致性**
```python
# 运行验证脚本检查:
# 1. 每个文件的汇总金额匹配
# 2. 业务员sheet各车小计之和 = 汇总中该人合计
# 3. 车辆追踪sheet发放合计 = 当月汇总合计
```
- [ ] **Step 2: 验证sheet数量**
每个文件应有1(规则) + 1(明细) + 1(过程) + 1(汇总) + 12(业务员) + 1(车辆) = 17 sheets
- [ ] **Step 3: Commit + Tag**
```bash
git add -A
git commit -m "V2.0.0 按月拆分Excel + 业务员/车辆维度展示"
git tag V2.0.0
```

View File

@@ -0,0 +1,113 @@
# 按月拆分Excel + 业务员/车辆维度展示
## 目标
将现有的单文件Q1汇总拆分为每月独立的Excel文件新增业务员维度和车辆维度sheet让旁人通过任意维度清晰理解考核绩效发放的计算来源。
## 输出文件
```
里程任务考核_1月核算.xlsx
里程任务考核_2月核算.xlsx
里程任务考核_3月核算.xlsx
```
## 每个文件的sheet结构
以2月为例1月和3月结构类似按月份调整列
### Sheet 1: 考核奖励规则
与现有一致,每个文件都包含一份。
### Sheet 2: 里程明细X月
与现有一致,逐条源数据记录,含达标判断和奖金计算。
### Sheet 3: X月计算过程
与现有一致,每个(车牌号+销售经理)组一行展示完整判断链。各类金额独立成列支持SUM对账。
### Sheet 4: X月汇总
与现有一致分section展示各类发放按销售人员和部门汇总。
### Sheet 5-16: 业务员_XXX12个每人一个
**头部汇总区2行**
- 业务员姓名、部门
- 本月考核车辆数、本月发放总额
**主体:按车分组,每车包含:**
车辆信息行:
- 车牌号、合同编号、客户名称、考核目标、月度目标里程、月奖励金额
本月考核数据行:
- 考核天数、应考核里程、实际里程、完成率、是否达标、本月奖金(天数折算)
历史累计行2月起
- 1月应考核、1月实际2月文件
- 1月应考核、1月实际、2月应考核、2月实际3月文件
- 累计应完成、累计实际、累计完成率、累计是否达标
发放结果行:
- 发放类型(当月达标/结转/补发X月/累计补发X月
- 发放金额
- 奖金池状态已发N期/共12期剩余M期
车辆小计行:
- 该车本月发放合计
**尾部汇总:**
- 本月考核车辆合计、本月发放总额
**车辆范围:** 该业务员在1-X月截至当前核算月有考核记录的所有车辆。同车跨多人时两人的sheet都显示。
### Sheet 17: 车辆考核追踪
全量492辆车每车一行
基本信息:
- 车牌号、车架号、归属公司、车型、考核目标、月度奖励金额
各月数据(截至当前核算月):
- X月业务员、X月应考核、X月实际、X月达标
累计2月起
- 累计应完成、累计实际、累计达标
本月发放:
- 发放金额、发放给谁、发放类型
奖金池:
- 累计已发期数、累计已发金额、剩余可发期数
## 计算逻辑
不变与V1.1.0完全一致:
1. 每条记录独立达标判断,奖金按天折算
2. 按(车牌号+销售经理)分组做结转/累计/补发
3. 结转floor(多跑/月度目标)>=1 → 完整月奖金,占当月名额
4. 补发:累计全部达标后补发未达标月份
5. 累计补发当月:当月未达标+无结转+累计达标
## 文件间数据传递
- 1月文件独立计算
- 2月文件需要1月源数据读取1月xlsx计算1+2月累计
- 3月文件需要1月和2月源数据计算1-3月累计
每个文件独立生成,不依赖上一个文件的输出结果,而是从源数据重新计算。
## 修改范围
重构 `generate_q1_summary.py`
- 主循环改为按月生成独立文件
- 计算引擎不变,复用现有的分组/结转/补发/累计逻辑
- 新增业务员sheet生成函数
- 新增车辆考核追踪sheet生成函数
- 现有的"车辆发放明细"和"车辆奖金池总览"合并到车辆考核追踪中
## 验证
- 每个文件的汇总金额与现有Q1汇总一致1月21212.26, 2月21152.14, 3月56607.10
- 业务员sheet各车小计之和 = 该人在汇总中的合计
- 车辆追踪sheet的发放合计 = 当月汇总合计
- 计算过程各列SUM = 汇总各section

524
excel_writer.py Normal file
View File

@@ -0,0 +1,524 @@
"""Excel输出工具函数 + 各类sheet生成"""
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from collections import defaultdict
from calc_engine import RULES, DAYS
hf = Font(bold=True)
hfl = PatternFill(start_color='D9E1F2', end_color='D9E1F2', fill_type='solid')
bd = Border(left=Side(style='thin'),right=Side(style='thin'),top=Side(style='thin'),bottom=Side(style='thin'))
def WH(ws, headers, row=1):
for c, h in enumerate(headers, 1):
cl = ws.cell(row=row, column=c, value=h)
cl.font=hf; cl.fill=hfl; cl.border=bd; cl.alignment=Alignment(horizontal='center', wrap_text=True)
def WR(ws, rn, vals):
for c, v in enumerate(vals, 1):
cl = ws.cell(row=rn, column=c, value=v); cl.border=bd
def AW(ws):
for col in ws.columns:
ml = max((len(str(c.value or '')) for c in col), default=0)
ws.column_dimensions[col[0].column_letter].width = min(ml+3, 22)
def R(v, n=2):
return round(v, n) if isinstance(v, (int,float)) and v else v
def agg(dl):
bp = defaultdict(lambda: {'n':0,'a':0})
for d in dl: bp[d['销售']]['n']+=1; bp[d['销售']]['a']+=d['']
return dict(bp)
def write_sec(ws, rn, title, dl):
ws.cell(row=rn, column=1, value=title).font=Font(bold=True, size=11); rn+=1
WH(ws, ['销售人员','车辆数','金额'], rn); rn+=1
bp=agg(dl); tv=ta=0
for p in sorted(bp.keys()):
WR(ws,rn,[p,bp[p]['n'],R(bp[p]['a'])]); tv+=bp[p]['n']; ta+=bp[p]['a']; rn+=1
WR(ws,rn,['总计',tv,R(ta)]); ws.cell(row=rn,column=1).font=hf; return rn+2
def write_total(ws, rn, month, all_data):
total_bp = defaultdict(lambda: {'部门':'','':0})
for cat,dl in all_data.items():
for d in dl: total_bp[d['销售']]['']+=d['']; total_bp[d['销售']]['部门']=d['部门']
ws.cell(row=rn,column=1,value=f'{month}月合计应发奖励(按销售人员)').font=Font(bold=True,size=11); rn+=1
WH(ws,['销售人员','部门名称','合计应发奖励'],rn); rn+=1
gt=0
for p in sorted(total_bp.keys()):
WR(ws,rn,[p,total_bp[p]['部门'],R(total_bp[p][''])]); gt+=total_bp[p]['']; rn+=1
WR(ws,rn,['合计','',R(gt)]); ws.cell(row=rn,column=1).font=hf; rn+=2
by_dept=defaultdict(float)
for p,d in total_bp.items(): by_dept[d['部门']]+=d['']
ws.cell(row=rn,column=1,value=f'{month}月合计应发奖励(按部门)').font=Font(bold=True,size=11); rn+=1
WH(ws,['部门名称','合计应发奖励'],rn); rn+=1
for dept in sorted(by_dept.keys()): WR(ws,rn,[dept,R(by_dept[dept])]); rn+=1
WR(ws,rn,['合计',R(gt)]); ws.cell(row=rn,column=1).font=hf
# ============================================================
# Sheet生成函数
# ============================================================
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])
AW(ws)
def write_detail_sheet(wb, records, month):
ws = wb.create_sheet(f'里程明细{month}')
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(t),R(a),R(a/t,4) if t>0 else 0,r['是否达标'],R(r['奖金']),R(r['多跑']),r['可结转'],
r.get('考核状态','')]); rn+=1
AW(ws)
def write_calc_process_jan(wb, G1):
ws = wb.create_sheet('1月计算过程')
WH(ws,['车牌号','销售经理','部门','①记录数','②总考核天数','③总应考核里程','④总实际里程',
'⑤有达标记录','⑥达标记录奖金合计','⑦总多跑里程','⑧月度目标里程','⑨可结转月数=floor(⑦/⑧)',
'→1月发放金额','→发放类型'])
rn=2
for k in sorted(G1.keys(), key=lambda x: (x[1],x[0])):
g=G1[k]
excess=sum(r['多跑'] for r in g['recs'] if r['是否达标']=='达标')
WR(ws,rn,[k[0],k[1],g['部门'],len(g['recs']),g['天数'],R(g['应考核']),R(g['实际']),
'' if g['有达标'] else '',R(g['奖金']),R(excess),g['目标km'],g['可结转'],
R(g['奖金']) if g['有达标'] else 0,'当月达标' if g['有达标'] else '未达标']); rn+=1
AW(ws)
def write_calc_process_feb(wb, G1, G2):
ws = wb.create_sheet('2月计算过程')
WH(ws,['车牌号','销售经理','部门',
'1月总应考核','1月总实际','1月有达标','1月奖金','1月多跑','1月可结转',
'2月记录数','2月总天数','2月总应考核','2月总实际','2月有达标','2月达标奖金',
'累计应完成','累计实际','累计是否达标',
'判断①结转','→结转金额(完整月)','判断②补发1月','→补发1月金额',
'判断③当月','→2月当月金额','判断④累计补发2月','→累计补发2月金额','2月发放合计'])
rn=2
for k in sorted(G2.keys(), key=lambda x: (x[1],x[0])):
g2=G2[k]; g1=G1.get(k)
j_q=g1['有达标'] if g1 else False; j_c=g1['可结转'] if g1 else 0
j_t=g1['应考核'] if g1 else 0; j_a=g1['实际'] if g1 else 0
j_bonus=g1['奖金'] if g1 else 0
je=sum(r['多跑'] for r in g1['recs'] if r['是否达标']=='达标') if g1 else 0
ct=g2.get('cum_t',0); ca=g2.get('cum_a',0); cq=g2.get('cum_q',False)
carry=g2.get('结转',0); bp1=g2.get('补发1月',0); bonus2=g2.get('当月奖金',0); cbp2=g2.get('累计补发2月',0)
c1=f"1月达标={j_q},可结转={j_c}{'' if carry>0 else ''}"
c2=f"1月未达标={not j_q},累计达标={cq}{'' if bp1>0 else ''}" if g1 else "无1月"
c3=f"2月达标={g2['有达标']},无结转={carry==0}{'' if bonus2>0 else ''}"
c4=f"2月未达标={not g2['有达标']},无结转={carry==0},累计达标={cq}{'' if cbp2>0 else ''}"
total=carry+bp1+bonus2+cbp2
WR(ws,rn,[k[0],k[1],g2['部门'],R(j_t),R(j_a),'' if j_q else '',R(j_bonus),R(je),j_c,
len(g2['recs']),g2['天数'],R(g2['应考核']),R(g2['实际']),'' if g2['有达标'] else '',R(g2['奖金']),
R(ct),R(ca),'达标' if cq else '未达标',
c1,R(carry) if carry>0 else 0,c2,R(bp1) if bp1>0 else 0,
c3,R(bonus2) if bonus2>0 else 0,c4,R(cbp2) if cbp2>0 else 0,R(total) if total>0 else 0]); rn+=1
AW(ws)
def write_calc_process_mar(wb, G1, G2, G3, feb_data):
ws = wb.create_sheet('3月计算过程')
WH(ws,['车牌号','销售经理','部门',
'1月应考核','1月实际','1月有达标','1月奖金已发',
'2月应考核','2月实际','2月有达标','2月奖金已发',
'3月记录数','3月总天数','3月总应考核','3月总实际','3月有达标','3月达标奖金',
'累计应完成','累计实际','累计是否达标',
'判断①结转','→结转金额','判断②补发1月','→补发1月金额',
'判断③补发2月','→补发2月金额','判断④当月','→3月当月金额',
'判断⑤累计补发3月','→累计补发3月金额','3月发放合计'])
rn=2
for k in sorted(G3.keys(), key=lambda x: (x[1],x[0])):
g3=G3[k]; g2=G2.get(k); g1=G1.get(k)
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
if g1 and j_q: j_paid=True
if g2 and g2.get('1月已补发',False): j_paid=True
f_t=g2['应考核'] if g2 else 0; f_a=g2['实际'] if g2 else 0
f_q=g2['有达标'] if g2 else False; f_paid=g2['2月已发'] if g2 else False
f_carry=g2['可结转'] if g2 else 0
ct=g3.get('cum_t',0); ca=g3.get('cum_a',0); cq=g3.get('cum_q',False)
carry=g3.get('结转',0); bj=g3.get('补发1月',0); bf2=g3.get('补发2月',0)
bonus3=g3.get('当月奖金',0); cbp3=g3.get('累计补发3月',0)
c1=f"2月可结转={f_carry}{'' if carry>0 else ''}"
c2=f"1月未发={not j_paid},累计达标={cq}{'' if bj>0 else ''}"
c3=f"2月未发={not f_paid},累计达标={cq}{'' if bf2>0 else ''}"
c4=f"3月达标={g3['有达标']},无结转={carry==0}{'' if bonus3>0 else ''}"
c5=f"3月未达标={not g3['有达标']},无结转={carry==0},累计达标={cq}{'' if cbp3>0 else ''}"
total=carry+bj+bf2+bonus3+cbp3
WR(ws,rn,[k[0],k[1],g3['部门'],R(j_t),R(j_a),'' if j_q else '','' if j_paid else '',
R(f_t),R(f_a),'' if f_q else '','' if f_paid else '',
len(g3['recs']),g3['天数'],R(g3['应考核']),R(g3['实际']),'' if g3['有达标'] else '',R(g3['奖金']),
R(ct),R(ca),'达标' if cq else '未达标',
c1,R(carry) if carry>0 else 0,c2,R(bj) if bj>0 else 0,
c3,R(bf2) if bf2>0 else 0,c4,R(bonus3) if bonus3>0 else 0,
c5,R(cbp3) if cbp3>0 else 0,R(total) if total>0 else 0]); rn+=1
AW(ws)
def write_summary_jan(wb, records):
ws = wb.create_sheet('1月汇总')
jan_dl=[{'车牌':r['车牌号'],'销售':r['销售经理'],'部门':r['部门名称'],'':r['奖金']} for r in records if r['是否达标']=='达标']
rn=write_sec(ws,1,'1月达标奖励',jan_dl)
write_total(ws,rn,1,{'达标':jan_dl})
AW(ws)
def write_summary_month(wb, month, month_data, section_names):
ws = wb.create_sheet(f'{month}月汇总')
rn=1
for i,cat in enumerate(section_names):
label_map = {'结转':f'一、结转:{month-1}月多跑在{month}月发',
'补发1月':'二、补发1月','补发2月':'三、补发2月',
'当月':f'{"" if month==2 else ""}{month}月当月奖励',
f'累计补发{month}':f'{"" if month==2 else ""}、累计达标补发{month}'}
rn=write_sec(ws,rn,label_map.get(cat,cat),month_data.get(cat,[]))
write_total(ws,rn,month,month_data)
AW(ws)
# ============================================================
# 新增业务员sheet
# ============================================================
def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments):
short_dept = dept.replace('业务','') if '业务' in dept else dept
ws = wb.create_sheet(f'{short_dept}-{person}')
ws.cell(row=1,column=1,value=f'{person} | {dept} | {settle_month}月绩效对账单').font=Font(bold=True,size=14)
plates = set()
for m in range(1, settle_month+1):
for k in G.get(m, {}):
if k[1] == person: plates.add(k[0])
person_total = sum(d[''] for cat,dl in month_data.items() for d in dl if d['销售']==person)
ws.cell(row=2,column=1,value=f'本月考核车辆: {len(plates)}辆 | 本月发放合计: {R(person_total)}').font=Font(bold=True,size=11)
# 表头:车辆信息 + 各月里程/目标 + 累计 + 奖金池
headers = ['车牌号','考核目标','月奖励']
for m in range(1, settle_month+1):
headers.append(f'{m}月里程/目标')
if settle_month >= 2:
headers += ['累计里程/目标']
headers.append('达标')
WH(ws, headers, 4)
rn = 5
for plate in sorted(plates):
g_cur = None
for m in range(settle_month, 0, -1):
g_cur = G.get(m, {}).get((plate, person))
if g_cur: break
if not g_cur: continue
first = g_cur['recs'][0]
mkm = g_cur['目标km']
# 第1行车辆信息 + 各月里程/目标
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))
if gm:
t=gm['应考核']; a=gm['实际']; cum_t+=t; cum_a+=a
row.append(f'{R(a,0)}/{R(t,0)}')
else:
row.append('-')
if settle_month >= 2:
row.append(f'{R(cum_a,0)}/{R(cum_t,0)}')
cum_q = cum_a >= cum_t and cum_t > 0
# 达标判断取settle_month的group
g_s = G.get(settle_month, {}).get((plate, person))
if g_s and g_s.get('有达标'):
row.append('达标')
elif cum_q:
row.append('累计达标')
else:
row.append('未达标')
WR(ws, rn, row)
ws.cell(row=rn, column=1).font = Font(bold=True)
rn += 1
# 第2-N行发放明细历史已发 + 本月发放)
WH(ws, ['', '发放项', '金额', '说明', '奖金池'], rn); rn += 1
pays = vehicle_payments.get(plate, [])
pays_to_date = [p for p in pays if p['结算月'] <= settle_month]
total_periods = len(pays_to_date)
# 历史已发m < settle_month
for m in range(1, settle_month):
gm = G.get(m, {}).get((plate, person))
# 查该月是否有发放记录
m_pays = [p for p in pays_to_date if p['结算月'] == m and p['业务员'] == person]
if m_pays:
amt = sum(p['金额'] for p in m_pays)
types = ', '.join(p['类型'] for p in m_pays)
WR(ws, rn, ['', f'{m}月: 已发', R(amt), types, ''])
elif gm:
# 有考核但未发
# 检查是否在后续月被补发过settle_month之前
bp_pays = [p for p in pays_to_date if p['对应考核月'] == m and p['业务员'] == person]
if bp_pays:
amt = sum(p['金额'] for p in bp_pays)
sm = bp_pays[0]['结算月']
WR(ws, rn, ['', f'{m}月: 已发', R(amt), f'{sm}月补发', ''])
else:
WR(ws, rn, ['', f'{m}月: 未发', 0, f'未达标(实际{R(gm["实际"],0)}<目标{R(gm["应考核"],0)})', ''])
else:
WR(ws, rn, ['', f'{m}月: 无记录', '', '', ''])
rn += 1
# 本月发放
g_s = G.get(settle_month, {}).get((plate, person), {})
if not isinstance(g_s, dict): g_s = {}
gm_s = G.get(settle_month, {}).get((plate, person))
plate_this_month = 0
# 结转
carry = g_s.get('结转', 0)
if carry > 0:
prev = settle_month - 1
g_prev = G.get(prev, {}).get((plate, person))
prev_excess = sum(r['多跑'] for r in g_prev['recs'] if r['是否达标'] == '达标') if g_prev else 0
WR(ws, rn, ['', '结转', R(carry), f'{prev}月多跑{R(prev_excess,0)}{mkm},结转(完整月奖金)', ''])
plate_this_month += carry; rn += 1
# 补发过去月份
for prev_m in range(1, settle_month):
bp_key = f'补发{prev_m}'
bp_amt = g_s.get(bp_key, 0)
if bp_amt > 0:
WR(ws, rn, ['', f'补发{prev_m}', R(bp_amt), f'累计{R(cum_a,0)}{R(cum_t,0)}达标,补发{prev_m}', ''])
plate_this_month += bp_amt; rn += 1
# 当月
bonus = g_s.get('当月奖金', 0)
if bonus > 0:
excess = sum(r['多跑'] for r in gm_s['recs'] if r['是否达标'] == '达标') if gm_s else 0
desc = '当月达标'
if excess > 0 and mkm > 0 and int(excess // mkm) >= 1:
desc += f',多跑{R(excess,0)}{mkm}可结转'
WR(ws, rn, ['', f'{settle_month}月当月', R(bonus), desc, ''])
plate_this_month += bonus; rn += 1
# 累计补发当月
cum_bp = g_s.get(f'累计补发{settle_month}', 0)
if cum_bp > 0:
WR(ws, rn, ['', f'累计补发{settle_month}', R(cum_bp), f'累计{R(cum_a,0)}{R(cum_t,0)}达标,补发{settle_month}', ''])
plate_this_month += cum_bp; rn += 1
# 无发放
if plate_this_month == 0 and carry == 0:
if gm_s:
a_s = gm_s['实际']; t_s = gm_s['应考核']
if gm_s.get('结转占位'):
WR(ws, rn, ['', '本月不发', 0, '结转占位,本月不另发', ''])
else:
WR(ws, rn, ['', '本月不发', 0, f'未达标(实际{R(a_s,0)}<目标{R(t_s,0)}),累计也未达标', ''])
else:
WR(ws, rn, ['', '本月不发', 0, '本月无考核记录', ''])
rn += 1
# 本月合计 + 奖金池
pool_str = f'已发{total_periods}期/共12期剩余{12-total_periods}'
WR(ws, rn, ['', '本月合计', R(plate_this_month), '', pool_str])
ws.cell(row=rn, column=2).font = Font(bold=True)
ws.cell(row=rn, column=5).font = Font(italic=True)
rn += 2 # 空行
# 尾部
ws.cell(row=rn,column=1,value=f'{person} {settle_month}月合计: {len(plates)}辆车, 发放 {R(person_total)}').font=Font(bold=True,size=12)
AW(ws)
# ============================================================
# 新增车辆考核追踪sheet
# ============================================================
def write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info):
ws = wb.create_sheet('车辆考核追踪')
from calc_engine import VEHICLE_TARGET_MAP, DAYS
center_top = Alignment(horizontal='center', vertical='top', wrap_text=True)
left_top = Alignment(vertical='top', wrap_text=True)
green_font = Font(color='006100')
green_fill = PatternFill(start_color='C6EFCE', end_color='C6EFCE', fill_type='solid')
red_font = Font(color='9C0006')
red_fill = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
grey_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
blue_fill = PatternFill(start_color='DAEEF3', end_color='DAEEF3', fill_type='solid')
gold_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
# 表头:基本信息 + 业务员 + 每月(应考核/实际/达标) + 累计 + 发放 + 奖金池
headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励','业务员']
info_cols = 6 # 前6列是车辆信息需要合并
for m in range(1, settle_month+1):
headers += [f'{m}月应考核', f'{m}月实际', f'{m}月达标']
if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标']
headers += ['本月发放金额','发放类型','已发期数','已发金额','剩余期数']
WH(ws, headers)
ws.freeze_panes = 'H2' # 冻结车辆信息+业务员列
rn = 2
for mv in master_vehicles:
plate = mv['车牌号']
info = vehicle_info.get(plate, {})
pays = vehicle_payments.get(plate, [])
target_name = info.get('考核目标','')
monthly_bonus = info.get('月度奖励',0)
if not target_name:
company = mv.get('归属公司','')
vtype = mv.get('车型确定','')
mapped = VEHICLE_TARGET_MAP.get((company, vtype))
if mapped:
target_name, _, monthly_bonus = mapped
# 收集该车所有(业务员) - 跨月去重
person_set = {}
for m in range(1, settle_month+1):
for k,g in G.get(m, {}).items():
if k[0] == plate:
person_set[k[1]] = g['部门']
persons = sorted(person_set.keys())
if not persons:
persons = [''] # 无考核记录也占一行
start_rn = rn
for pi, person in enumerate(persons):
sd = person_set.get(person,'').replace('业务','') if person else ''
person_label = f'{sd}-{person}' if person and sd else person
# 车辆信息(只在第一行写,后面留空等合并)
if pi == 0:
row_base = [plate, mv.get('车架号',''), mv.get('归属公司',''),
mv.get('车型确定',''), target_name or '', monthly_bonus or '']
else:
row_base = ['','','','','','']
row = row_base + [person_label]
# 每月数据按该业务员的group
cum_t = 0; cum_a = 0
for m in range(1, settle_month+1):
gm = G.get(m, {}).get((plate, person)) if person else None
if gm:
# 逐条记录汇总(同人同月可能多条)
t_sum = gm['应考核']; a_sum = gm['实际']
cum_t += t_sum; cum_a += a_sum
# 每条记录的达标情况
rec_details = []
for rec in gm['recs']:
rq = rec['是否达标'] == '达标'
rec_details.append(f"{R(rec['实际行驶里程(km)'],0)}/{R(rec['应考核里程(km)'],0)}{'' if rq else ''}")
all_q = all(rec['是否达标'] == '达标' for rec in gm['recs'])
if len(gm['recs']) == 1:
detail = rec_details[0].split('')[0].split('')[0] # 只取数字
row += [R(t_sum), R(a_sum), '' if all_q else '']
else:
# 多条:显示汇总,但单元格内注明各条
row += [R(t_sum), R(a_sum), '\n'.join(rec_details)]
else:
row += ['', '', '']
# 累计
if settle_month >= 2:
if cum_t > 0:
cum_q = cum_a >= cum_t
row += [R(cum_t), R(cum_a), '' if cum_q else '']
else:
row += ['', '', '']
cum_q = False
else:
cum_q = False
# 本月发放(该业务员的)
tp = [p for p in pays if p['结算月'] == settle_month and p['业务员'] == person]
if tp:
amt = sum(p['金额'] for p in tp)
types = ', '.join(p['类型'] for p in tp)
row += [R(amt), types]
else:
row += [0, '']
# 奖金池(整车,只在第一行显示)
if pi == 0:
pays_to_date = [p for p in pays if p['结算月'] <= settle_month]
tp_count = len(pays_to_date); tp_amt = sum(p['金额'] for p in pays_to_date)
row += [f'{tp_count}/12', R(tp_amt) if tp_amt > 0 else 0, 12 - tp_count]
else:
row += ['', '', '']
WR(ws, rn, row)
# 美化
# 车辆信息列灰底
for ci in range(1, info_cols + 1):
ws.cell(row=rn, column=ci).fill = grey_fill
# 月度达标列着色
for m in range(1, settle_month + 1):
col_base = info_cols + 1 + (m - 1) * 3 # 业务员列后面
qual_col = col_base + 3 # 达标列
cell = ws.cell(row=rn, column=qual_col)
cell.alignment = center_top
val = cell.value
if val and '' in str(val) and '' not in str(val):
cell.fill = green_fill; cell.font = green_font
elif val and '' in str(val):
cell.fill = red_fill; cell.font = red_font
# 累计达标着色
if settle_month >= 2:
cum_qual_col = info_cols + 1 + settle_month * 3 + 3 # 累计达标列
cell = ws.cell(row=rn, column=cum_qual_col)
if cell.value == '':
cell.fill = green_fill; cell.font = Font(bold=True, color='006100')
elif cell.value == '':
cell.fill = red_fill; cell.font = Font(bold=True, color='9C0006')
# 发放金底
pay_col = info_cols + 1 + settle_month * 3 + (3 if settle_month >= 2 else 0) + 1
if tp:
ws.cell(row=rn, column=pay_col).fill = gold_fill
# 奖金池蓝底
if pi == 0 and tp_count > 0:
pool_col = pay_col + 2
ws.cell(row=rn, column=pool_col).fill = blue_fill
ws.cell(row=rn, column=pool_col).font = Font(bold=True)
rn += 1
# 合并车辆信息列(如果多人)
if len(persons) > 1:
for ci in range(1, info_cols + 1):
ws.merge_cells(start_row=start_rn, start_column=ci,
end_row=start_rn + len(persons) - 1, end_column=ci)
ws.cell(row=start_rn, column=ci).alignment = Alignment(vertical='center', wrap_text=True)
# 奖金池列也合并
for offset in [2, 3, 4]: # 已发期数/已发金额/剩余期数
pool_col = pay_col + offset
ws.merge_cells(start_row=start_rn, start_column=pool_col,
end_row=start_rn + len(persons) - 1, end_column=pool_col)
# 列宽
col_widths = {'A':12,'B':18,'C':18,'D':10,'E':18,'F':8,'G':14}
for cl, w in col_widths.items():
ws.column_dimensions[cl].width = w
# 月度列
start = ord('H')
for m in range(settle_month):
for i in range(3):
cl = chr(start + m*3 + i)
if cl <= 'Z': ws.column_dimensions[cl].width = 12
# 剩余列
rem = start + settle_month * 3
for i in range(10):
cl = chr(rem + i)
if cl <= 'Z': ws.column_dimensions[cl].width = 14

90
main.py Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""按月生成独立Excel文件 - 里程考核绩效"""
import os, openpyxl
from calc_engine import *
from excel_writer import *
os.chdir('/Users/kkfluous/Downloads')
# 读取
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])}")
# 分组+计算
G = {m: grp(D[m], m) for m in [1,2,3]}
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)
# 发放追踪
vehicle_payments, vehicle_info = collect_vehicle_payments(G, feb_data, mar_data)
master_vehicles = read_master_vehicles()
# 所有业务员
all_persons = {}
for m in G:
for k, g in G[m].items():
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"\n1月: {jan_total:.2f}, 2月: {feb_total:.2f}, 3月: {mar_total:.2f}")
print(f"Q1总计: {jan_total + feb_total + mar_total:.2f}")
# ============================================================
# 按月生成
# ============================================================
for settle_month in [1, 2, 3]:
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
# Sheet 1: 考核奖励规则
write_rules_sheet(wb)
# Sheet 2: 里程明细
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)
# Sheet 4: 汇总
if settle_month == 1:
write_summary_jan(wb, D[1])
elif settle_month == 2:
write_summary_month(wb, 2, feb_data, ['结转','补发1月','当月','累计补发2月'])
else:
write_summary_month(wb, 3, mar_data, ['结转','补发1月','补发2月','当月','累计补发3月'])
# Sheet 5: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info)
# Sheet 6-17: 业务员
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'
wb.save(fname)
print(f"{fname} ({len(wb.sheetnames)} sheets)")