Files
skills/dameng-salary/compute_cross.py
T

255 lines
10 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.
#!/usr/bin/env python3
"""计算 (店, 部门, 班次) 三维交叉营收,并按比例校正到权威 部门业绩 合计。
输入: 月度账务目录(包含订单明细 xlsx + 菜品库 xlsx + 营收分析 xlsx
输出: 标准输出打印交叉矩阵 + 12 名员工的 (部门业绩 / 班次业绩 / 部门×班次业绩) 三元组
用法:
python3 compute_cross.py <月度账务目录>
# 例: python3 compute_cross.py ~/Downloads/大梦5月账务处理
依赖: openpyxl
"""
import glob
import sys
from collections import defaultdict
try:
import openpyxl
except ImportError:
sys.exit("缺少依赖: python3 -m pip install openpyxl")
# ============================================================
# 部门归口规则(来自 分析方法.md,已根据实际菜品库一级分类核对)
# ============================================================
def categorize(primary, secondary, store):
p = (primary or "").lower().strip()
s = (secondary or "").strip()
if store == "西湖":
if p in ["小吃", "主食", "brunch", "零食"]:
return "厨房"
if p in ["咖啡", "甜品", "茶饮tea"]:
return "咖啡"
if p == "软饮":
return "咖啡" if s in ["可尔必思", "海盐荔枝"] else "调酒"
if p in ["精酿", "精酿 老菜单"]:
return "精酿"
if p in ["鸡尾酒", "纯饮"]:
return "调酒"
elif store == "滨江":
if p in ["肉肉肉", "小吃", "主食", "brunch"]:
return "厨房"
if p in ["咖啡", "甜品点心", "茶饮tea"]:
return "咖啡"
if p == "无咖无醇":
return "调酒" if s == "无醇鸡尾酒" else "咖啡"
if p in ["精酿", "瓶罐精酿", "精酿 老菜单(已废弃)"]:
return "精酿"
if p in ["鸡尾酒", "纯饮酒"]:
return "调酒"
return None
# ============================================================
# 计算每店 (餐段, 部门) 营收
# ============================================================
def load_store(base_dir, store_cn):
"""计算指定店 (餐段, 部门) → 顾客实付 (POS 菜品收入) 矩阵"""
print(f"\n===== {store_cn}店 =====", file=sys.stderr)
# 1) 订单 → 餐段
order_files = glob.glob(f"{base_dir}/大梦可能实验室({store_cn}店)_全渠道订单明细_*.xlsx")
if not order_files:
sys.exit(f"找不到 {store_cn}店 全渠道订单明细 xlsx")
wb1 = openpyxl.load_workbook(order_files[0], data_only=True)
ws1 = wb1.active
order_shift = {}
header_row = None
for i, r in enumerate(ws1.iter_rows(values_only=True), start=1):
if r and r[0] == "营业日期":
header_row = i
cols = list(r)
col_segment = cols.index("餐段")
col_order = cols.index("订单号")
continue
if header_row and i > header_row and r[col_order]:
order_shift[str(r[col_order])] = str(r[col_segment]) if r[col_segment] else None
print(f" Loaded {len(order_shift)} 订单", file=sys.stderr)
# 2) 菜品名 → 一级/二级分类
menu_files = glob.glob(f"{base_dir}/大梦_可能实验室_{store_cn}店_菜品库_*.xlsx")
if not menu_files:
sys.exit(f"找不到 {store_cn}店 菜品库 xlsx")
wb2 = openpyxl.load_workbook(menu_files[0], data_only=True)
ws2 = wb2["菜品"]
name_to_cat = {}
header_row2 = None
for i, r in enumerate(ws2.iter_rows(values_only=True), start=1):
if r and r[0] == "菜品编码(SPUID":
header_row2 = i
cols = list(r)
ci_name = cols.index("菜品名称")
ci_cat = cols.index("基础分类")
continue
if header_row2 and i > header_row2 and r[ci_name] and r[ci_cat]:
parts = str(r[ci_cat]).split("/")
primary = parts[0]
secondary = parts[1] if len(parts) > 1 else ""
name_to_cat[str(r[ci_name]).strip()] = (primary, secondary)
print(f" Loaded {len(name_to_cat)} 菜品", file=sys.stderr)
# 3) 菜品明细 → 聚合
detail_files = glob.glob(f"{base_dir}/大梦_可能实验室_{store_cn}店__店内订单明细*.xlsx")
if not detail_files:
sys.exit(f"找不到 {store_cn}店 店内订单明细 xlsx")
wb3 = openpyxl.load_workbook(detail_files[0], data_only=True)
ws3 = wb3["菜品明细"]
bucket = defaultdict(float) # (shift, dept) -> revenue
unmatched_count = 0
header_row3 = None
for i, r in enumerate(ws3.iter_rows(values_only=True), start=1):
if r and r[0] == "订单编号":
header_row3 = i
cols = list(r)
ci_order = cols.index("订单编号")
ci_revenue = cols.index("菜品收入(元)")
ci_name = cols.index("菜品名称")
continue
if header_row3 and i > header_row3:
order = str(r[ci_order]) if r[ci_order] else None
name = str(r[ci_name]).strip() if r[ci_name] else None
revenue = r[ci_revenue]
if not order or revenue is None or not name:
continue
shift = order_shift.get(order)
cat = name_to_cat.get(name)
if not shift or not cat:
unmatched_count += 1
continue
dept = categorize(cat[0], cat[1], store_cn)
if not dept:
unmatched_count += 1
continue
bucket[(shift, dept)] += float(revenue)
print(f" unmatched: {unmatched_count}", file=sys.stderr)
return bucket
# ============================================================
# 读取权威 部门业绩 / 班次业绩(从月度营收分析 xlsx)
# ============================================================
def read_official(base_dir):
rev_files = glob.glob(f"{base_dir}/大梦可能实验室_*月营收分析_西湖店vs滨江店.xlsx")
if not rev_files:
sys.exit("找不到月度营收分析 xlsx")
wb = openpyxl.load_workbook(rev_files[0], data_only=True)
dept = {"西湖店": {}, "滨江店": {}}
ws = wb["部门收入"]
in_section = False
for r in ws.iter_rows(values_only=True):
if r and r[0] and "部门 |" in str(r[0]) or (r and r[0] == "部门"):
in_section = True
continue
if in_section and r and r[0]:
name = str(r[0]).strip()
if name in ["厨房", "咖啡", "精酿", "调酒"]:
# cols: 部门, 滨江_POS, 滨江_团购, 滨江_合计, 西湖_POS, 西湖_团购, 西湖_合计
dept["滨江店"][name] = float(r[3]) if r[3] else 0
dept["西湖店"][name] = float(r[6]) if r[6] else 0
else:
if "部门收入小计" in name:
break
shift = {"西湖店": {}, "滨江店": {}}
ws = wb["班次营收"]
for r in ws.iter_rows(values_only=True):
if r and r[0] in ["西湖店", "滨江店"] and r[1] in ["白班", "晚班"]:
# cols: 门店, 餐段, 订单数, 订单金额, 顾客实付
shift[str(r[0])][str(r[1])] = float(r[4]) if r[4] else 0
return dept, shift
# ============================================================
# 12 员工 (店, 部门, 班次) 配置(行号约定)
# ============================================================
EMPLOYEES = [
(14, "蔡逸丰", "西湖店", "精酿", "晚班"),
(15, "何简", "西湖店", "厨房", "白班"),
(16, "宋群喜", "西湖店", "咖啡", "白班"),
(17, "胡舒", "西湖店", "调酒", "晚班"),
(18, "郭思儒", "西湖店", "咖啡", "白班"),
(19, "秦天", "西湖店", "厨房", "晚班"),
(20, "李想", "滨江店", "调酒", "晚班"),
(21, "王瑛胤", "滨江店", "咖啡", "白班"),
(22, "刘润祥", "滨江店", "厨房", "晚班"), # 不算班次
(23, "朱秋风", "滨江店", "精酿", "晚班"),
(24, "叶磊", "滨江店", "厨房", "白班"), # 不算班次
(25, "尹志艳", "滨江店", "厨房", "中班"), # 不算班次/中班无班次业绩
]
# 滨江厨房团队:只算 部门业绩,班次 + 交叉 都为 0
BINJIANG_KITCHEN = {"刘润祥", "叶磊", "尹志艳"}
def main():
if len(sys.argv) < 2:
sys.exit("用法: python3 compute_cross.py <月度账务目录>")
base = sys.argv[1].rstrip("/")
# 计算 raw cross-tab
raw_xihu = load_store(base, "西湖")
raw_binjiang = load_store(base, "滨江")
raw = {"西湖店": raw_xihu, "滨江店": raw_binjiang}
# 读权威 部门 / 班次
dept_official, shift_official = read_official(base)
# 按部门比例校正:scale factor = 权威总 / raw 部门小计
scaled = {}
for store in ["西湖店", "滨江店"]:
for d in ["厨房", "咖啡", "精酿", "调酒"]:
raw_dept_sum = sum(raw[store].get((sh, d), 0) for sh in ["白班", "晚班"])
if raw_dept_sum > 0 and d in dept_official[store]:
factor = dept_official[store][d] / raw_dept_sum
for sh in ["白班", "晚班"]:
scaled[(store, d, sh)] = round(raw[store].get((sh, d), 0) * factor)
# 打印交叉表
print("\n=== 校正后 部门×班次(用作 V2 col 21 部门x班次业绩)===")
for store in ["西湖店", "滨江店"]:
print(f"\n{store}:")
print(f" {'部门':<6}{'白班':>10}{'晚班':>10}")
for d in ["厨房", "咖啡", "精酿", "调酒"]:
wb = scaled.get((store, d, "白班"), 0)
nb = scaled.get((store, d, "晚班"), 0)
print(f" {d:<6}{wb:>10}{nb:>10}")
# 输出 12 员工三元组
print("\n=== 12 员工 (部门业绩 / 班次业绩 / 部门×班次业绩) ===")
print(f"{'行':>3} {'姓名':<6} {'店':<5} {'部门':<5} {'班次':<5} {'部门业绩':>10} {'班次业绩':>10} {'部门×班次':>10}")
for row, name, store, dept, shift in EMPLOYEES:
is_dept = dept in dept_official[store]
dr = dept_official[store].get(dept, 0)
sr = shift_official[store].get(shift, 0)
cr = scaled.get((store, dept, shift), 0)
# 特殊规则:滨江厨房团队不算班次
if name in BINJIANG_KITCHEN:
sr = 0
cr = 0
# 中班无班次业绩
if shift == "中班":
sr = 0
cr = 0
# 前厅、空部门
if not is_dept:
dr = 0
cr = 0
print(f"{row:>3} {name:<6} {store:<5} {dept:<5} {shift:<5} {dr:>10.2f} {sr:>10.2f} {cr:>10}")
if __name__ == "__main__":
main()