Files
fmd26/build_estimate_card.py

364 lines
13 KiB
Python

# -*- coding: utf-8 -*-
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
OUT = 'Метод_Дегтярева_смета.docx'
GREEN = RGBColor(0x2E, 0x7D, 0x5B)
ROSE = RGBColor(0xB0, 0x3A, 0x55)
DARK = RGBColor(0x1A, 0x1A, 0x1A)
GRAY = RGBColor(0x5F, 0x66, 0x6B)
LIGHT = RGBColor(0x8A, 0x90, 0x95)
def set_run(run, name='Arial', size=9, bold=False, italic=False, color=DARK):
run.font.name = name
run.font.size = Pt(size)
run.font.bold = bold
run.font.italic = italic
run.font.color.rgb = color
rPr = run._element.get_or_add_rPr()
rFonts = rPr.find(qn('w:rFonts'))
if rFonts is None:
rFonts = OxmlElement('w:rFonts')
rPr.append(rFonts)
for attr in ('w:ascii', 'w:hAnsi', 'w:eastAsia', 'w:cs'):
rFonts.set(qn(attr), name)
def shade(cell, hex_color):
tcPr = cell._tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), hex_color)
tcPr.append(shd)
def cell_margins(table, top=60, bottom=60, left=100, right=100):
tblPr = table._tbl.tblPr
mar = OxmlElement('w:tblCellMar')
for tag, val in (('top', top), ('left', left), ('bottom', bottom), ('right', right)):
el = OxmlElement('w:' + tag)
el.set(qn('w:w'), str(val))
el.set(qn('w:type'), 'dxa')
mar.append(el)
tblPr.append(mar)
def table_borders(table, color='C9D6CF', size=6):
tblPr = table._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'), str(size))
el.set(qn('w:space'), '0')
el.set(qn('w:color'), color)
borders.append(el)
tblPr.append(borders)
def add_line_table(cell, lines=2, width_cm=8.0, height_cm=0.5):
tbl = cell.add_table(rows=lines, cols=1)
tbl.autofit = False
tblPr = tbl._tbl.tblPr
borders = OxmlElement('w:tblBorders')
for edge in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'):
el = OxmlElement('w:' + edge)
el.set(qn('w:val'), 'none')
borders.append(el)
tblPr.append(borders)
for row in tbl.rows:
row.height = Cm(height_cm)
c = row.cells[0]
c.width = Cm(width_cm)
tcPr = c._tc.get_or_add_tcPr()
tcBorders = OxmlElement('w:tcBorders')
bottom = OxmlElement('w:bottom')
bottom.set(qn('w:val'), 'dotted')
bottom.set(qn('w:sz'), '4')
bottom.set(qn('w:space'), '0')
bottom.set(qn('w:color'), 'AFC4B9')
tcBorders.append(bottom)
tcPr.append(tcBorders)
p = c.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
p.paragraph_format.line_spacing = 1.0
def add_block(cell, num, title, prompt, color=GREEN, fill='F3F9F6', lines=2, width_cm=8.0):
cell.text = ''
shade(cell, fill)
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(1)
r = p.add_run(num + ' ')
set_run(r, size=7.5, bold=True, color=color)
r = p.add_run(title)
set_run(r, size=9.5, bold=True, color=color)
p = cell.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(3)
p.paragraph_format.line_spacing = 1.0
r = p.add_run(prompt)
set_run(r, size=7.5, color=GRAY)
add_line_table(cell, lines=lines, width_cm=width_cm, height_cm=0.5)
def nested_grid(cell, rows, cols, widths, header_fill='EAF4EF'):
tbl = cell.add_table(rows=rows, cols=cols)
tbl.autofit = False
tblPr = tbl._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'), '4')
el.set(qn('w:space'), '0')
el.set(qn('w:color'), 'D9E2DD')
borders.append(el)
tblPr.append(borders)
for row in tbl.rows:
for i, c in enumerate(row.cells):
c.width = Cm(widths[i])
return tbl
def build():
doc = Document()
section = doc.sections[0]
section.page_width = Cm(21.0)
section.page_height = Cm(29.7)
section.top_margin = Cm(1.3)
section.bottom_margin = Cm(1.1)
section.left_margin = Cm(1.4)
section.right_margin = Cm(1.4)
normal = doc.styles['Normal']
normal.font.name = 'Arial'
normal.font.size = Pt(9)
normal.font.color.rgb = DARK
# Header
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run('СМЕТА')
set_run(r, name='Georgia', size=20, bold=True, color=GREEN)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(1)
p.paragraph_format.space_after = Pt(1)
r = p.add_run('Шаг 2. «Смета» • Метод Дегтярева')
set_run(r, size=9, bold=True, color=GRAY)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(1)
r = p.add_run('Смета — не бухгалтерия. Считаем эффект в людях и ресурсах.')
set_run(r, size=8, italic=True, color=ROSE)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(5)
r = p.add_run('Команда: ______________________________ Проблема: ______________________________________')
set_run(r, size=8.5, color=GRAY)
# Main table
table = doc.add_table(rows=7, cols=2)
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
table_borders(table)
cell_margins(table)
heights = [3.2, 3.2, 4.8, 2.6, 2.6, 2.4, 2.8]
for i, row in enumerate(table.rows):
row.height = Cm(heights[i])
for cell in row.cells:
cell.width = Cm(9.0)
# 01 User (merged)
c = table.cell(0, 0).merge(table.cell(0, 1))
add_block(c, '01', 'ПОЛЬЗОВАТЕЛЬ',
'Кто конкретно? Где и в каком контексте? Что для него важно? Что он делает сейчас?',
lines=3, width_cm=17.2)
# 02 Before / 03 After
add_block(table.cell(1, 0), '02', 'БЫЛО',
'Что человек делает сейчас? Сколько тратит времени, денег, сил?',
color=GRAY, fill='F7F8F8', lines=2, width_cm=8.2)
add_block(table.cell(1, 1), '03', 'СТАЛО',
'Что изменится? Быстрее, дешевле, проще, доступнее?',
color=GREEN, fill='F3F9F6', lines=2, width_cm=8.2)
# 04 Metrics (merged)
c = table.cell(2, 0).merge(table.cell(2, 1))
c.text = ''
shade(c, 'F3F9F6')
p = c.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(1)
r = p.add_run('04 ')
set_run(r, size=7.5, bold=True, color=GREEN)
r = p.add_run('ТРИ МЕТРИКИ ЭФФЕКТА')
set_run(r, size=9.5, bold=True, color=GREEN)
p = c.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(3)
r = p.add_run('Охват • Скорость / доступность • Качество / удовлетворённость')
set_run(r, size=7.5, color=GRAY)
metrics = nested_grid(c, 4, 3, [4.6, 6.4, 6.2])
metrics.rows[0].height = Cm(0.5)
for ri in range(1, 4):
metrics.rows[ri].height = Cm(0.95)
headers = ['МЕТРИКА', 'КАК СЧИТАЕМ', 'СЕЙЧАС → ЦЕЛЬ']
for i, h in enumerate(headers):
cell = metrics.cell(0, i)
cell.text = ''
shade(cell, 'EAF4EF')
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(h)
set_run(r, size=7, bold=True, color=GREEN)
labels = ['1. Охват', '2. Скорость', '3. Качество']
for ri in range(1, 4):
cell = metrics.cell(ri, 0)
cell.text = ''
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(labels[ri - 1])
set_run(r, size=7.5, bold=True, color=DARK)
for ci in (1, 2):
cell = metrics.cell(ri, ci)
cell.text = ''
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(' ')
set_run(r, size=7, color=RGBColor(0xFF, 0xFF, 0xFF))
# 05 Scale (merged)
c = table.cell(3, 0).merge(table.cell(3, 1))
c.text = ''
shade(c, 'FDF4F6')
p = c.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(1)
r = p.add_run('05 ')
set_run(r, size=7.5, bold=True, color=ROSE)
r = p.add_run('МАСШТАБ')
set_run(r, size=9.5, bold=True, color=ROSE)
p = c.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(3)
r = p.add_run('Сколько людей сталкивается, до кого дотянемся, кого охватим за первый год?')
set_run(r, size=7.5, color=GRAY)
scale = nested_grid(c, 2, 3, [5.7, 5.7, 5.7])
scale.rows[0].height = Cm(0.45)
scale.rows[1].height = Cm(0.85)
scale_headers = ['TAM — все, кто сталкивается', 'SAM — до кого дотянемся', 'SOM — охватим за год']
for i, h in enumerate(scale_headers):
cell = scale.cell(0, i)
cell.text = ''
shade(cell, 'FDF4F6')
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(h)
set_run(r, size=7, bold=True, color=ROSE)
for i in range(3):
cell = scale.cell(1, i)
cell.text = ''
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(' ')
set_run(r, size=7, color=RGBColor(0xFF, 0xFF, 0xFF))
# 06 Price (merged)
c = table.cell(4, 0).merge(table.cell(4, 1))
c.text = ''
shade(c, 'F3F9F6')
p = c.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(1)
r = p.add_run('06 ')
set_run(r, size=7.5, bold=True, color=GREEN)
r = p.add_run('ЦЕНА ЭФФЕКТА')
set_run(r, size=9.5, bold=True, color=GREEN)
p = c.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(3)
r = p.add_run('Что нужно, чтобы эффект случился? Не отчёт, а ресурсы.')
set_run(r, size=7.5, color=GRAY)
price = nested_grid(c, 2, 5, [3.4, 3.4, 3.4, 3.4, 3.4])
price.rows[0].height = Cm(0.45)
price.rows[1].height = Cm(0.85)
price_headers = ['ЛЮДИ', 'ВРЕМЯ', 'ДЕНЬГИ', 'ДАННЫЕ', 'ПАРТНЁРЫ']
for i, h in enumerate(price_headers):
cell = price.cell(0, i)
cell.text = ''
shade(cell, 'EAF4EF')
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(h)
set_run(r, size=7, bold=True, color=GREEN)
for i in range(5):
cell = price.cell(1, i)
cell.text = ''
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(' ')
set_run(r, size=7, color=RGBColor(0xFF, 0xFF, 0xFF))
# 07 Check (merged)
c = table.cell(5, 0).merge(table.cell(5, 1))
add_block(c, '07', 'ПРОВЕРКА',
'Самый дешёвый способ проверить эффект до разработки: интервью, лендинг, ручной прототип, пилот на 10 пользователях.',
color=ROSE, fill='FDF4F6', lines=2, width_cm=17.2)
# 08 Formulation (merged)
c = table.cell(6, 0).merge(table.cell(6, 1))
c.text = ''
shade(c, 'F3F9F6')
p = c.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(1)
r = p.add_run('08 ')
set_run(r, size=7.5, bold=True, color=GREEN)
r = p.add_run('ФОРМУЛИРОВКА ЭФФЕКТА')
set_run(r, size=9.5, bold=True, color=GREEN)
p = c.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(2)
r = p.add_run('«___________ получит ___________, потому что ___________. Мы измерим это по ___________.»')
set_run(r, size=8, italic=True, color=GRAY)
add_line_table(c, lines=2, width_cm=17.2, height_cm=0.5)
# Footer
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(5)
p.paragraph_format.space_after = Pt(0)
r = p.add_run('Считайте в людях, а не в лайках. • Метод Дегтярева • Форум молодёжи Донбасса, 2026')
set_run(r, size=7.5, color=LIGHT)
doc.save(OUT)
print('saved', OUT)
if __name__ == '__main__':
build()