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.
This commit is contained in:
@@ -0,0 +1,516 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user