from pathlib import Path
import json
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.worksheet.pagebreak import Break
from openpyxl.comments import Comment
from openpyxl.workbook.properties import CalcProperties
from openpyxl.utils import get_column_letter

ROOT = Path(__file__).resolve().parents[1]
REPO = ROOT.parents[5]
SRC = REPO / 'modules/product/product-group-ai-native/planning/2026-09-29-project-product-framework/2026-09-30-backfill-and-software-matrix/software-features.json'
OLD = json.loads(SRC.read_text())
OUT = ROOT / 'output'
OUT.mkdir(exist_ok=True)
NAVY, TEAL, GOLD, PALE, BLUE = '183B56', '007F79', 'F39C12', 'EFF6FA', '1769AA'
LINE = Side(style='hair', color='CBD8E2')
MONEY = '#,##0" 元";[Red]-#,##0" 元";0" 元"'
PACKS = ['基础结算', 'AI识别结算', '按克称重结算', '全部结算', '营养结算']
PRICES = [5000, 10000, 10000, 15000, 20000]
MODS = [
    ('M01', '人员与组织', '人员档案、部门、身份类别', ['✓']*5, 'PC'),
    ('M02', '卡证服务', '发卡、绑定、挂失、补退卡', ['✓']*5, '卡证/终端'),
    ('M03', '账户与补贴', '充值退款、补贴发放、流水', ['✓']*5, 'PC/手机'),
    ('M04', '订单与退款', '订单查询、退款、异常补单', ['✓']*5, 'PC/手机'),
    ('M05', '消费规则', '餐次时段、身份、次数与限额', ['✓']*5, 'PC/终端'),
    ('M06', '菜品与菜单', '菜品价格、菜谱、设备下发', ['✓']*5, '智能排菜选配'),
    ('M07', '手机订餐', '选餐、预约、支付、评价', ['✓']*5, '标准订餐'),
    ('M08', '出餐与取餐', '备餐、打印、叫号、扫码核销', ['○']*5, '打印/屏/柜机'),
    ('M09', '按克称重', '称重计价、菜位归集、餐食记录', ['—','—','✓','✓','待定'], '称重设备'),
    ('M10', 'AI菜品识别', '识别、确认纠错、自动结算', ['—','✓','—','✓','待定'], 'AI识别终端'),
    ('M11', '电子菜牌', '菜名价格、营养标签、多屏发布', ['○']*5, '菜牌屏'),
    ('M12', '膳食营养分析', '个人/团体摄入与餐厅营养分析', ['○','○','○','○','✓'], '真实餐食数据'),
    ('M13', '健康档案', '个人档案、个体/团体健康趋势', ['○','○','○','○','✓'], '测评接口选配'),
    ('M14', '运动与体重', '步数、体重体脂、报告与趋势', ['○','○','○','○','✓'], '手机端'),
    ('M15', '手机账户服务', '登录、余额、充值、订单查询', ['✓']*5, '标准支付配置'),
    ('M16', 'AI营养服务', '健康问答、记餐、报告解读', ['○']*5, '模型资源另计'),
    ('M17', 'K12家校服务', '学生档案、家长缴费、账单对账', ['○']*5, '家校专项'),
    ('M18', '多组织经营', '商户归属、分权、跨区域报表', ['按管理配置']*5, '见管理与部署'),
    ('M19', '统计与核算', '收入销量、充值补贴、对账', ['✓']*5, 'PC'),
    ('M20', '权限与设备', '角色、数据权限、设备配置', ['✓']*5, '标准兼容设备'),
    ('M21', '餐线排菜Pad', '称位状态、菜品下发、菜谱维护', ['—','—','○','○','○'], 'Pad/称重设备'),
    ('M22', '智能厨房协同', '烹饪机器人与菜谱接口', ['—','—','—','—','○'], '专项接口'),
]
FEATURES = []
for f in OLD['features']:
    mid = f['group'].split()[0]
    n = int(f['id'][1:])
    marks = [f['marks'][2]]*5
    reason = '旧标准结算＋最新基础公共模块规则'
    if n in (4,5): marks = ['○']*5; reason = 'K12学生档案按家校专项选配'
    if n == 31: marks = ['○']*5; reason = '智能排菜选配'
    if 32 <= n <= 35: marks = ['✓']*5; reason = '旧报价规则：标准手机订餐归入基础能力'
    if n == 36: marks = ['待核验']*5; reason = '旧细项未明确订餐截止/取消规则'
    if 37 <= n <= 43: marks = ['○']*5; reason = '出餐/叫号/柜机为独立选配'
    if 44 <= n <= 48: marks = ['—','—','✓','✓','待定']; reason = '按克/全部包含；营养版结算适配待确认'
    if n in (47,48): marks = ['—','—','○','○','待定']; reason += '；绑盘/免绑盘按设备形态选一种'
    if n == 49: marks = ['—','—','○','○','○']; reason = '余量联动按设备及范围选配'
    if 50 <= n <= 52: marks = ['—','✓','—','✓','待定']; reason = 'AI/全部包含；营养版AI适配待确认'
    if 53 <= n <= 58: marks = ['○']*5; reason = '展示/营养选配'
    if 56 <= n <= 58: marks[-1] = '✓'; reason = '旧营养版明确膳食分析'
    if n in (59,60): marks = ['○']*5; reason = '营养结果/展示范围按配置'
    if n == 60: marks[-1] = '✓'
    if 61 <= n <= 66: marks = ['○','○','○','○','✓']; reason = '旧营养版健康、运动与体重描述'
    if n == 63: marks = ['○']*5; reason = '测评数据接口独立选配'
    if 67 <= n <= 70: marks = ['✓']*5; reason = '合并基础手机账户与查询'
    if n == 71: marks = ['○','○','○','○','✓']; reason = '旧营养版日常膳食记录'
    if 72 <= n <= 81: marks = ['○']*5; reason = 'AI营养/K12专项选配'
    if 82 <= n <= 85: marks = ['按管理配置']*5; reason = '与管理范围关联，不由功能套餐赠送'
    if n == 86: marks = ['待核验']*5; reason = '不得承诺跨公司自动资金共享/清算'
    if 96 <= n <= 100: marks = ['—','—','○','○','○']; reason = '按餐线设备选配，不默认附送Pad'
    if n == 101: marks = ['—','—','—','—','○']; reason = '营养项目的厨房专项接口'
    marks = ['待核验' if x == '?' else x for x in marks]
    FEATURES.append({**f, 'module_id':mid, 'new_marks':marks, 'decision':reason})
