# -*- coding: utf-8 -*-
"""Build the 2026 research autumn promotion review report (Word)."""
import json
import os
import sys
from datetime import datetime
from collections import Counter

from PIL import Image
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.enum.section import WD_ORIENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DATA = os.path.join(BASE, '营销活动', '科研促销复盘_20261103', '数据', 'promo_review_2026.json')
OUT_DIR = os.path.join(BASE, '营销活动', '科研促销复盘_20261103')
OUT = os.path.join(OUT_DIR, '宝锐生物_科研秋季促销复盘报告_20261103_V01.docx')
CHART_DIR = os.path.join(OUT_DIR, '图表')
LOGO = os.path.expanduser('~/work/公司资料/logo_transparent.png')
LOGO_WHITE = '/tmp/biori_logo_white_bg.png'

SLOGAN = '守护生命健康，缔造美好生活'
COMPANY = '珠海宝锐生物科技有限公司'
TEL = '0756-8699969'
MAIL = 'marketing@biori.com'
SITE = 'www.biori.com'
ADDR = '珠海市香洲区水岸一路 88 号'

# ---------------------------------------------------------------------------
def load():
    with open(DATA, encoding='utf-8') as f:
        return json.load(f)


def f2(x):
    return f'{x:,.2f}'


def pct(x, digits=2):
    return f'{x * 100:.{digits}f}%'


def add_run_font(run, size=10.5, bold=False, color=None, east='宋体', latin='Times New Roman'):
    run.font.name = latin
    run.font.size = Pt(size)
    run.font.bold = bold
    rPr = run._element.get_or_add_rPr()
    rFonts = rPr.find(qn('w:rFonts'))
    if rFonts is None:
        rFonts = OxmlElement('w:rFonts')
        rPr.append(rFonts)
    rFonts.set(qn('w:ascii'), latin)
    rFonts.set(qn('w:hAnsi'), latin)
    rFonts.set(qn('w:eastAsia'), east)
    if color:
        run.font.color.rgb = RGBColor.from_string(color)


def shade(cell, hex_color):
    tcPr = cell._tc.get_or_add_tcPr()
    shd = OxmlElement('w:shd')
    shd.set(qn('w:fill'), hex_color)
    tcPr.append(shd)


def cell_margins(cell, top=40, bottom=40, left=70, right=70):
    tcPr = cell._tc.get_or_add_tcPr()
    tcMar = OxmlElement('w:tcMar')
    for side, val in [('top', top), ('bottom', bottom), ('left', left), ('right', right)]:
        el = OxmlElement('w:' + side)
        el.set(qn('w:w'), str(val))
        el.set(qn('w:type'), 'dxa')
        tcMar.append(el)
    tcPr.append(tcMar)


def table_borders(table, color='D9E2EF', sz='4'):
    tbl = table._tbl
    tblPr = tbl.tblPr
    borders = OxmlElement('w:tblBorders')
    for edge in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'):
        el = OxmlElement('w:' + edge)
        el.set(qn('w:val'), 'single')
        el.set(qn('w:sz'), sz)
        el.set(qn('w:space'), '0')
        el.set(qn('w:color'), color)
        borders.append(el)
    tblPr.append(borders)


def set_cell_text(cell, text, size=8.5, bold=False, color=None, align=WD_ALIGN_PARAGRAPH.LEFT):
    cell.text = ''
    p = cell.paragraphs[0]
    p.alignment = align
    p.paragraph_format.space_before = Pt(0)
    p.paragraph_format.space_after = Pt(0)
    p.paragraph_format.line_spacing = 1.15
    r = p.add_run(str(text))
    add_run_font(r, size=size, bold=bold, color=color)
    cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
    cell_margins(cell)


