初始发布: 21 个 skills (Claude Code / Codex / DSH)
This commit is contained in:
@@ -0,0 +1,533 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""大梦 by 可能实验室 — 月度营收分析引擎(按月参数化)
|
||||
|
||||
用法: python3 build_analysis.py <月度账务目录> [YYYY-MM] # 默认 2026-05
|
||||
生成 大梦可能实验室_{N}月营收分析_西湖店vs滨江店.xlsx(13 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()
|
||||
Reference in New Issue
Block a user