DATA = {
    'date':'2026-10-07', 'rules':{
        'software_fee':'一次性授权', 'canteen_count_in_software_quote':False,
        'headcount_in_software_quote':False, 'capacity_only_affects_server':True,
        'hardware_adaptation_basis':'不同类型/型号；同型号多台一次',
        'nutrition_ai_weight_scope':'待定', 'group_price':None,
    },
    'packages':[{'name':n,'price':p} for n,p in zip(PACKS,PRICES)],
    'modules':[{'id':m,'name':n,'description':d,'marks':k,'dependency':dep} for m,n,d,k,dep in MODS],
    'features':FEATURES, 'old_module_descriptions':OLD['original_modules'],
    'source':str(SRC.relative_to(REPO)),
    'server_assessment':{'inputs':['食堂数','启用就餐人数','峰值并发','订单量','可用性要求'],
                         'outputs':['服务器规格','服务器数量','服务器价格'],'in_software_quote':False},
}
(ROOT/'pricing-feature-data.json').write_text(json.dumps(DATA,ensure_ascii=False,indent=2))


def base(name, widths, paper='A4', landscape=False):
    w = Workbook(); s = w.active; s.title = name
    w.calculation = CalcProperties(calcId=191029, fullCalcOnLoad=True)
    for i,x in enumerate(widths,1): s.column_dimensions[get_column_letter(i)].width=x
    setup(s,paper,landscape)
    return w,s


def setup(s,paper='A4',landscape=False):
    s.sheet_view.showGridLines=False
    s.sheet_view.zoomScale=90
    s.page_setup.orientation='landscape' if landscape else 'portrait'
    s.page_setup.paperSize=s.PAPERSIZE_A3 if paper=='A3' else s.PAPERSIZE_A4
    s.page_setup.fitToWidth=1; s.page_setup.fitToHeight=1
    s.sheet_properties.pageSetUpPr.fitToPage=True
    s.page_margins.left=.22; s.page_margins.right=.22
    s.page_margins.top=.25; s.page_margins.bottom=.25
    s.oddFooter.center.text='康比特数字体育科技 · 2026-10-07'
    s.oddFooter.right.text='&P / &N'


