Files
mileage-bonus/calculate_august.py
T
shishengliang 65904c8270 feat: add assessment workbench and September reconciliation
Preserve August hydrogen revision and September audit evidence; calculate interrupted vehicle history across all prior months. Record remaining carry-policy and layout-baseline discrepancies.
2026-10-09 18:21:45 +08:00

517 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""2026年8月核算:只读原始数据,复用原引擎,输出供 Excel 构建器使用的 JSON。"""
from pathlib import Path
from datetime import date, datetime, timedelta
from collections import defaultdict, Counter
from decimal import Decimal, ROUND_HALF_UP
from zipfile import ZipFile
from xml.etree import ElementTree as ET
import hashlib
import json
import re
import warnings
import openpyxl
import calc_engine as engine
REPO = Path(__file__).resolve().parent
ROOT = Path('/Users/shishengliang/Documents/羚牛/里程考核核算/8月')
INPUT = ROOT / '输入数据'
SUPPORT = ROOT / '资料检查' / '运行支持'
OUTPUT = ROOT / '8月核算结果'
warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
def money(v):
return float(Decimal(str(v)).quantize(Decimal('.01'), rounding=ROUND_HALF_UP))
def day(v):
if isinstance(v, datetime): return v.date()
if isinstance(v, date): return v
return datetime.fromisoformat(str(v)).date()
def norm(v):
return re.sub(r'\s+', '', str(v or '')).upper()
def numeric(v):
if v is None or str(v).strip() == '': return None
x = Decimal(str(v))
if not x.is_finite() or x < 0: raise ValueError(f'非法里程: {v}')
return float(x)
def gap_reason(candidate, t, g, h, e, flag, comparison):
"""说明某车辆日无法得到最终计里程的原因,供缺口清单直接引用。"""
if candidate and h is None and flag is None:
if comparison is None:
return '测试候选且无纯氢、无法计算测试比较值,兜底需GPS但GPS来源缺失'
return '测试候选且无纯氢、无法判定测试(GPS缺失),兜底需GPS但GPS来源缺失'
if not candidate and h is None and t is None:
return '普通车辆无纯氢且仪表来源缺失'
return '无可用来源'
def read_rows(path, sheet=None, maximum=None):
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
ws = wb[sheet] if sheet else wb.worksheets[0]
ws.reset_dimensions()
result = list(ws.iter_rows(max_row=maximum, values_only=True))
wb.close()
return result
def matrix(path, header):
rows = read_rows(path, '里程查询' if header == 5 else None)
h = rows[header]
cols = {}
for i, value in enumerate(h):
if isinstance(value, (date, datetime)): cols[i] = day(value)
elif re.fullmatch(r'08月\d{2}日', str(value)): cols[i] = date(2026, 8, int(value[3:5]))
assert set(cols.values()) == {date(2026, 8, d) for d in range(1, 32)}
data, provenance = {}, {}
for ri, r in enumerate(rows[header+1:], header+2):
if not r or not r[0]: continue
plate = norm(r[0])
assert plate not in data, f'同来源重复车牌: {plate}'
data[plate] = {d: numeric(r[i]) if i < len(r) else None for i, d in cols.items()}
provenance[plate] = {d: f'{path.name}:{openpyxl.utils.get_column_letter(i+1)}{ri}' for i, d in cols.items()}
return data, provenance
def main():
SUPPORT.mkdir(parents=True, exist_ok=True)
OUTPUT.mkdir(parents=True, exist_ok=True)
cfg = json.loads((ROOT / '资料检查/8月已确认业务规则.json').read_text())
for name, checksum in cfg['source_sha256'].items():
assert hashlib.sha256((INPUT/name).read_bytes()).hexdigest() == checksum, f'已确认输入发生变化: {name}'
sources = list(INPUT.glob('*.xlsx')) + list((INPUT/'2026年8月每日行驶里程[纯氢里程]').glob('*.xlsx'))
source_hashes = {str(f): hashlib.sha256(f.read_bytes()).hexdigest() for f in sources}
tbox, tp = matrix(INPUT/'车辆里程查询_20260801-20260831_1024辆[仪表里程].xlsx', 5)
gps, gp = matrix(INPUT/'车辆里程查询_20260801-20260831_1024辆 【GPS里程】.xlsx', 5)
supplementary, sp = matrix(INPUT/'智能管车_车辆里程日报-163921799.xlsx', 0)
for p, values in supplementary.items():
for d, v in values.items():
if v is not None and gps.setdefault(p, {}).get(d) is None:
gps[p][d] = v
gp.setdefault(p, {})[d] = sp[p][d]
# 直接读取补充源,而不是依赖上次人工计算的缓存。
p = INPUT/'里程统计[天][2026-08-01至2026-08-31].xlsx'
rr = read_rows(p); header = rr[2]; unique = set()
for ri, r in enumerate(rr[3:], 4):
rec = dict(zip(header, r))
if norm(rec.get('车牌号码')) != '粤A00585F': continue
d = day(rec['日期']); v = numeric(rec['里程(km)'])
assert d not in unique and v is not None
unique.add(d)
if gps.setdefault('粤A00585F', {}).get(d) is None:
gps['粤A00585F'][d] = v
gp.setdefault('粤A00585F', {})[d] = f'{p.name}:P{ri}'
assert len(unique) == 31
hydrogen, electric = {}, {}
for p in sorted((INPUT/'2026年8月每日行驶里程[纯氢里程]').glob('*.xlsx')):
rr = read_rows(p); d = day(p.name[:10])
for r in rr[1:]:
rec = dict(zip(rr[0], r)); plate = norm(rec.get('车牌号码'))
if not plate: continue
assert d not in hydrogen.setdefault(plate, {})
hydrogen[plate][d] = numeric(rec['氢能源行驶里程(km)'])
electric.setdefault(plate, {})[d] = numeric(rec['插枪充电电量行驶里程(km)'])
# 测试名单的格式撑到1048576行,用XML定位有值的末行。
p = INPUT/'8月测试车辆明细参考.xlsx'; ns = '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'
with ZipFile(p) as z:
root = ET.fromstring(z.read('xl/worksheets/sheet1.xml'))
maximum = max(int(r.get('r')) for r in root.findall('.//'+ns+'row')
if any(c.find(ns+'v') is not None or c.find(ns+'is') is not None for c in r))
tests = {norm(r[0]) for r in read_rows(p, maximum=maximum)[1:] if r and r[0]}
new = {x['plate']: x['monthly_bonus'] for x in cfg['new_policy']['matched']}
owners = {x['source_row']: x for x in cfg['ownership_overrides']}
overlaps = {x['later_source_row']: x for x in cfg['overlap_adjustments']}
rr = read_rows(INPUT/'租赁任务考核_2026年8月.xlsx'); original_headers = list(rr[0])
records, daily, issues, used_days, gaps, basis_days = [], [], [], set(), [], Counter()
for ri, values in enumerate(rr[1:], 2):
r = dict(zip(original_headers, values)); plate = norm(r['车牌号']); r['车牌号'] = plate
start, finish = day(r['考核开始日期']), day(r['考核结束日期'])
original = dict(r); notes = []
if ri in owners:
change = owners[ri]
assert r['销售经理'] == change['old_sales'] and plate == change['plate']
r['销售经理'], r['部门名称'] = change['new_sales'], change['new_department']
notes.append(f"归属由{original['销售经理']}调整为{r['销售经理']}")
if ri in overlaps:
assert plate == overlaps[ri]['plate']
start = day(overlaps[ri]['new_start'])
notes.append('8月7日计前段,本段从8月8日起')
days = [start+timedelta(days=n) for n in range((finish-start).days+1)]
r['考核开始日期'], r['考核结束日期'] = start.isoformat(), finish.isoformat()
rule = engine.RULES[r['考核目标']]
target = money(rule['km'] * len(days) / 31)
assert ri in overlaps or abs(target-float(r['应考核里程(km)'])) < .005
r['考核天数'], r['应考核里程(km)'] = len(days), target
tvals = [tbox.get(plate, {}).get(d) for d in days]
ts = [v for v in tvals if v is not None]
ttotal = money(sum(ts)) if ts else None
has_h = any(hydrogen.get(plate, {}).get(d) is not None for d in days)
candidate = plate in tests
km = 0.; excluded_h = 0.; test_days = []; partial = []; gap_days = []
basis_counts, fallback_days = Counter(), []
for d in days:
key = plate, d
assert key not in used_days, f'重复计日: {key}'
used_days.add(key)
t = tbox.get(plate, {}).get(d); g = gps.get(plate, {}).get(d)
h = hydrogen.get(plate, {}).get(d); e = electric.get(plate, {}).get(d)
if t is not None:
comparison, comparison_source = t, '车机'
elif h is not None and e is not None:
comparison, comparison_source = h + e, '纯氢+纯电'
else:
comparison, comparison_source = None, '无来源'
# 新口径:仅测试候选逐日判定;普通车辆不因差值自动视为测试。
if candidate:
if t is not None:
flag = None if g is None else (t - g > 50)
else:
flag = None if (comparison is None or g is None) else (comparison - g > 50)
else:
flag = False
if flag is True:
attempted, decision, included = '测试日GPS', '测试日取GPS', g
elif h is not None:
# 非测试日优先纯氢,真实0同样采用。
attempted, decision, included = '非测试日纯氢', '非测试日取纯氢', h
elif candidate and flag is None:
# 测试名单且无纯氢:无法判定时按已确认兜底取GPS,缺则记为缺口。
attempted, decision, included = '测试名单无纯氢GPS', '测试名单无纯氢兜底取GPS', g
fallback_days.append(d.isoformat())
elif t is not None:
attempted, decision, included = '非测试日仪表', '非测试日取仪表', t
else:
attempted, decision, included = '缺失', '无可用来源', None
if flag is True:
if h is not None:
excluded_h += h
test_days.append(d.isoformat())
if included is None:
basis = '缺失'
decision = ('测试名单无纯氢兜底需GPS但GPS缺失'
if (candidate and h is None and flag is None) else '所需来源缺失')
partial.append(d.isoformat())
gap_days.append(d.isoformat())
gaps.append(dict(source_row=ri, plate=plate, sales=r['销售经理'], date=d.isoformat(),
test_candidate=candidate, tbox=t, gps=g, hydrogen=h, electric=e,
test_flag=(None if flag is None else bool(flag)),
needed='GPS' if (flag is True or (candidate and h is None)) else '仪表',
reason=gap_reason(candidate, t, g, h, e, flag, comparison)))
else:
basis = attempted
km += included
basis_counts[basis] += 1
daily.append([ri, plate, r['销售经理'], d.isoformat(), t, g, h, e,
money(h+e) if h is not None and e is not None else None,
comparison_source,
money(comparison) if comparison is not None else None,
money(comparison-g) if comparison is not None and g is not None else None,
'是' if candidate else '否',
('是' if flag is True else '否') if candidate and flag is not None else ('未判定' if candidate else '不适用'),
decision, included,
gp.get(plate, {}).get(d) if g is not None else '无来源',
gap_reason(candidate, t, g, h, e, flag, comparison) if included is None else ''])
km = money(km)
basis_days.update(basis_counts)
order = ['测试日GPS', '非测试日纯氢', '非测试日仪表', '测试名单无纯氢GPS', '缺失']
mode = ';'.join(f'{b}{basis_counts[b]}天' for b in order if basis_counts[b]) or '无有效里程'
dominant = max(basis_counts, key=basis_counts.get)
if partial:
notes.append(f'按新口径{len(partial)}天所需来源缺失,暂按已有记录累计')
issues.append([ri, plate, r['销售经理'], mode, '、'.join(partial), km, target,
'新口径下这些车辆日所需来源缺失,未计入;补数后累计、结转可能变化'])
r['实际行驶里程(km)'] = km
r['完成率(%)'] = money(km/target*100)
r['是否达标'] = '达标' if km >= target else '未达标'
for k in ['交车日期','预计还车日期','实际还车日期']:
if r[k] is not None: r[k] = day(r[k]).isoformat()
reward = new.get(plate, rule['good'])
r.update(月份=8, 月度目标里程=rule['km'], 奖励基准_达标=reward, 奖励基准_未达标=0,
奖励基准_结转=reward, 奖励基准_补发=reward,
奖金=reward*len(days)/31 if km >= target else 0., 多跑=max(0, km-target),
可结转=int(max(0, km-target)//rule['km']), 源表行=ri, 取数方式=mode,
主取数依据=dominant, 测试候选='是' if candidate else '否',
原里程=original['实际行驶里程(km)'], 原销售=original['销售经理'], 原部门=original['部门名称'],
车机里程=ttotal, GPS里程=money(sum(gps.get(plate, {}).get(d) or 0 for d in days)) if any(gps.get(plate, {}).get(d) is not None for d in days) else None,
纯氢里程=money(sum(hydrogen.get(plate, {}).get(d) or 0 for d in days)) if has_h else None,
纯电里程=money(sum(electric.get(plate, {}).get(d) or 0 for d in days)) if has_h else None,
剔除纯氢里程=money(excluded_h), 测试日期='、'.join(test_days), 测试天数=len(test_days),
GPS兜底天数=basis_counts.get('测试名单无纯氢GPS', 0), 缺口天数=len(gap_days),
新政策='是' if plate in new else '否', 说明=';'.join(notes))
records.append(r)
assert len(records) == 364 and len({r['车牌号'] for r in records}) == 354
assert len(used_days) == 9184 and len(new) == 31
D = {}
history_paths = {}
for m in range(1, 8):
path = REPO/f'租赁任务考核_2026年{m}月.xlsx' if m < 7 else REPO/'7月绩效重算_多源考核里程_20260817/租赁任务考核_2026年7月.xlsx'
history_paths[m] = path
D[m] = engine.read_file(path, m)
source_hashes[str(path)] = hashlib.sha256(path.read_bytes()).hexdigest()
D[8] = records
G = {m: engine.grp(D[m], m) for m in D}
engine.calc_jan_carryover(G[1])
all_data = {1: {'当月': [{'车牌': k[0], '销售': g['销售'], '部门': g['部门'], '额': g['奖金']}
for k, g in G[1].items() if g['奖金'] > 0]}}
for m in range(2, 8): engine.calc_month(m, G, all_data, G[m-1], G[m])
# 复算已确认的7月,不把8月新口径追溯到历史输出。
july_final = 0.
resigned = {'赵连飞', '伍仲文', '岑彦'}
for cat, items in all_data[7].items():
for item in items:
if item['销售'] == '钱婧': continue
if item['销售'] in resigned and cat != '当月': continue
july_final += item['额']
assert money(july_final) == 62616.73, f'历史基线不一致: {july_final}'
engine.calc_month(8, G, all_data, G[7], G[8])
# 原引擎的无考核记录结转会继承上月奖励。8月名单车辆同样应用本月金额。
for k, g in G[8].items():
if g.get('虚拟') and k[0] in new:
amount = new[k[0]]
g['结转'] = amount
for f in ['奖励达标', '奖励结转', '奖励补发']: g[f] = amount
for item in all_data[8]['结转']:
if (item['车牌'], item['销售']) == k: item['额'] = amount
# 离职业务处理(2026-09-15用户确认,覆盖此前转接口径):
# 伍仲文、岑彦、赵连飞、钱婧四人:旧引擎的当月、结转、历史补发、累计补发及旧当月口径
# 全部取消,均不发放、不转交;本人实发0,原计算额保留审计。其他人员完全保持原引擎结果。
# mode=exclude_all 时不生成任何转交项,最终 ledger 仅含非离职人员的原逐项金额。
resign_cfg = cfg.get('resignation', {})
resign_mode = resign_cfg.get('mode', 'transfer_current_month')
departed_people = sorted(set(resign_cfg.get('departed_people', resign_cfg.get('stop_all_people', []))))
departed_set = set(departed_people)
transfer_map = dict(resign_cfg.get('transfer_map', {}))
receiver_dept_cfg = dict(resign_cfg.get('receiver_department', {}))
transfer_reason = resign_cfg.get('reason', '已离职:本人奖金不发放')
denominator = int(resign_cfg.get('proration_denominator', 31))
categories = ['当月', '结转'] + [f'补发{m}月' for m in range(1, 8)] + ['累计补发8月']
if resign_mode != 'exclude_all':
receiver_pool = {r['销售经理'] for r in records}
for p in departed_people:
assert p in transfer_map and transfer_map[p] in receiver_pool, f'离职转接映射缺失: {p}'
assert set(transfer_map) == set(departed_people), '转交映射必须且仅覆盖离职四人'
# 1) 旧引擎本月全部发放项(含四人),仅用于口径对照与离职审计,不直接进入最终发放表。
legacy_ledger = []
for cat in categories:
for item in all_data[8].get(cat, []):
key = item['车牌'], item['销售']; g = G[8][key]
m = int(re.search(r'补发(\d+)月', cat).group(1)) if '补发' in cat else 8
if cat == '结转': base, eligible, divisor = g['奖励结转'], 1, 1
elif cat == '当月':
good = [r for r in g['recs'] if r['是否达标'] == '达标']
assert len({r['奖励基准_达标'] for r in good}) == 1
base, eligible, divisor = good[0]['奖励基准_达标'], sum(r['考核天数'] for r in good), 31
else:
old = G[m][key]
base, eligible, divisor = old['奖励补发'], old['天数'], engine.DAYS[m]
assert abs(base*eligible/divisor-item['额']) < 1e-6, (cat, item)
legacy_ledger.append(dict(plate=key[0], sales=key[1], owner_sales=key[1],
department=item['部门'], source_department=item['部门'],
assessment_month=m, category=cat, base=base, days=eligible,
month_days=divisor, gross_amount=money(item['额']),
unrounded=item['额'], virtual=bool(g.get('虚拟'))))
departed_legacy = [l for l in legacy_ledger if l['sales'] in departed_set]
legacy_gross_total = money(sum(l['gross_amount'] for l in legacy_ledger))
legacy_unrounded_total = money(sum(l['unrounded'] for l in legacy_ledger))
departed_legacy_gross = money(sum(l['gross_amount'] for l in departed_legacy))
departed_backpay_gross = money(sum(l['gross_amount'] for l in departed_legacy if l['category'] != '当月'))
non_departed_legacy_gross = money(sum(l['gross_amount'] for l in legacy_ledger if l['sales'] not in departed_set))
# 2) 转交项:exclude_all 模式不生成任何转交;其他模式沿用原转接口径。
transfers = []
if resign_mode != 'exclude_all':
# 从records重新生成四人8月当月达标转交项:按原同车同人分组;同组多记录合并达标天数后逐项取分,不重复。
for person in departed_people:
by_plate = defaultdict(list)
for r in records:
if r['销售经理'] == person and r['是否达标'] == '达标':
by_plate[r['车牌号']].append(r)
for plate in sorted(by_plate):
rows = by_plate[plate]
bases = {r['奖励基准_达标'] for r in rows}
assert len(bases) == 1, (person, plate, bases)
base = bases.pop()
qualifying_days = sum(r['考核天数'] for r in rows)
exact = base * qualifying_days / denominator
receiver = transfer_map[person]
receiver_dept = receiver_dept_cfg.get(receiver)
if receiver_dept is None:
cand = sorted({r['部门名称'] for r in records if r['销售经理'] == receiver})
receiver_dept = cand[0] if len(cand) == 1 else '跨部门待确认'
transfers.append(dict(owner_sales=person, owner_department=rows[0]['部门名称'],
source_department=rows[0]['部门名称'], plate=plate,
category=resign_cfg.get('current_month_category', '当月'),
assessment_month=8, base=base, days=qualifying_days,
month_days=denominator, amount=money(exact), unrounded=exact,
receiver=receiver, receiver_department=receiver_dept))
transfers.sort(key=lambda t: (t['receiver'], t['owner_sales'], t['plate']))
transfer_total = money(sum(t['amount'] for t in transfers))
# 3) 最终发放表:其他人员原样;四人旧项全部移除,exclude_all 模式不加入任何转交项。
ledger = []
for l in legacy_ledger:
if l['sales'] in departed_set:
continue
ledger.append(dict(l, id=len(ledger)+1, withheld=0., amount=l['gross_amount'],
unrounded_net=l['unrounded'], payment_status='released',
payment_status_text='正常发放', withhold_reason='',
transfer=False, receiver='', transfer_from=''))
for t in transfers:
ledger.append(dict(id=len(ledger)+1, plate=t['plate'], sales=t['receiver'],
owner_sales=t['owner_sales'], department=t['receiver_department'],
source_department=t['source_department'], assessment_month=8,
category=t['category'], base=t['base'], days=t['days'],
month_days=t['month_days'], gross_amount=t['amount'], withheld=0.,
amount=t['amount'], unrounded=t['unrounded'], unrounded_net=t['unrounded'],
payment_status='released', payment_status_text='正常发放',
withhold_reason='', virtual=False, transfer=True,
receiver=t['receiver'], transfer_from=t['owner_sales']))
by_person = defaultdict(lambda: defaultdict(float))
for l in ledger:
by_person[l['sales']][l['category']] += l['amount']
total = money(sum(l['amount'] for l in ledger))
gross_total = money(sum(l['gross_amount'] for l in ledger))
withheld_total = money(sum(l['withheld'] for l in ledger))
unrounded_gross_total = money(sum(l['unrounded'] for l in ledger))
unrounded_net_total = money(sum(l['unrounded_net'] for l in ledger))
assert money(gross_total - withheld_total) == total
assert withheld_total == 0., '新规则下最终发放表无停发扣减'
# 勾稽:其他人员旧引擎金额不变;新净额 = 其他人员应发 + 转交合计。
assert money(non_departed_legacy_gross + transfer_total) == gross_total
assert money(total - non_departed_legacy_gross) == transfer_total
for l in ledger:
assert l['withheld'] == 0 and l['amount'] == l['gross_amount'] and l['payment_status'] == 'released'
groups = []
for key, g in sorted(G[8].items(), key=lambda kv:(kv[0][1],kv[0][0])):
pays = [l for l in ledger if (l['plate'],l['sales']) == key]
groups.append(dict(plate=key[0], sales=key[1], department=g['部门'], rows=len(g['recs']), days=g['天数'],
target=money(g['应考核']), actual=money(g['实际']), qualified=g['有达标'],
cum_target=money(g['cum_t']), cum_actual=money(g['cum_a']), cum_qualified=g['cum_q'],
previous_carry=G[7].get(key,{}).get('可结转',0), carry_left=g['可结转'],
virtual=bool(g.get('虚拟')), reward=g['奖励达标'],
total=money(sum(l['amount'] for l in pays)),
gross_total=money(sum(l['gross_amount'] for l in pays)),
withheld_total=money(sum(l['withheld'] for l in pays)),
withhold_reason=(transfer_reason if key[1] in departed_set else ''),
history=[[money(G[m][key]['应考核']),money(G[m][key]['实际'])] if key in G[m] else [None,None] for m in range(1,9)]))
# 组汇总按源车源人保留可追溯;接收人的当月转交项单独列示,不并入源组,避免重复计入历史里程。
assert money(sum(g['total'] for g in groups)) == non_departed_legacy_gross
assert money(sum(g['gross_total'] for g in groups)) == non_departed_legacy_gross
assert money(sum(g['total'] for g in groups) + transfer_total) == total
assert money(sum(g['gross_total'] for g in groups) + transfer_total) == gross_total
assert money(sum(g['withheld_total'] for g in groups)) == withheld_total
people = sorted({r['销售经理'] for r in records} | {l['sales'] for l in ledger} | departed_set)
# 人员部门:其他人员用考核记录与正常发放项推导;离职人员另取旧引擎来源部门,转交行的
# “跨部门待确认”不并入接收人现有部门。
departments = {p: '、'.join(sorted(
{r['部门名称'] for r in records if r['销售经理']==p} |
{l['department'] for l in ledger if l['sales']==p and not l['transfer']} |
({l['source_department'] for l in departed_legacy if l['sales']==p} if p in departed_set else set())
)) for p in people}
summary_people = [p for p in people if p not in departed_set]
person_summary = [[p, departments[p]]+[money(by_person[p][cat]) for cat in categories]+[money(sum(by_person[p].values()))] for p in summary_people]
person_gross = defaultdict(float); person_canceled = defaultdict(float); person_net = defaultdict(float)
for l in legacy_ledger:
person_gross[l['sales']] += l['gross_amount']
for l in departed_legacy:
person_canceled[l['sales']] += l['gross_amount']
for l in ledger:
person_net[l['sales']] += l['amount']
person_audit = []
for p in people:
if p in departed_set:
person_audit.append([p, departments[p], money(person_gross[p]), money(person_canceled[p]), 0.,
'已离职,本人8月全部奖金取消,实发0,不转交' if resign_mode == 'exclude_all'
else '已离职,本人实发0;8月当月达标奖励已转交接收人'])
else:
person_audit.append([p, departments[p], money(person_gross[p]), 0., money(person_net[p]), '正常发放'])
assert money(sum(r[-1] for r in person_summary)) == total
assert money(sum(r[2] for r in person_audit)) == legacy_gross_total
assert money(sum(r[3] for r in person_audit)) == departed_legacy_gross
assert money(sum(r[4] for r in person_audit)) == total
# 唯一发放项、规则边界、来源有效性与历史输入完整性验证。
assert len({(l['plate'],l['sales'],l['category']) for l in ledger}) == len(ledger)
assert not any(l['sales'] in departed_set for l in ledger), '发放表不得出现离职人员本人记录'
for l in ledger:
if l['category'] == '结转': assert not any(x['category']=='当月' and x['plate']==l['plate'] and x['sales']==l['sales'] for x in ledger)
for f, digest in source_hashes.items(): assert hashlib.sha256(Path(f).read_bytes()).hexdigest() == digest
# 主表显示范围:剔除离职人员后的非离职 records / 车辆 / 达标 / 缺口计数,供汇总文案按实际展示重算。
display_records = [r for r in records if r['销售经理'] not in departed_set]
display_vehicles = len({r['车牌号'] for r in display_records})
display_qualified = sum(r['是否达标'] == '达标' for r in display_records)
display_gap_days = sum(r['缺口天数'] for r in display_records)
display_groups = [g for g in groups if g['sales'] not in departed_set]
departed_section = dict(
people=departed_people, mode=resign_mode, transfer_map=transfer_map, reason=transfer_reason,
scope=resign_cfg.get('scope',''), current_month_category=resign_cfg.get('current_month_category','当月'),
denominator=denominator,
legacy_items=[dict(id=i+1, plate=l['plate'], sales=l['sales'], owner_sales=l['sales'],
department=l['department'], source_department=l['source_department'],
assessment_month=l['assessment_month'], category=l['category'],
base=l['base'], days=l['days'], month_days=l['month_days'],
gross_amount=l['gross_amount'], virtual=l['virtual'],
handling=('按新规则转交接收人' if (resign_mode != 'exclude_all' and l['category']=='当月') else '取消,不发放本人'),
note=('旧当月口径已由8月达标记录重新生成后转交' if (resign_mode != 'exclude_all' and l['category']=='当月')
else '离职人员奖金取消,本人不发、不转交')) for i, l in enumerate(departed_legacy)],
transfers=[dict(id=i+1, owner_sales=t['owner_sales'], owner_department=t['owner_department'],
source_department=t['source_department'], plate=t['plate'],
category=t['category'], assessment_month=t['assessment_month'],
base=t['base'], days=t['days'], month_days=t['month_days'],
amount=t['amount'], receiver=t['receiver'],
receiver_department=t['receiver_department'],
note='8月当月达标奖励,按奖励基准×达标天数/31逐项取分后转交') for i, t in enumerate(transfers)],
legacy_gross_total=legacy_gross_total, legacy_unrounded_total=legacy_unrounded_total,
departed_legacy_gross=departed_legacy_gross, canceled_backpay_gross=departed_backpay_gross,
transfer_total=transfer_total, transfer_records=len(transfers))
result = dict(records=records, original_headers=original_headers, daily=daily, missing=issues, gaps=gaps,
ledger=ledger, groups=groups, person_summary=person_summary, person_audit=person_audit, people=summary_people,
departments=departments, categories=categories, rules=cfg, input_hashes=source_hashes,
departed=departed_section,
withholding=dict(people=departed_people, mode=resign_mode,
scope=resign_cfg.get('scope',''), gross_amount=legacy_gross_total,
legacy_gross_amount=legacy_gross_total,
canceled_amount=departed_legacy_gross, transfer_amount=transfer_total,
net_amount=total, transfer=(resign_mode != 'exclude_all'), transfer_map=transfer_map,
reason=transfer_reason, supersedes=resign_cfg.get('supersedes',''),
withheld_records=0),
stats=dict(records=len(records), vehicles=354, qualified_records=sum(r['是否达标']=='达标' for r in records),
display_records=len(display_records), display_vehicles=display_vehicles,
display_qualified_records=display_qualified, display_gap_days=display_gap_days,
display_groups=len(display_groups),
amount=total, gross_amount=gross_total, withheld_amount=withheld_total,
unrounded_total=unrounded_gross_total, unrounded_net_total=unrounded_net_total,
rounding_difference=money(gross_total-unrounded_gross_total),
net_rounding_difference=money(total-unrounded_net_total),
withheld_records=0,
legacy_gross_amount=legacy_gross_total, legacy_unrounded_total=legacy_unrounded_total,
canceled_departed_gross=departed_legacy_gross,
canceled_departed_backpay=departed_backpay_gross,
transfer_amount=transfer_total, transfer_records=len(transfers),
departed_people=departed_people,
july_reproduction=money(july_final), daily_rows=len(daily),
source_modes=dict(Counter(r['主取数依据'] for r in records)),
basis_days=dict(basis_days),
virtual_carry_records=sum(g['virtual'] for g in groups), missing_records=len(issues),
new_policy_vehicles=31, new_policy_records=sum(r['新政策']=='是' for r in records),
test_days=sum(r['测试天数'] for r in records),
fallback_gps_days=basis_days.get('测试名单无纯氢GPS', 0),
gap_days=len(gaps), gap_records=len(issues),
gap_vehicles=len({g['plate'] for g in gaps}),
excluded_hydrogen=money(sum(r['剔除纯氢里程'] for r in records))))
(SUPPORT/'august_calculation.json').write_text(json.dumps(result,ensure_ascii=False,default=str))
print(json.dumps(result['stats'],ensure_ascii=False,indent=2))
print(json.dumps(person_audit,ensure_ascii=False))
if __name__ == '__main__': main()