初始发布: 21 个 skills (Claude Code / Codex / DSH)
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: dameng-salary
|
||||
description: 大梦 by 可能实验室 月度工资计算 + 工资单生成 + 营收/出品分析。触发关键词:「大梦工资」「可能实验室工资」「N月工资单」「月度结薪」「算工资」「出工资单」「大梦N月账务」「大梦营收分析」「出品/菜单分析」。覆盖能力:(1) 从腾讯文档「工资表V2」读写月度数据 (2) 处理本地考勤资料(图片 OCR / xlsx / 腾讯文档夜班考勤)(3) 营收交叉聚合(部门×班次)+ 从原始订单生成营收分析 (4) 套用计薪公式(基本/加班/绩效/节假日/提成/社保)(5) 西湖/滨江两店分色 (6) 生成精美 HTML 工资单 + 一键导出 PNG (7) 部门收入起伏根因 + 菜单在架SKU卖最差分析。
|
||||
homepage: https://docs.qq.com/sheet/DVkxTQXZTdnF2WXpV
|
||||
version: 1.1.0
|
||||
author: william
|
||||
---
|
||||
|
||||
# 大梦 by 可能实验室 · 月度工资 SKILL
|
||||
|
||||
## ✅ 触发判断
|
||||
|
||||
用户说"出 N 月工资单"、"算 N 月工资"、"大梦 N 月账务"等 → 立即按下方流程执行。
|
||||
|
||||
## 📦 关键资源
|
||||
|
||||
| 资源 | ID / 路径 |
|
||||
|---|---|
|
||||
| **工资表 V2**(写入目标) | `file_id=VLSAvSvqvYzU`, `sheet_id=BB08J2`("员工档案"工作表) |
|
||||
| 工资表 V2 链接 | https://docs.qq.com/sheet/DVkxTQXZTdnF2WXpV |
|
||||
| 夜班考勤(腾讯文档) | `file_id=IEqftKNqdqKa` |
|
||||
| 本月账务目录 | `~/Downloads/大梦N月账务处理/` |
|
||||
| 考勤本地资料 | `~/Downloads/大梦N月账务处理/考勤表/` |
|
||||
| 营收分析 xlsx | `~/Downloads/大梦N月账务处理/大梦可能实验室_N月营收分析_西湖店vs滨江店.xlsx` |
|
||||
| 订单明细 / 菜品库 | 同上目录 |
|
||||
| **工资单生成器** | `~/Downloads/大梦N月账务处理/工资单生成器/`(首次创建,后续复用脚本+模板)|
|
||||
|
||||
## 👥 13 名员工(截至 5月 · 西湖7 + 滨江6)
|
||||
|
||||
> 行号每月递增(见下方「行号约定」);下表是**标准结构**,具体值以 V2 最新月为准。
|
||||
> 5月新增 **舒尧轩**(小胖,西湖调酒晚班)。
|
||||
|
||||
| 姓名 | 归属 | 部门 | 班次 | 岗位 | 兼任 | 基本std | KPI | 管理 | 行为 |
|
||||
|---|---|---|---|---|---|---:|---:|---:|---:|
|
||||
| 蔡逸丰 | 西湖 | 精酿 | 晚班 | 精酿侍酒师 | — | 5800 | 1200 | 0 | 500 |
|
||||
| 何简 | 西湖 | 厨房 | 白班 | 出品厨师 | — | 5500 | 300 | 0 | 300 |
|
||||
| 宋群喜 | 西湖 | 咖啡 | 白班 | 咖啡师 | — | 5400 | 1300 | 0 | 500 |
|
||||
| 胡舒 | 西湖 | 调酒 | 晚班 | 调酒师 | 晚班店长 | 8000 | 1300 | 2000 | 500 |
|
||||
| **舒尧轩** | 西湖 | 调酒 | 晚班 | 调酒师 | — | 5600 | 1000 | 0 | 500 |
|
||||
| 郭思儒 | 西湖 | 咖啡 | 白班 | 咖啡师 | 白班店长 | 6000 | 1300 | 2000 | 500 |
|
||||
| 秦天 | 西湖 | 厨房 | 晚班 | 主厨 | 总厨 | 8000 | 1300 | 2000 | 500 |
|
||||
| 李想 | 滨江 | 调酒 | 晚班 | 调酒师 | 晚班店长 | 8000 | 1100 | 1000 | 500 |
|
||||
| 王瑛胤 | 滨江 | 咖啡 | 白班 | 咖啡师 | 白班店长 | 5800 | 1100 | 1000 | 500 |
|
||||
| 刘润祥 | 滨江 | 厨房 | 晚班 | 主厨 | — | 6600 | 1100 | 1000 | 500 |
|
||||
| 朱秋风 | 滨江 | 精酿 | 晚班 | 前厅运营 | — | 5500 | 1000 | 0 | 500 |
|
||||
| 叶磊 | 滨江 | 厨房 | 白班 | 出品厨师 | — | 6000 | 800 | 0 | 300 |
|
||||
| 尹志艳 | 滨江 | 厨房 | 中班 | 出品厨师 | — | 6000 | 400 | 0 | 300 |
|
||||
|
||||
**行号约定**:4月 = rows 14-25;5月 = rows 27-39(西湖27-33/滨江34-39);**6月 = rows 41-53(西湖41-47/滨江48-53)**;月间留1空行。下月起始 = 上月末+2(7月预计 55-67)。**写前必须 `sheet.get_cell_data` 确认末行**。
|
||||
**昵称映射**:小胡=胡舒、丰丰=蔡逸丰、小宋=宋群喜、小儒=郭思儒、秋风=朱秋风、**小胖=舒尧轩**。
|
||||
**保洁曾阿姨**:兼职,不写入工资表。
|
||||
|
||||
**社保在册(4 人,每月扣 ¥523.53 个人 + 公司转个人 ¥1222.25)**:胡舒、王瑛胤、刘润祥、朱秋风
|
||||
|
||||
## 🔄 月度结薪标准流程
|
||||
|
||||
### Step 0 · ★ 生成伪菜品库(6月起必做)
|
||||
|
||||
`build_analysis.py` 依赖菜品库做部门归类,但本地菜品库是旧月份的,**当月新上的 SKU 不在库里**会掉进关键词兜底、容易归错。
|
||||
6月起老板提供「**菜品销售明细**」导出,自带 `菜品大类`/`菜品小类`(POS 真实归类)。用它反向生成菜品库,覆盖率 100%:
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/dameng-salary/make_menu_lib.py "$PWD" # 两店各生成一份
|
||||
```
|
||||
生成的文件名符合 `大梦_可能实验室_{店}店_菜品库_*.xlsx`,`build_analysis.py` 会自动 glob 到。
|
||||
6月实测:滨江 247 SKU / 西湖 219 SKU,未归类仅 2 笔(扑克/雨伞,本就不属四部门)。
|
||||
|
||||
### Step 1 · 采集考勤数据
|
||||
|
||||
读取 `~/Downloads/大梦N月账务处理/考勤表/` 下所有文件:
|
||||
|
||||
- **图片**(手写)→ 用 Read tool 直接看图识字
|
||||
- **xlsx 文件**(如 `李想N月考勤.xlsx`、`秋风N月考勤.xlsx`)→ openpyxl 读取
|
||||
- **xls 文件**(如 `评估N月-白班店长-王瑛胤 月度评估.xls`)→ xlrd 读取(pip 装一下)
|
||||
- **腾讯文档「大梦西湖店夜班员工考勤」** → `mcporter call tencent-docs get_content --args '{"file_id":"IEqftKNqdqKa"}'`
|
||||
|
||||
详见 `references/attendance_rules.md`。
|
||||
|
||||
### Step 2 · 写入考勤到 V2 的 N 月行
|
||||
|
||||
每个员工写入这些字段(如有数据):
|
||||
- col 22: 出勤天数
|
||||
- col 23: 法定假期天数(清明 1 天,国庆 3 天等)
|
||||
- col 24: 加班小时数("存"的也填进去,工资单 HTML 会自动按备注隐藏)
|
||||
- col 42: 备注(休息日期 + 年假说明 + 加班是"存"还是"换钱")
|
||||
|
||||
如果是首次写 N 月(V2 还没 N 月行):在末尾追加 12 行,紧跟上月之后留 1 空行分隔。
|
||||
|
||||
### Step 3 · 计算并写入营收数据(部门业绩 / 班次业绩 / 部门×班次业绩)
|
||||
|
||||
1. 打开月度营收 xlsx:
|
||||
- `部门收入` sheet → 取"含团购套餐合计"列 → `部门业绩`
|
||||
- `班次营收` sheet → 取"顾客实付"列 → `班次业绩`
|
||||
|
||||
2. 跑 `compute_cross.py` 计算 `部门×班次业绩`:
|
||||
```bash
|
||||
python3 ~/.claude/skills/dameng-salary/compute_cross.py "~/Downloads/大梦N月账务处理"
|
||||
```
|
||||
会输出按比例校正后的 (店, 部门, 班次) 矩阵。
|
||||
|
||||
3. 按行号写入 V2 cols 19/20/21。
|
||||
|
||||
**重要规则**:
|
||||
- **滨江厨房团队**(刘润祥/叶磊/尹志艳):班次业绩 = 0、部门×班次 = 0(只算部门业绩)
|
||||
- **中班**(尹志艳):班次业绩 = 0
|
||||
- **前厅运营/无部门**:部门业绩 = 0
|
||||
|
||||
详见 `references/revenue_methodology.md`。
|
||||
|
||||
### Step 4 · 套用公式计算
|
||||
|
||||
详见 `references/formulas.md`。关键公式:
|
||||
|
||||
```
|
||||
基本工资 = 基本工资标准 × 出勤天数 / 26.08
|
||||
加班工资 = (加班小时数 / 9) × 基本工资标准 / 26.08
|
||||
└─ 备注含"存"的不发,加班工资 = 0
|
||||
KPI绩效结果 = KPI标准 × KPI倍数 (默认 1.0)
|
||||
管理绩效奖金 = 管理标准 × 管理倍数 (默认 1.0)
|
||||
行为规范结果 = 行为标准 (合格全额)
|
||||
出品提成 = 部门业绩 × 角色费率 (西湖部分员工有,滨江暂无;见 commission_rates.md)
|
||||
节假日出勤补贴 = 基本工资标准 / 26.08 × 法定假期天数 × 2
|
||||
工资汇总 = 上述之和
|
||||
剩余应发 = 工资汇总 - 职工社保个人承担(公账代扣)
|
||||
```
|
||||
|
||||
写入字段(cols 2/3/26-37, 39/40 social insurance for 4 enrolled)。
|
||||
|
||||
### Step 5 · 应用店色
|
||||
|
||||
```bash
|
||||
mcporter call tencent-docs sheet.set_cell_style --args \
|
||||
'{"file_id":"VLSAvSvqvYzU","sheet_id":"BB08J2","start_row":<西湖起始>,"end_row":<西湖结束>,"start_col":0,"end_col":42,"bg_color":"FFE2EFDA"}'
|
||||
|
||||
mcporter call tencent-docs sheet.set_cell_style --args \
|
||||
'{"file_id":"VLSAvSvqvYzU","sheet_id":"BB08J2","start_row":<滨江起始>,"end_row":<滨江结束>,"start_col":0,"end_col":42,"bg_color":"FFDDEBF7"}'
|
||||
```
|
||||
|
||||
- 西湖店:`FFE2EFDA`(浅绿)
|
||||
- 滨江店:`FFDDEBF7`(浅蓝)
|
||||
|
||||
### Step 6 · 生成工资单
|
||||
|
||||
1. 确保 `~/Downloads/大梦N月账务处理/工资单生成器/` 存在;如不存在,从此 skill 复制:
|
||||
```bash
|
||||
mkdir -p "~/Downloads/大梦N月账务处理/工资单生成器"
|
||||
cp ~/.claude/skills/dameng-salary/fetch_salary.py "~/Downloads/大梦N月账务处理/工资单生成器/fetch_data.py"
|
||||
cp ~/.claude/skills/dameng-salary/slip_template.html "~/Downloads/大梦N月账务处理/工资单生成器/salary_slips.html"
|
||||
```
|
||||
|
||||
2. 拉取 N 月数据:
|
||||
```bash
|
||||
cd "~/Downloads/大梦N月账务处理/工资单生成器" && python3 fetch_data.py 2026NN
|
||||
```
|
||||
|
||||
3. 打开页面:
|
||||
```bash
|
||||
open "~/Downloads/大梦N月账务处理/工资单生成器/salary_slips.html"
|
||||
```
|
||||
|
||||
4. 用户点页面右上角「EXPORT ALL」或单卡片下方「DOWNLOAD PNG」导出工资单图片。
|
||||
|
||||
## 🔧 字段参考
|
||||
|
||||
详见 `references/columns.md`(V2 完整 43 列定义)。
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- **跨月不能动 N-1 及更早的数据**,仅写本月新行
|
||||
- **行号偏移坑**:腾讯文档 set_range_value 偶发 +1 偏移,写完务必读回校验
|
||||
- **岗位级别 / 兼任岗位等保留** 3 月模板设定,每月仅更新动态字段
|
||||
- **新增员工**:先问用户是否要写入 V2(保洁阿姨等兼职不写)
|
||||
|
||||
## 📂 文件清单
|
||||
|
||||
```
|
||||
~/.claude/skills/dameng-salary/
|
||||
├── SKILL.md ← 你正在读
|
||||
├── fetch_salary.py ← 拉V2数据→data.js (参数化, 默认本月)
|
||||
├── build_analysis.py ← 从原始订单生成营收分析xlsx+summary.json (参数: <目录> <YYYY-MM>)
|
||||
├── compute_cross.py ← 部门×班次交叉聚合 (旧版, build_analysis 已含同逻辑)
|
||||
├── dept_deepdive.py ← 各部门收入起伏 MoM 根因 SKU 拆解 (参数: <本月目录> <上月目录>)
|
||||
├── menu_onsale_ranking.py ← 各部门在架SKU卖最差排名 (参数: <月度目录>)
|
||||
├── slip_template.html ← HTML 工资单模板(店色/印章/大写金额/社保注明/提成行按需隐藏)
|
||||
└── references/
|
||||
├── formulas.md ← 公式手册
|
||||
├── columns.md ← V2 43 列详细定义
|
||||
├── workflow.md ← 完整月度流程(含逐月踩坑回顾)
|
||||
├── attendance_rules.md ← 考勤规则(年假计入出勤/调休/"存vs换钱")
|
||||
├── revenue_methodology.md ← 营收归口规则
|
||||
├── commission_rates.md ← 出品提成费率参考
|
||||
└── analysis_playbook.md ← 营收/出品分析打法(验真/在架口径/已知坑)
|
||||
```
|
||||
|
||||
## 📊 营收/出品分析能力(5月新增)
|
||||
|
||||
当用户要「N月营收分析 / 部门起伏 / 出品(菜单)分析」:
|
||||
1. 若无预制营收分析xlsx → `python3 build_analysis.py <目录> <YYYY-MM>` 生成(13 sheets + summary.json)。
|
||||
2. 部门起伏根因 → `python3 dept_deepdive.py <本月目录> <上月目录>` 出各部门 MoM SKU 拆解。
|
||||
3. 菜单卖最差 → `python3 menu_onsale_ranking.py <月度目录>`(**在架口径**:用"当月有售"代理,菜品库无售卖状态字段)。
|
||||
4. **强烈建议跑完用 Workflow 做多路独立复核** —— 5月就靠对抗式验证抓出 2 个真 bug(幽灵汇总行翻倍、酒头畅饮票误归调酒)。
|
||||
详见 `references/analysis_playbook.md`。
|
||||
@@ -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()
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""各部门收入起伏深挖:4部门 × 2店 × (5月vs4月)。
|
||||
输出 _deepdive_<dept>.json:总览/二级分类/SKU movers(含菜品库分类供验真)/量价/渗透/班次。
|
||||
"""
|
||||
import openpyxl, warnings, glob, json
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
import sys as _sys
|
||||
# 用法: python3 dept_deepdive.py <本月目录> <上月目录>
|
||||
# 例: python3 dept_deepdive.py ~/Downloads/大梦5月账务处理 ~/Downloads/大梦4月账务处理
|
||||
_cur=_sys.argv[1].rstrip("/") if len(_sys.argv)>1 else "."
|
||||
_prev=_sys.argv[2].rstrip("/") if len(_sys.argv)>2 else "."
|
||||
DIRS={"上月":_prev,"本月":_cur}
|
||||
OUT=_cur
|
||||
|
||||
def fh(ws,k):
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if r and any(c==k for c in r if c is not None): return i,[str(c).strip() if c else '' for c in r]
|
||||
return None,None
|
||||
def g(b,p): f=glob.glob(f"{b}/{p}"); return f[0] if f else None
|
||||
|
||||
def dept_of(primary, secondary, name, store):
|
||||
p=(primary or "").strip(); pl=p.lower(); s=(secondary or "").strip(); n=str(name)
|
||||
if any(k in n for k in ["美团团购","团购套餐","打卡套餐"]): return None
|
||||
if any(k in n for k in ["扑克","点歌","雨伞","毛毯","游戏卡牌","桌游","充电","寄存"]): return None
|
||||
# 类目优先
|
||||
if store=="西湖":
|
||||
if p in ["小吃","主食","零食"] or pl.startswith("brunch"): return "厨房"
|
||||
if 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 "调酒" if not any(k in n for k in ["酒头","畅饮"]) else "精酿"
|
||||
else:
|
||||
if p in ["肉肉肉","小吃","主食","零食"] or pl.startswith("brunch"): return "厨房"
|
||||
if p in ["咖啡","甜品点心","茶饮Tea","茶饮tea"]: return "咖啡"
|
||||
if p=="无咖无醇": return "调酒" if "无醇鸡尾酒" in s else "咖啡"
|
||||
if p.startswith("精酿") or p in ["瓶罐精酿","瓶装精酿"]: return "精酿"
|
||||
if p in ["鸡尾酒","纯饮酒","纯饮"]: return "调酒" if not any(k in n for k in ["酒头","畅饮"]) else "精酿"
|
||||
# 兜底按名
|
||||
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 ["拼盘","小食","沙拉","Tacos","吐司","蛋","焗饭","意面","薯","鸡","牛肉","猪","披萨","早餐","三明治","汉堡","面包","可颂","煮蛋"]): return "厨房"
|
||||
return None
|
||||
|
||||
def load(base, store):
|
||||
mf=g(base,f"大梦_可能实验室_{store}店_菜品库_*.xlsx"); wb=openpyxl.load_workbook(mf,data_only=True); ws=wb["菜品"]
|
||||
hr,hdr=fh(ws,"菜品编码(SPUID)"); cn=hdr.index("菜品名称"); cc=hdr.index("基础分类")
|
||||
menu={}
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if i<=hr: continue
|
||||
if r[cn] and r[cc]:
|
||||
parts=str(r[cc]).split("/"); menu[str(r[cn]).strip()]=(parts[0],parts[1] if len(parts)>1 else "")
|
||||
gf=g(base,f"大梦可能实验室({store}店)_全渠道订单明细_*.xlsx"); wb=openpyxl.load_workbook(gf,data_only=True); ws=wb.active
|
||||
hr,hdr=fh(ws,"营业日期"); co=hdr.index("订单号"); cs=hdr.index("餐段")
|
||||
o2s={}
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if i<=hr: continue
|
||||
if r[co]: o2s[str(r[co])]=str(r[cs]) if r[cs] else None
|
||||
df=g(base,f"大梦_可能实验室_{store}店__店内订单明细*.xlsx"); wb=openpyxl.load_workbook(df,data_only=True); ws=wb["菜品明细"]
|
||||
hr,hdr=fh(ws,"订单编号"); ci_o=hdr.index("订单编号"); ci_n=hdr.index("菜品名称"); ci_q=hdr.index("销售数量"); ci_r=hdr.index("菜品收入(元)")
|
||||
rows=[]
|
||||
tot_orders=set()
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if i<=hr: continue
|
||||
name=str(r[ci_n]).strip() if r[ci_n] else None
|
||||
if not name or name=="--": continue
|
||||
o=str(r[ci_o]); tot_orders.add(o)
|
||||
cat=menu.get(name); p=cat[0] if cat else ""; s=cat[1] if cat else ""
|
||||
rows.append((o,name,p,s,float(r[ci_q] or 0),float(r[ci_r] or 0),o2s.get(o)))
|
||||
return rows, len(tot_orders), menu
|
||||
|
||||
def decompose(dept):
|
||||
res={}
|
||||
for store in ["西湖","滨江"]:
|
||||
for m,base in DIRS.items():
|
||||
rows,tot_orders,menu=load(base,store)
|
||||
sku=defaultdict(lambda:[0.0,0.0]); sec=defaultdict(lambda:[0.0,0.0]); shift=defaultdict(float)
|
||||
ow=set(); trev=0.0; tqty=0.0
|
||||
skucat={}
|
||||
for o,name,p,s,q,rev,seg in rows:
|
||||
if dept_of(p,s,name,store)!=dept: continue
|
||||
sku[name][0]+=q; sku[name][1]+=rev
|
||||
skucat[name]=f"{p}/{s}" if s else (p or "无库")
|
||||
sk=(s or p or "其他"); sec[sk][0]+=q; sec[sk][1]+=rev
|
||||
trev+=rev; tqty+=q; ow.add(o)
|
||||
if seg in ("白班","晚班"): shift[seg]+=rev
|
||||
res[(store,m)]={"rev":trev,"qty":tqty,"orders_with":len(ow),"tot_orders":tot_orders,
|
||||
"attach":len(ow)/tot_orders if tot_orders else 0,"avg_price":trev/tqty if tqty else 0,
|
||||
"sec":{k:v[1] for k,v in sec.items()},"shift":dict(shift),
|
||||
"sku":{k:v[1] for k,v in sku.items()},"skucat":skucat}
|
||||
# movers per store
|
||||
movers={}
|
||||
for store in ["西湖","滨江"]:
|
||||
a=res[(store,"上月")]["sku"]; b=res[(store,"本月")]["sku"]
|
||||
ca=res[(store,"上月")]["skucat"]; cb=res[(store,"本月")]["skucat"]
|
||||
names=set(a)|set(b); mv=[]
|
||||
for n in names:
|
||||
ra=a.get(n,0); rb=b.get(n,0)
|
||||
mv.append({"sku":n,"apr":round(ra),"may":round(rb),"delta":round(rb-ra),
|
||||
"cat_apr":ca.get(n,"-"),"cat_may":cb.get(n,"-")})
|
||||
mv.sort(key=lambda x:-x["delta"])
|
||||
movers[store]={"up":[m for m in mv if m["delta"]>0][:10],"down":[m for m in mv if m["delta"]<0][-10:]}
|
||||
out={"dept":dept,
|
||||
"totals":{f"{s}|{m}":{k:round(res[(s,m)][k],1) for k in ["rev","qty","orders_with","tot_orders","attach","avg_price"]} for s in ["西湖","滨江"] for m in ["上月","本月"]},
|
||||
"sec":{f"{s}|{m}":{k:round(v) for k,v in res[(s,m)]["sec"].items() if v>30} for s in ["西湖","滨江"] for m in ["上月","本月"]},
|
||||
"shift":{f"{s}|{m}":{k:round(v) for k,v in res[(s,m)]["shift"].items()} for s in ["西湖","滨江"] for m in ["上月","本月"]},
|
||||
"movers":movers}
|
||||
with open(f"{OUT}/_deepdive_{dept}.json","w") as f: json.dump(out,f,ensure_ascii=False,indent=1)
|
||||
# 简报
|
||||
print(f"\n{'='*60}\n{dept}\n{'='*60}")
|
||||
for s in ["西湖","滨江"]:
|
||||
a=res[(s,'上月')]['rev']; b=res[(s,'本月')]['rev']
|
||||
print(f" {s}: 4月{a:>9.0f} → 5月{b:>9.0f} ({(b-a)/a*100 if a else 0:+.0f}%) | 渗透{res[(s,'上月')]['attach']*100:.0f}%→{res[(s,'本月')]['attach']*100:.0f}% 均价{res[(s,'上月')]['avg_price']:.0f}→{res[(s,'本月')]['avg_price']:.0f}")
|
||||
return out
|
||||
|
||||
if __name__=="__main__":
|
||||
for d in ["厨房","咖啡","精酿","调酒"]:
|
||||
decompose(d)
|
||||
print(f"\n✓ 4部门 decomposition JSON 已存 {OUT}/_deepdive_*.json")
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""读取腾讯文档「工资表V2」指定月份的数据,输出 data.js 给 salary_slips.html 使用。
|
||||
|
||||
用法:
|
||||
python3 fetch_salary.py 202604 # 拉 4 月
|
||||
python3 fetch_salary.py 202605 # 拉 5 月
|
||||
python3 fetch_salary.py # 默认本月(YYYYMM)
|
||||
|
||||
输出: ./data.js(与本脚本同目录)
|
||||
|
||||
依赖: mcporter(系统命令)+ tencent-docs mcp 已配置
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
FILE_ID = "VLSAvSvqvYzU" # 工资表V2
|
||||
SHEET_ID = "BB08J2" # 员工档案
|
||||
END_ROW = 80 # 足够覆盖所有月份
|
||||
END_COL = 42
|
||||
|
||||
OUT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data.js")
|
||||
|
||||
|
||||
def fetch_csv(file_id: str, sheet_id: str) -> list:
|
||||
args = {
|
||||
"file_id": file_id,
|
||||
"sheet_id": sheet_id,
|
||||
"start_row": 0,
|
||||
"end_row": END_ROW,
|
||||
"start_col": 0,
|
||||
"end_col": END_COL,
|
||||
"return_csv": True,
|
||||
}
|
||||
res = subprocess.run(
|
||||
["mcporter", "call", "tencent-docs", "sheet.get_cell_data",
|
||||
"--args", json.dumps(args)],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
data = json.loads(res.stdout)
|
||||
if data.get("error"):
|
||||
raise RuntimeError(f"API error: {data['error']}")
|
||||
return list(csv.reader(io.StringIO(data["csv_data"])))
|
||||
|
||||
|
||||
def default_month() -> str:
|
||||
"""Return YYYYMM for current month."""
|
||||
today = date.today()
|
||||
return f"{today.year}{today.month:02d}"
|
||||
|
||||
|
||||
def main():
|
||||
month = sys.argv[1] if len(sys.argv) > 1 else default_month()
|
||||
print(f"拉取月份: {month}")
|
||||
|
||||
rows = fetch_csv(FILE_ID, SHEET_ID)
|
||||
if not rows:
|
||||
sys.exit("空数据")
|
||||
header = rows[0]
|
||||
|
||||
def month_records(m):
|
||||
out = []
|
||||
for r in rows[1:]:
|
||||
if not r or not r[0].strip() or r[0] != m:
|
||||
continue
|
||||
out.append({header[i]: (r[i] if i < len(r) else "") for i in range(len(header))})
|
||||
return out
|
||||
|
||||
records = month_records(month)
|
||||
if not records:
|
||||
sys.exit(f"未找到 {month} 月份的记录")
|
||||
|
||||
# 上月业绩(同店逐人按姓名匹配),供工资单展示3种业绩环比涨跌
|
||||
y, mm = int(month[:4]), int(month[4:6])
|
||||
prev = f"{y-1}12" if mm == 1 else f"{y}{mm-1:02d}"
|
||||
prev_by_name = {}
|
||||
for r in month_records(prev):
|
||||
prev_by_name[r.get("姓名", "")] = {
|
||||
"部门业绩": r.get("部门业绩", ""),
|
||||
"班次业绩": r.get("班次业绩", ""),
|
||||
"部门x班次业绩": r.get("部门x班次业绩", ""),
|
||||
}
|
||||
for rec in records:
|
||||
rec["_prev"] = prev_by_name.get(rec.get("姓名", ""), None)
|
||||
|
||||
payload = {"month": month, "prev_month": prev, "header": header, "records": records}
|
||||
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
||||
f.write("window.SALARY_DATA = ")
|
||||
json.dump(payload, f, ensure_ascii=False, indent=2)
|
||||
f.write(";\n")
|
||||
|
||||
print(f"写入 {len(records)} 条记录到 {OUT_PATH}")
|
||||
for r in records:
|
||||
print(f" - {r.get('姓名','?')} ({r.get('归属','')} {r.get('部门','')} {r.get('岗位','')})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从「菜品销售明细」导出反推菜品库(6月起使用)
|
||||
|
||||
用法: python3 make_menu_lib.py <月度账务目录>
|
||||
|
||||
背景:build_analysis.py 依赖菜品库做部门归类,但本地菜品库是旧月份快照,
|
||||
当月新上的 SKU 不在库里会掉进关键词兜底、容易归错。
|
||||
菜品销售明细自带「菜品大类/菜品小类」= POS 系统里的真实归类,用它生成菜品库覆盖率 100%。
|
||||
|
||||
输出: 大梦_可能实验室_{店}店_菜品库_自销售明细生成_{YYYYMM}.xlsx
|
||||
(文件名符合 build_analysis.py 的 glob 模式,会被自动读到)
|
||||
"""
|
||||
import openpyxl, glob, sys, os, re
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
BASE = (sys.argv[1] if len(sys.argv) > 1 else ".").rstrip("/")
|
||||
|
||||
def build(store):
|
||||
fs = glob.glob(f"{BASE}/*{store}店__菜品销售明细*.xlsx")
|
||||
if not fs:
|
||||
print(f" [{store}] 未找到菜品销售明细,跳过")
|
||||
return
|
||||
wb = openpyxl.load_workbook(fs[0], data_only=True)
|
||||
ws = wb["已销售"]
|
||||
|
||||
# 表头在第 3 行
|
||||
hr = None
|
||||
for r in range(1, 8):
|
||||
row = [ws.cell(r, c).value for c in range(1, ws.max_column + 1)]
|
||||
if any(v and "菜品大类" in str(v) for v in row):
|
||||
hr = r
|
||||
H = [str(v).strip() if v else "" for v in row]
|
||||
break
|
||||
if hr is None:
|
||||
print(f" [{store}] 找不到含「菜品大类」的表头行,跳过")
|
||||
return
|
||||
|
||||
ci_nm = H.index("菜品名称") + 1
|
||||
ci_d = H.index("菜品大类") + 1
|
||||
ci_x = H.index("菜品小类") + 1
|
||||
|
||||
# 同名多类时取众数(如"深烘拿铁"既有 咖啡/经典 也有 经典咖啡)
|
||||
name2cats = defaultdict(Counter)
|
||||
ym = None
|
||||
ci_date = H.index("营业日期") + 1 if "营业日期" in H else None
|
||||
for r in range(hr + 1, ws.max_row + 1):
|
||||
nm = ws.cell(r, ci_nm).value
|
||||
if not nm:
|
||||
continue
|
||||
d = ws.cell(r, ci_d).value
|
||||
x = ws.cell(r, ci_x).value
|
||||
if d:
|
||||
name2cats[str(nm).strip()][(str(d).strip(), str(x).strip() if x else "")] += 1
|
||||
if ym is None and ci_date:
|
||||
dv = str(ws.cell(r, ci_date).value or "")
|
||||
m = re.search(r"(\d{4})[/-](\d{2})", dv)
|
||||
if m:
|
||||
ym = m.group(1) + m.group(2)
|
||||
wb.close()
|
||||
|
||||
conflicts = {n: c for n, c in name2cats.items() if len(c) > 1}
|
||||
|
||||
out = openpyxl.Workbook()
|
||||
ws2 = out.active
|
||||
ws2.title = "菜品"
|
||||
ws2.append(["菜品编码(SPUID)", "菜品名称", "基础分类"])
|
||||
for n, c in sorted(name2cats.items()):
|
||||
(d, x), _ = c.most_common(1)[0]
|
||||
ws2.append(["", n, f"{d}/{x}" if x else d])
|
||||
|
||||
fn = f"{BASE}/大梦_可能实验室_{store}店_菜品库_自销售明细生成_{ym or 'latest'}.xlsx"
|
||||
out.save(fn)
|
||||
|
||||
cats = Counter()
|
||||
for n, c in name2cats.items():
|
||||
cats[c.most_common(1)[0][0][0]] += 1
|
||||
print(f" [{store}] {len(name2cats)} 个 SKU → {os.path.basename(fn)}")
|
||||
print(f" 大类分布: {dict(cats.most_common(8))}")
|
||||
if conflicts:
|
||||
print(f" 同名多类 {len(conflicts)} 个(已取众数,正常现象): "
|
||||
+ ", ".join(list(conflicts)[:4]))
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"从菜品销售明细生成菜品库 — {BASE}")
|
||||
for st in ["滨江", "西湖"]:
|
||||
build(st)
|
||||
print("完成。接着跑: python3 build_analysis.py <目录> <YYYY-MM>")
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""各部门「在架SKU」卖最差排名(按白班/晚班拆分)。
|
||||
|
||||
⚠️ 口径要点(5月踩坑):
|
||||
- 菜品库导出**无「售卖状态」字段**(导出时是"全部状态",在售/下架混在一起无法区分)。
|
||||
- 因此用「当月有售(≥1件)」作为"在架"的代理 —— 下架的季节菜(披萨/牛排/汉堡线)自动排除。
|
||||
- 代价: 会漏掉极少数"在架但整月真没人点"的款。要100%精确, 需用户重新导出菜品库勾选「售卖状态=售卖中」。
|
||||
|
||||
用法: python3 menu_onsale_ranking.py <月度账务目录>
|
||||
输出: 每店每部门, 在架SKU按销量升序(白班/晚班分列), 标注濒死(≤3件)。
|
||||
"""
|
||||
import openpyxl, warnings, glob, sys
|
||||
from collections import defaultdict
|
||||
warnings.filterwarnings('ignore')
|
||||
BASE=(sys.argv[1] if len(sys.argv)>1 else ".").rstrip("/")
|
||||
def fh(ws,k):
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if r and any(c==k for c in r if c is not None): return i,[str(c).strip() if c else '' for c in r]
|
||||
def g(p): f=glob.glob(f"{BASE}/{p}"); return f[0]
|
||||
|
||||
# 各部门一级分类归属(与 build_analysis 一致)
|
||||
DEPTMAP={
|
||||
"西湖":{"厨房":["小吃","主食","零食"],"咖啡":["咖啡","甜品","茶饮Tea","茶饮tea"]},
|
||||
"滨江":{"厨房":["肉肉肉","小吃","主食","零食"],"咖啡":["咖啡","甜品点心","茶饮Tea","茶饮tea"]},
|
||||
}
|
||||
def dept_of(p,n,store):
|
||||
p=(p or "").strip();pl=p.lower()
|
||||
for d,cats in DEPTMAP[store].items():
|
||||
if p in cats or (d=="厨房" and pl.startswith("brunch")): return d
|
||||
if store=="西湖":
|
||||
if p=="软饮": return "咖啡" if False else "调酒"
|
||||
if p.startswith("精酿"): return "精酿"
|
||||
if p in ["鸡尾酒","纯饮","纯饮酒"]: return "精酿" if any(k in str(n) for k in ["酒头","畅饮"]) else "调酒"
|
||||
else:
|
||||
if p=="无咖无醇": return "咖啡"
|
||||
if p.startswith("精酿") or p in ["瓶罐精酿","瓶装精酿"]: return "精酿"
|
||||
if p in ["鸡尾酒","纯饮酒","纯饮"]: return "精酿" if any(k in str(n) for k in ["酒头","畅饮"]) else "调酒"
|
||||
return None
|
||||
|
||||
DEPTS=["厨房","咖啡","精酿","调酒"]
|
||||
for store in ["西湖","滨江"]:
|
||||
mf=g(f"大梦_可能实验室_{store}店_菜品库_*.xlsx"); wb=openpyxl.load_workbook(mf,data_only=True); ws=wb["菜品"]
|
||||
hr,hdr=fh(ws,"菜品编码(SPUID)"); cn=hdr.index("菜品名称"); cc=hdr.index("基础分类"); cpx=hdr.index("售卖价")
|
||||
menu={}
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if i<=hr: continue
|
||||
if r[cn] and r[cc]:
|
||||
nm=str(r[cn]).strip()
|
||||
if nm not in menu:
|
||||
try: px=float(r[cpx]) if r[cpx] not in (None,"") else None
|
||||
except: px=None
|
||||
menu[nm]=(str(r[cc]).split("/")[0],px)
|
||||
gf=g(f"大梦可能实验室({store}店)_全渠道订单明细_*.xlsx"); wb=openpyxl.load_workbook(gf,data_only=True); ws=wb.active
|
||||
hr,hdr=fh(ws,"营业日期"); co=hdr.index("订单号"); cs=hdr.index("餐段")
|
||||
o2s={str(r[co]):(str(r[cs]) if r[cs] else None) for i,r in enumerate(ws.iter_rows(values_only=True),1) if i>hr and r[co]}
|
||||
of=g(f"大梦_可能实验室_{store}店__店内订单明细*.xlsx"); wb=openpyxl.load_workbook(of,data_only=True); ws=wb["菜品明细"]
|
||||
hr,hdr=fh(ws,"订单编号"); cio=hdr.index("订单编号"); cin=hdr.index("菜品名称"); ciq=hdr.index("销售数量"); cir=hdr.index("菜品收入(元)")
|
||||
sales=defaultdict(lambda:defaultdict(lambda:[0.0,0.0]))
|
||||
for i,r in enumerate(ws.iter_rows(values_only=True),1):
|
||||
if i<=hr: continue
|
||||
nm=str(r[cin]).strip() if r[cin] else None
|
||||
if nm and nm!="--" and nm in menu:
|
||||
seg=o2s.get(str(r[cio]))
|
||||
if seg in ("白班","晚班"): sales[nm][seg][0]+=float(r[ciq] or 0); sales[nm][seg][1]+=float(r[cir] or 0)
|
||||
print("="*66); print(f"{store}店 在架(当月有售)出品 卖最差排名"); print("="*66)
|
||||
for dept in DEPTS:
|
||||
items=[nm for nm in sales if dept_of(menu[nm][0],nm,store)==dept]
|
||||
rows=[]
|
||||
for nm in items:
|
||||
wq,wr=sales[nm]["白班"]; nq,nr=sales[nm]["晚班"]; tq=wq+nq; tr=wr+nr
|
||||
rows.append((tr,tq,nm,wq,nq))
|
||||
rows.sort(key=lambda x:(x[0],x[1]))
|
||||
dying=sum(1 for r in rows if r[1]<=3)
|
||||
print(f"\n--- {dept}: 在架{len(rows)}款, 濒死(≤3件){dying}款, 卖最差Top8 ---")
|
||||
for tr,tq,nm,wq,nq in rows[:8]:
|
||||
print(f" 合计¥{tr:>6.0f}/{tq:>3.0f}件 (白{wq:.0f}/晚{nq:.0f}) {nm[:30]}")
|
||||
@@ -0,0 +1,69 @@
|
||||
# 营收 / 出品分析打法(5月固化)
|
||||
|
||||
## 三类分析 + 对应脚本
|
||||
|
||||
| 用户诉求 | 脚本 | 产出 |
|
||||
|---|---|---|
|
||||
| N月营收分析(无预制表时) | `build_analysis.py <目录> <YYYY-MM>` | 营收分析xlsx(13 sheets) + `_analysis_summary.json` |
|
||||
| 各部门收入起伏根因(MoM) | `dept_deepdive.py <本月目录> <上月目录>` | `_deepdive_<部门>.json`(总览/二级分类/班次/SKU涨跌含分类验真)|
|
||||
| 菜单/出品 卖最差 | `menu_onsale_ranking.py <月度目录>` | 各部门在架SKU升序榜 |
|
||||
|
||||
`_analysis_summary.json` 的 `employees` 字段 = 12(13)人 (部门业绩/班次业绩/部门×班次业绩) 三元组,直接写 V2 cols 19/20/21。
|
||||
|
||||
## 🔴 必守口径(5月踩坑总结)
|
||||
|
||||
1. **统一分类器重算两月**:做 MoM 对比时,4月也要用同一分类器重算,**不要拿4月预制xlsx对比5月自算**(分类器漂移会造出假象,如"西湖咖啡-21%"实为持平)。`dept_deepdive.py` 已对两月用同一分类器。
|
||||
|
||||
2. **幽灵汇总行**:POS「订单明细」末尾有一行 `订单来源/订单号/营业日期 全='--'` 的汇总行,金额=所有真实行之和,会让订单级字段翻倍2x。必须 `if str(订单号).strip()=='--': continue`(build_analysis 已修)。
|
||||
|
||||
3. **dish级 vs 订单级**:部门收入用「菜品明细」逐菜累加 `菜品收入(元)`(dish级);订单级列受联台重复污染,勿用。
|
||||
|
||||
4. **酒头/畅饮票=精酿**:`1-10酒头3小时畅饮票` 等"酒头/畅饮"SKU 是扎啤生啤,归精酿(非调酒)。
|
||||
|
||||
5. **部门×班次交叉**:营收xlsx不自带,需 build_analysis 用菜品名匹配(≈95%)+ 按部门合计比例校正。滨江厨房团队(刘/叶/尹)班次=0、中班=0、前厅部门=0。
|
||||
|
||||
6. **菜单"在架"口径**:菜品库导出**无「售卖状态」字段**(在售/下架混在一起)。用"当月有售(≥1件)"作在架代理 → 下架季节菜自动排除。代价:漏掉"在架但真没人点"的极少数款。要100%精确需用户重导菜品库勾「售卖状态=售卖中」。
|
||||
- 5月实测:西湖菜品库100款厨房菜→仅45在售;滨江138→45。**菜单严重冗余,大量下架菜没从系统清理**。
|
||||
|
||||
## 🟢 质量要求:跑完必做对抗式复核
|
||||
|
||||
每次营收分析跑完,**用 Workflow 起多个 agent 独立重算 + 对抗验证**(部门POS/班次/团购/交叉/环比 各一路)。5月正是靠这个抓出 2 个真bug(幽灵行翻倍、酒头误归)。验真手法:每个涨跌SKU比对 `cat_apr` vs `cat_may`,一致=真实业务变化,不一致=重归类伪变动需剔除(`cat='-'` 表示该月无此SKU=新上/下架,属真实,非伪变动)。
|
||||
|
||||
## 输出去向
|
||||
|
||||
- 营收分析 13 sheets:总览/部门收入/团购→部门归口/班次营收/部门x班次/品类(一级,二级)/每日营收×2/支付方式/12员工业绩/深度分析/改进建议/部门起伏根因
|
||||
- 出品分析:可加 sheet「西湖/滨江 餐食卖最差(在架)」「出品优化建议(该砍清单)」
|
||||
- 报告 md:`大梦N月各部门营收起伏深度报告.md`、`大梦N月_菜单精简与出品优化建议.md`
|
||||
|
||||
## 已知业务结论(5月,供下月对比基线)
|
||||
|
||||
- 双店本质夜间酒馆:晚班占 81-85%,20-23点占 57-61% 营收。
|
||||
- 西湖=精酿+调酒双驱动酒吧店;滨江=精酿单极社区店。
|
||||
- 白班餐食弱:西湖周末/节假日强(3.3x工作日)、滨江平(1.9x)、工作日白天日均仅¥160-190。
|
||||
- 会员质量:西湖健康(会员客单>非会员);滨江会员"次卡化"(纯咖啡会员单17%→33%,客单跌破非会员)。
|
||||
- 断货可恢复≈¥13.9k/月(健力士黑啤两店同步断供最易救)。
|
||||
|
||||
---
|
||||
|
||||
## 🆕 伪菜品库法(6月起·解决新品归类)
|
||||
|
||||
**问题**:菜品库是某个月导出的静态快照,次月新上的 SKU(尤其精酿新酒款)不在库中 → 落到 `dept_by_name` 关键词兜底 → 归类不可靠。手敲关键词还有宽词误伤风险("菜"/"面"/"饭")。
|
||||
|
||||
**解法**:用当月「菜品销售明细」导出反推菜品库。该文件 sheet「已销售」**表头在第 3 行**,含字段:
|
||||
`出品部门 | 营业日期 | 菜品名称 | 菜品大类 | 菜品小类 | 订单编号 | 销售数量 | 销售额 | 菜品优惠 | 菜品收入 | ...`
|
||||
|
||||
取 `菜品名称 → 菜品大类/菜品小类`,同名多类时取出现次数最多的那组,输出成 `菜品编码(SPUID) | 菜品名称 | 基础分类` 三列即可被 `load_menu()` 读取。
|
||||
|
||||
**注意**:
|
||||
- 同名多类是正常现象(如"深烘拿铁"既有 `咖啡/经典` 也有 `经典咖啡`),取众数即可,6月滨江有 11 个这类 SKU。
|
||||
- 大类值会随店而异:滨江有 `肉肉肉/无咖无醇/瓶罐精酿`,西湖有 `软饮/brunch轻食简餐/纯饮`,`dept_of_category()` 里两店分支已覆盖。
|
||||
- `美团团购套餐` 类 SKU 在 POS 侧**菜品收入为 0**(核销记 0、钱在平台侧归口),`is_teamgou_shell()` 会跳过,不会重复。
|
||||
|
||||
## 🔍 团购/收银重复性审查(每月建议做一次)
|
||||
|
||||
老板会问"班次业绩是不是把团购和收银的重复算了"。审查三步:
|
||||
1. **支付方式汇总**(店内订单明细→支付明细 sheet):确认支付方式列表里**没有**"美团团购券"之类的平台支付方式。6月滨江只有 微信/支付宝/会员卡/代金券/现金。
|
||||
2. **壳单实付**:含 `美团团购套餐` 大类 SKU 的订单,其全渠道「顾客实付」应为 **0**(6月实测 3 笔全 0)→ 只算了平台一边 ✓
|
||||
3. **代金券**:POS 侧计入顾客实付,平台归口时 `teamgou_dept_split()` 返回 None 明确跳过 → 只算了收银一边 ✓
|
||||
|
||||
**易误判**:会有一批订单「菜品收入=0 但有实付」(6月滨江 86 笔 5,455.60)——那是 **「会员卡-卡余额消费」**(储值卡买单,POS 记全额优惠、钱走卡余额),**与团购无关**。5月同样机制(21,667/207笔),环比可比,不要当成 bug。
|
||||
@@ -0,0 +1,73 @@
|
||||
# 考勤规则
|
||||
|
||||
## 资料来源
|
||||
|
||||
每月 `~/Downloads/大梦N月账务处理/考勤表/` 下会有:
|
||||
|
||||
| 文件 | 覆盖人员 | 形式 |
|
||||
|---|---|---|
|
||||
| `厨师考勤.jpg` | 西湖店厨房(秦天/何简)+ 滨江店全员(除王瑛胤)| 手写图片 |
|
||||
| `06f16921...jpg` 等 | 西湖店白班(小宋=宋群喜、小儒=郭思儒、保洁阿姨)| 手写图片 |
|
||||
| `李想N月考勤.xlsx` | 李想 | 月度档案 xlsx |
|
||||
| `秋风N月考勤(N).xlsx` | 朱秋风("秋风")| 月度档案 xlsx |
|
||||
| `评估N月-白班店长-王瑛胤 月度评估.xls` | 王瑛胤 | xls(旧版,要 xlrd 读取)|
|
||||
| 腾讯文档「大梦西湖店夜班员工考勤」 `file_id=IEqftKNqdqKa` | 西湖店夜班(胡舒=小胡、蔡逸丰=丰丰、小亮、凯南等)| 在线表格 |
|
||||
|
||||
## 昵称映射
|
||||
|
||||
| 称呼 | V2 姓名 |
|
||||
|---|---|
|
||||
| 小胡 | 胡舒 |
|
||||
| 丰丰 | 蔡逸丰 |
|
||||
| 小宋 | 宋群喜 |
|
||||
| 小儒 | 郭思儒 |
|
||||
| 秋风 | 朱秋风 |
|
||||
| 保洁曾阿姨 | (**兼职,不写入 V2**)|
|
||||
|
||||
## 出勤天数定义(5月已确认口径)
|
||||
|
||||
```
|
||||
出勤天数 = 当月天数 − 正常休息天数 (年假天数 计入出勤/带薪,不扣)
|
||||
```
|
||||
|
||||
- **年假 = 带薪出勤**:休年假的那天算出勤、照发工资。
|
||||
- 例:何简 5月 休 11,12,13,14,19,25(其中 14/19/25 是年假)→ 正常休 3 天 → **出勤 = 31 − 3 = 28**(3 个年假日计入出勤)。用户已确认"何简就算出勤28天"。
|
||||
- 例:郭思儒 5月 休 3,14,20,27(4正常)+ 28年假 → 出勤 = 31 − 4 = **27**。
|
||||
|
||||
> ⚠️ 4月时曾用"年假不计入出勤"(宋群喜26),**5月起统一改为年假计入出勤**。各手写考勤表通常会**直接写明出勤数**——以写明的为准;未写明的按上式算。
|
||||
> **以本月各考勤表写明的出勤数为最高优先**,公式仅用于未写明者。
|
||||
|
||||
## 加班小时数
|
||||
|
||||
- 备注里写「**加班X小时存**」→ 暂存(不发钱),加班工资 = 0
|
||||
- V2 仍把 X 写入 `加班小时数`(col 24),方便后续核算
|
||||
- HTML 工资单会自动隐藏这种情况的加班小时显示
|
||||
- 备注里写「**加班X小时换钱**」→ 当月发,加班工资 = (X/9) × 底薪/26.08
|
||||
- 没写明 → 默认发钱
|
||||
|
||||
## 法定假期天数(4 月示例)
|
||||
|
||||
| 月 | 节日 | 天数 |
|
||||
|---|---|---|
|
||||
| 1 | 元旦 | 1 |
|
||||
| 2 | 春节 | 3(实际放 7 但只算 3)|
|
||||
| 4 | 清明 | 1 |
|
||||
| 5 | 劳动节 | **2**(5月按 2 天法定假,5月用户确认)|
|
||||
| 6 | 端午 | 1 |
|
||||
| 9-10 | 中秋+国庆 | 3-4 |
|
||||
|
||||
> **5月口径(用户确认)**:五一 2 天法定假期,**全员**法定假期天数 = 2、全员给 2 天双倍工资(不论是否实际在岗)。即 `法定假期天数` 列对所有人填 2。
|
||||
> 一般原则:以用户每月的明确指示为准;若用户说"全员给N天双倍",则全员 `法定假期天数=N`。
|
||||
|
||||
## 各类休假在备注里的标记
|
||||
|
||||
```
|
||||
休1,10,15,23 ← 正常休息日期
|
||||
29号年假1天 ← 用了 1 天年假
|
||||
剩余年假5天 ← HR 库存
|
||||
请假合计3天 ← xls 文件汇总
|
||||
调休4天 ← 王瑛胤 月度评估口径
|
||||
加班3小时存 ← 存调休时间
|
||||
加班2小时换钱 ← 当月发钱
|
||||
滨江店X天 ← 串店帮忙(可能涉及交通补贴)
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# V2 工资表「员工档案」工作表 列定义
|
||||
|
||||
`file_id=VLSAvSvqvYzU`, `sheet_id=BB08J2`(注意 V2 第二个 sheet 是空的)
|
||||
|
||||
总 43 列(0-indexed),第 25 列空。
|
||||
|
||||
| Col | 字段 | 类型 | 来源 | 说明 |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | 月份 | STRING | 固定 | `YYYYMM` 格式,如 `202604` |
|
||||
| 1 | 姓名 | STRING | 固定 | 见员工列表 |
|
||||
| 2 | **工资汇总** | NUMBER | 公式 | sum of all earnings |
|
||||
| 3 | **剩余应发** | NUMBER | 公式 | 工资汇总 - 个人代扣社保 |
|
||||
| 4 | 身份证号 | STRING | 固定 | 通常空,仅 蔡逸丰有 |
|
||||
| 5 | 生日 | — | — | 通常空 |
|
||||
| 6 | 年龄 | — | — | 通常空 |
|
||||
| 7 | 性别 | — | — | 通常空 |
|
||||
| 8 | 归属 | STRING | 固定 | `西湖店` / `滨江店` |
|
||||
| 9 | 部门 | STRING | 固定 | `精酿`/`厨房`/`咖啡`/`调酒`/`前厅` |
|
||||
| 10 | 班次 | STRING | 固定 | `早班`/`白班`/`中班`/`晚班` |
|
||||
| 11 | 岗位 | STRING | 固定 | 主岗 |
|
||||
| 12 | 兼任岗位 | STRING | 固定 | 副岗(店长/总厨等) |
|
||||
| 13 | 当前状态 | STRING | 固定 | `在职`/`离职` |
|
||||
| 14 | 岗位级别 | — | — | 通常空 |
|
||||
| 15 | **基本工资标准** | NUMBER | 固定 | 底薪 |
|
||||
| 16 | KPI绩效标准 | NUMBER | 固定 | KPI 奖金基数 |
|
||||
| 17 | 管理绩效标准 | NUMBER | 固定 | 管理奖金基数(仅店长/总厨>0)|
|
||||
| 18 | 行为规范绩效 | NUMBER | 固定 | 行为奖金基数 |
|
||||
| 19 | **部门业绩** | NUMBER | 营收分析 | 部门收入 sheet 含团购套餐合计 |
|
||||
| 20 | **班次业绩** | NUMBER | 营收分析 | 班次营收 sheet 顾客实付 |
|
||||
| 21 | **部门x班次业绩** | NUMBER | 计算 | `compute_cross.py` 输出 |
|
||||
| 22 | **出勤天数** | NUMBER | 考勤 | 含年假 |
|
||||
| 23 | **法定假期天数** | NUMBER | 月历 | 清明/五一/国庆等 |
|
||||
| 24 | **加班小时数** | NUMBER | 考勤 | 原始小时数("存"也填)|
|
||||
| 25 | (空列) | — | — | 分隔 |
|
||||
| 26 | **基本工资** | NUMBER | 公式 | 底薪 × 出勤/26.08 |
|
||||
| 27 | KPI得分 | NUMBER | 评估 | 默认 1(=1档全额) |
|
||||
| 28 | **KPI绩效结果** | NUMBER | 公式 | KPI标准 × 倍数 |
|
||||
| 29 | 管理绩效得分 | NUMBER | 评估 | 默认 1(仅管理标准>0者)|
|
||||
| 30 | **行为规范绩效结果** | NUMBER | 公式 | 行为标准 × 合格判定 |
|
||||
| 31 | **加班工资** | NUMBER | 公式 | (加班/9) × 底薪/26.08,"存"则 0 |
|
||||
| 32 | 出品提成 | NUMBER | 公式 | 部门业绩 × 角色费率 |
|
||||
| 33 | **管理绩效奖金** | NUMBER | 公式 | 管理标准 × 倍数 |
|
||||
| 34 | 法定假期换薪 | NUMBER | — | 与节假日补贴重复,置 0 |
|
||||
| 35 | 串店交通补贴 | NUMBER | 手动 | 跨店帮忙补贴 |
|
||||
| 36 | **节假日出勤补贴** | NUMBER | 公式 | 底薪/26.08 × 法假天 × 2 |
|
||||
| 37 | 特别奖金 | NUMBER | 手动 | 偶发 |
|
||||
| 38 | 社保-公司承担 | NUMBER | 固定 | 通常空,仅在册者填 |
|
||||
| 39 | 社保-公司部分的个人承担 | NUMBER | 固定 | 1222.25(4 人)|
|
||||
| 40 | 职工社保个人承担部分(公账代扣) | NUMBER | 固定 | 523.53(4 人)|
|
||||
| 41 | 员工餐分担金额 | NUMBER | 手动 | 通常空 |
|
||||
| 42 | 备注 | STRING | 手动 | 休息日期/年假/加班"存vs换钱"等 |
|
||||
|
||||
## 月度行号(每月 12 行连续)
|
||||
|
||||
- 月份起始行 = 当月在 V2 的第一行(紧跟上月最后一行 + 1 空行分隔)
|
||||
- 例:2026/4 在 rows 14-25, 2026/5 在 rows 27-38(行 26 空)
|
||||
|
||||
## 写入注意
|
||||
|
||||
- `set_range_value` 偶发 **+1 行偏移**,写完务必读回校验
|
||||
- 修改任何月份**只动当月行**,绝不动历史
|
||||
- 写入完整 12 行后,记得用 `set_cell_style` 应用两店底色(FFE2EFDA / FFDDEBF7)
|
||||
@@ -0,0 +1,56 @@
|
||||
# 出品提成费率参考
|
||||
|
||||
## 公式
|
||||
|
||||
```
|
||||
出品提成 = 部门业绩 × 角色费率
|
||||
```
|
||||
|
||||
`部门业绩` 来自 V2 col 19(含团购套餐合计)。
|
||||
|
||||
## 已知费率(基于 3 月数据反推)
|
||||
|
||||
| 姓名 | 归属 | 部门 | 班次 | 岗位 | 费率 |
|
||||
|---|---|---|---|---|---:|
|
||||
| 宋群喜 | 西湖 | 咖啡 | 白班 | 咖啡师 | **3.0%** |
|
||||
| 胡舒 | 西湖 | 调酒 | 晚班 | 调酒师/晚班店长 | **2.0%** |
|
||||
| 郭思儒 | 西湖 | 咖啡 | 白班 | 咖啡师/白班店长 | **3.5%** |
|
||||
| 秦天 | 西湖 | 厨房 | 晚班 | 主厨/总厨 | **3.0%** |
|
||||
|
||||
## 暂无费率(待确认)
|
||||
|
||||
| 姓名 | 备注 |
|
||||
|---|---|
|
||||
| 蔡逸丰(精酿侍酒师 西湖晚班)| 无提成(4/5月均未配) |
|
||||
| 何简(出品厨师 西湖白班)| 无提成(出品厨师通常无提成)|
|
||||
| 朱秋风(精酿 滨江晚班)| 部门切换后未配置 |
|
||||
| **舒尧轩(调酒师 西湖晚班,5月新增)** | **5月暂无提成**(与蔡逸丰一致);他是调酒师非店长,胡舒的2%是店长身份。如要配比例需用户确认 |
|
||||
| 滨江店其他人 | 全员无提成(启动期)|
|
||||
|
||||
## 规则推断
|
||||
|
||||
观察 3 月数据可归纳:
|
||||
- **「店长」角色**(白/晚班店长)有提成(咖啡白班店长 3.5%、调酒晚班店长 2%)
|
||||
- **「师」角色**(咖啡师、调酒师、主厨)有提成(多为 3%)
|
||||
- **「出品厨师」/「前厅运营」** 无提成
|
||||
- **滨江店**:早期为启动期,所有人无提成;后续按西湖费率推开
|
||||
|
||||
## 算法
|
||||
|
||||
```python
|
||||
COMMISSION_RATE = {
|
||||
"宋群喜": 0.030,
|
||||
"胡舒": 0.020,
|
||||
"郭思儒": 0.035,
|
||||
"秦天": 0.030,
|
||||
# TODO: 5 月起新加员工费率
|
||||
}
|
||||
|
||||
commission = dept_revenue * COMMISSION_RATE.get(name, 0)
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- **费率随员工角色调整而变**:若某员工岗位变动(如朱秋风改部门),需重新与用户确认费率
|
||||
- 滨江店何时开始有提成,由用户决定
|
||||
- 写入 V2 col 32(出品提成),同时计入 工资汇总
|
||||
@@ -0,0 +1,146 @@
|
||||
# 计薪公式手册
|
||||
|
||||
> 所有公式分母 26.08 = 标准月工作日(含周末折算,全年/12)
|
||||
|
||||
## 基本工资
|
||||
|
||||
```
|
||||
基本工资 = 基本工资标准 × 出勤天数 / 26.08
|
||||
```
|
||||
|
||||
- **出勤天数** = 实际上班天数 + 年假天数(年假按工作计薪)
|
||||
- **不包含** 不计薪的休息日、调休抵扣后的额外休息
|
||||
- 案例:胡舒 4 月,标准 8000,出勤 28 → 8000 × 28/26.08 = **8,588.96**
|
||||
|
||||
## 加班工资
|
||||
|
||||
```
|
||||
加班工资 = (加班小时数 / 9) × 基本工资标准 / 26.08
|
||||
```
|
||||
|
||||
- 9 = 每日工时
|
||||
- 等价于:「(加班小时/9) 天的日薪」
|
||||
- **重要规则**:备注里写"存"(如 `加班3小时存`)的不发钱,加班工资 = 0;写"换钱"或无注的正常发
|
||||
- 案例:李想 4 月,标准 8000,加班 2h → (2/9) × 8000/26.08 = **68.17**
|
||||
|
||||
## KPI 绩效结果
|
||||
|
||||
```
|
||||
KPI绩效结果 = KPI绩效标准 × KPI倍数
|
||||
```
|
||||
|
||||
倍数表(从 王瑛胤 月度评估表):
|
||||
|
||||
| KPI 总分 | 倍数 |
|
||||
|---|---|
|
||||
| ≥ 4.5 | 1.5× |
|
||||
| ≥ 4.0 | 1.2× |
|
||||
| ≥ 3.5 | 1.0× |
|
||||
| ≥ 3.0 | 0.8× |
|
||||
| < 3.0 | 0.5× |
|
||||
|
||||
**默认值**:未评估时按 1× 全额发(V2 的 `KPI得分` 列填 `1`)
|
||||
|
||||
## 管理绩效奖金
|
||||
|
||||
```
|
||||
管理绩效奖金 = 管理绩效标准 × 管理倍数
|
||||
```
|
||||
|
||||
- 倍数表同 KPI
|
||||
- 默认 1×
|
||||
- **仅管理标准 > 0 的员工有此项**
|
||||
|
||||
## 行为规范结果
|
||||
|
||||
```
|
||||
行为规范绩效结果 = 行为规范绩效(全额发) // 合格
|
||||
行为规范绩效结果 = 0 或部分 // 未达标
|
||||
```
|
||||
|
||||
默认按"合格"发全额。
|
||||
|
||||
## 出品提成
|
||||
|
||||
```
|
||||
出品提成 = 部门业绩 × 角色费率
|
||||
```
|
||||
|
||||
费率详见 `commission_rates.md`。仅西湖店部分员工有,滨江店暂无出品提成。
|
||||
|
||||
## 节假日出勤补贴
|
||||
|
||||
```
|
||||
节假日出勤补贴 = 基本工资标准 / 26.08 × 法定假期天数 × 2
|
||||
```
|
||||
|
||||
- × 2 因法定节假日须支付 2 倍工资
|
||||
- 4 月清明 1 天,5 月劳动节 1 天(5.1 当天),10 月国庆 3 天等
|
||||
- 案例:胡舒 4 月,标准 8000,法假 1 天 → 8000/26.08 × 1 × 2 = **613.50**
|
||||
|
||||
## 工资汇总
|
||||
|
||||
```
|
||||
工资汇总 = 基本工资
|
||||
+ KPI绩效结果
|
||||
+ 管理绩效奖金
|
||||
+ 行为规范绩效结果
|
||||
+ 加班工资
|
||||
+ 出品提成
|
||||
+ 节假日出勤补贴
|
||||
+ (其他: 串店补贴 / 特别奖金 / 法定假期换薪 等)
|
||||
```
|
||||
|
||||
## 剩余应发(=实发,不计公司承担社保)
|
||||
|
||||
```
|
||||
剩余应发 = 工资汇总 - 职工社保个人承担(公账代扣)
|
||||
```
|
||||
|
||||
- 仅 4 人在册社保(每月每人扣 ¥523.53):胡舒、王瑛胤、刘润祥、朱秋风
|
||||
- 其余员工 剩余应发 = 工资汇总
|
||||
|
||||
## 公司承担社保(员工成本,不计入实发)
|
||||
|
||||
```
|
||||
社保-公司部分的个人承担 = 1222.25 // 每月每人,仅 4 在册者
|
||||
职工社保个人承担(公账代扣) = 523.53 // 同上
|
||||
两项合计 = 1745.78
|
||||
```
|
||||
|
||||
## 节假日补贴(法定假期双倍)
|
||||
|
||||
```
|
||||
节假日出勤补贴 = 基本工资标准 / 26.08 × 法定假期天数 × 2
|
||||
```
|
||||
- ×2 = 法定节假日双倍工资(基础那份已在基本工资里)。
|
||||
- 5月:法定假期天数=2(五一),全员都给 → 补贴 = 底薪/26.08 × 4。
|
||||
|
||||
## 工资单 HTML 的两条展示规则(slip_template.html 内置)
|
||||
|
||||
1. **未交社保者**:基本工资行下方强调注明
|
||||
*"你的基本工资中已包含公司应承担的社保金额和个人社保金额,总计 ¥1,745.78"*
|
||||
(金额=在册者两项加总 1222.25+523.53;按 col39/col40 是否有值自动判断)。
|
||||
2. **出品提成行**:仅当该员工**实际有提成金额**时才显示,否则整行隐藏(不发提成的人不展示这行)。
|
||||
|
||||
## 串店交通补贴 / 特别奖金 / 法定假期换薪
|
||||
|
||||
- **串店交通补贴**:当员工去另一家店帮忙(备注里"滨江店X天"等)时手动给
|
||||
- **特别奖金**:偶发,手动填
|
||||
- **法定假期换薪**:与节假日出勤补贴重复,目前并入节假日出勤补贴,此列保留为 0
|
||||
|
||||
---
|
||||
|
||||
## 反向校验
|
||||
|
||||
写完一行后用 Python 校验:
|
||||
|
||||
```python
|
||||
basic = base * att / 26.08
|
||||
ot_pay = 0 if banked else (oth / 9) * base / 26.08
|
||||
kpi_res = kpi_std * kpi_mult
|
||||
mgmt_res = mgmt_std * mgmt_mult if mgmt_std > 0 else 0
|
||||
holiday = base / 26.08 * lh_days * 2
|
||||
total = basic + ot_pay + kpi_res + mgmt_res + conduct_std + commission + holiday
|
||||
remaining = total - (523.53 if has_insurance else 0)
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# 营收归口规则
|
||||
|
||||
## 数据来源
|
||||
|
||||
每月营收分析 xlsx:`大梦可能实验室_N月营收分析_西湖店vs滨江店.xlsx`
|
||||
|
||||
关键 sheet:
|
||||
- **总览**:当月概况
|
||||
- **部门收入**:部门 × (POS + 团购套餐) 分布 → 取「含团购套餐合计」 → `部门业绩`
|
||||
- **班次营收**:白班/晚班分布 → 取「顾客实付」列 → `班次业绩`
|
||||
- 订单/菜品/团购明细 sheets:原始数据
|
||||
|
||||
> 此 xlsx 通常已由用户预先生成,本 SKILL 直接读取既有数据。
|
||||
|
||||
## 部门归口(来自 分析方法.md)
|
||||
|
||||
### 西湖店
|
||||
| 部门 | 一级分类 |
|
||||
|---|---|
|
||||
| 厨房 | 小吃 / 主食 / brunch / 零食 |
|
||||
| 咖啡 | 咖啡 / 甜品 / 茶饮Tea / 软饮(可尔必思+海盐荔枝)|
|
||||
| 精酿 | 精酿 / 精酿 老菜单 |
|
||||
| 调酒 | 鸡尾酒 / 纯饮 / 软饮分类的其他 |
|
||||
|
||||
### 滨江店
|
||||
| 部门 | 一级分类 |
|
||||
|---|---|
|
||||
| 厨房 | 肉肉肉 / 小吃 / 主食 / brunch |
|
||||
| 咖啡 | 咖啡 / 甜品点心 / 茶饮tea / 无咖无醇(排除无醇鸡尾酒)|
|
||||
| 精酿 | 精酿 / 瓶罐精酿 / 精酿 老菜单(已废弃)|
|
||||
| 调酒 | 鸡尾酒 / 纯饮酒 / 无醇鸡尾酒 |
|
||||
|
||||
> 实际菜品库的一级分类名带空格/版本号等小差异,`compute_cross.py` 已做容错。
|
||||
|
||||
## 部门×班次业绩(交叉项)
|
||||
|
||||
营收分析 xlsx 默认**不计算**店×部门×班次三维交叉。要算这个值:
|
||||
|
||||
1. 从 `店内订单明细` xlsx 的菜品明细 sheet 取每菜每单的 `菜品收入`
|
||||
2. 从 `全渠道订单明细` xlsx 取每单的 `餐段`(白/晚班)
|
||||
3. 从 `菜品库` xlsx 取每菜的 `基础分类`(一级/二级)
|
||||
4. join → 按 (店, 餐段, 部门) 聚合
|
||||
5. 因为菜品名匹配率 ≈ 95%(前缀编号差异),用**比例校正**:
|
||||
```
|
||||
scale_factor = 权威部门业绩(含团购) / raw部门小计
|
||||
```
|
||||
|
||||
由 `compute_cross.py` 自动完成。
|
||||
|
||||
## 4 月数据(仅供回溯校验)
|
||||
|
||||
| 部门 | 滨江总(含团购)| 西湖总(含团购)|
|
||||
|---|---:|---:|
|
||||
| 厨房 | 37,182.12 | 51,738.90 |
|
||||
| 咖啡 | 16,537.69 | 31,407.47 |
|
||||
| 精酿 | 56,939.31 | 83,995.44 |
|
||||
| 调酒 | 41,618.84 | 58,680.70 |
|
||||
|
||||
| 班次 | 滨江 | 西湖 |
|
||||
|---|---:|---:|
|
||||
| 白班 | 23,485.29 | 51,017.66 |
|
||||
| 晚班 | 130,345.80 | 173,628.80 |
|
||||
|
||||
## 部门业绩特殊规则
|
||||
|
||||
- **前厅运营** 不产生菜品收入 → 部门业绩 = 0
|
||||
- 例外:朱秋风 2026/4 改归"精酿"部门后,部门业绩 = 滨江精酿值
|
||||
- **保洁** 不产生菜品收入 → 不写入 V2
|
||||
|
||||
## 班次业绩特殊规则
|
||||
|
||||
> 团队主要在厨房后场工作,与营收班次解耦
|
||||
|
||||
- **滨江厨房团队(刘润祥/叶磊/尹志艳)**:班次业绩 = 0,部门×班次 = 0
|
||||
- **中班**(尹志艳):班次业绩 = 0,部门×班次 = 0
|
||||
- **早班**(如有):参考晚班/白班归口
|
||||
@@ -0,0 +1,208 @@
|
||||
# 月度结薪完整流程(含坑点回顾)
|
||||
|
||||
> 5 月跑通后请回流到本文档,把新经验记录下来。
|
||||
|
||||
## 完整步骤
|
||||
|
||||
### 0. 前置检查
|
||||
|
||||
```bash
|
||||
# 确认本月账务目录存在
|
||||
ls ~/Downloads/大梦N月账务处理/
|
||||
|
||||
# 必须有:
|
||||
# 考勤表/ ← 各种考勤资料
|
||||
# 大梦可能实验室_N月营收分析_*.xlsx ← 营收分析(用户预先生成)
|
||||
# 大梦_可能实验室_西湖店__店内订单明细*.xlsx
|
||||
# 大梦_可能实验室_滨江店__店内订单明细*.xlsx
|
||||
# 大梦可能实验室(西湖店)_全渠道订单明细_*.xlsx
|
||||
# 大梦可能实验室(滨江店)_全渠道订单明细_*.xlsx
|
||||
# 大梦_可能实验室_西湖店_菜品库_*.xlsx
|
||||
# 大梦_可能实验室_滨江店_菜品库_*.xlsx
|
||||
```
|
||||
|
||||
### 1. 在 V2 表追加 N 月空行 + 应用店色
|
||||
|
||||
确定 N 月起始行号(紧跟 N-1 月最后一行 + 1 空行分隔)。
|
||||
|
||||
```bash
|
||||
# 例: 5 月起始 row 27(4 月结束 row 25, row 26 留空)
|
||||
|
||||
# 西湖 6 人 (rows 27-32) 浅绿
|
||||
mcporter call tencent-docs sheet.set_cell_style --args \
|
||||
'{"file_id":"VLSAvSvqvYzU","sheet_id":"BB08J2","start_row":27,"end_row":32,"start_col":0,"end_col":42,"bg_color":"FFE2EFDA"}'
|
||||
|
||||
# 滨江 6 人 (rows 33-38) 浅蓝
|
||||
mcporter call tencent-docs sheet.set_cell_style --args \
|
||||
'{"file_id":"VLSAvSvqvYzU","sheet_id":"BB08J2","start_row":33,"end_row":38,"start_col":0,"end_col":42,"bg_color":"FFDDEBF7"}'
|
||||
```
|
||||
|
||||
### 2. 写入固定字段(每月不变)
|
||||
|
||||
为 12 名员工写入:月份、姓名、归属、部门、班次、岗位、兼任、状态、4 个标准(基本/KPI/管理/行为)。
|
||||
|
||||
可批量 set_range_value 一次发完。
|
||||
|
||||
### 3. 处理考勤资料
|
||||
|
||||
详见 `attendance_rules.md`。
|
||||
|
||||
针对 N 月:
|
||||
- 读取所有图片用 Read tool 看清字
|
||||
- 读取 xlsx 用 openpyxl
|
||||
- 读取 xls 用 xlrd(首次需 `python3 -m pip install xlrd`)
|
||||
- 读取腾讯文档夜班考勤(`file_id=IEqftKNqdqKa`)用 `get_content`
|
||||
|
||||
整理出每人的:出勤天数、加班小时数、备注(休息日期 + 年假说明 + 加班存/换钱)。
|
||||
|
||||
### 4. 写入考勤到 V2
|
||||
|
||||
set_range_value 写 cols 22 (出勤)、23 (法假)、24 (加班)、42 (备注)。
|
||||
|
||||
### 5. 写入营收数据
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/dameng-salary/compute_cross.py "~/Downloads/大梦N月账务处理"
|
||||
```
|
||||
|
||||
把脚本输出的 12 人 (部门业绩, 班次业绩, 部门×班次业绩) 写入 V2 cols 19/20/21。
|
||||
|
||||
记得应用特殊规则:
|
||||
- 滨江厨房团队(刘/叶/尹):班次 = 0,部门×班次 = 0
|
||||
- 中班(尹志艳):班次 = 0,部门×班次 = 0
|
||||
|
||||
### 6. 套用计薪公式
|
||||
|
||||
详见 `formulas.md`。对每个员工:
|
||||
|
||||
```python
|
||||
basic = base * att / 26.08
|
||||
ot_pay = 0 if banked else (oth / 9) * base / 26.08
|
||||
kpi_res = kpi_std * 1.0 # 默认 1×
|
||||
mgmt_res = mgmt_std * 1.0 if mgmt_std > 0 else 0
|
||||
conduct_res = conduct_std # 全额合格
|
||||
commission = dept_rev * COMMISSION_RATE.get(name, 0)
|
||||
holiday = base / 26.08 * lh_days * 2
|
||||
total = basic + ot_pay + kpi_res + mgmt_res + conduct_res + commission + holiday
|
||||
remaining = total - (523.53 if name in ENROLLED else 0)
|
||||
```
|
||||
|
||||
写入 V2 cols 2 (汇总)、3 (剩余)、26-37 (各计算项)、39/40 (社保,仅 4 人)。
|
||||
|
||||
### 7. 生成工资单 HTML
|
||||
|
||||
```bash
|
||||
# 首次本月运行:建立生成器目录
|
||||
mkdir -p "~/Downloads/大梦N月账务处理/工资单生成器"
|
||||
cp ~/.claude/skills/dameng-salary/fetch_salary.py "~/Downloads/大梦N月账务处理/工资单生成器/fetch_data.py"
|
||||
cp ~/.claude/skills/dameng-salary/slip_template.html "~/Downloads/大梦N月账务处理/工资单生成器/salary_slips.html"
|
||||
|
||||
# 拉数据
|
||||
cd "~/Downloads/大梦N月账务处理/工资单生成器" && python3 fetch_data.py 2026NN
|
||||
|
||||
# 打开
|
||||
open "~/Downloads/大梦N月账务处理/工资单生成器/salary_slips.html"
|
||||
```
|
||||
|
||||
### 8. 按需迭代
|
||||
|
||||
用户可能要求:
|
||||
- 改某人考勤(重算工资)
|
||||
- 调员工部门
|
||||
- 改 KPI 倍数
|
||||
- 改提成费率
|
||||
- 加新员工
|
||||
|
||||
每次修改后重新跑公式 → 重新 fetch_data → 用户刷新页面。
|
||||
|
||||
### 9. 用户验收 + 导出 PNG
|
||||
|
||||
用户在页面右上角点 `EXPORT ALL` 批量导出,或单卡片 `DOWNLOAD PNG`。
|
||||
|
||||
---
|
||||
|
||||
## 已知坑点
|
||||
|
||||
### 行号偏移
|
||||
腾讯文档 `set_range_value` 偶发 +1 行偏移。**写完务必读回校验**。
|
||||
|
||||
之前发生过:4 月写入时尹志艳被宋群喜覆盖。修复办法:append 到末尾再重排顺序。
|
||||
|
||||
### 出勤口径
|
||||
"出勤天数"的口径:
|
||||
- xlsx 文件用「实际出勤天数」= 当月到岗天数(不含调休/年假)
|
||||
- 夜班手写考勤的「出勤」列:含年假,约等于 30 - 真休
|
||||
- 用户最终口径(5 月起请遵循):**出勤 = 实际工作天数 + 年假天数(按工作日计薪)**
|
||||
- **若不确定,问用户**
|
||||
|
||||
### 加班 "存 vs 换钱"
|
||||
- 备注里看清楚
|
||||
- "存"则 加班工资 = 0
|
||||
- "换钱"或无注则正常发
|
||||
|
||||
### 朱秋风部门变动
|
||||
4 月起 朱秋风 从「前厅」改到「精酿」部门,但岗位仍是「前厅运营」。新月份继承。
|
||||
|
||||
### 节假日补贴 vs 法定假期换薪
|
||||
- V2 有两列:法定假期换薪 (col 34) 和 节假日出勤补贴 (col 36)
|
||||
- **只用 col 36**,col 34 保持 0
|
||||
- 工资单 HTML 现在只读 col 36
|
||||
|
||||
### 滨江店是否有提成
|
||||
3 月全员 0,4 月仍 0。**何时开始有,由用户决定**。
|
||||
|
||||
### "保洁阿姨" 不写入 V2
|
||||
她是兼职,5 月起若仍出现在考勤图片,只采集数据不写表。
|
||||
|
||||
---
|
||||
|
||||
## 营收分析引擎(5月新增 build_analysis.py)
|
||||
|
||||
当月若**没有**预生成的「N月营收分析xlsx」,用 skill 自带引擎从原始订单直接生成:
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/dameng-salary/build_analysis.py "~/Downloads/大梦N月账务处理" 2026-NN
|
||||
```
|
||||
|
||||
产出:`大梦可能实验室_N月营收分析_西湖店vs滨江店.xlsx`(13 sheets) + `_analysis_summary.json`(供工资写表)。
|
||||
summary.json 里 `employees` 即 12 人 (部门业绩/班次业绩/部门×班次业绩) 三元组,直接写 V2 cols 19/20/21。
|
||||
引擎已内置:菜品归口、团购→部门拆分、代金券剔除、部门×班次比例校正、滨江厨房/中班置0。
|
||||
|
||||
**强烈建议**:跑完用 workflow 做 5 路独立复核(部门POS/班次/团购/交叉/环比)——5 月就靠它抓出 2 个真 bug。
|
||||
|
||||
数据治理坑点(引擎已修,每月仍需注意):
|
||||
- 🔴 **幽灵汇总行**:POS「订单明细」末尾 `订单来源/订单号/营业日期 全='--'` 行,金额=全部真实行之和→订单级字段翻倍2x。必须 `if str(订单号).strip()=='--': continue`。
|
||||
- 🔴 **dish级 vs 订单级**:部门收入用「菜品明细」逐菜累加;订单级列被联台重复,勿用。
|
||||
- 🟡 **酒头/畅饮票=精酿**(非调酒)。
|
||||
- **分类名会变**:西湖 4月`brunch`→5月`brunch轻食简餐`,用 `startswith` 容错。
|
||||
- **新团购套餐每月扫一遍**(5月新增「咖啡任选7次卡」→咖啡)。
|
||||
- **约6%营收是"无菜品库"SKU** 靠关键词兜底,错归风险源。
|
||||
详见 `analysis_playbook.md`。
|
||||
|
||||
---
|
||||
|
||||
## 5 月跑通经验(已回填 · v1.1,覆盖更早的草稿口径)
|
||||
|
||||
**行号**:5月 = rows 27-39(13人,西湖27-33/滨江34-39),row 26 空行分隔。6月起始 = row 41。
|
||||
|
||||
**人员变动**:
|
||||
- 新增 **舒尧轩(昵称小胖)**,西湖调酒晚班/调酒师,底薪5600/KPI1000/行为500/管理0;无社保、暂无提成。
|
||||
- 现 **13 人**(西湖7+滨江6)。重排规则:西湖在前、滨江在后;舒尧轩紧跟胡舒(同调酒晚班)。
|
||||
|
||||
**出勤口径(用户确认,覆盖4月)**:
|
||||
- **年假计入出勤**(带薪)。出勤 = 当月天数 − 正常休息(不含年假)。各考勤表写明的出勤数为准。
|
||||
- 何简28(3天年假计入)、郭思儒27、秦天29、胡舒27、舒尧轩27 等。
|
||||
|
||||
**法定假期(用户确认)**:五一 **2 天**,**全员**法定假期天数=2、全给双倍 → 节假补贴 = 底薪/26.08 × 4。
|
||||
|
||||
**加班**:胡舒4h存、舒尧轩1.5h存 → 不发;其余"换钱"或未注明照发。蔡逸丰"3+9换钱(五一白班)"=12h换钱。
|
||||
|
||||
**工资单两条新展示规则**(slip_template.html 已内置,自动生效):
|
||||
- 未交社保者基本工资下注明"已含社保 ¥1,745.78"。
|
||||
- 出品提成行仅对有提成者显示,其余隐藏。
|
||||
|
||||
**考勤来源(5月实例)**:厨师考勤表.jpg(秦天/刘润祥/叶磊/尹志艳/何简)、小王考勤表.jpg(王瑛胤)、西湖店白班考勤表.jpg(宋群喜/郭思儒/保洁)、李想5月.xlsx、秋风5月.xlsx、腾讯夜班文档(胡舒/蔡逸丰/小胖=舒尧轩)。
|
||||
|
||||
**5月节假日**:五一2天(不是1天)。
|
||||
|
||||
**营收/出品分析**:本月新增完整分析能力,详见 `analysis_playbook.md`。5月双店POS四部门合计 西湖229,059/滨江156,326(+6.6%/+8.0%)。
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user