def row(s,r,values,kind=None,height=27):
    s.row_dimensions[r].height=height
    for c,v in enumerate(values,1):
        if v is None: continue
        x=s.cell(r,c,v)
        x.font=Font(name='Microsoft YaHei',size=11,color=NAVY)
        x.alignment=Alignment(vertical='center',wrap_text=True)
        x.border=Border(bottom=LINE)
        if kind=='head':
            x.fill=PatternFill('solid',fgColor=TEAL); x.font=Font(name='Microsoft YaHei',size=11,bold=True,color='FFFFFF')
        elif r%2==0: x.fill=PatternFill('solid',fgColor=PALE)


def banner(s,r,text,last,kind='title'):
    s.merge_cells(start_row=r,start_column=1,end_row=r,end_column=last)
    x=s.cell(r,1,text); x.alignment=Alignment(vertical='center',wrap_text=True)
    x.font=Font(name='Microsoft YaHei',size=18 if kind=='title' else 11,bold=kind=='title',color='FFFFFF' if kind=='title' else NAVY)
    x.fill=PatternFill('solid',fgColor=NAVY if kind=='title' else PALE)
    s.row_dimensions[r].height=36 if kind=='title' else 28


def section(s,r,text,last):
    banner(s,r,text,last,'note'); s.cell(r,1).font=Font(name='Microsoft YaHei',size=12,bold=True,color=TEAL)


def note(x,text): x.comment=Comment(text,'康比特 · 来源/规则')


def inputcell(x,money=False):
    x.font=Font(name='Microsoft YaHei',size=11,color=BLUE)
    x.fill=PatternFill('solid',fgColor='FFF3CF')
    if money: x.number_format=MONEY


def dropdown(s,addr,values):
    d=DataValidation(type='list',formula1='"'+','.join(values)+'"',allow_blank=False)
    d.errorTitle='请选择列表中的项目'; d.error='请使用下拉选项。'; d.showErrorMessage=True; d.errorStyle='stop'
    s.add_data_validation(d); d.add(addr)


def sourcecomment(s,rows,col=5):
    for r in rows: note(s.cell(r,col),'来源：本线程已确认价格，2026-10-07。空白项待核价；可人工修改。')


w,p=base('软件价格表',[12,24,41,8,15,12,26],paper='A3',landscape=True)
banner(p,1,'智慧食堂软件价格表',7)
banner(p,2,'一次性软件授权 ｜ 蓝字可改 ｜ 硬件、服务器另报',7,'note')
section(p,4,'01  功能套餐 · 选一档，基础结算不重复收费',7)
header=['分类','项目','功能简述','单位','价格（元）','周期','备注']
row(p,5,header,'head')
descs=['人员账户、补贴、结算、订餐、报表','基础结算＋菜品识别、纠错、计价','基础结算＋按克计价、餐食记录','基础结算＋AI识别＋按克称重','基础结算＋膳食分析、健康档案']
for r,n,d,pr in zip(range(6,11),PACKS,descs,PRICES):
    row(p,r,['软件',n,d,'套',pr,'一次性','AI/按克是否包含待定' if n=='营养结算' else '不含硬件实物'])
section(p,12,'02  管理能力 · 与功能套餐分开',7); row(p,13,header,'head')
row(p,14,['管理','单企业管理','同一单位统一管理，可覆盖多个食堂','项',0,'随套餐','已含，不另加价'])
row(p,15,['管理','集团/多单位管理','单位分权、分别对账、总部汇总','项',None,'一次性','待填写价格'])
section(p,17,'03  部署服务 · 选一种',7); row(p,18,header,'head')
row(p,19,['部署','云端部署','云端开通与基础部署','项',0,'一次性','云服务器另报'])
row(p,20,['部署','普通本地部署','客户环境安装与配置','项',5000,'一次性','客户服务器自备/另购'])
row(p,21,['部署','信创适配部署','约定信创环境适配与部署','项',20000,'一次性','已含本地部署费'])
section(p,23,'04  额外硬件适配',7); row(p,24,header,'head')
row(p,25,['适配','其他硬件适配','按不同类型/型号收一次','种',1000,'一次性','同型号多台不重复收'])
section(p,27,'05  按需选配 · 留空金额需核价',7); row(p,28,header,'head')
OPT = ['电子菜牌','K12家校服务','出餐与取餐','AI营养服务','餐线排菜Pad','菜品余量联动','智能厨房协同','其他系统接口','膳食营养分析','健康档案','运动与体重']
OPTD = ['菜名价格、营养标签、多屏发布','学生档案、家长缴费与账单','备餐、打印、叫号、柜机核销','问答、记餐、报告解读','称位状态与现场排菜','余量展示与补餐提醒','机器人与菜谱接口','约定外部系统与接口范围','个体/团体膳食与营养分析','健康档案与健康趋势','步数、体重体脂与报告']
for r,n,d in zip(range(29,40),OPT,OPTD): row(p,r,['选配',n,d,'项',None,'待确认','按范围核价'])
banner(p,41,'营养版保留旧营养健康功能；AI/按克范围待确认。已包含功能不重复购买。',7,'note')
banner(p,42,'税费、授权期限、升级与维保条款需确认；不含服务器、硬件实物及第三方资源费。',7,'note')
p.print_area='A1:G42'; p.freeze_panes='C6'
for r in [*range(6,11),14,15,19,20,21,25,*range(29,40)]: inputcell(p.cell(r,5),True)
sourcecomment(p,[*range(6,11),14,15,19,20,21,25])