def add_table(doc, headers, rows, widths, caption='', font_size=8.3, header_fill='0F3D6E', first_col_bold=False):
    if caption:
        p = doc.add_paragraph()
        p.paragraph_format.space_before = Pt(4)
        p.paragraph_format.space_after = Pt(2)
        p.paragraph_format.keep_with_next = True
        r = p.add_run(caption)
        add_run_font(r, size=8.5, bold=True, color='0F3D6E')
    table = doc.add_table(rows=1, cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    table.style = 'Table Grid'
    table_borders(table)
    hdr = table.rows[0].cells
    for i, h in enumerate(headers):
        set_cell_text(hdr[i], h, size=8.1, bold=True, color='FFFFFF', align=WD_ALIGN_PARAGRAPH.CENTER)
        shade(hdr[i], header_fill)
    for ri, row in enumerate(rows):
        cells = table.add_row().cells
        for i, val in enumerate(row):
            bold = first_col_bold and i == 0
            set_cell_text(cells[i], val, size=font_size, bold=bold,
                          color=('0F3D6E' if bold else None),
                          align=(WD_ALIGN_PARAGRAPH.LEFT if i == 0 else WD_ALIGN_PARAGRAPH.CENTER))
            if ri % 2 == 1:
                shade(cells[i], 'F6F9FD')
    for row in table.rows:
        trPr = row._tr.get_or_add_trPr()
        cant = OxmlElement('w:cantSplit')
        trPr.append(cant)
    for i, w in enumerate(widths):
        for row in table.rows:
            row.cells[i].width = Cm(w)
    return table


def add_caption(doc, text):
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after = Pt(4)
    r = p.add_run(text)
    add_run_font(r, size=8.3, color='64748B')


def add_picture(doc, path, caption):
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after = Pt(2)
    p.paragraph_format.keep_with_next = True
    run = p.add_run()
    run.add_picture(path, width=Cm(16.2))
    add_caption(doc, caption)


def H1(doc, text):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(10)
    p.paragraph_format.space_after = Pt(5)
    p.paragraph_format.keep_with_next = True
    r = p.add_run(text)
    add_run_font(r, size=16, bold=True, color='0F3D6E')
    pPr = p._p.get_or_add_pPr()
    ol = OxmlElement('w:outlineLvl')
    ol.set(qn('w:val'), '0')
    pPr.append(ol)
    pBdr = OxmlElement('w:pBdr')
    bottom = OxmlElement('w:bottom')
    bottom.set(qn('w:val'), 'single')
    bottom.set(qn('w:sz'), '10')
    bottom.set(qn('w:space'), '2')
    bottom.set(qn('w:color'), '0E9F9A')
    pBdr.append(bottom)
    pPr.append(pBdr)
    return p


def H2(doc, text):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(7)
    p.paragraph_format.space_after = Pt(3)
    p.paragraph_format.keep_with_next = True
    pPr = p._p.get_or_add_pPr()
    ol = OxmlElement('w:outlineLvl')
    ol.set(qn('w:val'), '1')
    pPr.append(ol)
    r = p.add_run(text)
    add_run_font(r, size=11.5, bold=True, color='0F3D6E')
    return p


def P(doc, text, size=10.2, bold=False, color=None, after=3, indent=True, line=1.55):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(0)
    p.paragraph_format.space_after = Pt(after)
    p.paragraph_format.line_spacing = line
    if indent:
        p.paragraph_format.first_line_indent = Cm(0.74)
    r = p.add_run(text)
    add_run_font(r, size=size, bold=bold, color=color)
    return p


def B(doc, text, size=10.2):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(0)
    p.paragraph_format.space_after = Pt(3)
    p.paragraph_format.line_spacing = 1.55
    p.paragraph_format.left_indent = Cm(0.35)
    p.paragraph_format.first_line_indent = Cm(-0.35)
    r0 = p.add_run('• ')
    add_run_font(r0, size=size, bold=True, color='0E9F9A')
    r = p.add_run(text)
    add_run_font(r, size=size)
    return p


def add_page_number_footer(section):
    footer = section.footer
    footer.is_linked_to_previous = False
    p1 = footer.paragraphs[0]
    p1.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p1.paragraph_format.space_after = Pt(1)
    r = p1.add_run(SLOGAN)
    add_run_font(r, size=8.5, bold=True, color='0F3D6E')

    p2 = footer.add_paragraph()
    p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p2.paragraph_format.space_after = Pt(1)
    r = p2.add_run(f'{COMPANY} ｜ {TEL} ｜ {MAIL} ｜ {SITE} ｜ {ADDR}')
    add_run_font(r, size=6.9, color='64748B')

    p3 = footer.add_paragraph()
    p3.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p3.paragraph_format.space_after = Pt(0)
    r = p3.add_run('第 ')
    add_run_font(r, size=7.2, color='64748B')
    _add_field(p3, 'PAGE')
    r = p3.add_run(' 页 / 共 ')
    add_run_font(r, size=7.2, color='64748B')
    _add_field(p3, 'NUMPAGES')
    r = p3.add_run(' 页')
    add_run_font(r, size=7.2, color='64748B')


def _add_field(paragraph, field):
    fld = OxmlElement('w:fldSimple')
    fld.set(qn('w:instr'), field)
    r = OxmlElement('w:r')
    t = OxmlElement('w:t')
    t.text = '1'
    r.append(t)
    fld.append(r)
    paragraph._p.append(fld)


def add_header(section):
    header = section.header
    header.is_linked_to_previous = False
    p = header.paragraphs[0]
    p.alignment = WD_ALIGN_PARAGRAPH.LEFT
    p.paragraph_format.space_after = Pt(0)
    run = p.add_run()
    run.add_picture(LOGO_WHITE, width=Cm(2.1))
    r = p.add_run(f'    {COMPANY}  ·  科研促销复盘  |  2026-11')
    add_run_font(r, size=7.8, color='64748B')


def make_logo_white():
    img = Image.open(LOGO).convert('RGBA')
    bg = Image.new('RGB', img.size, (255, 255, 255))
    bg.paste(img, mask=img.split()[3])
    bg.save(LOGO_WHITE, 'PNG')


def setup(doc):
    section = doc.sections[0]
    section.page_width = Cm(21)
    section.page_height = Cm(29.7)
    section.top_margin = Cm(2.0)
    section.bottom_margin = Cm(1.8)
    section.left_margin = Cm(2.2)
    section.right_margin = Cm(2.2)
    section.header_distance = Cm(0.9)
    section.footer_distance = Cm(0.8)
    style = doc.styles['Normal']
    style.font.name = 'Times New Roman'
    style.font.size = Pt(10.2)
    style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
    style.paragraph_format.space_after = Pt(3)
    style.paragraph_format.line_spacing = 1.55
    add_header(section)
    add_page_number_footer(section)
    # let Word update PAGE/NUMPAGES fields
    settings = doc.settings.element
    if settings.find(qn('w:updateFields')) is None:
        up = OxmlElement('w:updateFields')
        up.set(qn('w:val'), 'true')
        settings.append(up)


def cover(doc, d):
    s = d['summary']
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(8)
    r = p.add_run('BIORI BIOTECH · 营销中心')
    add_run_font(r, size=9, bold=True, color='0E9F9A')

    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(26)
    p.paragraph_format.space_after = Pt(4)
    r = p.add_run('2026 年科研秋季促销复盘报告')
    add_run_font(r, size=27, bold=True, color='0F3D6E')

    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(10)
    r = p.add_run('浙江 / 广东 · B 口径')
    add_run_font(r, size=14, bold=True, color='0E7490')

    # accent line
    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(16)
    pPr = p._p.get_or_add_pPr()
    pBdr = OxmlElement('w:pBdr')
    bottom = OxmlElement('w:bottom')
    bottom.set(qn('w:val'), 'single')
    bottom.set(qn('w:sz'), '16')
    bottom.set(qn('w:space'), '1')
    bottom.set(qn('w:color'), '0E9F9A')
    pBdr.append(bottom)
    pPr.append(pBdr)

    rows = [
        ('报告范围', '浙江 2026-08-20–09-30；广东 2026-09-01–10-16'),
        ('数据口径', '标准销售订单 + 已审核 + 非赠品 + 剔除宝锐/宝泰/横琴；含税价税合计'),
        ('基线', '促销前 4–9 月完整月度销售额（含促销日，保守口径）'),
        ('数据来源', 'ERP 销售订单（截至 2026-11-03）'),
        ('报告日期', '2026 年 11 月'),
    ]
    t = doc.add_table(rows=0, cols=2)
    t.alignment = WD_TABLE_ALIGNMENT.LEFT
    t.autofit = False
    table_borders(t, color='D9E2EF')
    for k, v in rows:
        cells = t.add_row().cells
        set_cell_text(cells[0], k, size=8.8, bold=True, color='0F3D6E')
        shade(cells[0], 'F1F7FD')
        set_cell_text(cells[1], v, size=8.5)
    for row in t.rows:
        row.cells[0].width = Cm(3.4)
        row.cells[1].width = Cm(12.8)

    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(20)
    r = p.add_run('核心结果：促销期销售额 110,887.50 元，较 4–9 月基线月均增长 88.99%')
    add_run_font(r, size=10, bold=True, color='0F3D6E')

    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(55)
    r = p.add_run('内部管理资料 · 请勿外传')
    add_run_font(r, size=8.5, color='9A3412')

    doc.add_page_break()


def section_results(doc, d):
    s = d['summary']
    H1(doc, '一、成果')
    P(doc, f'促销期实现销售额 {f2(s["total"])} 元，{s["orders"]} 单，{s["customers"]} 个客户，{s["groups"]} 个课题组。按两区活动天数分别折算为 30 天月均，合计 {f2(s["promo_monthly_equivalent"])} 元；4–9 月基线月均 {f2(s["baseline_monthly"])} 元，增幅 {pct(s["growth_vs_baseline_monthly"])}。')
    P(doc, f'新增课题组 {s["new_groups"]} 个，贡献 {f2(s["new_group_amount"])} 元，占 {pct(s["new_group_amount"]/s["total"])}；老课题组 {s["groups"]-s["new_groups"]} 个，贡献 {f2(s["old_group_amount"])} 元，占 {pct(s["old_group_amount"]/s["total"])}。新增组平均 {f2(s["new_group_amount"]/s["new_groups"])} 元，老组平均 {f2(s["old_group_amount"]/(s["groups"]-s["new_groups"]))} 元。')
    P(doc, f'浙江 {f2(d["region"][1]["amount"])} 元，占 {pct(d["region"][1]["share"])}，月均 {f2(d["region"][1]["promo_monthly"])} 元，增长 {pct(d["region"][1]["growth"])}；广东 {f2(d["region"][0]["amount"])} 元，占 {pct(d["region"][0]["share"])}，月均 {f2(d["region"][0]["promo_monthly"])} 元，增长 {pct(d["region"][0]["growth"])}。')
    P(doc, f'{s["reach_groups"]} 个课题组达到礼品档，{s["groups"]-s["reach_groups"]} 个未达 1,000 元；{s["reach_customers"]} 个客户达到客户档，{s["customers"]-s["reach_customers"]} 个未达。经销商 {s["dealer_orders"]} 单、{f2(s["dealer_amount"])} 元，占 {pct(s["dealer_amount"]/s["total"])}；其中 {s["dealer_for_group_orders"]} 单可归终端课题组 {f2(s["dealer_for_group_amount"])} 元，{s["dealer_orders"]-s["dealer_for_group_orders"]} 单未注明 {f2(s["dealer_self_amount"])} 元。')

    add_table(doc, ['指标', '数值', '口径'], [
        ('促销期销售额', f'{f2(s["total"])} 元', '含税价税合计'),
        ('订单数', str(s["orders"]), '按单据编号去重'),
        ('客户数', str(s["customers"]), '按客户名归并'),
        ('课题组数', str(s["groups"]), '客户 × 课题组'),
        ('新增课题组', f'{s["new_groups"]} 个 / {f2(s["new_group_amount"])} 元', f'占销售额 {pct(s["new_group_amount"]/s["total"])}'),
        ('老课题组', f'{s["groups"]-s["new_groups"]} 个 / {f2(s["old_group_amount"])} 元', f'占销售额 {pct(s["old_group_amount"]/s["total"])}'),
        ('经销商', f'{s["dealer_orders"]} 单 / {f2(s["dealer_amount"])} 元', f'占销售额 {pct(s["dealer_amount"]/s["total"])}'),
        ('基线月均', f'{f2(s["baseline_monthly"])} 元', '4–9 月（含促销日）'),
        ('促销折月', f'{f2(s["promo_monthly_equivalent"])} 元', '区域窗口折算 30 天'),
        ('增幅', pct(s["growth_vs_baseline_monthly"]), '促销折月 / 基线月均'),
        ('平均单', f'{f2(s["avg_order"])} 元', '销售额 / 订单数'),
        ('平均课题组', f'{f2(s["avg_group"])} 元', '销售额 / 课题组数'),
    ], [4.0, 6.2, 6.2], caption='表 1  促销期关键结果', font_size=8.5, first_col_bold=True)


def section_data(doc, d):
    s = d['summary']
    H1(doc, '二、数据')
    P(doc, '金额单位为元，百分比除外。数据按订单合同日期归月；课题组归属取 ERP「商机(飞书)#」，空值回落到客户名或经销商备注。')

    H2(doc, '2.1  总量与月度')
    P(doc, f'4–9 月基线合计 {f2(s["baseline_total"])} 元，{s["baseline_orders"]} 单，月均 {f2(s["baseline_monthly"])} 元。月度金额为 4 月 15,173.00、5 月 26,252.40、6 月 63,175.80、7 月 26,196.50、8 月 31,826.30、9 月 78,860.20 元。9 月已包含浙江 8/20–9/30、广东 9/1–9/30 的促销销售，基线因此偏高；88.99% 是按完整 4–9 月计算的保守增幅。')
    P(doc, '促销期分月：浙江 8 月 15,424.30、9 月 44,861.90；广东 9 月 33,998.30、10 月 16,603.00 元。10 月只有广东 3 单，活动尾段订单数明显减少。')
    add_picture(doc, os.path.join(CHART_DIR, '01_月度趋势.png'), '图 1  4–9 月月度销售额与促销折月对照（数据源：ERP；9 月含促销期销售）')

    H2(doc, '2.2  区域')
    r0, r1 = d['region'][0], d['region'][1]  # 广东, 浙江
    add_table(doc, ['区域', '销售额', '占比', '订单', '客户', '课题组', '新增组', '基线月均', '促销折月', '增幅'], [
        ('浙江', f2(r1['amount']), pct(r1['share']), str(r1['orders']), str(r1['customers']), str(r1['groups']), str(r1['new_groups']), f2(r1['baseline_monthly']), f2(r1['promo_monthly']), pct(r1['growth'])),
        ('广东', f2(r0['amount']), pct(r0['share']), str(r0['orders']), str(r0['customers']), str(r0['groups']), str(r0['new_groups']), f2(r0['baseline_monthly']), f2(r0['promo_monthly']), pct(r0['growth'])),
    ], [1.5, 2.2, 1.4, 1.1, 1.1, 1.4, 1.2, 2.2, 2.2, 2.1], caption='表 2  区域对比', font_size=8.0, first_col_bold=True)
    P(doc, '浙江 40 个课题组、28 个新增；广东 24 个课题组、16 个新增。浙江销售额占比 54.4%，月均增幅比广东高 25.5 个百分点。广东活动窗口比浙江多 4 天，但月均增幅仍低。')
    add_picture(doc, os.path.join(CHART_DIR, '02_区域对比.png'), '图 2  区域基线月均与促销折月（按区域窗口折算 30 天）')

    H2(doc, '2.3  销售员')
    rows = []
    for x in sorted(d['sales'], key=lambda z: -z['amount']):
        rows.append((x['sales'], x['region'], f2(x['amount']), pct(x['share']), str(x['orders']), str(x['groups']), str(x['new_groups']), f2(x['baseline_monthly']), f2(x['promo_monthly']), pct(x['growth'])))
    add_table(doc, ['销售员', '区域', '销售额', '占比', '订单', '课题组', '新增组', '基线月均', '促销折月', '增幅'], rows,
              [2.0, 1.1, 2.2, 1.3, 1.0, 1.2, 1.1, 2.1, 2.1, 2.0], caption='表 3  销售员对比', font_size=7.9, first_col_bold=True)
    s3 = sum(x['amount'] for x in sorted(d['sales'], key=lambda z: -z['amount'])[:3])
    s4 = sum(x['amount'] for x in sorted(d['sales'], key=lambda z: -z['amount'])[:4])
    P(doc, f'前三名合计 {f2(s3)} 元，占 {pct(s3/s["total"])}；前四名 {f2(s4)} 元，占 {pct(s4/s["total"])}。王诗香、叶信福月均分别增长 260.7%、223.1%；黄建华、袁殿果增长 23.6%、32.3%；韩远怀 -0.4%。')
    add_picture(doc, os.path.join(CHART_DIR, '03_销售员对比.png'), '图 3  销售员基线月均与促销折月')

    H2(doc, '2.4  课题组与客户')
    P(doc, f'64 个课题组中 29 个未达 1,000 元，16 个 1K，16 个 2K，2 个 5K，1 个 1W。{s["reach_groups"]} 个达到礼品档，礼品预算 {s["gift_low"]:,}–{s["gift_high"]:,} 元；{s["reach_customers"]} 个客户达到客户档，预算 {s["customer_gift_low"]:,}–{s["customer_gift_high"]:,} 元。')
    P(doc, '64 个课题组中 56 个只有一单（87.5%）；新组 40/44 只有一单，老组 16/20 只有一单。34 个客户中 22 个只有一单。')
    P(doc, '前 2 个客户占 35.7%，前 5 个占 54.8%；浙江大学 24,627.00 元（11 个课题组），南方科技大学 15,000.00 元（1 个课题组）。')
    add_picture(doc, os.path.join(CHART_DIR, '04_课题组档位.png'), '图 4  课题组档位分布：35 个达标，29 个未达 1,000 元')
    add_table(doc, ['档位', '课题组数', '客户数', '课题组礼品预算', '客户礼品预算'], [
        ('未达标', '29', '11', '0', '0'),
        ('1K', '16', '8', '50–100 元/组', '50–100 元/客户'),
        ('2K', '16', '9', '100–200 元/组', '100–200 元/客户'),
        ('5K', '2', '4', '250–500 元/组', '250–500 元/客户'),
        ('1W', '1', '1', '500–1,000 元/组', '500–1,000 元/客户'),
        ('2W', '0', '1', '—', '1,000–2,000 元/客户'),
    ], [2.2, 2.2, 2.2, 4.9, 4.9], caption='表 4  课题组与客户档位', font_size=8.4, first_col_bold=True)

    H2(doc, '2.5  产品结构')
    P(doc, '前 3 个货号 36,515.20 元，占 32.9%；前 10 个 81,318.00 元，占 73.3%。首位 BR1D303-01 为 14,599.20 元，占 13.2%。')
    p10 = d['products'][:10]
    rows = []
    for x in p10[:5]:
        rows.append((x['货号#'], x['物料名称'][:28], f2(x['amount']), pct(x['amount']/s['total']), str(x['orders']), str(x['customers'])))
    add_table(doc, ['货号', '产品', '金额', '占比', '订单', '客户'], rows,
              [3.4, 7.4, 2.3, 1.3, 1.0, 1.0], caption='表 5  销售额前 5 货号', font_size=8.0, first_col_bold=True)
    cat = d['categories']
    add_table(doc, ['销售分组（二级）', '金额', '占比', '订单'], [
        (x['销售分组（二级）'], f2(x['amount']), pct(x['share']), str(x['orders'])) for x in cat
    ], [8.0, 3.6, 2.4, 2.0], caption='表 6  品类结构', font_size=8.2, first_col_bold=True)
    P(doc, '生命科学-分子生物学 55,460.70 元，占 50.0%；诊断原料-恒温扩增 15,000.00 元，占 13.5%，来自 1 单；生命科学-蛋白质研究 11,262.80 元，占 10.2%；测序 10,813.00 元，占 9.8%；“基础物料数据（勿选）”8,520.00 元，占 7.7%，共 5 单，分类待核。')


def section_phenomena(doc, d):
    s = d['summary']
    H1(doc, '三、现象')
    P(doc, '1. 增长集中在 9 月，活动尾段订单减少。')
    P(doc, '9 月销售额 78,860.20 元，是 4–9 月最高月；10 月 1–16 日广东只有 3 单、16,603.00 元。活动结束后 10/17–11/3，8 名销售合计 35,298.40 元、27 单，日均 1,961 元，需求没有消失，但活动期的集中成交没有自然延续。', indent=False)
    P(doc, '2. 新增课题组数量多，复购没有接上。')
    P(doc, f'新增课题组 44 个，贡献 78,720.00 元，占 71.0%；但 40/44 只有一单，64 个课题组中 56 个只有一单。29 个课题组未达 1,000 元，35 个达到礼品档；新增组平均 1,789.09 元，老组 1,608.38 元，差距只有 180.71 元。', indent=False)
    P(doc, '3. 区域双增，浙江更强。')
    P(doc, '浙江 60,286.20 元，增长 100.89%，40 个课题组、28 个新增；广东 50,601.30 元，增长 75.43%，24 个课题组、16 个新增。浙江的课题组数和新增组数都高于广东，广东活动窗口多 4 天，但月均增幅仍低 25.5 个百分点。', indent=False)
    P(doc, '4. 销售员分化明显。')
    P(doc, '前三名占 59.9%，前四名占 74.8%。王诗香、叶信福月均增长 260.7%、223.1%；黄建华、袁殿果增长 23.6%、32.3%；韩远怀 -0.4%。新增组数量也分化：叶信福 10 个、侯海瑜 8 个、王诗香 7 个；黄建华 2 个、韩远怀 3 个。', indent=False)
    P(doc, '5. 客户和货号都集中。')
    P(doc, '前 2 个客户占 35.7%，前 5 个占 54.8%；22/34 个客户只有一单。前 3 个货号占 32.9%，前 10 个占 73.3%；生命科学-分子生物学占 50.0%，mRNA 只占 4.9%。', indent=False)
    P(doc, '6. 经销商和礼品数据留下缺口。')
    P(doc, '经销商 9 单、6,650.70 元，占 6.0%；5 单可归终端课题组 4,271.70 元，4 单未注明 2,379.00 元。35 个课题组达标、29 个未达标；礼品预算合计 7,200–14,400 元，占销售额 6.5%–13.0%。', indent=False)


def section_causes(doc, d):
    H1(doc, '四、原因分析')
    P(doc, '1. 促销机制奖励首单，缺少第二单设计。')
    P(doc, '56/64 个课题组只有一单，40/44 个新组只有一单；1K、2K 两档门槛覆盖了 32 个课题组，但 29 个组连 1,000 元都没到。礼品在成交后发放，没有和 14 天、30 天内的第二单绑定；从结果看，首单后没有形成稳定的第二单动作。', indent=False)
    P(doc, '2. 9 月开学季叠加经费启动，采购集中在前段。')
    P(doc, '6 月基线本身有 63,175.80 元的高点，9 月 78,860.20 元含促销销售；10 月广东只剩 3 单。单月峰值不能全部归因于活动，采购节奏的影响需要在 11 月访谈中确认。', indent=False)
    P(doc, '3. 销售执行差异来自新组转化和组合销售能力。')
    P(doc, '叶信福、侯海瑜、王诗香新增组分别 10、8、7 个，前三名销售额占 59.9%；黄建华新增组 2 个、韩远怀 3 个，增幅 23.6%、-0.4%。从新增组数量和单组金额看，差异主要落在新增组开发；组合推荐是否到位需访谈确认。', indent=False)
    P(doc, '4. 产品结构仍停留在常规分子试剂。')
    P(doc, '前 3 个货号占 32.9%，前 10 个占 73.3%；分子生物学占 50.0%，mRNA 只占 4.9%。诊断原料-恒温扩增的 15,000.00 元来自 1 单，不能代表品类整体起量。促销成交集中在常规 qPCR、逆转录、cDNA 货号；单组金额和一单一单的结构说明本轮更像一次性备货，未形成连续采购。', indent=False)
    P(doc, '5. 数据口径和流程没有支撑复购管理。')
    P(doc, '“基础物料数据（勿选）”出现 8,520.00 元、5 单，说明销售行里出现了非销售物料分类或分类异常；经销商 4 单未注明终端课题组，2,379.00 元无法归组；课题组字段依赖 ERP「商机(飞书)#」，未挂商机的订单只能按客户或经销商处理。这些缺口会影响 29 个未达标组的分派和追踪。', indent=False)


def section_actions(doc, d):
    H1(doc, '五、改善建议')
    P(doc, '以下动作按“先处理本轮未达标组，再改下一轮机制”排序；主责写到人，时限和验收可直接检查。')
    rows = [
        ('1', '29 个未达标课题组二次跟进：按“有单未复购 / 只询价未下单 / 测试中”分组，11/15 前完成分类，11/30 前至少 15 组产生第二单。', '29 组现归属销售（逐组见附录 B1）', '11/30', '15/29 组第二单，单组累计 ≥1,000 元'),
        ('2', '新组首单 14 天复购 SOP：首单 ≤2,000 元的新组，7 天内回访，14 天内给第二档组合。', '区域销售；区域负责人周检', '11/10 起', '新组 30 天复购率由 9.1%（4/44）提升到 25%（11/44）'),
        ('3', '区域打法：浙江复制叶信福/侯海瑜的新组+组合；广东王诗香/黄建华抓重点客户，韩远怀做存量激活。', '刘新元总责；叶信福、王诗香试点', '11 月', '11 月两区合计 ≥6.0 万元（活动后 18 天 3.53 万元，折月约 5.9 万元）'),
        ('4', '产品组合：出 3 个组合包（cDNA+qPCR、RT+qPCR、mRNA/蛋白加购），新组首单按组合推荐。', '市场/产品（刘新元牵头）＋区域销售', '11/20 出包', '新组首单含 ≥2 个二级品类占比 ≥50%'),
        ('5', '经销商终端课题组：所有经销商订单补终端课题组，无法确认的标“自购”，不再进入课题组新增。', '商务 / 对应销售', '11/15', '9 单 100% 备注，0 条空白'),
        ('6', '数据治理：剔除或映射“基础物料数据（勿选）”；统一课题组字段，未挂商机的订单单独标记。', '刘新元 ＋ IT', '11/30', '下一期报告该分类 0 行；课题组字段缺失率 <5%'),
        ('7', '礼品与门槛：本轮 35 组/23 客户按档发放，29 组不发；下一季 A/B 测试 2K/5K 门槛 + 500 元新组试用档。', '刘新元 ＋ 市场', '12/15 方案', '每区 20 组试点，复购率 ≥25%'),
        ('8', '重点客户：浙江大学、南方科技大学、广东药科大学、中山大学附属口腔医院、温州医科大学，11 月完成一次年度需求访谈。', '浙江大学：叶信福/侯海瑜/吴云/袁殿果；南方科技大学：王诗香；温州医科大学：袁殿果；广东药科大学、中山口腔：黄建华', '11/30', '5 家客户均形成 2027 年首单/框架计划'),
    ]
    add_table(doc, ['#', '动作', '主责', '时限', '验收'], rows,
              [0.8, 7.4, 3.4, 1.8, 3.0], caption='表 7  改善建议与验收', font_size=7.8, first_col_bold=True)


def appendix(doc, d):
    doc.add_page_break()
    H1(doc, '附录')
    H2(doc, 'A. 口径')
    P(doc, '促销期：浙江 2026-08-20–09-30（42 天）；广东 2026-09-01–10-16（46 天）。', indent=False)
    P(doc, '主表：2026 年 ERP 标准销售订单 + 已审核 + 非赠品；剔除宝锐、宝泰、横琴主体；金额为含税价税合计。', indent=False)
    P(doc, '基线：4–9 月完整月度销售额，含促销日，属保守口径；月均 = 基线合计 ÷ 6。', indent=False)
    P(doc, '促销折月：各区域销售额 ÷ 区域窗口天数 × 30，再相加；未按统一月数直接折算总销售额。', indent=False)
    P(doc, '新课题组：促销前（浙江 4/1–8/19、广东 4/1–8/31）无同客户同课题组订单；课题组取 ERP「商机(飞书)#」，空值回落到客户名或经销商备注。', indent=False)
    P(doc, '经销商：客户名不含大学、学院、医院、研究院、研究所、中心、疾控、海关、检验检疫、附属等机构词，且含“有限公司/有限责任公司”；代课题组采购按销售备注的终端课题组归组，自购/未注明不进入课题组新增。', indent=False)
    P(doc, '礼品档：课题组 1K=1,000、2K=2,000、5K=5,000、1W=10,000 元；客户档同档位翻倍预算；未达标不发。', indent=False)

    H2(doc, 'B. 课题组 Top 10')
    rows = []
    for x in d['groups'][:10]:
        rows.append((x['group'], x['customer'], x['region'], x['sales'], f2(x['amount']), str(x['orders']), '新增' if x['new_group'] else '老组'))
    add_table(doc, ['课题组', '客户', '区域', '销售', '金额', '订单', '新/老'], rows,
              [3.9, 4.6, 1.5, 1.6, 2.2, 1.1, 1.2], caption='表 B1  课题组销售额前 10', font_size=7.6, first_col_bold=True)

    H2(doc, 'C. 客户 Top 10')
    rows = []
    for x in d['customers'][:10]:
        rows.append((x['customer'], x['region'], x['sales'], f2(x['amount']), str(x['orders']), str(x['groups']), str(x['new_groups'])))
    add_table(doc, ['客户', '区域', '销售', '金额', '订单', '课题组', '新增组'], rows,
              [5.4, 1.7, 3.3, 2.3, 1.1, 1.3, 1.3], caption='表 B2  客户销售额前 10', font_size=7.8, first_col_bold=True)

    H2(doc, 'D. 经销商明细')
    rows = []
    for x in d['dealer_rows']:
        rows.append((x['date'], x['order'], x['sales'], x['customer'][:18], x['group'][:16], f2(x['amount']), x['dealer_type']))
    add_table(doc, ['日期', '单据号', '销售', '经销商', '终端课题组/备注', '金额', '类型'], rows,
              [1.8, 2.4, 1.1, 4.0, 3.4, 1.7, 2.0], caption='表 B3  经销商订单明细', font_size=7.3, first_col_bold=True)
    P(doc, '4 单自购/未注明终端课题组共 2,379.00 元；已计入促销总额，未计入课题组新增。', size=8.6, indent=False)

    H2(doc, 'E. 数据边界与待确认')
    B(doc, '9 月基线含促销日。88.99% 是按完整 4–9 月计算的保守增幅；剔除促销日后增幅会更高，但当前没有单独剔除，未作为主口径。')
    B(doc, '经销商 4 单未注明终端课题组，2,379.00 元未进入课题组新增；后续需补录，否则课题组数偏低。')
    B(doc, '“基础物料数据（勿选）”8,520.00 元、5 单，属于非销售物料或分类异常；当前未从总额剔除，需产品/IT 核对后在下一期报告中单独处理。')
    P(doc, '报告数据生成：2026-11-03；报告文件生成：2026-11-04。数据源：ERP 销售订单导出（2026-11-03）。', size=7.6, color='64748B', indent=False)


def main():
    make_logo_white()
    d = load()
    doc = Document()
    doc.core_properties.title = '2026 年科研秋季促销复盘报告'
    doc.core_properties.subject = '浙江 / 广东科研促销复盘（B 口径）'
    doc.core_properties.author = '宝锐生物 营销中心'
    doc.core_properties.comments = '内部管理资料，请勿外传。'
    setup(doc)
    cover(doc, d)
    section_results(doc, d)
    doc.add_page_break()
    section_data(doc, d)
    doc.add_page_break()
    section_phenomena(doc, d)
    doc.add_page_break()
    section_causes(doc, d)
    doc.add_page_break()
    section_actions(doc, d)
    appendix(doc, d)

    os.makedirs(OUT_DIR, exist_ok=True)
    doc.save(OUT)
    print('WROTE', OUT, os.path.getsize(OUT))
    print('paragraphs', len(doc.paragraphs), 'tables', len(doc.tables), 'sections', len(doc.sections))


if __name__ == '__main__':
    main()
