100 поручений и шестой отдел; правка по экспертному заключению (промпт)

This commit is contained in:
apuc committed 2026-10-20 21:21:36 +03:00
1 parent f33c0fbe34
commit 9d70f113af
10 files changed
+385 -294

No files matched your search

+12 -5
View File
@@ -11,7 +11,7 @@
| `materials/excel-ai-lecture.pptx` | Редактируемая презентация: 26 слайдов, заметки докладчика встроены |
| `materials/excel-ai-lecture.pdf` | Просмотр слайдов без PowerPoint и резерв для выступления |
| `materials/speaker-notes.md` | Подробный сценарий: 55 минут + 5 минут на вопросы |
| `materials/excel-practice.xlsx` | 60 вымышленных поручений, формулы и контрольные итоги |
| `materials/excel-practice.xlsx` | 100 вымышленных поручений в шести отделах, формулы и контрольные итоги |
| `materials/excel-prostoy-primer.xlsx` | Простой пример на 10 строк: 3 формулы и 4 подсчёта, лист-шпаргалка |
| `materials/telegram-qr.png` | QR-код на https://t.me/kirill_bouko_it |
| `materials/slides-manifest.json` | Заголовки и точный тайминг |
@@ -38,7 +38,7 @@
6. Сверить с листами **Проверка** и **Эталон отчёта**. Последний — статический эталон, а не объект сводной таблицы.
Ниже, с 15-й строки, там же лежит **отчёт на формулах** (СЧЁТЕСЛИ и СЧЁТЕСЛИМН по подразделениям): он считается прямо из «Реестра»
и пересчитывается сам, а две строки «Сверка» показывают «Совпадает». Это удобный способ показать разницу между формулой и сводной.
Граничные случаи формулы вынесены в лист **Тесты** и не включены в 60 исходных поручений.
Граничные случаи формулы вынесены в лист **Тесты** и не включены в 100 исходных поручений.
7. Построить диаграммы вживую: данные берутся из сводной (столбцы — «Контроль срока») либо через
СЧЁТЕСЛИМН по подразделениям, как в отчёте на формулах. Готового листа с диаграммами в книге нет
намеренно — слушатели не должны видеть ответ заранее.
@@ -50,11 +50,11 @@
Обычная сборка без флага возвращает книгу к восьми листам без диаграмм.
**Выпадающие списки.** Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе **Параметры**:
`A5:A7` — статусы, `A10:A14` — подразделения, `A17:A19` — категории. Значения защищены от опечаток,
`A5:A7` — статусы, `A10:A15` — подразделения, `A17:A19` — категории. Значения защищены от опечаток,
поэтому сводная таблица не получит лишних строк. Чтобы изменить набор, отредактируйте список на листе
**Параметры** — изменение сразу действует на листах Старт, Старт 2 и Реестр.
Контроль: **60** поручений, **24** выполнено, **36** открыто, **18** открытых просрочено. Доля — **50% среди открытых**. Дата среза фиксирована: **20.10.2026**. В выполненных есть 12 с задержкой, но они не относятся к открытой просрочке. Столбец H сам по себе не оценивает своевременность выполнения закрытых поручений.
Контроль: **100** поручений в **шести** отделах, **40** выполнено, **60** открыто, **30** открытых просрочено. Доля — **50% среди открытых** (30 из 60). Дата среза фиксирована: **20.10.2026**. Из 40 выполненных **20** исполнено в срок и **20** с задержкой, но они не относятся к открытой просрочке. Столбец H сам по себе не оценивает своевременность выполнения закрытых поручений.
QR ведёт в канал, а не на конкретную публикацию. Размещение материалов в канале — отдельное действие; оно не выполнялось автоматически.
@@ -83,12 +83,19 @@ py scripts/verify_materials.py
- Только вымышленные данные. Не загружать реальные персональные данные, служебную информацию ограниченного распространения и гостайну в публичные ИИ-сервисы.
- Локальный файл не означает локальную обработку: содержимое может передаваться провайдеру модели.
- Промпт не заменяет технические ограничения доступа, внутренние правила и согласование среды с ответственными специалистами.
- Текст задания не заменяет технические ограничения доступа, внутренние правила и согласование среды с ответственными специалистами.
- Не запускать непроверенные макросы, не устанавливать компоненты со сцены, не выдавать статический образец за живую работу ИИ.
- Схема OpenWork на слайде 16 — схема процесса, не скриншот и не обещание интеграции с ведомственными системами.
## Справочные материалы
### Требования экспертизы Российского общества «Знание»
По заключению внешней верификации от 19.10.2026 (слайд 17) иностранное слово «промпт» заменено русским аналогом:
во всех материалах используется **«текст задания»**, **«шаблон задания»**, **«тексты заданий»**.
При правках презентации и сценария это написание нужно сохранять — источником слайдов служит `scripts/build_materials.py`,
и достаточно одного слова «промпт» в коде, чтобы оно вернулось в презентацию.
- Microsoft: https://support.microsoft.com/ru-ru/excel — справка по Excel и функциям.
- Документация OpenWork, `cloud/share-with-your-team/managed-llm-provider.mdx`: управляемые провайдеры и импорт конфигурации. Точный интерфейс следует проверять в используемой версии.
- Фирменные шрифты, цвета и правила композиции взяты из предоставленного PPTX-шаблона. Права на шаблон и логотипы принадлежат их правообладателям.
+7 -7
View File
@@ -31,7 +31,7 @@
1. **Один слайд — одна идея.** Не смешивать два приёма на одном слайде.
2. **Практическая польза в каждом блоке.** После каждого приёма — короткая строка «Что это экономит».
3. **Никаких выдуманных цифр.** Любые проценты и статистика — только из загруженных источников; иначе формулировать без чисел.
4. **Без программирования.** Формулы и промпты показывать как готовый текст, который можно скопировать.
4. **Без программирования.** Формулы и тексты заданий показывать как готовый текст, который можно скопировать.
5. **Термины на русском.** Английские названия давать в скобках один раз: «сводная таблица (pivot)».
6. **Безопасность данных — обязательный блок.** Отдельный слайд с правилами, без смягчений.
7. **Заметки докладчика на каждом слайде.** 3–5 предложений: что сказать + что показать на демо.
@@ -162,8 +162,8 @@
- Заметки: произнести обязательно, сделать паузу. Подчеркнуть личную ответственность. *(2,5 мин)*
**Слайд 20 — Сценарий 1. Формула по описанию**
- Содержание: промпт «Напиши формулу: если дата в C2 раньше сегодняшней и статус D2 = "не выполнено", то "просрочено", иначе "в срок"»; вставить и проверить.
- Визуал: промпт слева, готовая формула и результат справа.
- Содержание: задание «Напиши формулу: если дата в C2 раньше сегодняшней и статус D2 = "не выполнено", то "просрочено", иначе "в срок"»; вставить и проверить.
- Визуал: задание слева, готовая формула и результат справа.
- Заметки: «минуты раздумий → секунды». Показать, как проверить на 3 строках. *(4 мин)*
**Слайд 21 — Сценарий 2. Объяснение формулы и автоматизация**
@@ -176,7 +176,7 @@
- Визуал: таблица → график → текст вывода.
- Заметки: показать, что ИИ делает черновик, финал проверяет человек. *(4 мин)*
**Слайд 23 — Шаблон безопасного промпта**
**Слайд 23 — Шаблон безопасного задания**
- Содержание (готовый текст для копирования):
> Роль: опытный аналитик в органе власти.
> Данные: [описание структуры, без реальных персональных данных].
@@ -205,7 +205,7 @@
- Заметки: призыв к конкретному действию. *(1,5 мин)*
**Слайд 27 — Материалы и QR**
- Содержание: QR на раздатку (шпаргалка 10 функций, шаблон промпта, демо-файл), контакты лектора, общество «Знание».
- Содержание: QR на раздатку (шпаргалка 10 функций, шаблон задания, демо-файл), контакты лектора, общество «Знание».
- Визуал: крупный QR + подпись.
- Заметки: сказать, где лежат материалы. *(0,5 мин)*
@@ -231,14 +231,14 @@
Чтобы генерация опиралась на факты и не выдумывала, приложить:
1. Этот документ (ТЗ + покадровый план).
2. Раздатку «10 функций-минимум» и шаблон промпта.
2. Раздатку «10 функций-минимум» и шаблон задания.
3. Демо-файл (обезличенный) — для описания сценариев.
4. Внутренний регламент органа по работе с ИИ (если разрешён к загрузке).
5. Официальную справку Microsoft по функциям Excel (для корректных названий).
---
## 7. Готовый промпт для NotebookLM
## 7. Готовое задание для NotebookLM
> На основе загруженных источников сгенерируй презентацию по ТЗ ниже.
>
Binary file not shown.
Binary file not shown.
Binary file not shown.
+53 -53
View File
@@ -8,7 +8,7 @@
**Планируемая среда:** Excel 2016/2019; OpenWork после предварительной настройки и обязательной репетиции в реальной конфигурации демонстрационного компьютера. Готовность провайдера, модели, инструментов и разрешений ещё необходимо проверить.
**Учебные материалы:** только синтетические данные, 60 вымышленных поручений, без персональных данных (ПДн).
**Учебные материалы:** только синтетические данные, 100 вымышленных поручений, без персональных данных (ПДн).
**Канал:** https://t.me/kirill_bouko_it.
@@ -22,23 +22,23 @@
- Заранее открыть Excel, увеличить масштаб для последнего ряда, проверить проектор и отключить уведомления. Не показывать недавние документы, почту и личные папки.
- Подготовить книгу `materials/excel-practice.xlsx`. Этот сценарий описывает требования к книге, но сам по себе не создаёт её, листы, отчёты или скриншоты и не подтверждает их наличие.
- На листе `Реестр` разместить заголовки в строке 1 и 60 синтетических поручений в строках 2–61. Поля: A `ID`, B `Подразделение`, C `Категория`, D `Дата поступления`, E `Срок исполнения`, F `Статус`, G `Дата исполнения`, H `Контроль срока`.
- На листе `Реестр` разместить заголовки в строке 1 и 100 синтетических поручений в строках 2–101. Поля: A `ID`, B `Подразделение`, C `Категория`, D `Дата поступления`, E `Срок исполнения`, F `Статус`, G `Дата исполнения`, H `Контроль срока`.
- На листе `Параметры` в B2 хранить настоящую дату Excel `20.10.2026`, а не текст. Подписать соседнюю ячейку как дату среза. Не заменять её на `СЕГОДНЯ()`: результаты должны воспроизводиться после даты лекции.
- Проверить уникальность и непустоту ID; одинаковое написание статусов; корректность и непустоту дат поступления и срока. Для выполненных поручений проверить дату исполнения. Для незавершённых пустая дата исполнения допустима и должна объясняться статусом.
- Использовать согласованный справочник статусов: `Выполнено`, `В работе`, `Новое`. Открытыми считать записи со статусами `В работе` и `Новое`. В формуле завершённость определяется строго значением `Выполнено`. Статусы с лишним пробелом недопустимы.
- Синтетические подразделения: `Организационный отдел`, `Правовой отдел`, `Финансовый отдел`, `Отдел обращений`, `ИТ-отдел`. В каждом по 12 поручений, всего 60. Это учебные названия, не сведения о реальных подразделениях.
- Синтетические подразделения: `Организационный отдел`, `Правовой отдел`, `Финансовый отдел`, `Отдел обращений`, `ИТ-отдел`, `Аналитический отдел`. В каждом от 16 до 17 поручений, всего 100. Это учебные названия, не сведения о реальных подразделениях.
- Для показа работать с отдельной демонстрационной копией. Сохранять исходный `materials/excel-practice.xlsx` неизменным. Все создаваемые результаты направлять в отдельные новые файлы; если имя уже занято, не перезаписывать его.
- В книге будет готовый лист `Проверка` с контрольными показателями: 60 всего, 24 выполнено, 36 открытых, 18 открытых просроченных, доля просрочки среди открытых 50% (`18 / 36`). На репетиции сверить эти заданные автором ориентиры с фактическими строками, дополнительно проверить ID, обязательные поля, дату среза и распределение по подразделениям. Сценарий не подменяет проверку самой книги.
- Среди 24 выполненных поручений 12 выполнено в срок и 12 с задержкой. Это отдельная проверка по E и G: для учебных дат `G <= E` означает в срок, `G > E` означает задержку. Формула H не делает такого сравнения: у всех 24 результат `Завершено`. Остальные результаты H: 18 `Просрочено` и 18 `В срок` среди открытых.
- В книге будет готовый лист `Проверка` с контрольными показателями: 100 всего, 40 выполнено, 60 открытых, 30 открытых просроченных, доля просрочки среди открытых 50% (`30 / 60`). На репетиции сверить эти заданные автором ориентиры с фактическими строками, дополнительно проверить ID, обязательные поля, дату среза и распределение по подразделениям. Сценарий не подменяет проверку самой книги.
- Среди 40 выполненных поручений 20 выполнено в срок и 20 с задержкой. Это отдельная проверка по E и G: для учебных дат `G <= E` означает в срок, `G > E` означает задержку. Формула H не делает такого сравнения: у всех 40 результат `Завершено`. Остальные результаты H: 30 `Просрочено` и 30 `В срок` среди открытых.
- В книге будет готовый лист `Эталон отчёта`: статическая контрольная агрегация, **не настоящая сводная таблица Excel (pivot)**. Он нужен для сверки, не обновляется как pivot и не заменяет упражнение. Настоящую сводную пользователь строит самостоятельно в Excel; на лекции её строит спикер на экране, слушатели наблюдают без ноутбуков.
- Подготовить локальные скриншоты Excel: исходный реестр, типы дат, таблица Ctrl+T, фильтр, формула H2, самостоятельно построенная сводная, листы `Проверка` и `Эталон отчёта`. Увеличить текст, удалить из кадров лишние окна. Их наличие проверить на репетиции. Готовых скриншотов интерфейса OpenWork не обещать: для резервного разбора ИИ использовать текст промптов и учебные примеры из сценария, не выдавая их за ответы сервиса.
- Подготовить локальные скриншоты Excel: исходный реестр, типы дат, таблица Ctrl+T, фильтр, формула H2, самостоятельно построенная сводная, листы `Проверка` и `Эталон отчёта`. Увеличить текст, удалить из кадров лишние окна. Их наличие проверить на репетиции. Готовых скриншотов интерфейса OpenWork не обещать: для резервного разбора ИИ использовать текст заданий и учебные примеры из сценария, не выдавая их за ответы сервиса.
- OpenWork необходимо настроить заранее и провести обязательную репетицию всей цепочки в реальной конфигурации: проверить выбранного провайдера и модель, разрешённую демонстрационную папку, доступные инструменты, запросы подтверждений и работу именно с учебной копией. До успешной репетиции живые демо считать планируемыми, а не готовыми. Не настраивать подключение и не устанавливать компоненты со сцены.
- Уточнить, куда выбранная конфигурация передаёт запросы и содержимое файлов. Локальный путь не означает локальную обработку: содержание может передаваться модели. Проверка настроек и организационных правил предшествует отправке; текст промпта не заменяет ограничения доступа.
- Уточнить, куда выбранная конфигурация передаёт запросы и содержимое файлов. Локальный путь не означает локальную обработку: содержание может передаваться модели. Проверка настроек и организационных правил предшествует отправке; текст задания не заменяет ограничения доступа.
- Не подключать реальные ведомственные системы. Не обещать интеграции с ними, автоматическое редактирование Excel или любые инструменты, которые не проверены в данной конфигурации.
### Офлайн-Резерв
Если сеть, модель или инструменты недоступны либо репетиция OpenWork не пройдена, продолжать в том же временном интервале. Показать готовые листы `Проверка` и `Эталон отчёта` в локальной демонстрационной копии, текст промптов и учебные примеры из сценария. Произнести: «Сейчас показываю учебный разбор, а не живой ответ ИИ. Эталон отчёта является статической агрегацией, не сводной Excel». Формулы, фильтры и настоящую сводную демонстрировать средствами Excel 2016/2019; если и книга не открывается, использовать скриншоты Excel, подготовленные и проверенные на репетиции. Скриншоты интерфейса OpenWork для резерва не требуются и не обещаются.
Если сеть, модель или инструменты недоступны либо репетиция OpenWork не пройдена, продолжать в том же временном интервале. Показать готовые листы `Проверка` и `Эталон отчёта` в локальной демонстрационной копии, текст заданий и учебные примеры из сценария. Произнести: «Сейчас показываю учебный разбор, а не живой ответ ИИ. Эталон отчёта является статической агрегацией, не сводной Excel». Формулы, фильтры и настоящую сводную демонстрировать средствами Excel 2016/2019; если и книга не открывается, использовать скриншоты Excel, подготовленные и проверенные на репетиции. Скриншоты интерфейса OpenWork для резерва не требуются и не обещаются.
На диагностику сбоя отводить не более 20 секунд внутри текущего слайда. Не устанавливать ПО, не менять разрешения, не отправлять данные в другой сервис ради спасения демонстрации. Если нужный результат не был подготовлен, объяснить порядок проверки на схеме без вымышленных чисел. Офлайн-резерв не означает, что ИИ-модель работает локально.
@@ -90,7 +90,7 @@
«Сначала приведём данные к форме, которую понимает Excel. Затем научимся задавать таблице вопросы с помощью формул и сводной. После этого посмотрим, где может помочь ИИ и как организовать безопасную работу с агентом.
У нас одна сквозная задача: реестр из шестидесяти вымышленных поручений. Срез фиксирован на 20 октября 2026 года. Поэтому завтра или через месяц учебный расчёт должен дать тот же результат.
У нас одна сквозная задача: реестр из ста вымышленных поручений. Срез фиксирован на 20 октября 2026 года. Поэтому завтра или через месяц учебный расчёт должен дать тот же результат.
В Excel 2016 и 2019 доступны все функции, которые сегодня используем. Новые функции и платные надстройки для этой части не нужны. На каждом этапе будем спрашивать: какой результат ожидаем, чем его проверяем и кто отвечает за итог?»
@@ -114,7 +114,7 @@
Обратите внимание: “Дата поступления” и “Срок исполнения” разнесены по разным столбцам. Если записать обе даты и комментарий в одну ячейку, считать станет гораздо труднее. По той же причине цвет строки не заменяет поле “Статус”. Цвет можно не заметить, а текстовое значение можно отфильтровать и посчитать.
В учебном реестре ровно шестьдесят записей, строки со второй по шестьдесят первую. Это наш первый контрольный ориентир. Если в отчёте вдруг окажется пятьдесят девять, сначала ищем потерянную строку, а не объяснение управленческой ситуации».
В учебном реестре ровно сто записей, строки со второй по сто первую. Это наш первый контрольный ориентир. Если в отчёте вдруг окажется пятьдесят девять, сначала ищем потерянную строку, а не объяснение управленческой ситуации».
### Действие / демо
@@ -154,7 +154,7 @@
«Сейчас обычный диапазон станет таблицей Excel. Здесь слово “умная” не означает искусственный интеллект. Это встроенный инструмент: он помогает сохранять структуру, фильтровать записи и распространять формулы.
Выделяем весь диапазон от A1 до H61, нажимаем Ctrl+T и обязательно отмечаем, что таблица с заголовками. Проверяем границы: заголовок попал, все шестьдесят поручений попали, лишние строки не добавлены.
Выделяем весь диапазон от A1 до H101, нажимаем Ctrl+T и обязательно отмечаем, что таблица с заголовками. Проверяем границы: заголовок попал, все сто поручений попали, лишние строки не добавлены.
Появились кнопки фильтра и оформление. Но полосатые строки не исправили даты и не убрали дубли. Таблица облегчает работу с уже осмысленными данными, а не делает их достоверными автоматически.
@@ -162,7 +162,7 @@
### Действие / демо
В демонстрационной копии выделить `A1:H61`, нажать Ctrl+T, проверить флажок заголовков и диапазон. Присвоить таблице понятное имя, например `РеестрПоручений`, если оно свободно. Показать вкладку конструктора и фильтры. При сбое использовать скриншот результата, не создавать новую исходную книгу.
В демонстрационной копии выделить `A1:H101`, нажать Ctrl+T, проверить флажок заголовков и диапазон. Присвоить таблице понятное имя, например `РеестрПоручений`, если оно свободно. Показать вкладку конструктора и фильтры. При сбое использовать скриншот результата, не создавать новую исходную книгу.
### Переход
@@ -218,9 +218,9 @@
### Текст устной речи
«Нам нужно узнать, сколько поручений выполнено. Читать глазами шестьдесят строк можно, но при каждом изменении придётся начинать заново. СЧЁТЕСЛИ получает диапазон и условие, затем возвращает число подходящих ячеек.
«Нам нужно узнать, сколько поручений выполнено. Читать глазами сто строк можно, но при каждом изменении придётся начинать заново. СЧЁТЕСЛИ получает диапазон и условие, затем возвращает число подходящих ячеек.
Внутри листа “Реестр” формула выглядит так: равно СЧЁТЕСЛИ, диапазон F2:F61, точка с запятой, слово “Выполнено” в кавычках. Диапазон содержит шестьдесят статусов без заголовка.
Внутри листа “Реестр” формула выглядит так: равно СЧЁТЕСЛИ, диапазон F2:F101, точка с запятой, слово “Выполнено” в кавычках. Диапазон содержит сто статусов без заголовка.
Мы считаем ячейки со статусом, а не уникальные ID. Если одно поручение по ошибке внесли дважды, формула добросовестно посчитает обе строки. Поэтому проверка дублей нужна отдельно.
@@ -231,16 +231,16 @@
Показать формулу для расчёта на листе `Реестр`:
```excel
=СЧЁТЕСЛИ(F2:F61;"Выполнено")
=СЧЁТЕСЛИ(F2:F101;"Выполнено")
```
На отдельном черновом листе копии использовать явную ссылку на источник, не заменяя готовые показатели листа `Проверка`:
```excel
=СЧЁТЕСЛИ(Реестр!F2:F61;"Выполнено")
=СЧЁТЕСЛИ(Реестр!F2:F101;"Выполнено")
```
Ожидаемый результат по заданному учебному набору: **24**. Сопоставить фактический расчёт с готовым листом `Проверка`; при расхождении не подгонять данные, а выяснить причину. При необходимости кратко сопоставить с фильтром, затем снять фильтр.
Ожидаемый результат по заданному учебному набору: **40**. Сопоставить фактический расчёт с готовым листом `Проверка`; при расхождении не подгонять данные, а выяснить причину. При необходимости кратко сопоставить с фильтром, затем снять фильтр.
### Переход
@@ -256,7 +256,7 @@
Почему сначала слова? Потому что формула может быть технически правильной и при этом считать не то, что нужно руководителю. Здесь мы контролируем состояние незавершённых поручений на дату среза, а не качество исполнения вообще.
Теперь принципиальное ограничение. Для выполненного поручения наша формула всегда вернёт “Завершено”. Она не сравнивает дату исполнения со сроком. В учебном наборе 12 поручений выполнено в срок и 12 с задержкой, но у всех 24 в H будет “Завершено”. Разделение 12 и 12 получают отдельным сравнением даты исполнения G со сроком E, не из формулы H.
Теперь принципиальное ограничение. Для выполненного поручения наша формула всегда вернёт “Завершено”. Она не сравнивает дату исполнения со сроком. В учебном наборе 20 поручений выполнено в срок и 20 с задержкой, но у всех 40 в H будет “Завершено”. Разделение 20 и 20 получают отдельным сравнением даты исполнения G со сроком E, не из формулы H.
Есть и требования к исходным данным: сроки должны быть непустыми корректными датами, дата среза тоже должна быть датой, а статусы должны быть записаны единообразно. Пустой срок нельзя молча считать обычным значением. Неизвестный статус нельзя автоматически объявлять нормальным открытым поручением.
@@ -264,7 +264,7 @@
### Действие / демо
Первые 40 секунд разобрать дерево из двух условий. Затем показать два шага в книге: на листе `Старт` простое условие `=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")`, на следующем листе `Старт 2` — вложенное условие. Следующие 50 секунд ввести в `Старт 2!H2` точную формулу и распространить на H2:H61:
Первые 40 секунд разобрать дерево из двух условий. Затем показать два шага в книге: на листе `Старт` простое условие `=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")`, на следующем листе `Старт 2` — вложенное условие. Следующие 50 секунд ввести в `Старт 2!H2` точную формулу и распространить на H2:H101:
```excel
=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))
@@ -278,11 +278,11 @@
- `Выполнено`, срок 19.10.2026, дата исполнения 20.10.2026: `Завершено`, без заключения о своевременности.
- Пустой или текстовый срок: сначала исправление или разбор данных; результат формулы не принимается как достоверная классификация.
Последние 20 секунд проверить, что во всех формулах закреплена именно `Параметры!$B$2`, и произнести переход. Контрольные примеры не добавлять к 60 строкам основного реестра.
Последние 20 секунд проверить, что во всех формулах закреплена именно `Параметры!$B$2`, и произнести переход. Контрольные примеры не добавлять к 100 строкам основного реестра.
### Переход
«На уровне строки правило понятно. Теперь соберём из шестидесяти строк обзор для руководителя».
«На уровне строки правило понятно. Теперь соберём из ста строк обзор для руководителя».
## Слайд 11. Сводная таблица
@@ -296,25 +296,25 @@
Такой подсчёт равен числу поручений только при непустых уникальных ID. Обычная сводная в нашем примере считает записи, а не гарантированно уникальные сущности. Поэтому проверка исходного реестра остаётся обязательной.
Посмотрите на общий итог. Для полного учебного набора без фильтров ожидаем шестьдесят. Затем смотрим отдельные группы. Столбец “Завершено” показывает завершённые поручения, но не доказывает, что они были исполнены вовремя. Это ограничение мы переносим вместе с показателем в любой отчёт.
Посмотрите на общий итог. Для полного учебного набора без фильтров ожидаем сто. Затем смотрим отдельные группы. Столбец “Завершено” показывает завершённые поручения, но не доказывает, что они были исполнены вовремя. Это ограничение мы переносим вместе с показателем в любой отчёт.
Теперь можно добавить категорию как фильтр. Получится ответ уже на более узкий вопрос. Итог при таком фильтре может стать меньше шестидесяти, и это не ошибка, если ограничение указано явно.
Теперь можно добавить категорию как фильтр. Получится ответ уже на более узкий вопрос. Итог при таком фильтре может стать меньше ста, и это не ошибка, если ограничение указано явно.
Сводная не всегда пересчитывается сама после изменения исходных данных. Нужно обновление. И ещё нужно проверить источник: если диапазон не захватил новые строки, кнопка “Обновить” не исправит неверные границы обычного диапазона. Поэтому мы предварительно сделали таблицу Ctrl+T, но и с ней результат всё равно проверяем.
В книге есть лист “Эталон отчёта”. Это готовая статическая контрольная агрегация, а не настоящая сводная Excel. У неё нет поведения pivot: нельзя считать, что перестановка полей или обновление сводной изменит эти фиксированные итоги. Настоящую сводную сейчас строим самостоятельно из реестра, а затем сравниваем с эталоном. При повторении после лекции пользователь проходит те же шаги сам.
В строках должны быть пять учебных подразделений: Организационный отдел, Правовой отдел, Финансовый отдел, Отдел обращений и ИТ-отдел. У каждого всего двенадцать поручений. Общие итоги по контролю срока: двадцать четыре завершённых, восемнадцать открытых просроченных и восемнадцать открытых в срок. Открытых всего тридцать шесть. Какая доля из них просрочена? Восемнадцать из тридцати шести, то есть пятьдесят процентов. Делить на шестьдесят здесь было бы ответом на другой вопрос».
В строках должны быть шесть учебных подразделений: Организационный отдел, Правовой отдел, Финансовый отдел, Отдел обращений, ИТ-отдел и Аналитический отдел. У каждого от 16 до 17 поручений. Общие итоги по контролю срока: сорок завершённых, тридцать открытых просроченных и тридцать открытых в срок. Открытых всего шестьдесят. Какая доля из них просрочена? Тридцать из шестидесяти, то есть пятьдесят процентов. Делить на сто здесь было бы ответом на другой вопрос».
### Действие / демо
- 0–45 секунд: выбрать ячейку таблицы, выполнить «Вставка → Сводная таблица», проверить источник, разместить на новом листе демонстрационной копии.
- 45–115 секунд: перетащить `Подразделение` в строки, `Контроль срока` в столбцы, `ID` в значения; открыть параметры поля значений и выбрать `Количество`, не `Сумма`.
- 115–165 секунд: показать общий итог 60 при полном источнике и непустых ID. Если итог не равен 60, не скрывать проблему: проверить фильтры, источник и ID; при нехватке времени перейти к проверенному скриншоту.
- 115–165 секунд: показать общий итог 100 при полном источнике и непустых ID. Если итог не равен 100, не скрывать проблему: проверить фильтры, источник и ID; при нехватке времени перейти к проверенному скриншоту.
- 165–215 секунд: добавить `Категория` в фильтры, показать смену смысла отчёта, затем вернуть все категории. Показать команду обновления.
- 215–260 секунд: открыть `Эталон отчёта`, явно назвать его статической агрегацией, затем вернуться в самостоятельно построенную сводную и показать список полей. Объяснить, что готовый эталон не является pivot и не заменяет построение сводной пользователем.
- 260–305 секунд: сверить пять подразделений и итоги по 12 поручений; общие итоги столбцов H: 24 `Завершено`, 18 `Просрочено`, 18 `В срок`. Не придумывать распределение статусов внутри отдельных подразделений: сверять его по реестру и эталону.
- 305–335 секунд: спросить зал о знаменателе доли просрочки среди открытых. Показать `18 / (18 + 18) = 50%` и сопоставить с листом `Проверка`. Напомнить, что 12 выполненных с задержкой не входят в открытые просроченные.
- 260–305 секунд: сверить шесть подразделений и итоги по 16–17 поручений; общие итоги столбцов H: 40 `Завершено`, 30 `Просрочено`, 30 `В срок`. Не придумывать распределение статусов внутри отдельных подразделений: сверять его по реестру и эталону.
- 305–335 секунд: спросить зал о знаменателе доли просрочки среди открытых. Показать `30 / (30 + 30) = 50%` и сопоставить с листом `Проверка`. Напомнить, что 20 выполненных с задержкой не входят в открытые просроченные.
- 335–360 секунд: спросить зал: «Можно ли по количеству просроченных сразу назвать худшее подразделение?» Ответ: нет, требуется учитывать объём, состав и контекст поручений. Произнести переход.
### Переход
@@ -329,7 +329,7 @@
«Проверка отчёта начинается не с оформления, а с четырёх вещей: какой источник, какая дата среза, какие фильтры и каким способом считали.
Наши контрольные числа должны сходиться. В исходнике и сводной шестьдесят поручений: двадцать четыре выполнено, тридцать шесть открыто. Из открытых восемнадцать просрочено, доля составляет пятьдесят процентов: восемнадцать делим на тридцать шесть. В H сумма групп “Завершено”, “Просрочено” и “В срок” равна 24 плюс 18 плюс 18, всего 60. Все 24 завершённых совпадают со статусом “Выполнено”, но только 12 из них исполнены в срок; другие 12 с задержкой. Это различие устанавливаем по датам, а не по H.
Наши контрольные числа должны сходиться. В исходнике и сводной сто поручений: сорок выполнено, шестьдесят открыто. Из открытых тридцать просрочено, доля составляет пятьдесят процентов: тридцать делим на шестьдесят. В H сумма групп “Завершено”, “Просрочено” и “В срок” равна 40 плюс 30 плюс 30, всего 100. Все 40 завершённых совпадают со статусом “Выполнено”, но только 20 из них исполнены в срок; другие 20 с задержкой. Это различие устанавливаем по датам, а не по H.
Но совпадение общего итога недостаточно. Можно потерять одну запись и задвоить другую. Поэтому дополнительно проверяем ID, пропуски, границы срока и несколько конкретных строк.
@@ -337,7 +337,7 @@
### Действие / демо
Открыть готовый лист `Проверка` и пройти показатели 60, 24, 36, 18 и 50%. Указать дату среза, проверить ID, сравнить СЧЁТЕСЛИ с самостоятельно построенной сводной и проверить три случая около даты среза. Сверить минимум одну группу независимым фильтром по исходнику. Напомнить: `Эталон отчёта` является статическим контрольным листом, не pivot. Не утверждать, что проверка независима, если обе цифры просто скопированы из одного вывода ИИ.
Открыть готовый лист `Проверка` и пройти показатели 100, 40, 60, 30 и 50%. Указать дату среза, проверить ID, сравнить СЧЁТЕСЛИ с самостоятельно построенной сводной и проверить три случая около даты среза. Сверить минимум одну группу независимым фильтром по исходнику. Напомнить: `Эталон отчёта` является статическим контрольным листом, не pivot. Не утверждать, что проверка независима, если обе цифры просто скопированы из одного вывода ИИ.
### Переход
@@ -373,7 +373,7 @@
Это не жёсткая классификация всех продуктов. Важна практическая разница: одно дело получить текст, другое дело разрешить изменение файлов или отправку сведений. Чем больше действий может выполнить помощник, тем важнее ограничить его доступ и точки остановки.
Пример ассистента: “Объясни формулу контроля срока”. Пример агентной задачи: “Проанализируй учебную копию, проверь шестьдесят записей, подготовь отдельный отчёт и перечисли проверки”. Но второй вариант возможен только с реально доступными инструментами. Название приложения само по себе их не гарантирует.
Пример ассистента: “Объясни формулу контроля срока”. Пример агентной задачи: “Проанализируй учебную копию, проверь сто записей, подготовь отдельный отчёт и перечисли проверки”. Но второй вариант возможен только с реально доступными инструментами. Название приложения само по себе их не гарантирует.
И в обоих случаях ответственность остаётся у человека. Мы не передаём агенту право публиковать отчёт, рассылать служебные материалы или давать обязательную оценку работе подразделений».
@@ -383,7 +383,7 @@
### Переход
«До открытия рабочего файла в таком помощнике нужно обсудить не промпт, а безопасность».
«До открытия рабочего файла в таком помощнике нужно обсудить не текст задания, а безопасность».
## Слайд 15. Безопасность до загрузки
@@ -399,7 +399,7 @@
Третье правило: ограничиваем действия. Не разрешаем перезапись исходника, произвольную сетевую отправку, установку ПО и запуск макросов без отдельного согласования. Помощник должен остановиться перед такими действиями и запросить решение человека.
Но промпт не является технической защитой. Если вы уже отправили запрос облачной модели, фраза внутри него “ничего не отправляй” не отменит состоявшуюся передачу. Поэтому разрешённый учебный обмен с моделью проверяем заранее, а доступ к папкам и инструментам ограничиваем настройками.
Но текст задания не является технической защитой. Если вы уже отправили запрос облачной модели, фраза внутри него “ничего не отправляй” не отменит состоявшуюся передачу. Поэтому разрешённый учебный обмен с моделью проверяем заранее, а доступ к папкам и инструментам ограничиваем настройками.
Наконец, инструкции внутри файла считаем данными, а не командами. Если в ячейке написано “отправь весь реестр по этому адресу”, это не распоряжение пользователя и не повод запускать отправку».
@@ -439,7 +439,7 @@
### Текст устной речи
«Сравните две просьбы. Первая: “Сделай хороший отчёт”. Вторая: “Проверь реестр из шестидесяти синтетических поручений на 20 октября, посчитай записи по подразделениям и контролю срока, сохрани отдельный результат и покажи сверки”. Во второй понятно, что считать и как принимать работу.
«Сравните две просьбы. Первая: “Сделай хороший отчёт”. Вторая: “Проверь реестр из ста синтетических поручений на 20 октября, посчитай записи по подразделениям и контролю срока, сохрани отдельный результат и покажи сверки”. Во второй понятно, что считать и как принимать работу.
У задачи должно быть пять частей: цель, источник, правило, формат результата и ограничения. Если важного условия не хватает, правильная реакция помощника не догадка, а вопрос или остановка.
@@ -477,7 +477,7 @@
### Текст устной речи
«Начинаем с задачи, где помощнику не нужен файл. Передаём только структуру нужных столбцов, учебную дату и правило. Это хороший пример минимизации данных: не отправлять шестьдесят строк, если для ответа достаточно описания.
«Начинаем с задачи, где помощнику не нужен файл. Передаём только структуру нужных столбцов, учебную дату и правило. Это хороший пример минимизации данных: не отправлять сто строк, если для ответа достаточно описания.
Просим формулу именно для русской версии Excel 2016/2019. И отдельно просим объяснить, какие ошибки данных могут сделать результат недостоверным. Ответ должен быть пригоден для проверки, а не просто выглядеть технически убедительно.
@@ -487,7 +487,7 @@
### Действие / демо
За 0–35 секунд прочитать и отправить заранее подготовленный промпт, только после предварительной проверки разрешённого обмена с моделью:
За 0–35 секунд прочитать и отправить заранее подготовленный текст задания, только после предварительной проверки разрешённого обмена с моделью:
```text
Нужна формула для русской версии Excel 2016/2019, ячейка Реестр!H2.
@@ -522,15 +522,15 @@ F2 содержит статус, E2 содержит срок исполнен
«Во втором демо появляются инструменты и файл, поэтому границы становятся важнее. Исходная книга не должна меняться. Результат нужен отдельно, причём существующий файл с таким именем тоже нельзя молча заменить.
Сначала просим план и проверку входных данных. Есть ли нужные листы и столбцы? Действительно ли записей шестьдесят? Нет ли пустых ID, дублей, неверных дат и неожиданных статусов? Если базовые проверки не пройдены, помощник должен остановиться, а не выпускать красивый отчёт по сомнительным данным.
Сначала просим план и проверку входных данных. Есть ли нужные листы и столбцы? Действительно ли записей сто? Нет ли пустых ID, дублей, неверных дат и неожиданных статусов? Если базовые проверки не пройдены, помощник должен остановиться, а не выпускать красивый отчёт по сомнительным данным.
Обратите внимание на вычисления. Некоторые инструменты умеют читать формулы в xlsx, но не пересчитывают их так, как Excel. Поэтому значение формулы из кэша может отсутствовать или быть устаревшим. В отчёте нужно указать, откуда взялись цифры: из пересчёта в Excel или из повторного применения согласованного правила к исходным полям. Неизвестный способ расчёта нас не устраивает.
Когда результат появился, мы ещё не закончили работу. Проверяем число строк, дату среза, фильтры, распределения и несколько конкретных ID. Исходник должен остаться неизменным. Если помощник сообщает “сохранил файл”, но файла нет или он не открывается, задача не выполнена.
Теперь отдельная проверка смысла показателей. Всего шестьдесят поручений, выполнено двадцать четыре, открыто тридцать шесть. Из открытых просрочено восемнадцать: доля пятьдесят процентов. Если отчёт делит восемнадцать на шестьдесят, арифметика даст тридцать процентов, но это уже доля открытых просроченных среди всех поручений, а не среди открытых.
Теперь отдельная проверка смысла показателей. Всего сто поручений, выполнено сорок, открыто шестьдесят. Из открытых просрочено тридцать: доля пятьдесят процентов. Если отчёт делит тридцать на сто, арифметика даст тридцать процентов, но это уже доля открытых просроченных среди всех поручений, а не среди открытых.
Среди выполненных двенадцать с задержкой, но добавлять их к восемнадцати открытым просроченным нельзя без изменения определения показателя. В H все двадцать четыре выполненных помечены одинаково: “Завершено”. Проверим одну запись, выполненную в срок, и одну с задержкой по датам E и G, чтобы увидеть эту границу своими глазами.
Среди выполненных двадцать с задержкой, но добавлять их к тридцати открытым просроченным нельзя без изменения определения показателя. В H все сорок выполненных помечены одинаково: “Завершено”. Проверим одну запись, выполненную в срок, и одну с задержкой по датам E и G, чтобы увидеть эту границу своими глазами.
Если инструментов для такого процесса в нашей конфигурации нет, это не повод устанавливать что-то без согласования. Я покажу готовые контрольные листы “Проверка” и “Эталон отчёта” и разберу те же контрольные точки. Эталон является статической агрегацией, не результатом живой работы агента и не настоящим pivot. Ценность демо в последовательности решений, а не в эффекте автономной работы».
@@ -538,7 +538,7 @@ F2 содержит статус, E2 содержит срок исполнен
0–50 секунд: показать исходную книгу, учебную копию и область разрешённой работы. Объяснить, что доступ к содержимому и передача модели требуют предварительной проверки именно для синтетики. Только после успешной репетиции назвать фактически проверенные условия. Если маршрут данных не проверен, не загружать файл, использовать резерв.
50–105 секунд: показать промпт. При живом запуске подставить фактические заранее проверенные пути учебной копии и нового результата вместо меток, не давать доступ к другим папкам:
50–105 секунд: показать текст задания. При живом запуске подставить фактические заранее проверенные пути учебной копии и нового результата вместо меток, не давать доступ к другим папкам:
```text
Работай только с согласованной синтетической учебной копией
@@ -550,14 +550,14 @@ F2 содержит статус, E2 содержит срок исполнен
Реестр: A ID, B Подразделение,
C Категория, D Дата поступления, E Срок исполнения, F Статус,
G Дата исполнения, H Контроль срока. Заголовки в строке 1,
ожидается 60 записей в строках 2–61.
ожидается 100 записей в строках 2–101.
Дата среза: Параметры!B2 = 20.10.2026, настоящая дата Excel.
Сначала выдай план без инструментальных действий и дождись подтверждения.
После подтверждения плана проверь схему, число записей, уникальность
и непустоту ID, обязательные даты, согласованный справочник статусов:
Выполнено, В работе, Новое. Открытые: В работе и Новое.
Подразделения: Организационный отдел, Правовой отдел, Финансовый отдел,
Отдел обращений, ИТ-отдел; ожидается по 12 поручений в каждом.
Отдел обращений, ИТ-отдел, Аналитический отдел; ожидается от 16 до 17 поручений в каждом.
Если есть проблемы данных, остановись и перечисли их, не исправляй молча.
Правило H: =ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))
Это контроль открытых поручений, не оценка своевременности завершённых.
@@ -566,11 +566,11 @@ G Дата исполнения, H Контроль срока. Заголовк
Не считай кэш формул надёжным без проверки актуальности. Если инструмент
не пересчитывает Excel, явно опиши повторный расчёт по исходным полям
и необходимость сверки в Excel; если это невозможно, остановись.
Сверь с ожидаемыми показателями листа Проверка: 60 всего, 24 выполнено,
36 открытых, 18 открытых просроченных, доля среди открытых 18/36 = 50%.
Ожидаемые итоги H: 24 Завершено, 18 Просрочено, 18 В срок.
Отдельно проверь по E и G среди выполненных: 12 в срок (G <= E),
12 с задержкой (G > E). У всех этих 24 формула H даёт только Завершено.
Сверь с ожидаемыми показателями листа Проверка: 100 всего, 40 выполнено,
60 открытых, 30 открытых просроченных, доля среди открытых 30/60 = 50%.
Ожидаемые итоги H: 40 Завершено, 30 Просрочено, 30 В срок.
Отдельно проверь по E и G среди выполненных: 20 в срок (G <= E),
20 с задержкой (G > E). У всех этих 40 формула H даёт только Завершено.
Покажи граничные сроки и исходные ID для сверки. Не подгоняй расчёты
под ожидаемые числа: при расхождении остановись и укажи его.
После проверки подготовь отдельный новый Excel-отчёт (.xlsx)
@@ -596,9 +596,9 @@ G Дата исполнения, H Контроль срока. Заголовк
230–285 секунд: при успешном живом запуске открыть новый отчёт, сопоставить с листом `Проверка`, показать одно распределение, общий итог и исходные ID. Указать способ расчёта. Проверить, что исходник не сохранён поверх оригинала; при подготовке можно предусмотреть сравнение контрольной суммы до и после, но не утверждать неизменность только на основании слов агента. В резервном режиме разобрать те же пункты на реестре и контрольных листах без заявления о созданном отчёте.
285–345 секунд: пройти контрольные показатели 60, 24, 36, 18 и 50% по листу `Проверка`. Попросить зал выбрать знаменатель, сопоставить `18 / 36 = 50%` с `18 / 60 = 30%` и проговорить разные определения этих долей. Сверить итоги пяти подразделений по 12 с `Эталон отчёта`; напомнить, что это статическая агрегация, не pivot.
285–345 секунд: пройти контрольные показатели 100, 40, 60, 30 и 50% по листу `Проверка`. Попросить зал выбрать знаменатель, сопоставить `30 / 60 = 50%` с `30 / 100 = 30%` и проговорить разные определения этих долей. Сверить итоги шести подразделений по 16–17 с `Эталон отчёта`; напомнить, что это статическая агрегация, не pivot.
345–405 секунд: показать по одному исходному ID выполненного в срок и выполненного с задержкой, сравнить E и G, затем H. Подтвердить отдельный итог 12 и 12 и одинаковое `Завершено` для всех 24. Показать, почему 12 выполненных с задержкой не входят в показатель 18 открытых просроченных. При расхождении зафиксировать проблему, не исправлять исходник ради совпадения с эталоном.
345–405 секунд: показать по одному исходному ID выполненного в срок и выполненного с задержкой, сравнить E и G, затем H. Подтвердить отдельный итог 20 и 20 и одинаковое `Завершено` для всех 40. Показать, почему 20 выполненных с задержкой не входят в показатель 30 открытых просроченных. При расхождении зафиксировать проблему, не исправлять исходник ради совпадения с эталоном.
405–420 секунд: назвать границу доказанного результата и перейти дальше. При офлайн-показе явно сказать, что разобраны готовые контрольные листы, живого результата агента нет. Дополнительные 120 секунд отведены на проверки, не на ожидание сервиса.
@@ -616,7 +616,7 @@ G Дата исполнения, H Контроль срока. Заголовк
Фраза “в подразделении больше открытых просроченных поручений” может подтверждаться подсчётом. А фраза “сотрудники работают хуже” уже требует сведений о сложности задач, нагрузке, ресурсах и обстоятельствах. Наш реестр этого не содержит.
Просим разделить три вещи: проверяемые факты, ограничения и вопросы для уточнения. Если приводится доля, нужен знаменатель. В нашей книге восемнадцать открытых просроченных из тридцати шести открытых дают пятьдесят процентов. Это не означает, что просрочены пятьдесят процентов всех шестидесяти поручений. И даже правильно рассчитанная доля сама по себе не устанавливает причину.
Просим разделить три вещи: проверяемые факты, ограничения и вопросы для уточнения. Если приводится доля, нужен знаменатель. В нашей книге тридцать открытых просроченных из шестидесяти открытых дают пятьдесят процентов. Это не означает, что просрочены пятьдесят процентов всех ста поручений. И даже правильно рассчитанная доля сама по себе не устанавливает причину.
Нельзя говорить о росте или снижении, если у нас только один срез. Нельзя оценивать своевременность завершённых по нашему столбцу H. Нельзя выдавать предположение о причинах за установленный факт.
@@ -624,10 +624,10 @@ G Дата исполнения, H Контроль срока. Заголовк
### Действие / демо
0–40 секунд: показать только проверенную агрегированную таблицу из предыдущего этапа, без дополнительных исходных строк. В резервном режиме взять показатели с готовых листов `Проверка` и `Эталон отчёта`, указав их происхождение, и разобрать пример без отправки модели. Для живого запуска после репетиции использовать следующий промпт:
0–40 секунд: показать только проверенную агрегированную таблицу из предыдущего этапа, без дополнительных исходных строк. В резервном режиме взять показатели с готовых листов `Проверка` и `Эталон отчёта`, указав их происхождение, и разобрать пример без отправки модели. Для живого запуска после репетиции использовать следующее задание:
```text
Ниже проверенные агрегаты по 60 синтетическим поручениям на 20.10.2026.
Ниже проверенные агрегаты по 100 синтетическим поручениям на 20.10.2026.
[Вставить фактически проверенную таблицу с названиями показателей.]
Подготовь краткую служебную аналитическую заметку в трёх блоках:
1. Факты: каждый вывод с числом и ссылкой на строку/показатель таблицы.
@@ -661,7 +661,7 @@ G Дата исполнения, H Контроль срока. Заголовк
«Проверять ИИ означает проверять не уверенность ответа, а доказательства. Первый вопрос: тот ли источник использован? Второй: правильная ли дата среза и те ли определения? Третий: сходятся ли контрольные числа? Четвёртый: не появились ли неразрешённые действия или выводы, которых нет в данных?
Для нашей книги проверка очень конкретна. Шестьдесят исходных записей, непустые уникальные ID, дата 20 октября, точные статусы, корректные сроки. Формула проверяется на вчера, сегодня, завтра и завершённое с опозданием. Сводная сверяется с исходником и СЧЁТЕСЛИ.
Для нашей книги проверка очень конкретна. Сто исходных записей, непустые уникальные ID, дата 20 октября, точные статусы, корректные сроки. Формула проверяется на вчера, сегодня, завтра и завершённое с опозданием. Сводная сверяется с исходником и СЧЁТЕСЛИ.
Фраза модели “я всё проверила” не заменяет эти действия. Повторный вопрос той же модели тоже не является независимым доказательством. Нужен другой способ сверки: Excel, исходные строки, заранее проверенные контрольные значения.
@@ -669,7 +669,7 @@ G Дата исполнения, H Контроль срока. Заголовк
### Действие / демо
Показать контрольный список: источник, дата, правила, числа, граничные случаи, допустимость действий, ограничения выводов. Взять одну строку и пройти путь от F и E к H и затем к соответствующей группе отчёта. Отдельно показать способ вычислений: H не оценивает своевременность завершённых; отдельный итог 12 в срок и 12 с задержкой проверяется по E и G, а не выводится из H.
Показать контрольный список: источник, дата, правила, числа, граничные случаи, допустимость действий, ограничения выводов. Взять одну строку и пройти путь от F и E к H и затем к соответствующей группе отчёта. Отдельно показать способ вычислений: H не оценивает своевременность завершённых; отдельный итог 20 в срок и 20 с задержкой проверяется по E и G, а не выводится из H.
### Переход
+7 -5
View File
@@ -3,13 +3,15 @@
"total_seconds": 3600,
"notes_embedded": true,
"pdf_pages": 26,
"synthetic_rows": 60,
"completed": 24,
"open": 36,
"overdue_open": 18,
"synthetic_rows": 100,
"departments": 6,
"completed": 40,
"open": 60,
"overdue_open": 30,
"overdue_share_open": 0.5,
"completed_late": 20,
"qr": "decoded from PNG and PDF slide 25",
"render_text_warnings": [],
"chart_sheet": "лист «Диаграмма»: блок данных и две диаграммы совпадают с эталоном",
"chart_sheet": "лист «Диаграмма» не включён (ожидаемо: диаграммы строятся на лекции вживую)",
"recalculated_workbook": "not performed in this check"
}
+3 -3
View File
@@ -83,7 +83,7 @@
- **Сценарий 3. Анализ и отчёт.** На обезличенном файле: «Сделай сводку по регионам, найди топ-5 отклонений и напиши 5 выводов для служебной записки». Ассистент выдаёт и таблицу, и черновик текста.
**3.3. Как правильно ставить задачу (3 мин)**
Шаблон безопасного промпта:
Шаблон безопасного задания:
> Роль: ты опытный аналитик в органе власти.
> Данные: [описание структуры, без реальных ПДн].
> Задача: [что нужно получить].
@@ -100,7 +100,7 @@
### Блок 4. Итоги и вопросы — 5 минут (0:55–1:00)
- Повторить три правила (правильные данные / автоматизируй рутину / проверяй ИИ).
- Раздатка: шпаргалка функций + шаблон промпта + QR на материалы.
- Раздатка: шпаргалка функций + шаблон задания + QR на материалы.
- Призыв: «Один приём внедрите в работу уже на этой неделе».
- Вопросы и ответы (2–3 минуты), при нехватке времени — «подойдите после».
@@ -109,7 +109,7 @@
## Что подготовить заранее
- [ ] Демо-файл (обезличенный): лист с «грязными» данными + справочник + пример для сводной.
- [ ] Файл-раздатка: шпаргалка 10 функций + шаблон промпта + список инструментов.
- [ ] Файл-раздатка: шпаргалка 10 функций + шаблон задания + список инструментов.
- [ ] Слайды: ~20 штук, крупный шрифт, скриншоты Excel, 1 идея на слайд.
- [ ] Согласовать с ИБ/ответственным: какие сервисы ИИ допустимы, какие данные нельзя вводить.
- [ ] Проверить интернет/лицензии на демо-стенде; иметь офлайн-запись демо на случай сбоя.
+110 -74
View File
@@ -26,28 +26,54 @@ 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 = ['Организационный отдел','Правовой отдел','Финансовый отдел','Отдел обращений','ИТ-отдел']
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 = []
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])
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 counts == {'Завершено':24,'Просрочено':18,'В срок':18}
assert all(r[1] in DEPTS and r[2] in CATEGORIES and r[5] in STATUSES 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):
@@ -67,19 +93,22 @@ def build_chart_sheet(book,head,normal,wrap):
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$61,$A{r},'Реестр'!$H$2:$H$61,\"Завершено\")",normal,done)
charts.write_formula(r-1,2,f"=COUNTIFS('Реестр'!$B$2:$B$61,$A{r},'Реестр'!$H$2:$H$61,\"Просрочено\")",normal,late)
charts.write_formula(r-1,3,f"=COUNTIFS('Реестр'!$B$2:$B$61,$A{r},'Реестр'!$H$2:$H$61,\"В срок\")",normal,soon)
charts.write(6,0,'ИТОГО',head)
charts.write_formula(6,1,"=COUNTIF('Реестр'!$H$2:$H$61,\"Завершено\")",head,24)
charts.write_formula(6,2,"=COUNTIF('Реестр'!$H$2:$H$61,\"Просрочено\")",head,18)
charts.write_formula(6,3,"=COUNTIF('Реестр'!$H$2:$H$61,\"В срок\")",head,18)
charts.merge_range(8,0,8,3,'Блок слева пересчитывается из «Реестра». Диаграммы обновляются, как сводная: их можно обновить или перестроить.',wrap)
charts.set_row(8,42)
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,5,0],'values':['Диаграмма',1,1+col,5,1+col],
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':'Подразделение'})
@@ -88,7 +117,8 @@ def build_chart_sheet(book,head,normal,wrap):
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':['Диаграмма',6,1,6,3],
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}})
@@ -111,17 +141,17 @@ def build_workbook(with_charts=False):
inst = book.add_worksheet('Инструкция')
inst.set_column('A:A',30); inst.set_column('B:B',100)
instructions = [
('Учебный материал','60 вымышленных поручений. Лекция 20.10.2026. Никаких реальных ПДн.'),
('Учебный материал',f'{ROWS_TOTAL} вымышленных поручений в шести подразделениях. Лекция 20.10.2026. Никаких реальных ПДн.'),
('Порядок демонстрации','Старт: простое условие (одно ЕСЛИ, без дат). Старт 2: вложенное условие (два ЕСЛИ, с датой среза). Реестр: готовый итог.'),
('Старт','Простое условие по статусу: =ЕСЛИ(F2="Выполнено";"Закрыто";"В работе"). Даты не используются.'),
('Старт 2','Вложенное условие: сначала статус, затем срок против Параметры!B2. Здесь появляется «Просрочено».'),
('Реестр','Готовая умная таблица и формулы H2:H61. Проверенный образец, не исходник для очистки.'),
('Реестр',f'Готовая умная таблица и формулы H2:H{LAST_DATA}. Проверенный образец, не исходник для очистки.'),
('Параметры!B2','Фиксированная дата среза 20.10.2026. СЕГОДНЯ() намеренно не используется.'),
('Сводная','Вставка → Сводная таблица. Источник РеестрПоручений. Строки: Подразделение; столбцы: Контроль срока; значения: количество ID.'),
('Эталон отчёта','Строки 1–9: статический эталон, НЕ настоящая сводная. С 15-й строки — отчёт на формулах, который считается из Реестра и обновляется сам.'),
('Проверка','Ожидаемые итоги: 60 всего; 24 выполнено; 36 открыто; 18 открытых просрочено; 50% просроченных среди открытых.'),
('Эталон отчёта','Строки 1–10: статический эталон, НЕ настоящая сводная. С 15-й строки — отчёт на формулах, который считается из Реестра и обновляется сам.'),
('Проверка','Ожидаемые итоги: 100 всего; 40 выполнено; 60 открыто; 30 открытых просрочено; 50% просроченных среди открытых.'),
('Смысл формулы H','Выполненные получают Завершено независимо от соблюдения срока. Проверка своевременности выполненных — отдельный расчёт.'),
('Границы примера','Выполненных с задержкой 12, без задержки 12. Учебный флаг просрочки считает только открытые.'),
('Границы примера','Выполненных с задержкой 20, без задержки 20. Учебный флаг просрочки считает только открытые.'),
('Выпадающие списки','Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе Параметры. Опечатки исключены.'),
('Диаграмма','Лист намеренно не включён: диаграммы строим вживую. Данные проще всего взять из сводной (Контроль срока) или получить СЧЁТЕСЛИМН по подразделениям, как в отчёте на формулах.'),
('Не делать','Не перезаписывать оригинал при демонстрации, не отправлять реальные служебные данные внешней модели.'),
@@ -169,26 +199,26 @@ def build_workbook(with_charts=False):
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.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(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.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=[
('Всего поручений',"=COUNTA('Реестр'!A2:A61)",60),
('Выполнено','=COUNTIF(\'Реестр\'!F2:F61,"Выполнено")',24),
('Открыто','=B2-B3',36),
('Открытых просрочено','=COUNTIF(\'Реестр\'!H2:H61,"Просрочено")',18),
('Всего поручений',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,60),
('Пропусков обязательных полей (A:F)','=COUNTBLANK(\'Реестр\'!A2:F61)',0),
('Выполненных с задержкой (контроль при сборке)',None,12),
('Уникальных 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)
@@ -201,11 +231,15 @@ def build_workbook(with_charts=False):
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)
for n,dept in enumerate(DEPTS,3):
static_first=4
for n,dept in enumerate(DEPTS):
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)
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)
@@ -215,28 +249,30 @@ def build_workbook(with_charts=False):
report.write_row(15,0,['Подразделение','Всего','Завершено','Просрочено','В срок',
'Доля просроченных среди открытых'],head)
report.set_row(15,40)
rep_first,rep_last=17,21 # Excel rows of the five departments
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$61,$A{r})",normal,12)
report.write_formula(r-1,2,f"=COUNTIFS('Реестр'!$B$2:$B$61,$A{r},'Реестр'!$H$2:$H$61,\"Завершено\")",normal,done)
report.write_formula(r-1,3,f"=COUNTIFS('Реестр'!$B$2:$B$61,$A{r},'Реестр'!$H$2:$H$61,\"Просрочено\")",normal,late)
report.write_formula(r-1,4,f"=COUNTIFS('Реестр'!$B$2:$B$61,$A{r},'Реестр'!$H$2:$H$61,\"В срок\")",normal,soon)
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 22
total_row=rep_last+1 # Excel 23
report.write(total_row-1,0,'ИТОГО',head)
report.write_formula(total_row-1,1,"=COUNTA('Реестр'!$A$2:$A$61)",head,60)
report.write_formula(total_row-1,2,"=COUNTIF('Реестр'!$H$2:$H$61,\"Завершено\")",head,24)
report.write_formula(total_row-1,3,"=COUNTIF('Реестр'!$H$2:$H$61,\"Просрочено\")",head,18)
report.write_formula(total_row-1,4,"=COUNTIF('Реестр'!$H$2:$H$61,\"В срок\")",head,18)
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 24
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},'
@@ -365,7 +401,7 @@ def build_slides():
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.')
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)
@@ -401,8 +437,8 @@ def build_slides():
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)
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('ЕСЛИ: ПРОЗРАЧНАЯ ЛОГИКА КОНТРОЛЯ СРОКОВ')
@@ -413,16 +449,16 @@ def build_slides():
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)
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([('60','всего'),('24','выполнено'),('36','открыто'),('18','просрочено\nиз открытых')]):
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,'18 / 36 = 50% открытых поручений просрочено.\nДругой знаменатель — другой показатель.',24,BLUE,True)
text(s,.6,5.02,11.9,.85,'30 / 60 = 50% открытых поручений просрочено.\nДругой знаменатель — другой показатель.',24,BLUE,True)
banner(s,'Контрольный лист «Проверка» — независимая точка сверки, а не украшение отчёта.')
s=slide('ОТ НАВЫКА К ПОМОЩНИКУ',dark=True)
@@ -447,12 +483,12 @@ def build_slides():
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('ХОРОШЕЕ ЗАДАНИЕ: ПЯТЬ СОСТАВЛЯЮЩИХ','ИИ · ШАБЛОН ПРОМПТА')
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,'Промпт не заменяет технические ограничения доступа и правила организации.')
banner(s,'Текст задания не заменяет технические ограничения доступа и правила организации.')
s=slide('ДЕМО 1. ПОЛУЧАЕМ И ПРОВЕРЯЕМ ФОРМУЛУ','ИИ · 3 МИНУТЫ')
box(s,.5,1.97,12.3,2.47,LIGHT,radius=True)
@@ -463,15 +499,15 @@ def build_slides():
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. Полное задание — в сценарии.')
banner(s,'Результат принимаем после сверки: 100 / 40 / 60 / 30. Полное задание — в сценарии.')
s=slide('ДЕМО 3. ВЫВОДЫ БЕЗ ДОМЫСЛОВ','ИИ · ЧЕРНОВИК СЛУЖЕБНОЙ ЗАПИСКИ')
card(s,.5,1.95,5.96,3.95,'Факт из учебного отчёта','«На 20.10.2026 открыто\n36 поручений; у 18 истёк срок.\nДоля среди открытых — 50%».\n\nПоказать источник расчёта.')
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([('Данные','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)
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('ЧТО НЕ ДЕЛЕГИРУЕМ ИИ','ИИ · ГРАНИЦЫ ОТВЕТСТВЕННОСТИ')
@@ -505,7 +541,7 @@ def build_slides():
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.')
print(f'Saved 26 slides, 3600 seconds; workbook: {ROWS_TOTAL} synthetic rows in {len(DEPTS)} departments.')
if __name__=='__main__':
+193 -147
View File
@@ -1,4 +1,7 @@
"""Check generated content and export contact sheets for visual review."""
"""Check generated content and export contact sheets for visual review.
Run: py scripts/verify_materials.py [--recalculated-workbook <path>]
"""
from pathlib import Path
from collections import Counter
import json
@@ -11,179 +14,222 @@ import openpyxl
from PIL import Image, ImageDraw
from pptx import Presentation
ROOT=Path(__file__).resolve().parents[1]
parser=argparse.ArgumentParser()
parser.add_argument('--recalculated-workbook',type=Path,help='Optional independent LibreOffice/Excel recalculation copy')
args=parser.parse_args()
OUT=ROOT/'materials'
PREV=OUT/'preview'
PREV.mkdir(exist_ok=True)
p=Presentation(OUT/'excel-ai-lecture.pptx')
assert len(p.slides)==26
manifest=json.loads((OUT/'slides-manifest.json').read_text(encoding='utf-8'))
assert sum(s['seconds'] for s in manifest)==3600
notes=(OUT/'speaker-notes.md').read_text(encoding='utf-8')
durations=[int(v) for v in re.findall(r'\*\*Длительность:\*\* (\d+) секунд',notes)]
assert durations==[s['seconds'] for s in manifest],durations
assert all(s.notes_slide.notes_text_frame.text.strip() for s in p.slides)
for n,s in enumerate(p.slides,1):
for sh in s.shapes:
assert sh.left>=0 and sh.top>=0,(n,sh.name,'negative position')
assert sh.left+sh.width<=p.slide_width+10000,(n,sh.name,'too wide')
assert sh.top+sh.height<=p.slide_height+10000,(n,sh.name,'too tall')
# Ожидаемые контрольные значения учебного набора.
ROWS_TOTAL = 100
LAST = ROWS_TOTAL + 1 # последняя строка данных листа «Реестр»
DEPTS_TOTAL = 6
FIRST_DATA = 2
STATIC_FIRST, STATIC_TOTAL = 4, 10 # строки статического эталона
REP_FIRST = 17 # первая строка отчёта на формулах
REP_LAST = REP_FIRST + DEPTS_TOTAL - 1
REP_TOTAL = REP_LAST + 1
REP_CHECKS = (REP_TOTAL + 2, REP_TOTAL + 3)
book=openpyxl.load_workbook(OUT/'excel-practice.xlsx',data_only=True)
rows=list(book['Реестр'].iter_rows(min_row=2,max_row=61,values_only=True))
assert len(rows)==60 and len({r[0] for r in rows})==60
ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser()
parser.add_argument('--recalculated-workbook', type=Path,
help='Optional independent LibreOffice/Excel recalculation copy')
args = parser.parse_args()
OUT = ROOT / 'materials'
PREV = OUT / 'preview'
PREV.mkdir(exist_ok=True)
WORKBOOK = OUT / 'excel-practice.xlsx'
p = Presentation(OUT / 'excel-ai-lecture.pptx')
assert len(p.slides) == 26
manifest = json.loads((OUT / 'slides-manifest.json').read_text(encoding='utf-8'))
assert sum(s['seconds'] for s in manifest) == 3600
notes = (OUT / 'speaker-notes.md').read_text(encoding='utf-8')
durations = [int(v) for v in re.findall(r'\*\*Длительность:\*\* (\d+) секунд', notes)]
assert durations == [s['seconds'] for s in manifest], durations
assert all(s.notes_slide.notes_text_frame.text.strip() for s in p.slides)
for n, s in enumerate(p.slides, 1):
for sh in s.shapes:
assert sh.left >= 0 and sh.top >= 0, (n, sh.name, 'negative position')
assert sh.left + sh.width <= p.slide_width + 10000, (n, sh.name, 'too wide')
assert sh.top + sh.height <= p.slide_height + 10000, (n, sh.name, 'too tall')
book = openpyxl.load_workbook(WORKBOOK, data_only=True)
formulas = openpyxl.load_workbook(WORKBOOK, data_only=False)
rows = list(book['Реестр'].iter_rows(min_row=FIRST_DATA, max_row=LAST, values_only=True))
assert len(rows) == ROWS_TOTAL and len({r[0] for r in rows}) == ROWS_TOTAL
assert all(all(v is not None for v in r[:6]) for r in rows)
assert Counter(r[7] for r in rows)=={'Завершено':24,'Просрочено':18,'В срок':18}
assert sum(r[5]=='Выполнено' and r[6]>r[4] for r in rows)==12
assert all(r[6] is None or r[6]<=book['Параметры']['B2'].value for r in rows)
assert book['Проверка']['B6'].value==.5
assert len(book['Реестр'].tables)==1
assert len(book['Старт'].tables)==0 and len(book['Старт 2'].tables)==0
assert Counter(r[7] for r in rows) == {'Завершено':40,'Просрочено':30,'В срок':30}
assert sum(1 for r in rows if r[5] == 'Выполнено') == 40
assert sum(1 for r in rows if r[5] == 'Выполнено' and r[6] > r[4]) == 20
assert all(r[6] is None or r[6] <= book['Параметры']['B2'].value for r in rows)
assert all(r[3] < r[4] for r in rows), 'дата поступления должна быть раньше срока'
assert len({r[1] for r in rows}) == DEPTS_TOTAL
assert book['Проверка']['B2'].value == 100 and book['Проверка']['B3'].value == 40
assert book['Проверка']['B4'].value == 60 and book['Проверка']['B5'].value == 30
assert book['Проверка']['B6'].value == .5
assert len(book['Реестр'].tables) == 1
assert len(book['Старт'].tables) == 0 and len(book['Старт 2'].tables) == 0
# Teaching progression: simple IF on Старт, nested IF on Старт 2, identical to Реестр.
simple=[book['Старт'].cell(r,8).value for r in range(2,62)]
nested=[book['Старт 2'].cell(r,8).value for r in range(2,62)]
assert simple==['Закрыто' if r[5]=='Выполнено' else 'В работе' for r in rows]
assert nested==[r[7] for r in rows]
formulas=openpyxl.load_workbook(OUT/'excel-practice.xlsx',data_only=False)
assert all(formulas['Реестр'].cell(r,8).data_type=='f' for r in range(2,62))
assert all(formulas['Старт'].cell(r,8).data_type=='f' for r in range(2,62))
assert all(formulas['Старт 2'].cell(r,8).data_type=='f' for r in range(2,62))
assert 'IF(F2="Выполнено","Закрыто","В работе")' in formulas['Старт'].cell(2,8).value
assert formulas['Старт 2'].cell(2,8).value.count('IF(')==2
data_range = range(FIRST_DATA, LAST + 1)
simple = [book['Старт'].cell(r, 8).value for r in data_range]
nested = [book['Старт 2'].cell(r, 8).value for r in data_range]
assert simple == ['Закрыто' if r[5] == 'Выполнено' else 'В работе' for r in rows]
assert nested == [r[7] for r in rows]
for name in ('Реестр', 'Старт', 'Старт 2'):
assert all(formulas[name].cell(r, 8).data_type == 'f' for r in data_range), name
assert 'IF(F2="Выполнено","Закрыто","В работе")' in formulas['Старт'].cell(2, 8).value
assert formulas['Старт 2'].cell(2, 8).value.count('IF(') == 2
# Dropdowns: every editable field is a pick list backed by a named range on Параметры.
statuses=[book['Параметры'].cell(r,1).value for r in range(5,8)]
depts=[book['Параметры'].cell(r,1).value for r in range(10,15)]
cats=[book['Параметры'].cell(r,1).value for r in range(17,20)]
expected_dv={('Старт','B'):'AllowedDepartments',('Старт','C'):'AllowedCategories',('Старт','F'):'AllowedStatuses',
('Старт 2','B'):'AllowedDepartments',('Старт 2','C'):'AllowedCategories',('Старт 2','F'):'AllowedStatuses',
('Реестр','B'):'AllowedDepartments',('Реестр','C'):'AllowedCategories',('Реестр','F'):'AllowedStatuses'}
for (sheet,column),name in expected_dv.items():
found=[dv for dv in formulas[sheet].data_validations.dataValidation if f'{column}2:{column}1000'==str(dv.sqref)]
assert found,(sheet,column,'нет выпадающего списка')
assert found[0].type=='list' and found[0].formula1==name,(sheet,column,found[0].formula1)
assert all(r[1] in depts for r in rows),( 'подразделение вне списка')
assert all(r[2] in cats for r in rows),'категория вне списка'
assert all(r[5] in statuses for r in rows),'статус вне списка'
# Формульный отчёт на листе «Эталон отчёта» (с 15-й строки) должен сходиться со статическим эталоном.
rep=book['Эталон отчёта']
static={rep.cell(r,1).value:(rep.cell(r,2).value,rep.cell(r,3).value,rep.cell(r,4).value,rep.cell(r,5).value)
for r in range(4,9)}
assert len(static)==5
for r in range(17,22):
dept=rep.cell(r,1).value
done,late,soon,total=static[dept]
assert (rep.cell(r,2).value,rep.cell(r,3).value,rep.cell(r,4).value)==(total,done,late),(dept,'отчёт на формулах')
assert rep.cell(r,5).value==soon,(dept,'в срок')
assert abs(rep.cell(r,6).value-late/(late+soon))<1e-9,(dept,'доля просроченных среди открытых')
assert (rep.cell(22,2).value,rep.cell(22,3).value,rep.cell(22,4).value,rep.cell(22,5).value)==(60,24,18,18)
assert abs(rep.cell(22,6).value-.5)<1e-9
assert rep.cell(24,2).value=='Совпадает' and rep.cell(25,2).value=='Совпадает'
report_formulas=openpyxl.load_workbook(OUT/'excel-practice.xlsx',data_only=False)['Эталон отчёта']
assert all(report_formulas.cell(r,2).data_type=='f' for r in list(range(17,23))+[24,25])
assert 'COUNTIFS' in report_formulas.cell(17,3).value and 'Реестр' in report_formulas.cell(17,3).value
statuses = [book['Параметры'].cell(r, 1).value for r in range(5, 8)]
depts = [book['Параметры'].cell(r, 1).value for r in range(10, 10 + DEPTS_TOTAL)]
cats = [book['Параметры'].cell(r, 1).value for r in range(17, 20)]
assert len(depts) == DEPTS_TOTAL and len(set(depts)) == DEPTS_TOTAL
expected_dv = {(sheet, col): name
for sheet in ('Старт', 'Старт 2', 'Реестр')
for col, name in (('B', 'AllowedDepartments'), ('C', 'AllowedCategories'), ('F', 'AllowedStatuses'))}
for (sheet, column), name in expected_dv.items():
found = [dv for dv in formulas[sheet].data_validations.dataValidation
if f'{column}2:{column}1000' == str(dv.sqref)]
assert found, (sheet, column, 'нет выпадающего списка')
assert found[0].type == 'list' and found[0].formula1 == name, (sheet, column, found[0].formula1)
assert all(r[1] in depts for r in rows), 'подразделение вне списка'
assert all(r[2] in cats for r in rows), 'категория вне списка'
assert all(r[5] in statuses for r in rows), 'статус вне списка'
assert set(r[1] for r in rows) == set(depts), 'все подразделения из списка должны встречаться в реестре'
# Статический эталон и отчёт на формулах должны сходиться между собой.
rep = book['Эталон отчёта']
static = {rep.cell(r, 1).value: (rep.cell(r, 2).value, rep.cell(r, 3).value, rep.cell(r, 4).value, rep.cell(r, 5).value)
for r in range(STATIC_FIRST, STATIC_TOTAL)}
assert len(static) == DEPTS_TOTAL, static
assert (rep.cell(STATIC_TOTAL, 2).value, rep.cell(STATIC_TOTAL, 3).value,
rep.cell(STATIC_TOTAL, 4).value, rep.cell(STATIC_TOTAL, 5).value) == (40, 30, 30, 100)
for r in range(REP_FIRST, REP_TOTAL):
dept = rep.cell(r, 1).value
done, late, soon, total = static[dept]
assert (rep.cell(r, 2).value, rep.cell(r, 3).value, rep.cell(r, 4).value) == (total, done, late), (dept, 'отчёт на формулах')
assert rep.cell(r, 5).value == soon, (dept, 'в срок')
assert abs(rep.cell(r, 6).value - late / (late + soon)) < 1e-9, (dept, 'доля просроченных среди открытых')
assert (rep.cell(REP_TOTAL, 2).value, rep.cell(REP_TOTAL, 3).value,
rep.cell(REP_TOTAL, 4).value, rep.cell(REP_TOTAL, 5).value) == (100, 40, 30, 30)
assert abs(rep.cell(REP_TOTAL, 6).value - .5) < 1e-9
assert all(rep.cell(r, 2).value == 'Совпадает' for r in REP_CHECKS), 'обе строки сверки должны совпадать'
report_formulas = formulas['Эталон отчёта']
assert all(report_formulas.cell(r, 2).data_type == 'f'
for r in list(range(REP_FIRST, REP_TOTAL + 1)) + list(REP_CHECKS))
assert 'COUNTIFS' in report_formulas.cell(REP_FIRST, 3).value
assert f'$B${LAST}' in report_formulas.cell(REP_FIRST, 3).value, 'диапазон должен охватывать весь реестр'
# Лист с диаграммами включается флагом --with-chart-sheet. Проверяем его только если он есть.
chart_sheet=book['Диаграмма'] if 'Диаграмма' in book.sheetnames else None
chart_sheet = book['Диаграмма'] if 'Диаграмма' in book.sheetnames else None
if chart_sheet is None:
chart_status='лист «Диаграмма» не включён (ожидаемо: диаграммы строятся на лекции вживую)'
with zipfile.ZipFile(OUT/'excel-practice.xlsx') as archive:
assert not [n for n in archive.namelist() if n.startswith('xl/charts/')],'найдены диаграммы без листа'
chart_status = 'лист «Диаграмма» не включён (ожидаемо: диаграммы строятся на лекции вживую)'
with zipfile.ZipFile(WORKBOOK) as archive:
assert not [n for n in archive.namelist() if n.startswith('xl/charts/')], 'найдены диаграммы без листа'
else:
for n,dept in enumerate(static):
r=2+n
done,late,soon,total=static[dept]
assert chart_sheet.cell(r,1).value==dept
assert (chart_sheet.cell(r,2).value,chart_sheet.cell(r,3).value,chart_sheet.cell(r,4).value)==(done,late,soon),(dept,'блок диаграммы')
assert (chart_sheet.cell(7,2).value,chart_sheet.cell(7,3).value,chart_sheet.cell(7,4).value)==(24,18,18)
with zipfile.ZipFile(OUT/'excel-practice.xlsx') as archive:
chart_parts=[n for n in archive.namelist() if n.startswith('xl/charts/chart') and n.endswith('.xml')]
assert len(chart_parts)==2,chart_parts
blob=''.join(archive.read(n).decode('utf-8') for n in chart_parts)
assert 'Диаграмма!' in blob and 'Реестр!' not in blob,'диаграммы должны ссылаться на лист Диаграмма'
assert 'Диаграмма!$A$2:$A$6' in blob,'нет категорий по подразделениям'
assert 'Диаграмма!$B$7:$D$7' in blob,'круговая диаграмма должна брать строку ИТОГО'
chart_status='лист «Диаграмма»: блок данных и две диаграммы совпадают с эталоном'
chart_last = 1 + DEPTS_TOTAL
for n, dept in enumerate(static):
r = 2 + n
done, late, soon, total = static[dept]
assert chart_sheet.cell(r, 1).value == dept
assert (chart_sheet.cell(r, 2).value, chart_sheet.cell(r, 3).value,
chart_sheet.cell(r, 4).value) == (done, late, soon), (dept, 'блок диаграммы')
assert (chart_sheet.cell(chart_last + 1, 2).value, chart_sheet.cell(chart_last + 1, 3).value,
chart_sheet.cell(chart_last + 1, 4).value) == (40, 30, 30)
with zipfile.ZipFile(WORKBOOK) as archive:
chart_parts = [n for n in archive.namelist() if n.startswith('xl/charts/chart') and n.endswith('.xml')]
assert len(chart_parts) == 2, chart_parts
blob = ''.join(archive.read(n).decode('utf-8') for n in chart_parts)
assert 'Диаграмма!' in blob and 'Реестр!' not in blob, 'диаграммы должны ссылаться на лист Диаграмма'
assert f'Диаграмма!$A$2:$A${chart_last + 1}' in blob, 'нет категорий по подразделениям'
assert f'Диаграмма!$B${chart_last + 1}:$D${chart_last + 1}' in blob, 'круговая диаграмма должна брать строку ИТОГО'
chart_status = 'лист «Диаграмма»: блок данных и две диаграммы совпадают с эталоном'
for r in rows:
expected='Завершено' if r[5]=='Выполнено' else ('Просрочено' if r[4]<book['Параметры']['B2'].value else 'В срок')
assert expected==r[7]
assert all(book['Тесты'].cell(n,4).value==book['Тесты'].cell(n,5).value for n in range(2,6))
recalc_status='not performed in this check'
expected = 'Завершено' if r[5] == 'Выполнено' else ('Просрочено' if r[4] < book['Параметры']['B2'].value else 'В срок')
assert expected == r[7]
assert all(book['Тесты'].cell(n, 4).value == book['Тесты'].cell(n, 5).value for n in range(2, 6))
recalc_status = 'not performed in this check'
if args.recalculated_workbook:
recalculated=openpyxl.load_workbook(args.recalculated_workbook,data_only=True)
assert list(recalculated['Реестр'].iter_rows(min_row=2,max_row=61,values_only=True))==rows
for n in range(2,10):
assert recalculated['Проверка'].cell(n,2).value==book['Проверка'].cell(n,2).value
assert all(recalculated['Тесты'].cell(n,4).value==recalculated['Тесты'].cell(n,5).value for n in range(2,6))
assert [recalculated['Старт'].cell(r,8).value for r in range(2,62)]==simple
assert [recalculated['Старт 2'].cell(r,8).value for r in range(2,62)]==nested
recalc_report=recalculated['Эталон отчёта']
for r in range(17,23):
for col in range(2,7):
a,b=recalc_report.cell(r,col).value,rep.cell(r,col).value
same=abs(a-b)<1e-9 if isinstance(a,(int,float)) and isinstance(b,(int,float)) else a==b
assert same,(r,col,a,b)
assert recalc_report.cell(24,2).value=='Совпадает' and recalc_report.cell(25,2).value=='Совпадает'
recalculated = openpyxl.load_workbook(args.recalculated_workbook, data_only=True)
assert list(recalculated['Реестр'].iter_rows(min_row=FIRST_DATA, max_row=LAST, values_only=True)) == rows
for n in range(2, 10):
assert recalculated['Проверка'].cell(n, 2).value == book['Проверка'].cell(n, 2).value, n
assert all(recalculated['Тесты'].cell(n, 4).value == recalculated['Тесты'].cell(n, 5).value for n in range(2, 6))
assert [recalculated['Старт'].cell(r, 8).value for r in data_range] == simple
assert [recalculated['Старт 2'].cell(r, 8).value for r in data_range] == nested
recalc_report = recalculated['Эталон отчёта']
for r in list(range(REP_FIRST, REP_TOTAL + 1)) + list(REP_CHECKS):
for col in range(2, 7):
a, b = recalc_report.cell(r, col).value, rep.cell(r, col).value
same = abs(a - b) < 1e-9 if isinstance(a, (int, float)) and isinstance(b, (int, float)) else a == b
assert same, (r, col, a, b)
if chart_sheet is not None:
recalc_charts=recalculated['Диаграмма']
for r in range(2,8):
for col in range(2,5):
assert recalc_charts.cell(r,col).value==chart_sheet.cell(r,col).value,(r,col)
recalc_status='independent recalculation matches 60 rows, both teaching sheets, formula report, totals and boundary tests'
pdf=pymupdf.open(OUT/'excel-ai-lecture.pdf')
assert len(pdf)==26
recalc_charts = recalculated['Диаграмма']
for r in range(2, chart_last + 2):
for col in range(2, 5):
assert recalc_charts.cell(r, col).value == chart_sheet.cell(r, col).value, (r, col)
recalc_status = ('independent recalculation matches 100 rows, both teaching sheets, '
'formula report, totals and boundary tests')
pdf = pymupdf.open(OUT / 'excel-ai-lecture.pdf')
assert len(pdf) == 26
def norm(t):
return ''.join(c for c in unicodedata.normalize('NFKC',t).lower() if c.isalnum())
return ''.join(c for c in unicodedata.normalize('NFKC', t).lower() if c.isalnum())
warnings=[]
for i,s in enumerate(p.slides):
page=pdf[i]
pdf_text=norm(page.get_text())
warnings = []
for i, s in enumerate(p.slides):
page = pdf[i]
pdf_text = norm(page.get_text())
for sh in s.shapes:
if not sh.has_text_frame:continue
if not sh.has_text_frame:
continue
for para in sh.text_frame.paragraphs:
expected=norm(para.text)
expected = norm(para.text)
if expected and expected not in pdf_text:
warnings.append(f'Slide {i+1}: text not found intact: {para.text}')
for block in page.get_text('dict')['blocks']:
if 'lines' not in block:continue
if 'lines' not in block:
continue
for line in block['lines']:
for span in line['spans']:
x0,y0,x1,y1=span['bbox']
if min(x0,y0)<-1 or x1>page.rect.width+1 or y1>page.rect.height+1:
x0, y0, x1, y1 = span['bbox']
if min(x0, y0) < -1 or x1 > page.rect.width + 1 or y1 > page.rect.height + 1:
warnings.append(f'Slide {i+1}: text exceeds page: {span["text"]}')
page.get_pixmap(matrix=pymupdf.Matrix(1.4,1.4)).save(PREV/f'slide-{i+1:02}.png')
page.get_pixmap(matrix=pymupdf.Matrix(1.4, 1.4)).save(PREV / f'slide-{i+1:02}.png')
for start in range(0,26,8):
ids=list(range(start,min(start+8,26)))
sheet=Image.new('RGB',(1200,360*((len(ids)+1)//2)),'#dedee8')
for k,i in enumerate(ids):
pm=pdf[i].get_pixmap(matrix=pymupdf.Matrix(.625,.625))
im=Image.frombytes('RGB',(pm.width,pm.height),pm.samples)
x=(k%2)*600;y=(k//2)*360
sheet.paste(im,(x,y))
ImageDraw.Draw(sheet).text((x+12,y+339),f'SLIDE {i+1:02}',fill='black')
sheet.save(PREV/f'contact-{start//8+1}.png')
for start in range(0, 26, 8):
ids = list(range(start, min(start + 8, 26)))
sheet = Image.new('RGB', (1200, 360 * ((len(ids) + 1) // 2)), '#dedee8')
for k, i in enumerate(ids):
pm = pdf[i].get_pixmap(matrix=pymupdf.Matrix(.625, .625))
im = Image.frombytes('RGB', (pm.width, pm.height), pm.samples)
x = (k % 2) * 600
y = (k // 2) * 360
sheet.paste(im, (x, y))
ImageDraw.Draw(sheet).text((x + 12, y + 339), f'SLIDE {i+1:02}', fill='black')
sheet.save(PREV / f'contact-{start//8+1}.png')
try:
import zxingcpp
result=zxingcpp.read_barcode(Image.open(OUT/'telegram-qr.png'))
assert result and result.text=='https://t.me/kirill_bouko_it'
result = zxingcpp.read_barcode(Image.open(OUT / 'telegram-qr.png'))
assert result and result.text == 'https://t.me/kirill_bouko_it'
# Verify QR survives PDF export, not only its standalone source image.
result=zxingcpp.read_barcode(Image.open(PREV/'slide-25.png'))
assert result and result.text=='https://t.me/kirill_bouko_it'
qr_status='decoded from PNG and PDF slide 25'
result = zxingcpp.read_barcode(Image.open(PREV / 'slide-25.png'))
assert result and result.text == 'https://t.me/kirill_bouko_it'
qr_status = 'decoded from PNG and PDF slide 25'
except ImportError:
qr_status='decoder unavailable; image generated from exact URL'
qr_status = 'decoder unavailable; image generated from exact URL'
report={
'slides':26,'total_seconds':3600,'notes_embedded':True,'pdf_pages':len(pdf),
'synthetic_rows':60,'completed':24,'open':36,'overdue_open':18,'overdue_share_open':.5,
'qr':qr_status,'render_text_warnings':warnings,'chart_sheet':chart_status,
report = {
'slides':26, 'total_seconds':3600, 'notes_embedded':True, 'pdf_pages':len(pdf),
'synthetic_rows':ROWS_TOTAL, 'departments':DEPTS_TOTAL, 'completed':40, 'open':60,
'overdue_open':30, 'overdue_share_open':.5, 'completed_late':20,
'qr':qr_status, 'render_text_warnings':warnings, 'chart_sheet':chart_status,
'recalculated_workbook':recalc_status,
}
(OUT/'validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(report,ensure_ascii=False,indent=2))
assert not warnings,'Inspect PDF text warnings before delivering'
(OUT / 'validation.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps(report, ensure_ascii=False, indent=2))
assert not warnings, 'Inspect PDF text warnings before delivering'