q=w.create_sheet('项目报价'); setup(q)
for i,x in enumerate([6,24,39,8,9,15,16],1): q.column_dimensions[get_column_letter(i)].width=x
banner(q,1,'智慧食堂软件项目报价',7)
banner(q,2,'选择套餐、管理范围、部署方式；选配按需添加',7,'note')
for r,label,val in [(3,'项目',''),(4,'客户',''),(6,'套餐',PACKS[0]),(7,'管理','单企业'),(8,'部署','云端部署')]:
    row(q,r,[label,val],height=29); q.merge_cells(start_row=r,start_column=2,end_row=r,end_column=4); inputcell(q.cell(r,2))
row(q,9,['额外型号',0,'按不同类型/型号填种数；同型号多台只算一种'],height=38); inputcell(q['B9']); q.merge_cells('C9:G9')
dropdown(q,'B6',PACKS); dropdown(q,'B7',['单企业','集团/多单位']); dropdown(q,'B8',['云端部署','普通本地部署','信创适配部署'])
dv=DataValidation(type='whole',operator='greaterThanOrEqual',formula1=0,allow_blank=False); dv.showErrorMessage=True; dv.error='请输入非负整数种类数。'; q.add_data_validation(dv); dv.add(q['B9'])
row(q,12,['序号','项目','简述','单位','数量','单价（元）','金额（元）'],'head')
P="'软件价格表'!"
def lookup_price(key,start,end):
    ref=f'INDEX({P}$E${start}:$E${end},MATCH({key},{P}$B${start}:$B${end},0))'
    return f'IFERROR(IF(COUNT({ref})=1,{ref},""),"")'
row(q,13,[1,'=B6',f'=IFERROR(INDEX({P}$C$6:$C$10,MATCH(B6,{P}$B$6:$B$10,0)),"")','套',1,'='+lookup_price('B6',6,10)])
row(q,14,[2,'=B7','=IF(B7="单企业","公共管理已含","单位分权与总部汇总")','项',1,f'=IF(B7="单企业",{P}E14,IF(ISNUMBER({P}E15),{P}E15,""))'])
row(q,15,[3,'=B8','=IF(B8="信创适配部署","已含本地部署，不叠加",IF(B8="云端部署","云端部署服务免费","客户环境安装与配置"))','项',1,'='+lookup_price('B8',19,21)])
row(q,16,[4,'其他硬件适配','按额外类型/型号去重','种','=B9',f'={P}E25'])
for r in range(13,17): q.cell(r,7,f'=IF(OR(NOT(ISNUMBER(E{r})),E{r}<0,NOT(ISNUMBER(F{r})),F{r}<0),"",E{r}*F{r})')
section(q,18,'选配 · 不需要时选择“不选”',7)
for r in range(19,24):
    row(q,r,[r-14,'不选',f'=IF(B{r}="不选","—",IFERROR(INDEX({P}$C$29:$C$39,MATCH(B{r},{P}$B$29:$B$39,0)),"待核价"))','项',1,f'=IF(B{r}="不选",0,{lookup_price(f"B{r}",29,39)})',f'=IF(B{r}="不选",0,IF(AND(ISNUMBER(F{r}),F{r}>=0),F{r},""))'])
    dropdown(q,f'B{r}',['不选']+OPT); inputcell(q.cell(r,2))
section(q,24,'补充项目 · 可人工填写',7)
for r in range(25,28):
    row(q,r,[r-14,None,None,'项',1,None,f'=IF(B{r}="",0,IF(AND(ISNUMBER(F{r}),F{r}>=0,ISNUMBER(E{r}),E{r}>=0),E{r}*F{r},""))'])
    for c in [2,3,5,6]: inputcell(q.cell(r,c),c==6)
