"""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 = ['Организация','Документы','Отчётность'] rows = [] for i in range(60): k = i % 10 dept = DEPTS[i // 12] category = CATEGORIES[i % 3] if k < 4: deadline = DAY - timedelta(days=12-i % 5) status = 'Выполнено' completed = deadline + timedelta(days=-2 if k < 2 else 3) control = 'Завершено' else: deadline = DAY + timedelta(days=-(1+i % 9) if k < 7 else i % 7) status = 'В работе' if i % 2 == 0 else 'Новое' completed = None control = 'Просрочено' if deadline < DAY else 'В срок' rows.append([f'П-{i+1:03}',dept,category,deadline-timedelta(days=14),deadline,status,completed,control]) counts = Counter(r[7] for r in rows) assert counts == {'Завершено':24,'Просрочено':18,'В срок':18} assert all(r[1] in DEPTS and r[2] in CATEGORIES and r[5] in STATUSES for r in rows), 'значение вне выпадающего списка' def build_workbook(): 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 = [ ('Учебный материал','60 вымышленных поручений. Лекция 20.10.2026. Никаких реальных ПДн.'), ('Порядок демонстрации','Старт: простое условие (одно ЕСЛИ, без дат). Старт 2: вложенное условие (два ЕСЛИ, с датой среза). Реестр: готовый итог.'), ('Старт','Простое условие по статусу: =ЕСЛИ(F2="Выполнено";"Закрыто";"В работе"). Даты не используются.'), ('Старт 2','Вложенное условие: сначала статус, затем срок против Параметры!B2. Здесь появляется «Просрочено».'), ('Реестр','Готовая умная таблица и формулы H2:H61. Проверенный образец, не исходник для очистки.'), ('Параметры!B2','Фиксированная дата среза 20.10.2026. СЕГОДНЯ() намеренно не используется.'), ('Сводная','Вставка → Сводная таблица. Источник РеестрПоручений. Строки: Подразделение; столбцы: Контроль срока; значения: количество ID.'), ('Эталон отчёта','Статическая контрольная агрегация, НЕ настоящая сводная таблица Excel. Сравните с созданной вручную.'), ('Проверка','Ожидаемые итоги: 60 всего; 24 выполнено; 36 открыто; 18 открытых просрочено; 50% просроченных среди открытых.'), ('Смысл формулы H','Выполненные получают Завершено независимо от соблюдения срока. Проверка своевременности выполненных — отдельный расчёт.'), ('Границы примера','Выполненных с задержкой 12, без задержки 12. Учебный флаг просрочки считает только открытые.'), ('Выпадающие списки','Подразделение (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,60,7,{'name':'РеестрПоручений','style':'Table Style Medium 2','columns':[{'header':h} for h in headers]}) sh.conditional_format('H2:H61',{'type':'text','criteria':'containing','value':'Просрочено','format':alert}) sh.conditional_format('H2:H61',{'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(62,0,62,7,note_text,wrap);sh.set_row(62,26) sh.merge_range(63,0,63,7,'Формула: '+formula_text,wrap);sh.set_row(63,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=[ ('Всего поручений',"=COUNTA('Реестр'!A2:A61)",60), ('Выполнено','=COUNTIF(\'Реестр\'!F2:F61,"Выполнено")',24), ('Открыто','=B2-B3',36), ('Открытых просрочено','=COUNTIF(\'Реестр\'!H2:H61,"Просрочено")',18), ('Доля просроченных среди открытых','=IF(B4=0,0,B5/B4)',.5), ('Уникальных ID (контроль при сборке)',None,60), ('Пропусков обязательных полей (A:F)','=COUNTBLANK(\'Реестр\'!A2:F61)',0), ('Выполненных с задержкой (контроль при сборке)',None,12), ] 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.merge_range('A1:E1','СТАТИЧЕСКИЙ ЭТАЛОН — не сводная таблица Excel',head) report.write_row('A3',['Подразделение','Завершено','Просрочено','В срок','Всего'],head) for n,dept in enumerate(DEPTS,3): c=Counter(r[7] for r in rows if r[1]==dept) report.write_row(n,0,[dept,c['Завершено'],c['Просрочено'],c['В срок'],12],normal) report.set_row(n,28) report.write_row('A9',['ИТОГО',24,18,18,60],head) report.write('A12','Для обновляемого отчёта создайте сводную через Вставка → Сводная таблица.',wrap) report.set_row(11,70) 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,'Учебный реестр: 60 вымышленных поручений. Дата среза: 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,'24',64,BLUE,True) text(s,3.3,4.53,9.2,1.4,'Из 60 учебных поручений.\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,2.05,[2.15,1.18,1.22,1.15],[['Отдел','Готово','Проср.','В срок'],['Организ.',6,3,3],['Правовой',6,3,3],['Финансовый',4,5,3],['Обращений',4,4,4],['ИТ-отдел',4,3,5]],.57,17) text(s,.6,5.37,11.9,.52,'Проверка: 24 завершено + 18 просрочено + 18 в срок = 60.',24,BLUE,True) banner(s,'После изменения исходника обновляем сводную. В XLSX есть статический эталон.') s=slide('ПРОВЕРЯЕМ ОТЧЁТ, А НЕ ТОЛЬКО ЕГО ВИД') for j,(num,label) in enumerate([('60','всего'),('24','выполнено'),('36','открыто'),('18','просрочено\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,'18 / 36 = 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,'Результат принимаем после сверки: 60 / 24 / 36 / 18. Полное задание — в сценарии.') s=slide('ДЕМО 3. ВЫВОДЫ БЕЗ ДОМЫСЛОВ','ИИ · ЧЕРНОВИК СЛУЖЕБНОЙ ЗАПИСКИ') card(s,.5,1.95,5.96,3.95,'Факт из учебного отчёта','«На 20.10.2026 открыто\n36 поручений; у 18 истёк срок.\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([('Данные','60 строк, уникальные ID.\nНет потерь и дублей.\nИсходник не изменён.'),('Расчёт','Дата среза 20.10.2026.\n18 из 36, не из 60.\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('Saved 26 slides, 3600 seconds; workbook: 60 synthetic rows.') if __name__=='__main__': parser=argparse.ArgumentParser() parser.add_argument('--only',choices=['all','workbook','slides'],default='all', help='Rebuild everything, or only one artifact') 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() if args.only in ('all','slides'): build_slides()