add Word report + button in index.html + server route .docx
This commit is contained in:
parent
dd9fc2f758
commit
6858572cf6
@ -12,3 +12,9 @@ server on port 3000
|
|||||||
--- run Sun Sep 27 04:42:10 PM UTC 2026 :: node server.js
|
--- run Sun Sep 27 04:42:10 PM UTC 2026 :: node server.js
|
||||||
CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
|
CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
|
||||||
server on port 3000
|
server on port 3000
|
||||||
|
--- run Sun Sep 27 04:58:42 PM UTC 2026 :: node server.js
|
||||||
|
CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
|
||||||
|
server on port 3000
|
||||||
|
--- run Sun Sep 27 05:13:37 PM UTC 2026 :: node server.js
|
||||||
|
CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
|
||||||
|
server on port 3000
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
2597
|
4826
|
||||||
|
|||||||
@ -107,6 +107,10 @@
|
|||||||
<svg style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#external"></use></svg>
|
<svg style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#external"></use></svg>
|
||||||
Показать презентацию
|
Показать презентацию
|
||||||
</a>
|
</a>
|
||||||
|
<a class="kt-ai-btn" data-variant="ghost" data-size="sm" href="Анализ_расходов_по_сервисным_контрактам.docx" download>
|
||||||
|
<svg style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#download"></use></svg>
|
||||||
|
Анализ_расходов_по_сервисным_контрактам.docx
|
||||||
|
</a>
|
||||||
<span class="kt-ai-fg-faint" style="font-size:var(--kt-ai-text-xs)">По кнопке «Опубликовать» откроется таблица; презентация живёт на отдельном URL — её можно открыть в другом окне.</span>
|
<span class="kt-ai-fg-faint" style="font-size:var(--kt-ai-text-xs)">По кнопке «Опубликовать» откроется таблица; презентация живёт на отдельном URL — её можно открыть в другом окне.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
331
mkword.py
Normal file
331
mkword.py
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import json, datetime
|
||||||
|
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, WD_ALIGN_VERTICAL
|
||||||
|
from docx.oxml.ns import qn, nsmap
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
|
||||||
|
DARK = RGBColor(0x0D, 0x1B, 0x2A)
|
||||||
|
PANEL = RGBColor(0x14, 0x24, 0x33)
|
||||||
|
ACCENT = RGBColor(0x1B, 0x4F, 0x72)
|
||||||
|
OK = RGBColor(0x1A, 0x7F, 0x4B)
|
||||||
|
RISK = RGBColor(0xC0, 0x39, 0x2B)
|
||||||
|
WARN = RGBColor(0xD4, 0x68, 0x0A)
|
||||||
|
MUTE = RGBColor(0x88, 0x99, 0xA6)
|
||||||
|
DIM = RGBColor(0x6C, 0x8F, 0xA8)
|
||||||
|
FG = RGBColor(0xF0, 0xF4, 0xF8)
|
||||||
|
TXT = RGBColor(0x0C, 0x0A, 0x09)
|
||||||
|
TXT2 = RGBColor(0x78, 0x71, 0x6C)
|
||||||
|
|
||||||
|
doc = Document()
|
||||||
|
# А4, поля
|
||||||
|
for s in doc.sections:
|
||||||
|
s.page_height = Cm(29.7); s.page_width = Cm(21.0)
|
||||||
|
s.top_margin = Cm(1.8); s.bottom_margin = Cm(1.5)
|
||||||
|
s.left_margin = Cm(1.5); s.right_margin = Cm(1.5)
|
||||||
|
|
||||||
|
d = json.load(open('data.json'))
|
||||||
|
rows = d['rows']
|
||||||
|
t23 = sum(r.get('y2023') or 0 for r in rows)
|
||||||
|
t24 = sum(r.get('y2024') or 0 for r in rows)
|
||||||
|
t25 = sum(r.get('y2025') or 0 for r in rows)
|
||||||
|
q26 = sum(r.get('q1_2026') or 0 for r in rows)
|
||||||
|
b = lambda v: v / 1e9
|
||||||
|
m = lambda v: v / 1e6
|
||||||
|
|
||||||
|
# ---------- Заголовок ----------
|
||||||
|
t = doc.add_paragraph()
|
||||||
|
t.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = t.add_run('АНАЛИЗ РАСХОДОВ ПО СЕРВИСНЫМ КОНТРАКТАМ')
|
||||||
|
r.bold = True; r.font.size = Pt(22); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
sub = doc.add_paragraph()
|
||||||
|
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = sub.add_run('Детализация 2023 г. – 1-е полугодие 2026 г. · 103 позиции · сумма в тенге (факт)')
|
||||||
|
r.font.size = Pt(11); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
|
||||||
|
sub.paragraph_format.space_after = Pt(14)
|
||||||
|
|
||||||
|
# ---------- KPI ----------
|
||||||
|
def kpi_card(label, value, sub='', color=DARK):
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.paragraph_format.space_after = Pt(2)
|
||||||
|
r = p.add_run(value)
|
||||||
|
r.bold = True; r.font.size = Pt(20); r.font.color.rgb = color; r.font.name = 'Segoe UI'
|
||||||
|
p2 = doc.add_paragraph()
|
||||||
|
p2.paragraph_format.space_after = Pt(0)
|
||||||
|
r = p2.add_run(label)
|
||||||
|
r.font.size = Pt(9); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
|
||||||
|
if sub:
|
||||||
|
p3 = doc.add_paragraph()
|
||||||
|
p3.paragraph_format.space_after = Pt(4)
|
||||||
|
r = p3.add_run(sub)
|
||||||
|
r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
tbl = doc.add_table(rows=1, cols=4)
|
||||||
|
tbl.style = 'Table Grid'
|
||||||
|
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
for i, (lbl, val, sub, col) in enumerate([
|
||||||
|
('2023', f'{b(t23):,.2f} млрд тг', '55 позиций', DARK),
|
||||||
|
('2024', f'{b(t24):,.2f} млрд тг', '54 позиции · пик', RISK),
|
||||||
|
('2025', f'{b(t25):,.2f} млрд тг', '45 позиций', DARK),
|
||||||
|
('1пг 2026', f'{b(q26):,.2f} млрд тг', '31 позиция', OK),
|
||||||
|
]):
|
||||||
|
cell = tbl.rows[0].cells[i]
|
||||||
|
cell.width = Cm(3.8)
|
||||||
|
# Кнопка-фон: светлый
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'F0F4F8'); tcPr.append(shd)
|
||||||
|
p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER; p.paragraph_format.space_after = Pt(2)
|
||||||
|
r = p.add_run(val)
|
||||||
|
r.bold = True; r.font.size = Pt(18); r.font.color.rgb = col; r.font.name = 'Segoe UI'
|
||||||
|
p2 = cell.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER; p2.paragraph_format.space_after = Pt(0)
|
||||||
|
r = p2.add_run(lbl)
|
||||||
|
r.bold = True; r.font.size = Pt(10); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
|
||||||
|
p3 = cell.add_paragraph(); p3.alignment = WD_ALIGN_PARAGRAPH.CENTER; p3.paragraph_format.space_after = Pt(2)
|
||||||
|
r = p3.add_run(sub)
|
||||||
|
r.font.size = Pt(8); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
doc.add_paragraph()
|
||||||
|
|
||||||
|
# ---------- Сводка ----------
|
||||||
|
h = doc.add_paragraph()
|
||||||
|
h.paragraph_format.space_before = Pt(6); h.paragraph_format.space_after = Pt(6)
|
||||||
|
r = h.add_run('1. Итоговый свод')
|
||||||
|
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
def bullet(text, bold_lead=None):
|
||||||
|
p = doc.add_paragraph(style='List Bullet')
|
||||||
|
p.paragraph_format.space_after = Pt(6)
|
||||||
|
if bold_lead:
|
||||||
|
r = p.add_run(bold_lead + ' ')
|
||||||
|
r.bold = True; r.font.size = Pt(10.5); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
|
||||||
|
r = p.add_run(text)
|
||||||
|
r.font.size = Pt(10.5); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
|
||||||
|
return p
|
||||||
|
|
||||||
|
d24_25 = t25 - t24; p24_25 = (t24 and d24_25 / t24 * 100)
|
||||||
|
d23_24 = t24 - t23; p23_24 = d23_24 / t23 * 100
|
||||||
|
bullet(f'Изменение 2024 → 2025: <b>−{b(abs(d24_25)):,.2f} млрд тг ({p24_25:+.1f} %)</b> — это не экономия, а смена номенклатуры (FLM Expenses → FLM, Ericsson CS/5G part → общий договор, MS EA/COGS/G&A).'.replace('<b>','').replace('</b>',''),
|
||||||
|
bold_lead='Ключевой вывод')
|
||||||
|
bullet(f'2023 → 2024: рост на {b(d23_24):,.2f} млрд тг ({p23_24:+.1f} %) — пик расходов.',
|
||||||
|
bold_lead='Динамика')
|
||||||
|
bullet(f'Темп 1пг 2026 ({b(q26):,.2f} млрд за полгода), удвоенный на год ≈ {b(q26*2):,.2f} млрд — на уровне 2024.',
|
||||||
|
bold_lead='Прогноз 2026')
|
||||||
|
bullet('Структура 2025: FLM 3 232 млн · Ericsson 790 млн · MS EA 630 млн · MS EA COGS 513 млн · FLM Expenses 3 481 млн (2024) — крупнейшие статьи.',
|
||||||
|
bold_lead='Топ статей 2025')
|
||||||
|
|
||||||
|
# ---------- Отклонения 2025 vs 2024 ----------
|
||||||
|
doc.add_paragraph()
|
||||||
|
h = doc.add_paragraph()
|
||||||
|
h.paragraph_format.space_before = Pt(10)
|
||||||
|
r = h.add_run('2. Отклонения 2025 vs 2024 (существующие статьи, |Δ| ≥ 50 млн тг)')
|
||||||
|
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
tab = doc.add_table(rows=1, cols=5)
|
||||||
|
tab.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
hdr = tab.rows[0].cells
|
||||||
|
labels = ['Позиция', '2024, млн тг', '2025, млн тг', 'Δ, млн тг', 'Δ %']
|
||||||
|
for i, l in enumerate(labels):
|
||||||
|
cell = hdr[i]
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), '142433'); tcPr.append(shd)
|
||||||
|
p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT if i > 0 else WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
r = p.add_run(l)
|
||||||
|
r.bold = True; r.font.size = Pt(9); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
cands = [r for r in rows if (r.get('d25') or 0) not in (0, None) and (r.get('y2025') or 0) > 0 and (r.get('y2024') or 0) > 0]
|
||||||
|
cands.sort(key=lambda r: abs(r.get('d25') or 0), reverse=True)
|
||||||
|
cands = [c for c in cands if abs(c['d25']) >= 50e6][:15]
|
||||||
|
ri = 0
|
||||||
|
for it in cands:
|
||||||
|
row = tab.add_row().cells
|
||||||
|
for i, (v, fmt) in enumerate([
|
||||||
|
(it.get('name'), '{:s}'),
|
||||||
|
(it.get('y2024') / 1e6, '{:,.1f}'),
|
||||||
|
(it.get('y2025') / 1e6, '{:,.1f}'),
|
||||||
|
(it.get('d25') / 1e6, '{:+,.1f}'),
|
||||||
|
(it.get('p25'), '{:+.1f} %'),
|
||||||
|
]):
|
||||||
|
cell = row[i]
|
||||||
|
if v is None or v == '':
|
||||||
|
continue
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd')
|
||||||
|
if i == 0:
|
||||||
|
shd.set(qn('w:fill'), 'F0F4F8' if ri % 2 else 'FFFFFF')
|
||||||
|
else:
|
||||||
|
shd.set(qn('w:fill'), 'F0F4F8' if ri % 2 else 'FFFFFF')
|
||||||
|
tcPr.append(shd)
|
||||||
|
if i == 0:
|
||||||
|
p = cell.paragraphs[0]; r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
|
||||||
|
else:
|
||||||
|
p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.name = 'Segoe UI'
|
||||||
|
if i == 3 or i == 4:
|
||||||
|
color = RISK if v > 0 else OK
|
||||||
|
r.bold = True; r.font.color.rgb = color
|
||||||
|
ri += 1
|
||||||
|
cap = doc.add_paragraph()
|
||||||
|
cap.paragraph_format.space_before = Pt(4)
|
||||||
|
r = cap.add_run('Красное — рост относительно 2024, зелёное — снижение. Полный список — лист «Детализация» в Excel-отчёте.')
|
||||||
|
r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
# ---------- 1пг 2026 ----------
|
||||||
|
doc.add_paragraph()
|
||||||
|
h = doc.add_paragraph()
|
||||||
|
h.paragraph_format.space_before = Pt(10)
|
||||||
|
r = h.add_run('3. 1-е полугодие 2026: существующие статьи (|Δ| ≥ 50 млн тг, к темпу 2025)')
|
||||||
|
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
tab = doc.add_table(rows=1, cols=5)
|
||||||
|
tab.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
hdr = tab.rows[0].cells
|
||||||
|
labels = ['Позиция', 'Темп 1пг 2025, млн', '1пг 2026, млн', 'Δ, млн тг', 'Δ %']
|
||||||
|
for i, l in enumerate(labels):
|
||||||
|
cell = hdr[i]
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), '142433'); tcPr.append(shd)
|
||||||
|
p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT if i > 0 else WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
r = p.add_run(l)
|
||||||
|
r.bold = True; r.font.size = Pt(9); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
cands26 = [r for r in rows if (r.get('q1_2026') or 0) > 0 and (r.get('y2025') or 0) > 0]
|
||||||
|
for c in cands26:
|
||||||
|
c['q1_2025'] = c['y2025'] / 2
|
||||||
|
c['d26'] = c['q1_2026'] - c['q1_2025']
|
||||||
|
c['p26'] = c['d26'] / c['q1_2025'] * 100 if c['q1_2025'] else 0
|
||||||
|
cands26.sort(key=lambda r: abs(r.get('d26') or 0), reverse=True)
|
||||||
|
cands26 = [c for c in cands26 if abs(c['d26']) >= 50e6][:15]
|
||||||
|
ri = 0
|
||||||
|
for it in cands26:
|
||||||
|
row = tab.add_row().cells
|
||||||
|
for i, (v, fmt) in enumerate([
|
||||||
|
(it.get('name'), '{:s}'),
|
||||||
|
(it.get('q1_2025') / 1e6, '{:,.1f}'),
|
||||||
|
(it.get('q1_2026') / 1e6, '{:,.1f}'),
|
||||||
|
(it.get('d26') / 1e6, '{:+,.1f}'),
|
||||||
|
(it.get('p26'), '{:+.1f} %'),
|
||||||
|
]):
|
||||||
|
cell = row[i]
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'F0F4F8' if ri % 2 else 'FFFFFF'); tcPr.append(shd)
|
||||||
|
if i == 0:
|
||||||
|
p = cell.paragraphs[0]; r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
|
||||||
|
else:
|
||||||
|
p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.name = 'Segoe UI'
|
||||||
|
if i == 3 or i == 4:
|
||||||
|
color = RISK if v > 0 else OK
|
||||||
|
r.bold = True; r.font.color.rgb = color
|
||||||
|
ri += 1
|
||||||
|
cap = doc.add_paragraph()
|
||||||
|
cap.paragraph_format.space_before = Pt(4)
|
||||||
|
r = cap.add_run('«Темп 1пг 2025» = факт 2025 / 2 (ориентир при равномерном распределении). Красное — рост, зелёное — снижение.')
|
||||||
|
r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
# ---------- Новые / исчезнувшие ----------
|
||||||
|
doc.add_paragraph()
|
||||||
|
h = doc.add_paragraph()
|
||||||
|
h.paragraph_format.space_before = Pt(10)
|
||||||
|
r = h.add_run('4. Смена номенклатуры: что появилось / исчезло в 2025')
|
||||||
|
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
names24 = set(r['name'] for r in rows if (r.get('y2024') or 0) > 0 and (r.get('y2025') or 0) == 0)
|
||||||
|
new25 = [r for r in rows if (r.get('y2025') or 0) > 0 and (r.get('y2024') or 0) == 0]
|
||||||
|
gone25 = [r for r in rows if (r.get('y2024') or 0) > 0 and (r.get('y2025') or 0) == 0]
|
||||||
|
new25.sort(key=lambda r: r['y2025'], reverse=True)
|
||||||
|
gone25.sort(key=lambda r: r['y2024'], reverse=True)
|
||||||
|
|
||||||
|
def small_table(rows_, idx, ykey, title):
|
||||||
|
h2 = doc.add_paragraph()
|
||||||
|
h2.paragraph_format.space_before = Pt(6); h2.paragraph_format.space_after = Pt(4)
|
||||||
|
r = h2.add_run(title)
|
||||||
|
r.bold = True; r.font.size = Pt(11); r.font.color.rgb = ACCENT; r.font.name = 'Segoe UI'
|
||||||
|
t2 = doc.add_table(rows=1, cols=3)
|
||||||
|
t2.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
hdr = t2.rows[0].cells
|
||||||
|
for i, l in enumerate(['Позиция', f'{ykey}, млн тг', 'Коммент']):
|
||||||
|
cell = hdr[i]
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'F0F4F8'); tcPr.append(shd)
|
||||||
|
p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT if i == 1 else WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
r = p.add_run(l)
|
||||||
|
r.bold = True; r.font.size = Pt(8); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
for it in rows_:
|
||||||
|
row = t2.add_row().cells
|
||||||
|
row[0].paragraphs[0].add_run(it['name']).font.size = Pt(8)
|
||||||
|
p = row[1].paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
r = p.add_run(f"{it[ykey]/1e6:,.1f}"); r.font.size = Pt(8)
|
||||||
|
row[2].paragraphs[0].add_run('').font.size = Pt(8)
|
||||||
|
# Итог
|
||||||
|
tr = t2.add_row().cells
|
||||||
|
tr[0].paragraphs[0].add_run(f"Итого: {len(rows_)} шт.").bold = True
|
||||||
|
p = tr[1].paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
r = p.add_run(f"{sum(r_[ykey] for r_ in rows_)/1e6:,.1f}"); r.bold = True; r.font.size = Pt(8)
|
||||||
|
# Заполнить фон итога
|
||||||
|
for cell in tr:
|
||||||
|
tcPr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'EEF4FB'); tcPr.append(shd)
|
||||||
|
|
||||||
|
small_table(new25[:12], 0, 'y2025', f'Появились в 2025 (нет в 2024): {len(new25)} шт.')
|
||||||
|
small_table(gone25[:12], 1, 'y2024', f'Исчезли в 2025 (были в 2024): {len(gone25)} шт.')
|
||||||
|
cap = doc.add_paragraph()
|
||||||
|
cap.paragraph_format.space_before = Pt(4)
|
||||||
|
sum_new = sum(r['y2025'] for r in new25)
|
||||||
|
sum_gone = sum(r['y2024'] for r in gone25)
|
||||||
|
r = cap.add_run(f'Пришло {b(sum_new):,.2f} млрд тг, ушло {b(sum_gone):,.2f} млрд тг. Чистая смена номенклатуры: {b(sum_new - sum_gone):+.2f} млрд тг — это механика спада 2025.')
|
||||||
|
r.font.size = Pt(9); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
# ---------- Рекомендации ----------
|
||||||
|
doc.add_paragraph()
|
||||||
|
h = doc.add_paragraph()
|
||||||
|
h.paragraph_format.space_before = Pt(10)
|
||||||
|
r = h.add_run('5. Рекомендации')
|
||||||
|
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
recs = [
|
||||||
|
('Приоритет 1 — сверить периметры 1пг 2026.',
|
||||||
|
'MS EA (базовая строка 630 млн = как за весь 2025), Oracle HW (376 млн, новая), Ericsson «общий новый договор» (827 млн, объединение CS part + 5G part).'),
|
||||||
|
('Приоритет 2 — консолидация Ericsson.',
|
||||||
|
'Объединение в один договор уже сделано в 2026. Следующий шаг — переход на гибридную модель (P1 + RMA за вендора, P2/P3 силами КТ).'),
|
||||||
|
('Приоритет 3 — проверить FLM.',
|
||||||
|
'+35,1 % к темпу 2025. 2 184 млн тг за 1пг 2026. Если рост продолжается — пересмотреть условия.'),
|
||||||
|
('Приоритет 4 — зафиксировать сокращения.',
|
||||||
|
'CORM-Ронекс −26,9 млн, Aitu Passport −21,9 млн, Nexign −68 млн (к темпу 2025). Заложить в план 2027.'),
|
||||||
|
('Приоритет 5 — реестр статусов.',
|
||||||
|
'К каждой статье: «прямой / посредник / дистрибьютор», «готов к инсорсингу / нужен вендор», P1/RMA за период.'),
|
||||||
|
]
|
||||||
|
for i, (b_, s_) in enumerate(recs, 1):
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.paragraph_format.space_after = Pt(4)
|
||||||
|
r = p.add_run(f' {b_}')
|
||||||
|
r.bold = True; r.font.size = Pt(10.5); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
p2 = doc.add_paragraph()
|
||||||
|
p2.paragraph_format.space_after = Pt(10)
|
||||||
|
p2.paragraph_format.left_indent = Cm(0.75)
|
||||||
|
r = p2.add_run(s_)
|
||||||
|
r.font.size = Pt(9.5); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
# ---------- Итог ----------
|
||||||
|
doc.add_paragraph()
|
||||||
|
h = doc.add_paragraph()
|
||||||
|
h.paragraph_format.space_before = Pt(10)
|
||||||
|
r = h.add_run('6. Итог')
|
||||||
|
r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.paragraph_format.space_after = Pt(6)
|
||||||
|
r = p.add_run(f'−{b(abs(t25-t24)):,.2f} млрд тг ({p24_25:+.1f} %) к 2025')
|
||||||
|
r.bold = True; r.font.size = Pt(20); r.font.color.rgb = ACCENT; r.font.name = 'Segoe UI'
|
||||||
|
bullets = [
|
||||||
|
'Падение 2025 — не экономия, а смена номенклатуры (FLM Expenses → FLM, Ericsson part → общий договор, MS EA COGS/G&A).',
|
||||||
|
'1пг 2026 показывает рост +1,3 % к темпу 2025: драйверы — MS EA +15,7 млн, Checkpoint +64,9 млн.',
|
||||||
|
'Главный риск 2027 — MS EA: 630 млн за 1пг 2026 = как за весь 2025. При сохранении темпа год ≈ +100 %. Следующий шаг — сверить периметры договоров и зафиксировать реестр статусов (прямой / посредник, инсорсинг / вендор).',
|
||||||
|
]
|
||||||
|
for b_ in bullets:
|
||||||
|
bullet(b_)
|
||||||
|
|
||||||
|
# ---------- Подпись ----------
|
||||||
|
doc.add_paragraph()
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
r = p.add_run('Источник: «Расходы по сервисным контрактам.xlsx», факт. Детали: Excel-отчёт (6 листов) и презентация Power Point.')
|
||||||
|
r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
|
||||||
|
|
||||||
|
doc.save('Анализ_расходов_по_сервисным_контрактам.docx')
|
||||||
|
print('OK: Анализ_расходов_по_сервисным_контрактам.docx')
|
||||||
@ -32,14 +32,15 @@ const srv = http.createServer((req, res) => {
|
|||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return fs.createReadStream(path.join(__dirname, "data.json")).pipe(res);
|
return fs.createReadStream(path.join(__dirname, "data.json")).pipe(res);
|
||||||
}
|
}
|
||||||
// Отчёт — xlsx. Отдаём как приложение, чтобы браузер скачал, а не попробовал показать.
|
// Отчёт — xlsx/docx. Отдаём как приложение, чтобы браузер скачал, а не попробовал показать.
|
||||||
const m = url.match(/^\/([^\/]+\.(xlsx|csv|json|html|js|css))/i);
|
const m = url.match(/^\/([^\/]+\.(xlsx|docx|csv|json|html|js|css))/i);
|
||||||
if (m) {
|
if (m) {
|
||||||
const file = path.basename(decodeURIComponent(m[1]));
|
const file = path.basename(decodeURIComponent(m[1]));
|
||||||
const fp = path.join(__dirname, file);
|
const fp = path.join(__dirname, file);
|
||||||
if (fs.existsSync(fp)) {
|
if (fs.existsSync(fp)) {
|
||||||
const ct =
|
const ct =
|
||||||
file.toLowerCase().endsWith(".xlsx") ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" :
|
file.toLowerCase().endsWith(".xlsx") ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" :
|
||||||
|
file.toLowerCase().endsWith(".docx") ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" :
|
||||||
file.toLowerCase().endsWith(".csv") ? "text/csv; charset=utf-8" :
|
file.toLowerCase().endsWith(".csv") ? "text/csv; charset=utf-8" :
|
||||||
file.toLowerCase().endsWith(".json") ? "application/json" :
|
file.toLowerCase().endsWith(".json") ? "application/json" :
|
||||||
file.toLowerCase().endsWith(".js") ? "text/javascript" :
|
file.toLowerCase().endsWith(".js") ? "text/javascript" :
|
||||||
|
|||||||
BIN
Анализ_расходов_по_сервисным_контрактам.docx
Normal file
BIN
Анализ_расходов_по_сервисным_контрактам.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user