pending=['COUNTBLANK(G13:G16)>0','NOT(ISNUMBER(B9))','B9<0','MOD(B9,1)<>0']
pending += [f'AND(B{r}<>"不选",NOT(ISNUMBER(G{r})))' for r in range(19,24)]
pending += [f'AND(B{r}<>"",NOT(ISNUMBER(G{r})))' for r in range(25,28)]
duplicate = '+'.join(f'IF(B{r}="不选",0,COUNTIF(B19:B23,B{r})-1)' for r in range(19,24))
pending.append(f'({duplicate})>0')
row(q,29,[None,'核价状态',None,None,None,None,'=IF(IFERROR(OR('+','.join(pending)+'),TRUE),"待核价","已核价")'],height=25)
row(q,30,[None,'软件及服务合计',None,None,None,None,'=IF(G29="已核价",SUM(G13:G27),"待核价")'],'head',34); q.merge_cells('B30:F30')
banner(q,31,'不含硬件、服务器及第三方资源费；一次性授权，金额单位：元。',7,'note')
banner(q,32,'空价、重复选配或非法输入须核定；营养版AI/按克范围待定。',7,'note')
banner(q,33,'税费、使用期限、升级及维保：待确认。',7,'note')
q.print_area='A1:G33'; q.freeze_panes='C13'
for r in [*range(13,17),*range(19,24),*range(25,28),30]:
    for c in [6,7]: q.cell(r,c).number_format=MONEY
note(q['B9'],'本线程规则：按不同类型/型号，同型号多台只收一次。填写不在当前套餐标准接入范围内的额外型号种类。')
note(q['G30'],'不含服务器与硬件；选中未定价项、重复选配或非法值不输出完整合计。')
w.save(OUT/'智慧食堂软件报价单.xlsx')

v,s=base('模块对比',[6,23,39,15,15,15,15,15,24],paper='A3',landscape=True)
banner(s,1,'智慧食堂软件版本 · 模块对比',9)
banner(s,2,'五档功能套餐＋管理配置＋部署服务 ｜ 软件一次性授权',9,'note')
row(s,3,['序号','模块','简述']+PACKS+['依赖/边界'],'head',34)
row(s,4,[None,'授权费（元）',None]+PRICES+['集团附加价待填'],height=29); s.merge_cells('B4:C4')
for c in range(4,9): s.cell(4,c).number_format=MONEY; inputcell(s.cell(4,c),True); note(s.cell(4,c),'来源：本线程2026-10-07确认，一次性软件授权费。')
row(s,5,[None,'适合场景/人群',None,'企业/机关\n刷卡、扫码、刷脸','小碗自选\n需要快速结算','自助取餐\n按实际重量计费','混合餐线\n多种结算并用','营养健康餐厅\n就餐者/营养师','按项目设备配置'],height=40); s.merge_cells('B5:C5')
row(s,6,[None,'痛点',None,'账务分散\n餐补难核对','人工计价慢\n容易错漏','份量固定\n取餐浪费','多设备、多模式\n系统不统一','测量与餐食脱节\n难持续跟踪','人群/身份需关联'],height=38); s.merge_cells('B6:C6')
row(s,7,[None,'主要流程',None,'身份→选餐\n结算→对账','识别→确认\n计价→结算','身份→称重\n计价→结算','按餐线选餐\n统一订单对账','建档→餐食记录\n分析→持续查看','未确认能力不承诺'],height=42); s.merge_cells('B7:C7')
row(s,8,['序号','模块','功能简述']+PACKS+['依赖/边界'],'head',30)
for r,(mid,n,d,marks,dep) in enumerate(MODS,9):
    row(s,r,[r-8,n,d]+marks+[dep],height=30)
    for c,k in enumerate(marks,4):
        s.cell(r,c).alignment=Alignment(horizontal='center',vertical='center',wrap_text=True)
        if k=='✓': s.cell(r,c).font=Font(name='Microsoft YaHei',size=13,bold=True,color=TEAL)
        if k in ['待定','按管理配置']: s.cell(r,c).font=Font(name='Microsoft YaHei',size=10,color='A36700')
    note(s.cell(r,2),'来源：历史功能描述 '+mid+'；细项见“细项参考”。符号表示模块主体范围，个别子功能可为选配或待核验。')
