Files

534 lines
29 KiB
Python
Raw Permalink 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
# -*- coding: utf-8 -*-
"""大梦 by 可能实验室 — 月度营收分析引擎(按月参数化)
用法: python3 build_analysis.py <月度账务目录> [YYYY-MM] # 默认 2026-05
生成 大梦可能实验室_{N}月营收分析_西湖店vs滨江店.xlsx13 sheets+ _analysis_summary.json
并打印工资所需的部门/班次/部门×班次业绩三元组。
⚠️ 跑本脚本前先跑 make_menu_lib.py 生成当月菜品库,否则新上 SKU 会落到关键词兜底。
数据源(同目录):
POS 店内订单明细 (菜品明细 / 订单明细 / 优惠明细 sheets)
全渠道订单明细 (含 餐段)
菜品库 (SKU→基础分类)
团购收益(美团大梦) + 新版收益(点评/可能实验室)
"""
import openpyxl, warnings, glob, sys, re
from collections import defaultdict
warnings.filterwarnings('ignore')
import sys as _sys, calendar as _cal
# 用法: python3 build_analysis.py <月度账务目录> [YYYY-MM]
BASE = (_sys.argv[1] if len(_sys.argv) > 1 else ".").rstrip("/")
MONTH = _sys.argv[2] if len(_sys.argv) > 2 else "2026-05"
_y,_m = int(MONTH[:4]), int(MONTH[5:7])
DAYS = _cal.monthrange(_y,_m)[1]
# ===================== 部门归口规则(菜品一级分类)=====================
def dept_of_category(primary, secondary, store):
p = (primary or "").strip()
pl = p.lower()
s = (secondary or "").strip()
if store == "西湖":
if p in ["小吃","主食","零食"] or pl.startswith("brunch"):
return "厨房"
if p in ["咖啡","甜品"] or p in ["茶饮Tea","茶饮tea"]:
return "咖啡"
if p == "软饮":
return "咖啡" if any(k in s for k in ["可尔必思","海盐荔枝"]) else "调酒"
if p.startswith("精酿"):
return "精酿"
if p in ["鸡尾酒","纯饮","纯饮酒"]:
return "调酒"
else: # 滨江
if p in ["肉肉肉","小吃","主食","零食"] or pl.startswith("brunch"):
return "厨房"
if p in ["咖啡","甜品点心"] or p in ["茶饮Tea","茶饮tea"]:
return "咖啡"
if p == "无咖无醇":
return "调酒" if "无醇鸡尾酒" in s else "咖啡"
if p.startswith("精酿") or p in ["瓶罐精酿","瓶装精酿"]:
return "精酿"
if p in ["鸡尾酒","纯饮酒","纯饮"]:
return "调酒"
return None # 其他/团购套餐/特惠套餐/加料 → 需 SKU 级处理或跳过
# 周边/服务(非部门,单列)
def is_peripheral(name):
n=str(name)
return any(k in n for k in ["扑克","点歌","雨伞","毛毯","游戏卡牌","桌游","充电","寄存"])
# POS 端团购套餐壳(收入计平台侧,POS 侧多为 0,跳过避免重复)
def is_teamgou_shell(name):
return "美团团购" in str(name) or "团购套餐" in str(name) or "打卡套餐" in str(name)
# 「其他」类 SKU 按菜品名关键词推断部门(复刻 4 月 SKU重归类思路)
# 注意:酒类关键词优先(避免"果酒优格奶昔"等被咖啡词误捕)
def dept_by_name(name, store):
n = str(name)
# —— 精酿(啤酒/西打/果酒/气泡酒/品牌)优先 ——
# 酒头/畅饮票=扎啤生啤(精酿),与菜品库同类SKU一致
if any(k in n for k in ["IPA","Lager","Stout","Ale","拉格","精酿","世涛","酸啤","古斯","Gose","西打","啤酒","札幌","健力士","三宝乐","制乐场","制乐厂","沙坡尾","气泡实验室","做梦去吧","滇麻","果酒","气泡酒","酒头","畅饮"]):
return "精酿"
# —— 调酒(烈酒/鸡尾酒/特调)——
if any(k in n for k in ["特调","鸡尾酒","威士忌","金酒","朗姆","龙舌兰","伏特加","僵尸","Zombie","Negroni","内格罗尼","Margarita","玛格丽特","Old Fashion","古典","SHOT","纯饮","清酒","葡萄酒","红酒","白葡萄","金刚芭比","混四喜"]):
return "调酒"
# —— 咖啡/茶/无醇饮品 ——
if any(k in n for k in ["美式","拿铁","咖啡","冷萃","澳白","Dirty","卡布","摩卡","瑰夏","耶加","曼特宁","葡萄成熟","优格","冰淇淋","奶昔","波旁","庄园"]):
return "咖啡"
if any(k in n for k in ["茶","龙井","乌龙","普洱","大梦冰茶","果茶"]):
return "咖啡"
# —— 厨房 ——
if any(k in n for k in ["拼盘","小食","沙拉","Tacos","吐司","蛋","焗饭","意面","薯","鸡","牛肉","猪","披萨","brunch","早餐","三明治","汉堡","面包","可颂","煮蛋"]):
return "厨房"
return None
# ===================== 团购项目 → 部门 =====================
def teamgou_dept_split(name):
"""返回 {dept: ratio} 或 None(=代金券跳过)"""
n = str(name)
if "代金券" in n:
return None # 跳过,POS 已计入
# 拆分套餐(先匹配,避免被"厨房/咖啡"单部门误判)
if "单人轻食" in n or "温馨时光" in n:
return {"厨房":0.6, "咖啡":0.4}
if "营养满溢" in n or ("南瓜沙拉" in n and "Tacos" in n):
return {"厨房":0.7, "咖啡":0.3}
if "豪华烤肉拼盘" in n:
return {"厨房":0.7, "调酒":0.3}
if "香菜" in n and ("葡萄酒" in n or "Tacos" in n):
return {"厨房":0.5, "调酒":0.5}
# 单部门
if any(k in n for k in ["咖啡任选","经典咖啡","美式","下午茶","白日梦"]):
return {"咖啡":1.0}
if "精酿" in n or "盲盒" in n:
return {"精酿":1.0}
if any(k in n for k in ["鸡尾酒","SHOT","小酌","HappyHour"]):
# HappyHour 精酿 已被上面拦截;这里是鸡尾酒/SHOT
if "精酿" in n: return {"精酿":1.0}
return {"调酒":1.0}
return {"未拆分":1.0}
# ===================== 读取工具 =====================
def find_header(ws, key):
for i, r in enumerate(ws.iter_rows(values_only=True), start=1):
if r and any(c == key for c in r if c is not None):
return i, [str(c).strip() if c is not None else '' for c in r]
return None, None
def g(path_glob):
fs = glob.glob(f"{BASE}/{path_glob}")
if not fs: sys.exit(f"缺少文件: {path_glob}")
return fs[0]
STORES = {"西湖":"西湖", "滨江":"滨江"}
# ===================== 1) 菜品库: SKU/名称 → 分类 =====================
def load_menu(store):
wb = openpyxl.load_workbook(g(f"大梦_可能实验室_{store}店_菜品库_*.xlsx"), data_only=True)
ws = wb["菜品"]; hr,hdr = find_header(ws,"菜品编码(SPUID")
ci_name=hdr.index("菜品名称"); ci_cat=hdr.index("基础分类")
name2cat={}
for i,r in enumerate(ws.iter_rows(values_only=True),start=1):
if i<=hr: continue
if r[ci_name] and r[ci_cat]:
parts=str(r[ci_cat]).split("/")
name2cat[str(r[ci_name]).strip()]=(parts[0], parts[1] if len(parts)>1 else "")
return name2cat
# ===================== 2) 全渠道: 订单号 → 餐段 =====================
def load_shift(store):
wb=openpyxl.load_workbook(g(f"大梦可能实验室({store}店)_全渠道订单明细_*.xlsx"),data_only=True)
ws=wb.active; hr,hdr=find_header(ws,"营业日期")
ci_o=hdr.index("订单号"); ci_s=hdr.index("餐段")
ci_pay=hdr.index("顾客实付"); ci_amt=hdr.index("订单金额")
o2shift={}; shift_rows=[]
for i,r in enumerate(ws.iter_rows(values_only=True),start=1):
if i<=hr: continue
if r[ci_o]:
sh=str(r[ci_s]) if r[ci_s] else None
o2shift[str(r[ci_o])]=sh
shift_rows.append((sh, float(r[ci_pay] or 0), float(r[ci_amt] or 0)))
return o2shift, shift_rows
# ===================== 3) POS 订单明细 (顾客应付/服务费/优惠/每日) =====================
def load_orders(store):
wb=openpyxl.load_workbook(g(f"大梦_可能实验室_{store}店__店内订单明细*.xlsx"),data_only=True)
ws=wb["订单明细"]; hr,hdr=find_header(ws,"营业日期")
idx={k:hdr.index(k) for k in ["营业日期","订单号","订单金额(元)","顾客应付(元)","订单优惠(元)","菜品收入(元)","服务费收入(元)"]}
orders=[]
for i,r in enumerate(ws.iter_rows(values_only=True),start=1):
if i<=hr: continue
no=r[idx["订单号"]]
if not no or str(no).strip()=="--": continue # 跳过幽灵汇总行
if str(r[idx["营业日期"]]).strip()=="--": continue
orders.append({
"date":str(r[idx["营业日期"]]),
"order":str(r[idx["订单号"]]),
"amount":float(r[idx["订单金额(元)"]] or 0),
"payable":float(r[idx["顾客应付(元)"]] or 0),
"discount":float(r[idx["订单优惠(元)"]] or 0),
"dish_rev":float(r[idx["菜品收入(元)"]] or 0),
"service":float(r[idx["服务费收入(元)"]] or 0),
})
# 优惠明细(赠菜)
ws2=wb["优惠明细"]; hr2,hdr2=find_header(ws2,"营业日期")
gifts=[]
if hr2:
gi={k:(hdr2.index(k) if k in hdr2 else None) for k in ["营业日期","折扣优惠类型","折扣优惠名称","金额(¥)","折扣金额(元)","优惠金额(元)"]}
amt_col = gi["金额(¥)"] or gi["折扣金额(元)"] or gi["优惠金额(元)"]
for i,r in enumerate(ws2.iter_rows(values_only=True),start=1):
if i<=hr2: continue
typ=str(r[gi["折扣优惠类型"]]) if gi["折扣优惠类型"] is not None and r[gi["折扣优惠类型"]] else ""
if "赠菜" in typ:
gifts.append({"date":str(r[gi["营业日期"]]), "amt":float(r[amt_col] or 0) if amt_col is not None else 0})
# 支付方式分布
ws3=wb["支付明细"]; hr3,hdr3=find_header(ws3,"支付方式") if find_header(ws3,"支付方式")[0] else (None,None)
pays=defaultdict(lambda:[0,0.0])
if hr3:
pi_way=hdr3.index("支付方式")
pi_amt=None
for cand in ["支付金额(元)","实收金额(元)","金额(元)","支付金额(¥)"]:
if cand in hdr3: pi_amt=hdr3.index(cand); break
for i,r in enumerate(ws3.iter_rows(values_only=True),start=1):
if i<=hr3: continue
if r[pi_way]:
pays[str(r[pi_way])][0]+=1
pays[str(r[pi_way])][1]+=float(r[pi_amt] or 0) if pi_amt is not None else 0
return orders, gifts, dict(pays)
# ===================== 4) POS 菜品明细 → 部门/分类/班次 =====================
def load_dishes(store, name2cat, o2shift):
wb=openpyxl.load_workbook(g(f"大梦_可能实验室_{store}店__店内订单明细*.xlsx"),data_only=True)
ws=wb["菜品明细"]; hr,hdr=find_header(ws,"订单编号")
ci_o=hdr.index("订单编号"); ci_name=hdr.index("菜品名称")
ci_qty=hdr.index("销售数量"); ci_amt=hdr.index("金额合计(元)")
ci_disc=hdr.index("菜品优惠(元)"); ci_rev=hdr.index("菜品收入(元)")
dept_pos=defaultdict(float) # dept -> 菜品收入
cross=defaultdict(float) # (dept,shift) -> 菜品收入
cat1=defaultdict(lambda:[0,0.0,0.0,0.0]) # primary -> [qty, 原价, 优惠, 收入]
cat2=defaultdict(lambda:[set(),0,0.0,0.0,0.0]) # basecat -> [skus,qty,原价,优惠,收入]
other_skus=defaultdict(lambda:[0,0.0]) # name -> [qty, rev] (其他/未归类)
unmatched=0
for i,r in enumerate(ws.iter_rows(values_only=True),start=1):
if i<=hr: continue
name=str(r[ci_name]).strip() if r[ci_name] else None
if not name: continue
rev=float(r[ci_rev] or 0); amt=float(r[ci_amt] or 0)
disc=float(r[ci_disc] or 0); qty=float(r[ci_qty] or 0)
cat=name2cat.get(name)
primary = cat[0] if cat else "(无菜品库)"
secondary = cat[1] if cat else ""
basecat = f"{primary}/{secondary}" if secondary else primary
cat1[primary][0]+=qty; cat1[primary][1]+=amt; cat1[primary][2]+=disc; cat1[primary][3]+=rev
cat2[basecat][0].add(name); cat2[basecat][1]+=qty; cat2[basecat][2]+=amt; cat2[basecat][3]+=disc; cat2[basecat][4]+=rev
if is_teamgou_shell(name) or is_peripheral(name):
other_skus[name][0]+=qty; other_skus[name][1]+=rev
continue
dept = dept_of_category(primary, secondary, store)
if dept is None:
dept = dept_by_name(name, store)
if dept is None:
other_skus[name][0]+=qty; other_skus[name][1]+=rev
unmatched+=1
continue
dept_pos[dept]+=rev
sh=o2shift.get(str(r[ci_o]))
if sh in ("白班","晚班"):
cross[(dept,sh)]+=rev
return dept_pos, cross, cat1, cat2, other_skus, unmatched
# ===================== 5) 团购 → 部门 =====================
def load_teamgou():
# 美团大梦
rows=[] # (store, brand, name, cnt, price, settle)
wb=openpyxl.load_workbook(g("42323734_团购收益明细_*.xlsx"),data_only=True)
ws=wb["收益明细表"]; data=list(ws.iter_rows(values_only=True))
hdr=[str(c).strip() if c else '' for c in data[1]]
ci_store=hdr.index("消费门店"); ci_pkg=hdr.index("套餐名")
ci_total=hdr.index("总收入(元)")
ci_settle=hdr.index("结算价(总收入-美团点评技术服务费-商家营销费用-消费后退-其他调整)(元)")
ag=defaultdict(lambda:[0,0.0,0.0])
for r in data[2:]:
if not r[ci_pkg]: continue
store="西湖" if "西湖" in str(r[ci_store]) else "滨江"
k=(store,"大梦",str(r[ci_pkg]))
ag[k][0]+=1; ag[k][1]+=float(r[ci_total] or 0); ag[k][2]+=float(r[ci_settle] or 0)
# 点评可能实验室
wb2=openpyxl.load_workbook(g("新版收益明细_*团购_*.xlsx"),data_only=True)
ws2=wb2["收益明细"]; data2=list(ws2.iter_rows(values_only=True))
hdr2=[str(c).strip() if c else '' for c in data2[0]]
ci_store2=hdr2.index("美团门店名称"); ci_pkg2=hdr2.index("项目名称")
ci_price2=hdr2.index("售价(美团售价)"); ci_merch2=hdr2.index("商家应得")
for r in data2[1:]:
if not r[ci_pkg2]: continue
store="西湖" if "西湖" in str(r[ci_store2]) else "滨江"
k=(store,"可能实验室",str(r[ci_pkg2]))
ag[k][0]+=1; ag[k][1]+=float(r[ci_price2] or 0); ag[k][2]+=float(r[ci_merch2] or 0)
# 拆部门(用结算/商家应得)
dept_tg=defaultdict(lambda: defaultdict(float)) # store -> dept -> settle
voucher=defaultdict(lambda:[0,0.0,0.0]) # store -> [cnt,price,settle]
detail=[]
for (store,brand,name),(c,p,s) in sorted(ag.items()):
split=teamgou_dept_split(name)
if split is None:
voucher[store][0]+=c; voucher[store][1]+=p; voucher[store][2]+=s
detail.append((brand,store,name,c,p,s,"⚠️代金券跳过","—"))
continue
rule="; ".join(f"{d} {int(r*100)}%" for d,r in split.items())
for d,ratio in split.items():
dept_tg[store][d]+=s*ratio
detail.append((brand,store,name,c,p,s,rule,rule))
return dept_tg, voucher, detail
# ===================== 主流程 =====================
def main():
result={}
menus={st:load_menu(st) for st in STORES}
shifts={st:load_shift(st) for st in STORES}
dept_pos={}; cross={}; cat1={}; cat2={}; others={}; unmatched={}
orders={}; gifts={}; pays={}
for st in STORES:
o2shift=shifts[st][0]
dept_pos[st],cross[st],cat1[st],cat2[st],others[st],unmatched[st]=load_dishes(st,menus[st],o2shift)
orders[st],gifts[st],pays[st]=load_orders(st)
# 每日营收
daily={}
for st in STORES:
dd=defaultdict(lambda:[0,0.0,0.0,0.0]) # date->[orders,amount,payable,discount]
for o in orders[st]:
d=o["date"]
dd[d][0]+=1; dd[d][1]+=o["amount"]; dd[d][2]+=o["payable"]; dd[d][3]+=o["discount"]
gd=defaultdict(lambda:[0,0.0])
for gft in gifts[st]:
gd[gft["date"]][0]+=1; gd[gft["date"]][1]+=gft["amt"]
daily[st]={d:(dd[d],gd.get(d,[0,0.0])) for d in sorted(dd)}
dept_tg, voucher, tg_detail = load_teamgou()
# ---- 部门收入合计 ----
DEPTS=["厨房","咖啡","精酿","调酒"]
print("="*72)
print(f" 大梦 {_m} 月营收分析 — 部门收入(POS菜品 + 团购套餐结算)")
print("="*72)
print(f" {'部门':<6}{'滨江POS':>11}{'滨江团购':>10}{'滨江合计':>11}{'西湖POS':>11}{'西湖团购':>10}{'西湖合计':>11}")
dept_total={}
for d in DEPTS:
bp=dept_pos["滨江"].get(d,0); bt=dept_tg["滨江"].get(d,0); bc=bp+bt
xp=dept_pos["西湖"].get(d,0); xt=dept_tg["西湖"].get(d,0); xc=xp+xt
dept_total[d]={"滨江":bc,"西湖":xc}
print(f" {d:<6}{bp:>11.2f}{bt:>10.2f}{bc:>11.2f}{xp:>11.2f}{xt:>10.2f}{xc:>11.2f}")
# ---- 班次营收 ----
shift_sum=defaultdict(lambda:[0,0.0,0.0]) # (store,shift)->[orders, amount, payable]
for st in STORES:
for sh,pay,amt in shifts[st][1]:
if sh in ("白班","晚班"):
shift_sum[(st,sh)][0]+=1
shift_sum[(st,sh)][1]+=amt
shift_sum[(st,sh)][2]+=pay
print("\n 班次营收(顾客实付):")
for st in STORES:
for sh in ["白班","晚班"]:
o,a,p=shift_sum[(st,sh)]
print(f" {st}{sh}: 订单{o:>5} 实付{p:>11.2f} 客单{p/o if o else 0:>7.2f}")
# ---- 部门×班次(按部门合计比例校正)----
print("\n 部门×班次(校正到部门合计):")
cross_scaled={}
for st in STORES:
for d in DEPTS:
raw_sum=sum(cross[st].get((d,sh),0) for sh in ["白班","晚班"])
target=dept_total[d][st]
for sh in ["白班","晚班"]:
raw=cross[st].get((d,sh),0)
cross_scaled[(st,d,sh)]= round(raw/raw_sum*target) if raw_sum>0 else 0
for st in STORES:
line=f" {st}: "+" ".join(f"{d}(白{cross_scaled[(st,d,'白班')]}/晚{cross_scaled[(st,d,'晚班')]})" for d in DEPTS)
print(line)
# ---- 12 员工 三元组 ----
EMP=[
("蔡逸丰","西湖","精酿","晚班"),("何简","西湖","厨房","白班"),("宋群喜","西湖","咖啡","白班"),
("胡舒","西湖","调酒","晚班"),("郭思儒","西湖","咖啡","白班"),("秦天","西湖","厨房","晚班"),
("李想","滨江","调酒","晚班"),("王瑛胤","滨江","咖啡","白班"),("刘润祥","滨江","厨房","晚班"),
("朱秋风","滨江","精酿","晚班"),("叶磊","滨江","厨房","白班"),("尹志艳","滨江","厨房","中班"),
]
BJ_KITCHEN={"刘润祥","叶磊","尹志艳"}
print("\n"+"="*72)
print(" 12 员工 业绩三元组 (部门业绩 / 班次业绩 / 部门×班次业绩)")
print("="*72)
emp_out=[]
for name,st,dept,sh in EMP:
dr=dept_total.get(dept,{}).get(st,0)
sr=shift_sum.get((st,sh),[0,0,0])[2] if sh in ("白班","晚班") else 0
cr=cross_scaled.get((st,dept,sh),0)
if name in BJ_KITCHEN: sr=0; cr=0
if sh=="中班": sr=0; cr=0
emp_out.append((name,st,dept,sh,dr,sr,cr))
print(f" {name:<6}{st}{dept}/{sh:<3} 部门{dr:>11.2f} 班次{sr:>11.2f} 部门×班次{cr:>9}")
# 存盘供 workflow 校验 / 写表
import json
summary={
"month":MONTH,
"dept_pos":{st:dict(dept_pos[st]) for st in STORES},
"dept_tg":{st:dict(dept_tg[st]) for st in STORES},
"dept_total":dept_total,
"shift_sum":{f"{st}|{sh}":shift_sum[(st,sh)] for st in STORES for sh in ["白班","晚班"]},
"cross_scaled":{f"{st}|{d}|{sh}":cross_scaled[(st,d,sh)] for st in STORES for d in DEPTS for sh in ["白班","晚班"]},
"voucher":{st:voucher[st] for st in STORES},
"unmatched":unmatched,
"others":{st:dict(others[st]) for st in STORES},
"employees":emp_out,
"orders_count":{st:len(orders[st]) for st in STORES},
"pos_payable":{st:round(sum(o["payable"] for o in orders[st]),2) for st in STORES},
"pos_dishrev":{st:round(sum(o["dish_rev"] for o in orders[st]),2) for st in STORES},
}
with open(f"{BASE}/_analysis_summary.json","w") as f:
json.dump(summary,f,ensure_ascii=False,indent=2,default=str)
print(f"\n 未归类菜品笔数: 西湖={unmatched['西湖']} 滨江={unmatched['滨江']}")
print(f" POS订单数: 西湖={len(orders['西湖'])} 滨江={len(orders['滨江'])}")
write_xlsx(summary, dept_total, dept_pos, dept_tg, shift_sum, cross_scaled,
cat1, cat2, daily, pays, voucher, tg_detail, DEPTS, EMP, BJ_KITCHEN)
print(f"\n 汇总已存: {BASE}/_analysis_summary.json")
print(f" 分析表已存: {BASE}/大梦可能实验室_{_m}月营收分析_西湖店vs滨江店.xlsx")
return summary
def write_xlsx(s, dept_total, dept_pos, dept_tg, shift_sum, cross_scaled,
cat1, cat2, daily, pays, voucher, tg_detail, DEPTS, EMP, BJ_KITCHEN):
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
wb=Workbook(); wb.remove(wb.active)
H=Font(bold=True); TITLE=Font(bold=True,size=13)
GREEN=PatternFill("solid",fgColor="E2EFDA"); BLUE=PatternFill("solid",fgColor="DDEBF7")
HEADER=PatternFill("solid",fgColor="44546A"); HW=Font(bold=True,color="FFFFFF")
def sheet(name): return wb.create_sheet(name)
def hdr(ws,row,cols,fill=True):
for j,c in enumerate(cols,1):
cell=ws.cell(row=row,column=j,value=c)
if fill: cell.fill=HEADER; cell.font=HW
# —— 总览 ——
ws=sheet("总览"); ws["A1"]=f"大梦·可能实验室 — {_y}{_m}月营收总览(西湖店 vs 滨江店)"; ws["A1"].font=TITLE
ws["A2"]=f"区间 {_y}/{_m:02d}/01{_m:02d}/{DAYS} | 数据源:POS店内订单明细 + 美团团购收益 + 点评/可能实验室收益"
r=4; ws.cell(r,1,"指标").font=H; ws.cell(r,2,"滨江店").font=H; ws.cell(r,3,"西湖店").font=H; ws.cell(r,4,"两店合计").font=H
bj_dep=sum(dept_total[d]["滨江"] for d in DEPTS); xh_dep=sum(dept_total[d]["西湖"] for d in DEPTS)
rows=[
("4部门收入合计(POS+团购)", bj_dep, xh_dep),
("POS菜品收入", sum(dept_pos["滨江"].values()), sum(dept_pos["西湖"].values())),
("团购套餐结算", sum(dept_tg["滨江"].values()), sum(dept_tg["西湖"].values())),
("白班实付", shift_sum[("滨江","白班")][2], shift_sum[("西湖","白班")][2]),
("晚班实付", shift_sum[("滨江","晚班")][2], shift_sum[("西湖","晚班")][2]),
("POS订单数", s["orders_count"]["滨江"], s["orders_count"]["西湖"]),
]
for i,(k,b,x) in enumerate(rows):
rr=r+1+i; ws.cell(rr,1,k); ws.cell(rr,2,round(b,2)); ws.cell(rr,3,round(x,2)); ws.cell(rr,4,round(b+x,2))
# —— 部门收入 ——
ws=sheet("部门收入"); ws["A1"]="负责部门收入 — POS菜品收入 + 团购套餐结算(代金券不重算)"; ws["A1"].font=TITLE
hdr(ws,3,["部门","滨江_POS","滨江_团购","滨江_合计","西湖_POS","西湖_团购","西湖_合计","两店合计"])
for i,d in enumerate(DEPTS):
rr=4+i; bp=dept_pos["滨江"].get(d,0); bt=dept_tg["滨江"].get(d,0); xp=dept_pos["西湖"].get(d,0); xt=dept_tg["西湖"].get(d,0)
for j,v in enumerate([d,round(bp,2),round(bt,2),round(bp+bt,2),round(xp,2),round(xt,2),round(xp+xt,2),round(bp+bt+xp+xt,2)],1):
ws.cell(rr,j,v)
tot_r=4+len(DEPTS)
ws.cell(tot_r,1,"合计").font=H
for j,col in enumerate(["滨江_POS","滨江_团购","滨江_合计","西湖_POS","西湖_团购","西湖_合计","两店合计"],2):
ws.cell(tot_r,j,round(sum(ws.cell(4+i,j).value for i in range(len(DEPTS))),2)).font=H
# 占比块
ws.cell(tot_r+2,1,"② 4部门占比").font=H
hdr(ws,tot_r+3,["部门","滨江合计","西湖合计","两店合计","滨江占比","西湖占比"])
for i,d in enumerate(sorted(DEPTS,key=lambda x:-(dept_total[x]['滨江']+dept_total[x]['西湖']))):
rr=tot_r+4+i; bc=dept_total[d]["滨江"]; xc=dept_total[d]["西湖"]
ws.cell(rr,1,d); ws.cell(rr,2,round(bc,2)); ws.cell(rr,3,round(xc,2)); ws.cell(rr,4,round(bc+xc,2))
ws.cell(rr,5,round(bc/bj_dep,4)); ws.cell(rr,6,round(xc/xh_dep,4))
# —— 团购→部门归口 ——
ws=sheet("团购→部门归口"); ws["A1"]="团购平台项目 → 部门归口明细(结算/商家应得口径)"; ws["A1"].font=TITLE
hdr(ws,3,["品牌","门店","项目名称","笔数","售价","结算/应得","归口规则"])
for i,(brand,store,name,c,p,sv,rule,_) in enumerate(tg_detail):
rr=4+i
for j,v in enumerate([brand,store+"店",name,c,round(p,2),round(sv,2),rule],1): ws.cell(rr,j,v)
base=4+len(tg_detail)+1
ws.cell(base,1,"② 团购套餐→部门 汇总(剔除代金券)").font=H
hdr(ws,base+1,["门店","部门","金额"])
rr=base+2
for st in ["滨江","西湖"]:
for d in DEPTS:
v=dept_tg[st].get(d,0)
if v: ws.cell(rr,1,st+"店"); ws.cell(rr,2,d); ws.cell(rr,3,round(v,2)); rr+=1
ws.cell(rr,1,"③ 代金券(跳过,未重算)").font=H; rr+=1
hdr(ws,rr,["门店","笔数","售价","结算"]); rr+=1
for st in ["滨江","西湖"]:
v=voucher[st]; ws.cell(rr,1,st+"店"); ws.cell(rr,2,v[0]); ws.cell(rr,3,round(v[1],2)); ws.cell(rr,4,round(v[2],2)); rr+=1
# —— 班次营收 ——
ws=sheet("班次营收"); ws["A1"]="班次营收对比 — 白班 vs 晚班(顾客实付)"; ws["A1"].font=TITLE
hdr(ws,3,["门店","餐段","订单数","订单金额(原价)","顾客实付","日均订单","日均实付","客单价(实付)"])
rr=4
for st in ["滨江","西湖"]:
for sh in ["白班","晚班"]:
o,a,p=shift_sum[(st,sh)]
for j,v in enumerate([st+"店",sh,o,round(a,2),round(p,2),round(o/DAYS,1),round(p/DAYS,1),round(p/o if o else 0,2)],1): ws.cell(rr,j,v); rr+=0
rr+=1
# —— 部门×班次 ——
ws=sheet("部门x班次"); ws["A1"]="部门 × 班次 营收(校正到部门合计)"; ws["A1"].font=TITLE
hdr(ws,3,["门店","部门","白班","晚班","合计"])
rr=4
for st in ["滨江","西湖"]:
for d in DEPTS:
wv=cross_scaled[(st,d,"白班")]; nv=cross_scaled[(st,d,"晚班")]
for j,v in enumerate([st+"店",d,wv,nv,wv+nv],1): ws.cell(rr,j,v)
rr+=1
# —— 品类营收(一级) ——
ws=sheet("品类营收(一级)"); ws["A1"]="品类营收 — 一级分类 西湖 vs 滨江"; ws["A1"].font=TITLE
hdr(ws,3,["一级分类","滨江_件数","滨江_收入","西湖_件数","西湖_收入"])
allcat=sorted(set(cat1["滨江"])|set(cat1["西湖"]), key=lambda c:-(cat1['滨江'].get(c,[0,0,0,0])[3]+cat1['西湖'].get(c,[0,0,0,0])[3]))
for i,c in enumerate(allcat):
rr=4+i; b=cat1["滨江"].get(c,[0,0,0,0]); x=cat1["西湖"].get(c,[0,0,0,0])
for j,v in enumerate([c,int(b[0]),round(b[3],2),int(x[0]),round(x[3],2)],1): ws.cell(rr,j,v)
# —— 品类营收(二级) ——
ws=sheet("品类营收(二级)"); ws["A1"]="品类营收 — 二级分类(基础分类) 各店Top"; ws["A1"].font=TITLE
r0=3
for st in ["滨江","西湖"]:
ws.cell(r0,1,f"■ {st}店").font=H; r0+=1
hdr(ws,r0,["基础分类","SKU数","销售件数","菜品收入"]); r0+=1
top=sorted(cat2[st].items(), key=lambda x:-x[1][4])[:30]
for c,v in top:
ws.cell(r0,1,c); ws.cell(r0,2,len(v[0])); ws.cell(r0,3,int(v[1])); ws.cell(r0,4,round(v[4],2)); r0+=1
r0+=1
# —— 每日营收 ——
for st in ["滨江","西湖"]:
ws=sheet(f"{st}店_每日营收"); ws["A1"]=f"{st}店 — {_y}{_m}月每日营收"; ws["A1"].font=TITLE
hdr(ws,3,["日期","订单数","订单金额(原价)","顾客应付","订单优惠","赠菜笔数","赠菜金额"])
rr=4
for d,(dd,gd) in daily[st].items():
for j,v in enumerate([d,dd[0],round(dd[1],2),round(dd[2],2),round(dd[3],2),gd[0],round(gd[1],2)],1): ws.cell(rr,j,v)
rr+=1
# —— 支付方式分布 ——
ws=sheet("支付方式分布"); ws["A1"]="支付方式分布(POS端)"; ws["A1"].font=TITLE
r0=3
for st in ["滨江","西湖"]:
ws.cell(r0,1,f"■ {st}店").font=H; r0+=1
hdr(ws,r0,["支付方式","笔数","支付金额"]); r0+=1
for way,(c,amt) in sorted(pays[st].items(), key=lambda x:-x[1][1]):
ws.cell(r0,1,way); ws.cell(r0,2,c); ws.cell(r0,3,round(amt,2)); r0+=1
r0+=1
# —— 12员工业绩(工资交接)——
ws=sheet("12员工业绩"); ws["A1"]="12 员工业绩三元组(→ 工资表V2 cols 19/20/21"; ws["A1"].font=TITLE
hdr(ws,3,["姓名","归属","部门","班次","部门业绩","班次业绩","部门×班次业绩"])
for i,(name,st,dept,sh,dr,sr,cr) in enumerate(s["employees"]):
rr=4+i
for j,v in enumerate([name,st+"店",dept,sh,round(dr,2),round(sr,2),cr],1): ws.cell(rr,j,v)
fill=GREEN if st=="西湖" else BLUE
for j in range(1,8): ws.cell(rr,j).fill=fill
# —— 深度分析 / 改进建议 占位(workflow 填充)——
ws=sheet("深度分析"); ws["A1"]="深度分析 — 异常项与关键洞察(见正文)"; ws["A1"].font=TITLE
ws=sheet("改进建议"); ws["A1"]=f"基于{_m}月数据的改进建议(见正文)"; ws["A1"].font=TITLE
for ws in wb.worksheets:
ws.column_dimensions["A"].width=26
for col in "BCDEFGH": ws.column_dimensions[col].width=14
wb.save(f"{BASE}/大梦可能实验室_{_m}月营收分析_西湖店vs滨江店.xlsx")
if __name__=="__main__":
main()