560 lines
48 KiB
Python
560 lines
48 KiB
Python
"""Rebuild the editable lecture deck and synthetic workbook. No network access.
|
||
|
||
Run from any directory: py scripts/build_materials.py
|
||
Uses the supplied PPTX theme/layouts, not a re-created logo.
|
||
"""
|
||
from pathlib import Path
|
||
from datetime import datetime, timedelta
|
||
from collections import Counter
|
||
import argparse
|
||
import json
|
||
import re
|
||
import qrcode
|
||
import xlsxwriter
|
||
from pptx import Presentation
|
||
from pptx.util import Inches, Pt
|
||
from pptx.dml.color import RGBColor
|
||
from pptx.enum.shapes import MSO_SHAPE
|
||
from pptx.enum.text import MSO_ANCHOR
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
OUT = ROOT / 'materials'
|
||
OUT.mkdir(exist_ok=True)
|
||
BLUE, GREEN, PINK, ORANGE, YELLOW = '0501CE', '15E585', 'F72485', 'FF6839', 'FEDD00'
|
||
BLACK, GRAY, LIGHT, WHITE = '171724', '555566', 'F4F4F4', 'FFFFFF'
|
||
URL = 'https://t.me/kirill_bouko_it'
|
||
DAY = datetime(2026, 10, 20)
|
||
TIMES = [60,60,60,120,120,120,120,120,120,180,360,120,60,120,180,120,120,180,420,180,120,60,90,60,30,300]
|
||
assert sum(TIMES) == 3600
|
||
DEPTS = ['Организационный отдел','Правовой отдел','Финансовый отдел','Отдел обращений','ИТ-отдел',
|
||
'Аналитический отдел']
|
||
STATUSES = ['Выполнено','В работе','Новое']
|
||
CATEGORIES = ['Организация','Документы','Отчётность']
|
||
# План по подразделениям: (подразделение, выполнено в срок, выполнено с задержкой,
|
||
# открытых просрочено, открытых в срок).
|
||
# Итого: 100 поручений, 40 выполнено (20 в срок и 20 с задержкой), 30 открытых просрочено, 30 в срок.
|
||
PLAN = [
|
||
('Организационный отдел', 4, 3, 6, 4),
|
||
('Правовой отдел', 4, 3, 6, 4),
|
||
('Финансовый отдел', 3, 4, 4, 6),
|
||
('Отдел обращений', 3, 4, 4, 6),
|
||
('ИТ-отдел', 3, 3, 5, 5),
|
||
('Аналитический отдел', 3, 3, 5, 5),
|
||
]
|
||
rows = []
|
||
seq = 0
|
||
for dept, in_time, late, overdue, on_time in PLAN:
|
||
for kind, count in (('in_time', in_time), ('late', late), ('overdue', overdue), ('on_time', on_time)):
|
||
for _ in range(count):
|
||
seq += 1
|
||
if kind == 'in_time':
|
||
deadline = DAY - timedelta(days=5 + seq % 24)
|
||
completed = deadline - timedelta(days=1 + seq % 3)
|
||
elif kind == 'late':
|
||
deadline = DAY - timedelta(days=8 + seq % 18)
|
||
completed = deadline + timedelta(days=1 + seq % 4)
|
||
elif kind == 'overdue':
|
||
deadline = DAY - timedelta(days=1 + seq % 18)
|
||
completed = None
|
||
else:
|
||
deadline = DAY + timedelta(days=seq % 21)
|
||
completed = None
|
||
received = min(deadline - timedelta(days=7 + seq % 12), DAY - timedelta(days=1))
|
||
status = 'Выполнено' if kind in ('in_time', 'late') else ('В работе' if seq % 2 == 0 else 'Новое')
|
||
control = ('Завершено' if status == 'Выполнено'
|
||
else ('Просрочено' if deadline < DAY else 'В срок'))
|
||
rows.append([f'П-{seq:03}',dept,CATEGORIES[seq % 3],received,deadline,status,completed,control])
|
||
ROWS_TOTAL = len(rows)
|
||
FIRST_DATA, LAST_DATA = 2, ROWS_TOTAL + 1 # строки Excel с данными листа «Реестр»
|
||
counts = Counter(r[7] for r in rows)
|
||
assert ROWS_TOTAL == 100 and counts == {'Завершено':40,'Просрочено':30,'В срок':30}, counts
|
||
assert sum(1 for r in rows if r[5]=='Выполнено')==40
|
||
assert sum(1 for r in rows if r[6] is not None and r[6]>r[4])==20,'должно быть 20 выполненных с задержкой'
|
||
assert all(r[3] < r[4] for r in rows),'дата поступления должна быть раньше срока'
|
||
assert all(r[3] < DAY and (r[6] is None or r[6] <= DAY) for r in rows),'даты не должны выходить за дату среза'
|
||
assert len({r[1] for r in rows})==6 and all(r[1] in DEPTS for r in rows)
|
||
assert all(r[2] in CATEGORIES and r[5] in STATUSES for r in rows), 'значение вне выпадающего списка'
|
||
|
||
|
||
def build_chart_sheet(book,head,normal,wrap):
|
||
"""Лист с диаграммами: данные считаются из «Реестра», диаграммы строятся по этому блоку.
|
||
|
||
На лекции лист не используется: диаграммы спикер строит вживую.
|
||
Включается флагом --with-chart-sheet.
|
||
"""
|
||
charts=book.add_worksheet('Диаграмма')
|
||
charts.set_column('A:A',32);charts.set_column('B:D',13);charts.set_column('E:E',2)
|
||
charts.write_row(0,0,['Подразделение','Завершено','Просрочено','В срок'],head)
|
||
charts.set_row(0,34)
|
||
for n,dept in enumerate(DEPTS):
|
||
r=n+2
|
||
done=sum(1 for x in rows if x[1]==dept and x[7]=='Завершено')
|
||
late=sum(1 for x in rows if x[1]==dept and x[7]=='Просрочено')
|
||
soon=sum(1 for x in rows if x[1]==dept and x[7]=='В срок')
|
||
charts.set_row(r-1,22)
|
||
charts.write(r-1,0,dept,normal)
|
||
charts.write_formula(r-1,1,f"=COUNTIFS('Реестр'!$B$2:$B${LAST_DATA},$A{r},'Реестр'!$H$2:$H${LAST_DATA},\"Завершено\")",normal,done)
|
||
charts.write_formula(r-1,2,f"=COUNTIFS('Реестр'!$B$2:$B${LAST_DATA},$A{r},'Реестр'!$H$2:$H${LAST_DATA},\"Просрочено\")",normal,late)
|
||
charts.write_formula(r-1,3,f"=COUNTIFS('Реестр'!$B$2:$B${LAST_DATA},$A{r},'Реестр'!$H$2:$H${LAST_DATA},\"В срок\")",normal,soon)
|
||
total_chart_row=len(DEPTS)+2 # Excel row of ИТОГО in the chart block
|
||
charts.write(total_chart_row-1,0,'ИТОГО',head)
|
||
charts.write_formula(total_chart_row-1,1,f"=COUNTIF('Реестр'!$H$2:$H${LAST_DATA},\"Завершено\")",head,40)
|
||
charts.write_formula(total_chart_row-1,2,f"=COUNTIF('Реестр'!$H$2:$H${LAST_DATA},\"Просрочено\")",head,30)
|
||
charts.write_formula(total_chart_row-1,3,f"=COUNTIF('Реестр'!$H$2:$H${LAST_DATA},\"В срок\")",head,30)
|
||
charts.merge_range(total_chart_row,0,total_chart_row,3,'Блок слева пересчитывается из «Реестра». Диаграммы обновляются, как сводная: их можно обновить или перестроить.',wrap)
|
||
charts.set_row(total_chart_row,42)
|
||
charts.set_landscape();charts.set_paper(9);charts.fit_to_pages(1,0)
|
||
last_dept_row=len(DEPTS)+1
|
||
column=book.add_chart({'type':'column','subtype':'stacked'})
|
||
for col,(label,color) in enumerate([('Завершено',GREEN),('Просрочено',PINK),('В срок',BLUE)]):
|
||
column.add_series({'name':label,'categories':['Диаграмма',1,0,last_dept_row,0],
|
||
'values':['Диаграмма',1,1+col,last_dept_row,1+col],
|
||
'fill':{'color':'#'+color},'border':{'color':'#FFFFFF'}})
|
||
column.set_title({'name':'Поручения по подразделениям','name_font':{'name':'Segoe UI Semibold','size':14}})
|
||
column.set_x_axis({'name':'Подразделение'})
|
||
column.set_y_axis({'name':'Количество поручений','major_gridlines':{'visible':True}})
|
||
column.set_legend({'position':'bottom'})
|
||
column.set_size({'width':720,'height':360})
|
||
charts.insert_chart('F2',column)
|
||
pie=book.add_chart({'type':'pie'})
|
||
pie.add_series({'name':'Контроль срока','categories':['Диаграмма',0,1,0,3],
|
||
'values':['Диаграмма',total_chart_row-1,1,total_chart_row-1,3],
|
||
'data_labels':{'percentage':True,'font':{'size':11}},
|
||
'points':[{'fill':{'color':'#'+GREEN}},{'fill':{'color':'#'+PINK}},{'fill':{'color':'#'+BLUE}}]})
|
||
pie.set_title({'name':'Структура по контролю срока','name_font':{'name':'Segoe UI Semibold','size':14}})
|
||
pie.set_legend({'position':'bottom'})
|
||
pie.set_size({'width':420,'height':340})
|
||
charts.insert_chart('F21',pie)
|
||
|
||
|
||
def build_workbook(with_charts=False):
|
||
book = xlsxwriter.Workbook(OUT / 'excel-practice.xlsx', {'strings_to_formulas': False})
|
||
book.set_properties({'title':'Учебный реестр поручений','author':'Бойко Кирилл','comments':'Синтетические данные. Не реальные сведения органа власти.'})
|
||
head = book.add_format({'bold':True,'bg_color':'#'+BLUE,'font_color':'white','text_wrap':True,'valign':'vcenter'})
|
||
normal = book.add_format({'font_name':'Calibri','font_size':12,'valign':'vcenter'})
|
||
date = book.add_format({'num_format':'dd.mm.yyyy','font_size':12})
|
||
pct = book.add_format({'num_format':'0%','font_size':12})
|
||
wrap = book.add_format({'font_size':12,'text_wrap':True,'valign':'top'})
|
||
alert = book.add_format({'bg_color':'#FFE3EA','font_color':'#9D123E'})
|
||
ok = book.add_format({'bg_color':'#DDFAEC','font_color':'#086B40'})
|
||
headers = ['ID','Подразделение','Категория','Дата поступления','Срок исполнения','Статус','Дата исполнения','Контроль срока']
|
||
inst = book.add_worksheet('Инструкция')
|
||
inst.set_column('A:A',30); inst.set_column('B:B',100)
|
||
instructions = [
|
||
('Учебный материал',f'{ROWS_TOTAL} вымышленных поручений в шести подразделениях. Лекция 20.10.2026. Никаких реальных ПДн.'),
|
||
('Порядок демонстрации','Старт: простое условие (одно ЕСЛИ, без дат). Старт 2: вложенное условие (два ЕСЛИ, с датой среза). Реестр: готовый итог.'),
|
||
('Старт','Простое условие по статусу: =ЕСЛИ(F2="Выполнено";"Закрыто";"В работе"). Даты не используются.'),
|
||
('Старт 2','Вложенное условие: сначала статус, затем срок против Параметры!B2. Здесь появляется «Просрочено».'),
|
||
('Реестр',f'Готовая умная таблица и формулы H2:H{LAST_DATA}. Проверенный образец, не исходник для очистки.'),
|
||
('Параметры!B2','Фиксированная дата среза 20.10.2026. СЕГОДНЯ() намеренно не используется.'),
|
||
('Сводная','Вставка → Сводная таблица. Источник РеестрПоручений. Строки: Подразделение; столбцы: Контроль срока; значения: количество ID.'),
|
||
('Эталон отчёта','Строки 1–10: статический эталон, НЕ настоящая сводная. С 15-й строки — отчёт на формулах, который считается из Реестра и обновляется сам.'),
|
||
('Проверка','Ожидаемые итоги: 100 всего; 40 выполнено; 60 открыто; 30 открытых просрочено; 50% просроченных среди открытых.'),
|
||
('Смысл формулы H','Выполненные получают Завершено независимо от соблюдения срока. Проверка своевременности выполненных — отдельный расчёт.'),
|
||
('Границы примера','Выполненных с задержкой 20, без задержки 20. Учебный флаг просрочки считает только открытые.'),
|
||
('Выпадающие списки','Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе Параметры. Опечатки исключены.'),
|
||
('Диаграмма','Лист намеренно не включён: диаграммы строим вживую. Данные проще всего взять из сводной (Контроль срока) или получить СЧЁТЕСЛИМН по подразделениям, как в отчёте на формулах.'),
|
||
('Не делать','Не перезаписывать оригинал при демонстрации, не отправлять реальные служебные данные внешней модели.'),
|
||
('Формулы в файле','Внутри XLSX функции сохранены на английском по стандарту. Excel показывает локализованные имена.'),
|
||
]
|
||
for n,(a,b) in enumerate(instructions):
|
||
inst.write(n,0,a,head);inst.write(n,1,b,wrap);inst.set_row(n,52)
|
||
params = book.add_worksheet('Параметры');params.set_column('A:A',30);params.set_column('B:B',26)
|
||
params.write('A1','Параметр',head);params.write('B1','Значение',head)
|
||
params.write('A2','Дата среза',normal);params.write_datetime('B2',DAY,date)
|
||
# Источники выпадающих списков. Правятся здесь и сразу действуют на всех листах.
|
||
picklists=[('Допустимые статусы',STATUSES,3,'AllowedStatuses'),
|
||
('Допустимые подразделения',DEPTS,8,'AllowedDepartments'),
|
||
('Допустимые категории',CATEGORIES,15,'AllowedCategories')]
|
||
for label,values,head_row,name in picklists:
|
||
params.write(head_row,0,label,head)
|
||
params.set_row(head_row,22)
|
||
for k,v in enumerate(values):
|
||
params.write(head_row+1+k,0,v,normal)
|
||
params.set_row(head_row+k,20)
|
||
first,last=head_row+2,head_row+1+len(values)
|
||
book.define_name(name,f"='Параметры'!$A${first}:$A${last}")
|
||
def simple_formula(r): return f'=IF(F{r}="Выполнено","Закрыто","В работе")'
|
||
def nested_formula(r): return f'=IF(F{r}="Выполнено","Завершено",IF(E{r}<\'Параметры\'!$B$2,"Просрочено","В срок"))'
|
||
demo = [
|
||
('Старт', simple_formula, lambda row:'Закрыто' if row[5]=='Выполнено' else 'В работе',
|
||
'ШАГ 1. Простое условие: одно ЕСЛИ, только по статусу. Даты пока не используем.',
|
||
'=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")', False),
|
||
('Старт 2', nested_formula, lambda row:row[7],
|
||
'ШАГ 2. Вложенное условие: сначала статус, затем срок против даты среза из Параметры!B2.',
|
||
'=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))', False),
|
||
('Реестр', nested_formula, lambda row:row[7],
|
||
'ГОТОВЫЙ ИТОГ. Умная таблица с формулами H2:H61 и подсветкой значений.',
|
||
'=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))', True),
|
||
]
|
||
for name, formula, expected_value, note_text, formula_text, ready in demo:
|
||
sh=book.add_worksheet(name);sh.freeze_panes(1,2);sh.set_zoom(110)
|
||
for c,w in enumerate([12,29,19,19,19,18,19,20]):sh.set_column(c,c,w,normal)
|
||
sh.set_row(0,36)
|
||
sh.write_row(0,0,headers,head)
|
||
for n,row in enumerate(rows,1):
|
||
sh.set_row(n,23)
|
||
for c,v in enumerate(row[:7]):
|
||
if isinstance(v,datetime):sh.write_datetime(n,c,v,date)
|
||
elif v is not None:sh.write(n,c,v,normal)
|
||
sh.write_formula(n,7,formula(n+1),normal,expected_value(row))
|
||
if ready:
|
||
sh.add_table(0,0,LAST_DATA-1,7,{'name':'РеестрПоручений','style':'Table Style Medium 2','columns':[{'header':h} for h in headers]})
|
||
sh.conditional_format(f'H2:H{LAST_DATA}',{'type':'text','criteria':'containing','value':'Просрочено','format':alert})
|
||
sh.conditional_format(f'H2:H{LAST_DATA}',{'type':'text','criteria':'containing','value':'Завершено','format':ok})
|
||
sh.data_validation('F2:F1000',{'validate':'list','source':'=AllowedStatuses','error_title':'Выберите статус','error_message':'Используйте значение из списка.'})
|
||
sh.data_validation('B2:B1000',{'validate':'list','source':'=AllowedDepartments','error_title':'Выберите подразделение','error_message':'Используйте значение из списка.'})
|
||
sh.data_validation('C2:C1000',{'validate':'list','source':'=AllowedCategories','error_title':'Выберите категорию','error_message':'Используйте значение из списка.'})
|
||
sh.merge_range(LAST_DATA+1,0,LAST_DATA+1,7,note_text,wrap);sh.set_row(LAST_DATA+1,26)
|
||
sh.merge_range(LAST_DATA+2,0,LAST_DATA+2,7,'Формула: '+formula_text,wrap);sh.set_row(LAST_DATA+2,26)
|
||
sh.set_landscape();sh.fit_to_pages(1,0);sh.repeat_rows(0)
|
||
check=book.add_worksheet('Проверка');check.set_column('A:A',47);check.set_column('B:C',22)
|
||
check.write_row('A1',['Показатель','Расчёт','Ожидается'],head)
|
||
checks=[
|
||
('Всего поручений',f"=COUNTA('Реестр'!A2:A{LAST_DATA})",100),
|
||
('Выполнено',f'=COUNTIF(\'Реестр\'!F2:F{LAST_DATA},"Выполнено")',40),
|
||
('Открыто','=B2-B3',60),
|
||
('Открытых просрочено',f'=COUNTIF(\'Реестр\'!H2:H{LAST_DATA},"Просрочено")',30),
|
||
('Доля просроченных среди открытых','=IF(B4=0,0,B5/B4)',.5),
|
||
('Уникальных ID (контроль при сборке)',None,100),
|
||
('Пропусков обязательных полей (A:F)',f'=COUNTBLANK(\'Реестр\'!A2:F{LAST_DATA})',0),
|
||
('Выполненных с задержкой (контроль при сборке)',None,20),
|
||
]
|
||
for n,(label,formula,expected) in enumerate(checks,1):
|
||
check.set_row(n,32);check.write(n,0,label,wrap)
|
||
fmt=pct if n==5 else normal
|
||
if formula:check.write_formula(n,1,formula,fmt,expected)
|
||
else:check.write(n,1,expected,fmt)
|
||
check.write(n,2,expected,fmt)
|
||
check.write('A12','Статические проверки нужно повторить после изменения данных.',wrap)
|
||
check.set_row(11,45)
|
||
report=book.add_worksheet('Эталон отчёта');report.set_column('A:A',32);report.set_column('B:E',19);report.set_column('F:F',26)
|
||
report.merge_range('A1:E1','СТАТИЧЕСКИЙ ЭТАЛОН — не сводная таблица Excel',head)
|
||
report.write_row('A3',['Подразделение','Завершено','Просрочено','В срок','Всего'],head)
|
||
static_first=4
|
||
for n,dept in enumerate(DEPTS):
|
||
c=Counter(r[7] for r in rows if r[1]==dept)
|
||
total=sum(1 for r in rows if r[1]==dept)
|
||
report.write_row(static_first-1+n,0,[dept,c['Завершено'],c['Просрочено'],c['В срок'],total],normal)
|
||
report.set_row(static_first-1+n,28)
|
||
static_last=static_first+len(DEPTS)-1 # Excel 9
|
||
static_total=static_last+1 # Excel 10
|
||
report.write_row(static_total-1,0,['ИТОГО',40,30,30,100],head)
|
||
report.write('A12','Для обновляемого отчёта создайте сводную через Вставка → Сводная таблица.',wrap)
|
||
report.set_row(11,70)
|
||
|
||
# Отчёт на формулах: считается из листа «Реестр» и обновляется сам. Начинается с 15-й строки.
|
||
report.merge_range('A15:F15','ОТЧЁТ НА ФОРМУЛАХ — считается из листа «Реестр» и обновляется сам',head)
|
||
report.set_row(14,26)
|
||
report.write_row(15,0,['Подразделение','Всего','Завершено','Просрочено','В срок',
|
||
'Доля просроченных среди открытых'],head)
|
||
report.set_row(15,40)
|
||
rep_first=17
|
||
rep_last=rep_first+len(DEPTS)-1 # Excel 22
|
||
for n,dept in enumerate(DEPTS):
|
||
r=rep_first+n
|
||
total=sum(1 for x in rows if x[1]==dept)
|
||
done=sum(1 for x in rows if x[1]==dept and x[7]=='Завершено')
|
||
late=sum(1 for x in rows if x[1]==dept and x[7]=='Просрочено')
|
||
soon=sum(1 for x in rows if x[1]==dept and x[7]=='В срок')
|
||
report.set_row(r-1,22)
|
||
report.write(r-1,0,dept,normal)
|
||
report.write_formula(r-1,1,f"=COUNTIF('Реестр'!$B$2:$B${LAST_DATA},$A{r})",normal,total)
|
||
report.write_formula(r-1,2,f"=COUNTIFS('Реестр'!$B$2:$B${LAST_DATA},$A{r},'Реестр'!$H$2:$H${LAST_DATA},\"Завершено\")",normal,done)
|
||
report.write_formula(r-1,3,f"=COUNTIFS('Реестр'!$B$2:$B${LAST_DATA},$A{r},'Реестр'!$H$2:$H${LAST_DATA},\"Просрочено\")",normal,late)
|
||
report.write_formula(r-1,4,f"=COUNTIFS('Реестр'!$B$2:$B${LAST_DATA},$A{r},'Реестр'!$H$2:$H${LAST_DATA},\"В срок\")",normal,soon)
|
||
report.write_formula(r-1,5,f'=IF((D{r}+E{r})=0,0,D{r}/(D{r}+E{r}))',pct,late/(late+soon))
|
||
total_row=rep_last+1 # Excel 23
|
||
report.write(total_row-1,0,'ИТОГО',head)
|
||
report.write_formula(total_row-1,1,f"=COUNTA('Реестр'!$A$2:$A${LAST_DATA})",head,100)
|
||
report.write_formula(total_row-1,2,f"=COUNTIF('Реестр'!$H$2:$H${LAST_DATA},\"Завершено\")",head,40)
|
||
report.write_formula(total_row-1,3,f"=COUNTIF('Реестр'!$H$2:$H${LAST_DATA},\"Просрочено\")",head,30)
|
||
report.write_formula(total_row-1,4,f"=COUNTIF('Реестр'!$H$2:$H${LAST_DATA},\"В срок\")",head,30)
|
||
report.write_formula(total_row-1,5,
|
||
f'=IF((D{total_row}+E{total_row})=0,0,D{total_row}/(D{total_row}+E{total_row}))',head,.5)
|
||
sums_check=total_row+2 # Excel 25
|
||
report.write(sums_check-1,0,'Сверка: сумма по отделам равна итогу',normal)
|
||
report.write_formula(sums_check-1,1,
|
||
f'=IF(AND(SUM(C{rep_first}:C{rep_last})=C{total_row},SUM(D{rep_first}:D{rep_last})=D{total_row},'
|
||
f'SUM(E{rep_first}:E{rep_last})=E{total_row}),"Совпадает","Расхождение")',normal,'Совпадает')
|
||
checks_check=sums_check+1 # Excel 25
|
||
report.write(checks_check-1,0,'Сверка: итог совпадает с листом «Проверка»',normal)
|
||
report.write_formula(checks_check-1,1,
|
||
f'=IF(AND(B{total_row}=Проверка!B2,C{total_row}=Проверка!B3,D{total_row}=Проверка!B5),'
|
||
f'"Совпадает","Расхождение")',normal,'Совпадает')
|
||
report.write(checks_check+1,0,'Правите данные в «Реестре» — этот отчёт пересчитывается сам. Сводная требует обновления.',wrap)
|
||
report.set_row(checks_check,30)
|
||
# Лист с диаграммами строится только по запросу: на лекции диаграммы показываются вживую.
|
||
if with_charts:
|
||
build_chart_sheet(book, head, normal, wrap)
|
||
tests=book.add_worksheet('Тесты')
|
||
tests.set_column('A:A',36);tests.set_column('B:E',21)
|
||
tests.write_row('A1',['Случай','Статус','Срок','Ожидается','Формула'],head)
|
||
cases=[('Открыто, срок вчера','В работе',DAY-timedelta(days=1),'Просрочено'),
|
||
('Открыто, срок сегодня','Новое',DAY,'В срок'),
|
||
('Открыто, срок завтра','В работе',DAY+timedelta(days=1),'В срок'),
|
||
('Выполнено с задержкой','Выполнено',DAY-timedelta(days=1),'Завершено')]
|
||
for n,(label,status,due,expected) in enumerate(cases,1):
|
||
tests.write(n,0,label,normal);tests.write(n,1,status,normal)
|
||
tests.write_datetime(n,2,due,date);tests.write(n,3,expected,normal)
|
||
tests.write_formula(n,4,f'=IF(B{n+1}="Выполнено","Завершено",IF(C{n+1}<\'Параметры\'!$B$2,"Просрочено","В срок"))',normal,expected)
|
||
tests.set_row(n,28)
|
||
tests.merge_range('A8:E8','Последний случай: срок 19.10, исполнено 20.10. Формула не оценивает своевременность завершённых.',wrap)
|
||
tests.set_row(7,42)
|
||
tests.merge_range('A10:E10','Пустые/текстовые сроки и неизвестные статусы: сначала проверить данные; ответ формулы нельзя считать надёжным.',wrap)
|
||
tests.set_row(9,42)
|
||
book.close()
|
||
|
||
|
||
def rgb(code): return RGBColor.from_string(code)
|
||
def box(s,x,y,w,h,fill=LIGHT,line=None,radius=False):
|
||
shape=s.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE if radius else MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
|
||
shape.fill.solid();shape.fill.fore_color.rgb=rgb(fill)
|
||
if line:shape.line.color.rgb=rgb(line)
|
||
else:shape.line.fill.background()
|
||
if radius:
|
||
try:shape.adjustments[0]=.12
|
||
except IndexError:pass
|
||
return shape
|
||
|
||
|
||
def text(s,x,y,w,h,content,size=22,color=BLACK,bold=False,font=None):
|
||
tb=s.shapes.add_textbox(Inches(x),Inches(y),Inches(w),Inches(h))
|
||
tf=tb.text_frame;tf.clear();tf.word_wrap=True
|
||
tf.margin_left=tf.margin_right=0
|
||
tf.margin_top=tf.margin_bottom=0
|
||
for n,line in enumerate(content.split('\n')):
|
||
p=tf.paragraphs[0] if n==0 else tf.add_paragraph()
|
||
p.text=line;p.font.name=font or ('Segoe UI Semibold' if bold else 'Segoe UI')
|
||
p.font.size=Pt(size);p.font.color.rgb=rgb(color)
|
||
p.space_after=Pt(6);p.line_spacing=1.08
|
||
return tb
|
||
|
||
|
||
def card(s,x,y,w,h,title,body,accent=BLUE):
|
||
box(s,x,y,w,h,LIGHT,radius=True)
|
||
box(s,x,y,.055,h,accent)
|
||
text(s,x+.23,y+.22,w-.46,.66,title,24,accent,True)
|
||
text(s,x+.23,y+1.02,w-.46,h-1.12,body,21)
|
||
|
||
|
||
def banner(s,content,y=6.25,fill=BLUE,color=WHITE):
|
||
box(s,.48,y,12.34,.65,fill,radius=True)
|
||
text(s,.72,y+.12,11.86,.46,content,19,color,True)
|
||
|
||
|
||
def table(s,x,y,widths,data,row_h=.55,size=19):
|
||
for r,row in enumerate(data):
|
||
xx=x
|
||
for c,v in enumerate(row):
|
||
box(s,xx,y+r*row_h,widths[c]-.015,row_h-.015,BLUE if r==0 else ('F0F0FF' if r%2 else LIGHT))
|
||
text(s,xx+.12,y+r*row_h+.1,widths[c]-.24,row_h-.12,str(v),size,WHITE if r==0 else BLACK,bold=r==0)
|
||
xx+=widths[c]
|
||
|
||
|
||
def build_slides():
|
||
p=Presentation(ROOT/'Шаблон_лекции_новый_с_новым_лого.pptx')
|
||
# Retain original masters, theme, logos and layouts, remove sample slides.
|
||
for sid in list(p.slides._sldIdLst):
|
||
p.part.drop_rel(sid.rId);p.slides._sldIdLst.remove(sid)
|
||
p.core_properties.title='От формулы к ИИ-агенту: Excel в работе'
|
||
p.core_properties.author='Бойко Кирилл'
|
||
p.core_properties.subject='Лекция для государственных служащих · 20.10.2026'
|
||
p.core_properties.keywords='Excel 2016, Excel 2019, OpenWork, учебные данные'
|
||
notes=(OUT/'speaker-notes.md').read_text(encoding='utf-8')
|
||
sections=re.split(r'(?m)^## Слайд \d+\. ',notes)[1:]
|
||
assert len(sections)==26
|
||
manifest=[]
|
||
elapsed=0
|
||
|
||
def slide(title,section='EXCEL',dark=False,title_page=False):
|
||
nonlocal elapsed
|
||
n=len(p.slides)+1
|
||
s=p.slides.add_slide(p.slide_layouts[0 if title_page else (5 if dark else 1)])
|
||
if not title_page:
|
||
text(s,.48,.27,11.65,1.05,title,32,WHITE if dark else BLACK,True)
|
||
footer=WHITE if dark else GRAY
|
||
text(s,.5,7.15,11.4,.23,'БОЙКО КИРИЛЛ / 20.10.2026 / EXCEL И ИИ',10,footer)
|
||
text(s,12.3,7.1,.48,.3,f'{n:02}',12,footer)
|
||
s.notes_slide.notes_text_frame.text='Слайд '+str(n)+'. '+sections[n-1].strip()
|
||
manifest.append({'slide':n,'title':title,'seconds':TIMES[n-1],'start_seconds':elapsed})
|
||
elapsed+=TIMES[n-1]
|
||
return s
|
||
|
||
s=slide('От формулы к ИИ-агенту: Excel в работе',title_page=True)
|
||
text(s,.5,1.6,7.2,2.35,'ОТ ФОРМУЛЫ\nК ИИ-АГЕНТУ:\nEXCEL В РАБОТЕ',42,BLACK,True)
|
||
text(s,.52,4.3,6.7,.76,'Практические приёмы. Меньше рутины.\nПроверяемый результат.',22,GRAY)
|
||
text(s,.52,5.35,6.7,.46,'Бойко Кирилл',26,BLUE,True)
|
||
text(s,.52,5.95,6.7,.95,'Председатель республиканского отделения\nОО «ОПОРА РОССИИ»',20)
|
||
box(s,8.0,1.45,4.82,5.35,BLUE,radius=True)
|
||
text(s,8.35,1.8,4.1,.45,'ОДИН РАБОЧИЙ МАРШРУТ',18,WHITE,True)
|
||
for j,(num,label) in enumerate([('01','Таблица'),('02','Формула'),('03','Отчёт'),('04','ИИ-агент')]):
|
||
text(s,8.35,2.62+j*.83,.6,.48,num,22,GREEN,True)
|
||
text(s,9.12,2.62+j*.83,3.2,.48,label,26,WHITE,True)
|
||
text(s,8.35,6.18,4.0,.33,'60 МИНУТ • EXCEL 2016/2019',14,WHITE)
|
||
|
||
s=slide('ГДЕ ТЕРЯЕТСЯ ВРЕМЯ','СТАРТ · ВОПРОС В ЗАЛ')
|
||
text(s,.5,1.92,11.8,.9,'Кто пересобирал отчёт после\nпоявления новой версии таблицы?',30,BLUE,True)
|
||
for j,(h,b) in enumerate([('Собрать','Свести данные\nиз разных файлов'),('Пересчитать','Исправить даты\nи проверить формулы'),('Проверить','Найти расхождения\nперед отправкой')]):card(s,.5+j*4.18,3.35,3.97,2.35,h,b)
|
||
banner(s,'Автоматизация начинается с понятного и повторяемого процесса.')
|
||
|
||
s=slide('МАРШРУТ НА 60 МИНУТ','СТАРТ · ЧТО ПОЛУЧИТСЯ')
|
||
for j,(h,b) in enumerate([('03 мин · Старт','Сквозная задача\nи правила работы'),('23 мин · Excel','Таблица → формулы\n→ сводный отчёт'),('29 мин · ИИ и итоги','Агент → файл\n→ проверка результата')]):card(s,.5+j*4.18,2.05,3.97,2.7,h,b)
|
||
text(s,.65,5.1,11.9,.6,'+ 5 минут на вопросы. Ноутбуки участникам не нужны.',24,BLUE,True)
|
||
banner(s,'Учебный реестр: 100 вымышленных поручений в шести отделах. Дата среза: 20.10.2026.')
|
||
|
||
s=slide('ПРАВИЛЬНАЯ ТАБЛИЦА — ОСНОВА ОТЧЁТА')
|
||
table(s,.5,1.95,[1.25,3.6,2.55,2.4,2.55],[['ID','Подразделение','Срок','Статус','Контроль'],['П-005','Организационный','15.10.2026','В работе','Просрочено'],['П-008','Организационный','20.10.2026','Новое','В срок']],.68,19)
|
||
text(s,.6,4.3,5.7,1.5,'Одна строка — одно поручение.\nОдин столбец — один признак.',25,BLUE,True)
|
||
text(s,7.0,4.3,5.65,1.5,'Без объединённых ячеек.\nБез пустых строк внутри.\nЦвет не заменяет статус.',22)
|
||
banner(s,'Полный реестр в учебном XLSX; на слайде показаны только выбранные поля.')
|
||
|
||
s=slide('ДАТА, ЧИСЛО И ТЕКСТ — НЕ ОДНО И ТО ЖЕ')
|
||
card(s,.5,1.95,5.95,3.55,'Выглядит одинаково','20.10.2026 как дата\n20.10.2026 как текст\n\nОтображение ≠ тип значения')
|
||
card(s,6.72,1.95,6.1,3.55,'Проверяем до расчёта','Даты хранятся как даты.\nСтатусы — по единому списку.\nID с ведущими нулями — текст.')
|
||
banner(s,'Параметры!B2 = 20.10.2026 — фиксированный срез, а не СЕГОДНЯ().')
|
||
|
||
s=slide('CTRL+T: ДИАПАЗОН СТАНОВИТСЯ ТАБЛИЦЕЙ','EXCEL · ЖИВАЯ ДЕМОНСТРАЦИЯ')
|
||
text(s,.55,1.97,4.8,1.1,'Ctrl + T',54,BLUE,True)
|
||
text(s,.6,3.38,5.3,1.65,'Выделить данные → Ctrl+T\n→ «Таблица с заголовками»\n→ дать понятное имя',24)
|
||
card(s,6.55,1.95,6.25,3.95,'Что меняется','Фильтры в заголовках.\nРасширение при новых строках.\nЕдиный стиль и расчётный столбец.\nИсточник для сводной.')
|
||
banner(s,'Показываем на копии листа «Старт». Готовый образец — лист «Реестр».')
|
||
|
||
s=slide('ФИЛЬТР И СОРТИРОВКА: ДВА РАЗНЫХ ДЕЙСТВИЯ')
|
||
card(s,.5,1.95,5.96,3.66,'Фильтр','Оставить на экране\nнужные записи.\n\nПример: статус «Выполнено».')
|
||
card(s,6.72,1.95,6.1,3.66,'Сортировка','Изменить порядок строк.\n\nПример: сначала\nближайшие сроки.')
|
||
banner(s,'Сортируем всю таблицу, а не один столбец: строки должны оставаться целыми.')
|
||
|
||
s=slide('ПЕРВАЯ ФОРМУЛА И ЗНАК $')
|
||
text(s,.55,1.94,11.6,.52,'Сколько дней от даты среза до срока исполнения?',25,BLUE,True)
|
||
box(s,.5,2.85,12.3,1.03,LIGHT,radius=True)
|
||
text(s,.77,3.06,11.75,.6,'=E2-Параметры!$B$2',32,BLUE,True)
|
||
card(s,.5,4.23,5.96,1.62,'E2 меняется','При копировании вниз: E3, E4…')
|
||
card(s,6.72,4.23,6.1,1.62,'$B$2 не меняется','Дата среза закреплена.')
|
||
banner(s,'Формат результата — число. СУММ складывает числа, а не статусы.')
|
||
|
||
s=slide('СЧЁТЕСЛИ: ОТВЕЧАЕМ НА КОНКРЕТНЫЙ ВОПРОС')
|
||
text(s,.55,1.92,11.6,.55,'Сколько поручений имеют статус «Выполнено»?',26,BLUE,True)
|
||
box(s,.5,2.86,12.3,1.15,LIGHT,radius=True)
|
||
text(s,.8,3.15,11.75,.55,'=СЧЁТЕСЛИ(F2:F61;"Выполнено")',29,BLUE,True)
|
||
text(s,.6,4.5,2.6,1.2,'40',64,BLUE,True)
|
||
text(s,3.3,4.53,9.2,1.4,'Из 100 учебных поручений.\nСчитаем совпадения, а не складываем строки.',25)
|
||
banner(s,'Обычный СЧЁТ считает числовые ячейки — для текстовых статусов он не подходит.')
|
||
|
||
s=slide('ЕСЛИ: ПРОЗРАЧНАЯ ЛОГИКА КОНТРОЛЯ СРОКОВ')
|
||
for j,(h,b) in enumerate([('1. Выполнено?','Да → «Завершено»'),('2. Срок < среза?','Да → «Просрочено»'),('3. Иначе','→ «В срок»')]):card(s,.5+j*4.18,1.98,3.97,1.95,h,b)
|
||
box(s,.5,4.22,12.3,1.6,LIGHT,radius=True)
|
||
text(s,.8,4.46,11.72,1.2,'=ЕСЛИ(F2="Выполнено";"Завершено";\nЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))',23,BLUE)
|
||
banner(s,'Нужны корректные даты и статусы. Своевременность выполненных считаем отдельно.')
|
||
|
||
s=slide('СВОДНАЯ: ИЗ РЕЕСТРА В ОТЧЁТ','EXCEL · ДЕМОНСТРАЦИЯ 6 МИНУТ')
|
||
text(s,.6,1.95,5.55,2.6,'Вставка → Сводная таблица\n\nСтроки: подразделение\nСтолбцы: контроль срока\nЗначения: количество ID',24)
|
||
table(s,6.6,1.98,[2.15,1.18,1.22,1.15],[['Отдел','Готово','Проср.','В срок'],['Организ.',7,6,4],['Правовой',7,6,4],['Финансовый',7,4,6],['Обращений',7,4,6],['ИТ-отдел',6,5,5],['Аналитич.',6,5,5]],.45,15)
|
||
text(s,.6,5.37,11.9,.52,'Проверка: 40 завершено + 30 просрочено + 30 в срок = 100.',24,BLUE,True)
|
||
banner(s,'После изменения исходника обновляем сводную. В XLSX есть статический эталон.')
|
||
|
||
s=slide('ПРОВЕРЯЕМ ОТЧЁТ, А НЕ ТОЛЬКО ЕГО ВИД')
|
||
for j,(num,label) in enumerate([('100','всего'),('40','выполнено'),('60','открыто'),('30','просрочено\nиз открытых')]):
|
||
box(s,.5+j*3.12,2.05,2.94,2.38,LIGHT,radius=True)
|
||
text(s,.73+j*3.12,2.3,2.47,.95,num,52,BLUE,True)
|
||
text(s,.73+j*3.12,3.36,2.47,.95,label,22)
|
||
text(s,.6,5.02,11.9,.85,'30 / 60 = 50% открытых поручений просрочено.\nДругой знаменатель — другой показатель.',24,BLUE,True)
|
||
banner(s,'Контрольный лист «Проверка» — независимая точка сверки, а не украшение отчёта.')
|
||
|
||
s=slide('ОТ НАВЫКА К ПОМОЩНИКУ',dark=True)
|
||
text(s,.6,2.6,11.6,1.9,'Мы научились объяснять задачу Excel.\nТеперь объясним её ИИ-агенту.',36,WHITE,True)
|
||
text(s,.6,5.03,11.5,1.12,'Поручаем рутину.\nСохраняем контроль над данными и результатом.',26,WHITE)
|
||
|
||
s=slide('АССИСТЕНТ И АГЕНТ: ЧТО МЫ ДЕЛЕГИРУЕМ','ИИ · БЕЗ МАГИИ')
|
||
card(s,.5,1.97,5.96,3.84,'Ассистент','Объясняет формулу.\nПредлагает шаги.\nГотовит текст задания.\n\nЧеловек выполняет действия.')
|
||
card(s,6.72,1.97,6.1,3.84,'Агент с инструментами','Может прочитать файл.\nВыполнить обработку.\nСоздать новый результат.\n\nДоступы и возможности ограничены.')
|
||
banner(s,'Возможности зависят от модели, инструментов, разрешений и настроек среды.')
|
||
|
||
s=slide('БЕЗОПАСНОСТЬ — ДО ЗАГРУЗКИ ФАЙЛА','ИИ · ОБЯЗАТЕЛЬНОЕ УСЛОВИЕ')
|
||
card(s,.5,1.95,5.96,3.82,'Не отправляем в публичный ИИ','Персональные данные.\nСлужебную информацию\nограниченного распространения.\nГосударственную тайну.',PINK)
|
||
card(s,6.72,1.95,6.1,3.82,'Для нашей демонстрации','Только вымышленные данные.\nОтдельная учебная папка.\nПроверенные модель и доступы.\nНикаких ведомственных систем.')
|
||
banner(s,'Локальный файл ≠ локальная обработка: содержимое может уйти провайдеру модели.',fill=PINK)
|
||
|
||
s=slide('OPENWORK: ОТ ЗАДАЧИ К НОВОМУ ФАЙЛУ','ИИ · СХЕМА, НЕ СКРИНШОТ ИНТЕРФЕЙСА')
|
||
steps=[('01','Рабочая папка','Только учебные файлы'),('02','Задача и границы','Что сделать и не менять'),('03','Работа агента','Разрешённые инструменты'),('04','Проверка','Новый XLSX + сверка')]
|
||
for j,(n,h,b) in enumerate(steps):
|
||
y=1.87+j*1.03
|
||
box(s,.5,y,.68,.68,BLUE,radius=True);text(s,.62,y+.14,.5,.4,n,20,WHITE,True)
|
||
text(s,1.5,y+.05,5.2,.48,h,25,BLUE,True);text(s,7.0,y+.08,5.75,.55,b,22)
|
||
banner(s,'Перед выступлением нужна репетиция в выбранной конфигурации OpenWork.')
|
||
|
||
s=slide('ХОРОШЕЕ ЗАДАНИЕ: ПЯТЬ СОСТАВЛЯЮЩИХ','ИИ · ШАБЛОН ЗАДАНИЯ')
|
||
items=[('Цель','Какой рабочий вопрос решаем?'),('Данные','Какой файл, листы и дата среза?'),('Результат','Новый XLSX, отчёт, формула или текст?'),('Ограничения','Что нельзя менять, отправлять и запускать?'),('Проверка','Какими числами и тестами сверять итог?')]
|
||
for j,(a,b) in enumerate(items):
|
||
y=1.9+j*.76
|
||
text(s,.6,y,3.0,.55,a,24,BLUE,True);text(s,3.85,y,8.85,.58,b,23)
|
||
banner(s,'Текст задания не заменяет технические ограничения доступа и правила организации.')
|
||
|
||
s=slide('ДЕМО 1. ПОЛУЧАЕМ И ПРОВЕРЯЕМ ФОРМУЛУ','ИИ · 3 МИНУТЫ')
|
||
box(s,.5,1.97,12.3,2.47,LIGHT,radius=True)
|
||
text(s,.8,2.22,11.7,2.0,'Excel 2016/2019, русские функции, разделитель «;».\nВ F2 — статус, в E2 — срок, в Параметры!$B$2 — дата среза.\nНужны «Завершено», «Просрочено» или «В срок».\nДай формулу, объяснение и граничные тесты.',24)
|
||
text(s,.6,4.93,11.85,1.0,'Тесты: выполнено • срок вчера • срок сегодня • срок завтра.\nОтдельно спросить: что будет с пустой датой?',24,BLUE,True)
|
||
banner(s,'Не принимаем ответ на веру: вставляем формулу в копию и сверяем результаты.')
|
||
|
||
s=slide('ДЕМО 2. АГЕНТ РАБОТАЕТ С КНИГОЙ','ИИ · 7 МИНУТ')
|
||
card(s,.5,1.95,5.96,3.93,'Что поручаем','Прочитать учебный реестр.\nПроверить структуру и пропуски.\nПосчитать итоги по отделам.\nСоздать новый отчёт XLSX.')
|
||
card(s,6.72,1.95,6.1,3.93,'Что ограничиваем','Исходник не изменять.\nНе публиковать и не отправлять файл.\nНе запускать макросы.\nПеред установкой ПО — остановиться.')
|
||
banner(s,'Результат принимаем после сверки: 100 / 40 / 60 / 30. Полное задание — в сценарии.')
|
||
|
||
s=slide('ДЕМО 3. ВЫВОДЫ БЕЗ ДОМЫСЛОВ','ИИ · ЧЕРНОВИК СЛУЖЕБНОЙ ЗАПИСКИ')
|
||
card(s,.5,1.95,5.96,3.95,'Факт из учебного отчёта','«На 20.10.2026 открыто\n60 поручений; у 30 истёк срок.\nДоля среди открытых — 50%».\n\nПоказать источник расчёта.')
|
||
card(s,6.72,1.95,6.1,3.95,'Недопустимый домысел','«Причина — низкая\nдисциплина сотрудников».\n\nВ таблице нет данных\nдля такого вывода.',PINK)
|
||
banner(s,'Отделяем наблюдение от причины. ИИ готовит черновик, человек редактирует.')
|
||
|
||
s=slide('КАК ПРОВЕРИТЬ РЕЗУЛЬТАТ ИИ','ИИ · ПРИЁМКА РЕЗУЛЬТАТА')
|
||
for j,(h,b) in enumerate([('Данные','100 строк, уникальные ID.\nНет потерь и дублей.\nИсходник не изменён.'),('Расчёт','Дата среза 20.10.2026.\n30 из 60, не из 100.\nГраничные тесты пройдены.'),('Файл','Открывается в Excel.\nФормулы понятны.\nИзменения перечислены.')]):card(s,.5+j*4.18,1.97,3.97,3.72,h,b)
|
||
banner(s,'При расхождении сначала ищем причину, а не просим ИИ «сделать цифры красивее».')
|
||
|
||
s=slide('ЧТО НЕ ДЕЛЕГИРУЕМ ИИ','ИИ · ГРАНИЦЫ ОТВЕТСТВЕННОСТИ')
|
||
text(s,.6,2.0,11.6,2.8,'Решения о гражданах и сотрудниках.\nОфициальное утверждение и отправку отчёта.\nВыбор режима доступа к данным.\nЗапуск непроверенных макросов и команд.',29,BLUE,True)
|
||
banner(s,'Помощник готовит материал. Уполномоченный человек проверяет и принимает решение.')
|
||
|
||
s=slide('ЧТО СДЕЛАТЬ НА ЭТОЙ НЕДЕЛЕ','ИТОГИ · ЧЕТЫРЕ ШАГА')
|
||
for j,(n,h) in enumerate([('01','Привести один реестр к правильной структуре'),('02','Заменить ручной подсчёт формулой или сводной'),('03','Проверить одно задание ИИ на учебном файле'),('04','Уточнить допустимую ИИ-среду в своей организации')]):
|
||
y=1.93+j*.95
|
||
text(s,.6,y,.75,.6,n,28,BLUE,True);text(s,1.65,y+.04,11.05,.75,h,25)
|
||
banner(s,'Начните с одного повторяемого отчёта, а не с автоматизации всей работы.')
|
||
|
||
s=slide('ТРИ ПРАВИЛА, КОТОРЫЕ СТОИТ ЗАПОМНИТЬ',dark=True)
|
||
for j,(n,h) in enumerate([('01','Порядок в данных — основа автоматизации.'),('02','Задача, ограничения и проверка — до запуска ИИ.'),('03','Ответственность за результат остаётся у человека.')]):
|
||
y=2.15+j*1.23
|
||
text(s,.6,y,.75,.65,n,30,GREEN,True);text(s,1.7,y,10.8,1.0,h,29,WHITE,True)
|
||
|
||
s=slide('МАТЕРИАЛЫ И КАНАЛ','ИТОГИ · ОСТАЁМСЯ НА СВЯЗИ')
|
||
text(s,.6,2.1,7.0,.9,'Бойко Кирилл',36,BLUE,True)
|
||
text(s,.6,3.13,7.0,.95,'Председатель республиканского\nотделения ОО «ОПОРА РОССИИ»',22)
|
||
text(s,.6,4.55,7.0,.65,'t.me/kirill_bouko_it',29,BLUE,True)
|
||
text(s,.6,5.5,7.0,.85,'QR ведёт в Telegram-канал.\nМатериалы подготовлены отдельно.',20,GRAY)
|
||
s.shapes.add_picture(str(OUT/'telegram-qr.png'), Inches(8.5), Inches(2.0), width=Inches(3.95),height=Inches(3.95))
|
||
text(s,8.5,6.1,3.95,.45,'ОТСКАНИРУЙТЕ КАМЕРОЙ',15,BLUE,True)
|
||
|
||
s=slide('ВОПРОСЫ',dark=True)
|
||
text(s,.6,2.45,11.6,1.3,'Какую повторяющуюся задачу\nвы бы поручили помощнику?',36,WHITE,True)
|
||
text(s,.6,4.55,11.7,1.0,'Что должно остаться под вашим контролем?',27,WHITE)
|
||
text(s,.6,6.25,11.5,.5,'Бойко Кирилл · t.me/kirill_bouko_it',22,WHITE)
|
||
|
||
assert len(p.slides)==26
|
||
p.save(OUT/'excel-ai-lecture.pptx')
|
||
(OUT/'slides-manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||
print(f'Saved 26 slides, 3600 seconds; workbook: {ROWS_TOTAL} synthetic rows in {len(DEPTS)} departments.')
|
||
|
||
|
||
if __name__=='__main__':
|
||
parser=argparse.ArgumentParser()
|
||
parser.add_argument('--only',choices=['all','workbook','slides'],default='all',
|
||
help='Rebuild everything, or only one artifact')
|
||
parser.add_argument('--with-chart-sheet',action='store_true',
|
||
help='Добавить лист «Диаграмма». По умолчанию его нет: диаграммы строятся на лекции вживую.')
|
||
args=parser.parse_args()
|
||
if args.only in ('all','workbook'):
|
||
qr=qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M,box_size=16,border=4)
|
||
qr.add_data(URL);qr.make(fit=True);qr.make_image(fill_color='black',back_color='white').save(OUT/'telegram-qr.png')
|
||
build_workbook(with_charts=args.with_chart_sheet)
|
||
if args.only in ('all','slides'):
|
||
build_slides()
|