banner(s,32,'✓ 包含  ○ 选配  — 不含  待定：范围未确认；“按管理配置”见下一页。',9,'note')
banner(s,33,'AI识别、按克已含基础；营养版AI/按克待定。绑盘/免绑盘按设备选型。',9,'note')
banner(s,34,'服务器单独评估；软件价格不随食堂数、人数变化。硬件实物另购。',9,'note')
s.freeze_panes='D9'; s.auto_filter.ref='A8:I30'; s.print_area='A1:I34'

t=v.create_sheet('管理与部署'); setup(t,landscape=True)
for i,x in enumerate([22,30,30,35,35],1): t.column_dimensions[get_column_letter(i)].width=x
banner(t,1,'管理范围与部署方式',5)
section(t,3,'01  管理范围 · 按主体和权限选择',5)
row(t,4,['对比项','单企业 · 单食堂','单企业 · 多食堂','集团 / 多单位','一句话解释'],'head',35)
org=[
    ['定义','同一单位统一管理','同一单位统一管理','各单位分别管理，上层汇总','不按客户名称判断'],
    ['人员与补贴','本单位统一管理','本单位统一管理','各单位分别管理','单位是餐补/账务主体'],
    ['账务与报表','档口/商户对账','食堂/档口/商户对账','单位分别对账，上层汇总','自动资金清算待核验'],
    ['权限范围','食堂、档口、商户','增加跨食堂分权','总部、单位、食堂分权','实际角色按授权'],
    ['商户经营','按商户分别经营与对账','按商户分别经营与对账','增加单位与商户关系','五家商户不等于五个单位'],
    ['软件费用','按功能套餐','同套餐同价，不按食堂增加','功能套餐＋集团附加价待填','食堂数、人数只影响服务器'],
    ['典型客户','工厂/机关食堂','同一单位多个餐厅','集团、园区多企业','园区多企业按多单位评估'],
]
for r,vals in enumerate(org,5): row(t,r,vals,height=38)
section(t,13,'02  部署服务 · 三种选一',5)
row(t,14,['项目','云端部署','普通本地部署','信创适配部署','边界'],'head',30)
row(t,15,['部署费（元）',0,5000,20000,'信创已含本地，不叠加'])
for c in [2,3,4]: t.cell(15,c).number_format=MONEY
row(t,16,['运行位置','供应商云环境','客户服务器/客户云环境','约定的信创环境','客户专属云按实际方案确认'],height=35)
row(t,17,['前置条件','联网、主体与支付配置','服务器、网络、域名与配置','约定CPU/OS/数据库及接口','资源/第三方费用另报'],height=40)
row(t,18,['服务器费用','另行评估','客户自备或另购','按约定资源组合评估','不计入软件报价合计'],height=35)
banner(t,20,'先选功能套餐，再选管理范围，最后定部署；食堂数/人数只用于服务器数量与价格评估。',5,'note')
t.freeze_panes='B5'; t.print_area='A1:E20'

d=v.create_sheet('细项参考'); setup(d,landscape=True)
widths=[9,24,32,20,14,14,14,14,14,37]
for i,x in enumerate(widths,1): d.column_dimensions[get_column_letter(i)].width=x
banner(d,1,'软件功能细项参考 · 101项',10)
banner(d,2,'沿用历史描述；套餐主体与具体子功能分别标示。来源保留在单元格批注。',10,'note')
row(d,3,['原编号','模块','功能细项','软件端']+PACKS+['范围说明'],'head',34)
for r,f in enumerate(FEATURES,4):
    name=next(m[1] for m in MODS if m[0]==f['module_id'])
    brief=f['decision'].split('；')[0]
    if len(brief)>28: brief=brief[:27]+'…'
    row(d,r,[f['id'],name,f['feature'],f['terminal']]+f['new_marks']+[brief],height=34)
    note(d.cell(r,3),'来源：'+f['source']+'\n历史模块：'+f['group']+'\n处理：'+f['decision']+'\n旧注：'+f['note'])
    for c in range(5,10): d.cell(r,c).alignment=Alignment(horizontal='center',vertical='center',wrap_text=True)
d.freeze_panes='E4'; d.auto_filter.ref='A3:J104'; d.print_area='A1:J104'; d.print_title_rows='1:3'
d.page_setup.fitToHeight=0
for r in [29,54,79]: d.row_breaks.append(Break(id=r))
v.save(OUT/'智慧食堂软件版本功能对比.xlsx')
print(json.dumps({'outputs':[str(x) for x in OUT.glob('*.xlsx')], 'modules':len(MODS),'features':len(FEATURES)},ensure_ascii=False))
