Учебные примеры Excel: простой журнал, шаги ЕСЛИ и выпадающие списки

This commit is contained in:
apuc committed 2026-10-19 23:20:00 +03:00
1 parent 401b02b680
commit 63c4a5ff40
8 files changed
+314 -23

No files matched your search

+21 -5
View File
@@ -12,6 +12,7 @@
| `materials/excel-ai-lecture.pdf` | Просмотр слайдов без PowerPoint и резерв для выступления |
| `materials/speaker-notes.md` | Подробный сценарий: 55 минут + 5 минут на вопросы |
| `materials/excel-practice.xlsx` | 60 вымышленных поручений, формулы и контрольные итоги |
| `materials/excel-prostoy-primer.xlsx` | Простой пример на 10 строк: 3 формулы и 4 подсчёта, лист-шпаргалка |
| `materials/telegram-qr.png` | QR-код на https://t.me/kirill_bouko_it |
| `materials/slides-manifest.json` | Заголовки и точный тайминг |
| `materials/validation.json` | Результаты автоматических проверок |
@@ -21,13 +22,27 @@
## Как провести демонстрацию
Для объяснения базового блока используйте простой пример `materials/excel-prostoy-primer.xlsx`:
журнал из 10 вымышленных обращений, три формулы и четыре подсчёта, плюс лист «Формулы»
с формулировками словами. Дата среза — ячейка `C13` (20.10.2026). Контроль: 10 обращений,
4 выполнено, 6 в работе, 2 просрочено, среднее время 9,5 дня, доля просроченных среди
открытых — 2 из 6 (33%).
Далее — расширенный пример `materials/excel-practice.xlsx`:
1. Сделать отдельную копию учебной книги, исходник не изменять.
2. На листе **Старт** показать Ctrl+T, фильтры и формулы. Столбец H в нём пустой.
3. На листе **Реестр** находится готовая умная таблица и формулы контроля сроков.
4. Создать настоящую сводную в Excel: строки — подразделения, столбцы — контроль срока, значения — количество ID.
5. Сверить с листами **Проверка** и **Эталон отчёта**. Последний — статический эталон, а не объект сводной таблицы.
2. На листе **Старт** показать Ctrl+T, фильтры и **простое условие**: `=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")`. Одна проверка, даты не используются.
3. На следующем листе **Старт 2** показать **вложенное условие**: сначала статус, затем срок против `Параметры!$B$2`. Здесь появляется «Просрочено».
4. На листе **Реестр** — готовый итог: умная таблица и формулы H2:H61.
5. Создать настоящую сводную в Excel: строки — подразделения, столбцы — контроль срока, значения — количество ID.
6. Сверить с листами **Проверка** и **Эталон отчёта**. Последний — статический эталон, а не объект сводной таблицы.
Граничные случаи формулы вынесены в лист **Тесты** и не включены в 60 исходных поручений.
6. Показать работу OpenWork только после репетиции с выбранными моделью, инструментами и разрешениями. Слайды не утверждают, что конкретная конфигурация уже настроена. Полные задания — в сценарии.
7. Показать работу OpenWork только после репетиции с выбранными моделью, инструментами и разрешениями. Слайды не утверждают, что конкретная конфигурация уже настроена. Полные задания — в сценарии.
**Выпадающие списки.** Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе **Параметры**:
`A5:A7` — статусы, `A10:A14` — подразделения, `A17:A19` — категории. Значения защищены от опечаток,
поэтому сводная таблица не получит лишних строк. Чтобы изменить набор, отредактируйте список на листе
**Параметры** — изменение сразу действует на листах Старт, Старт 2 и Реестр.
Контроль: **60** поручений, **24** выполнено, **36** открыто, **18** открытых просрочено. Доля — **50% среди открытых**. Дата среза фиксирована: **20.10.2026**. В выполненных есть 12 с задержкой, но они не относятся к открытой просрочке. Столбец H сам по себе не оценивает своевременность выполнения закрытых поручений.
@@ -40,6 +55,7 @@ Python 3.12; зависимости перечислены в `scripts/requireme
```powershell
py -m pip install -r scripts/requirements.txt
py scripts/build_materials.py
py scripts/build_simple_example.py
```
Скрипт использует исходный файл `Шаблон_лекции_новый_с_новым_лого.pptx`: сохраняет его тему, макеты и логотипы, убирает образцы слайдов. Исходник не изменяется. При повторной сборке перезаписываются только генерируемые PPTX, XLSX, PNG QR и JSON таймингов; ручные изменения в этих файлах следует сохранить отдельно.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -264,7 +264,7 @@
### Действие / демо
Первые 40 секунд разобрать дерево из двух условий. Следующие 50 секунд ввести в `Реестр!H2` точную формулу и распространить на H2:H61:
Первые 40 секунд разобрать дерево из двух условий. Затем показать два шага в книге: на листе `Старт` простое условие `=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")`, на следующем листе `Старт 2` — вложенное условие. Следующие 50 секунд ввести в `Старт 2!H2` точную формулу и распространить на H2:H61:
```excel
=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))
+1 -1
View File
@@ -10,5 +10,5 @@
"overdue_share_open": 0.5,
"qr": "decoded from PNG and PDF slide 25",
"render_text_warnings": [],
"recalculated_workbook": "not performed in this check"
"recalculated_workbook": "independent recalculation matches 60 rows, both teaching sheets, totals and boundary tests"
}
+51 -14
View File
@@ -6,6 +6,7 @@ Uses the supplied PPTX theme/layouts, not a re-created logo.
from pathlib import Path
from datetime import datetime, timedelta
from collections import Counter
import argparse
import json
import re
import qrcode
@@ -26,11 +27,13 @@ DAY = datetime(2026, 10, 20)
TIMES = [60,60,60,120,120,120,120,120,120,180,360,120,60,120,180,120,120,180,420,180,120,60,90,60,30,300]
assert sum(TIMES) == 3600
DEPTS = ['Организационный отдел','Правовой отдел','Финансовый отдел','Отдел обращений','ИТ-отдел']
STATUSES = ['Выполнено','В работе','Новое']
CATEGORIES = ['Организация','Документы','Отчётность']
rows = []
for i in range(60):
k = i % 10
dept = DEPTS[i // 12]
category = ['Организация','Документы','Отчётность'][i % 3]
category = CATEGORIES[i % 3]
if k < 4:
deadline = DAY - timedelta(days=12-i % 5)
status = 'Выполнено'
@@ -44,10 +47,11 @@ for i in range(60):
rows.append([f'П-{i+1:03}',dept,category,deadline-timedelta(days=14),deadline,status,completed,control])
counts = Counter(r[7] for r in rows)
assert counts == {'Завершено':24,'Просрочено':18,'В срок':18}
assert all(r[1] in DEPTS and r[2] in CATEGORIES and r[5] in STATUSES for r in rows), 'значение вне выпадающего списка'
def build_workbook():
book = xlsxwriter.Workbook(OUT / 'excel-practice.xlsx')
book = xlsxwriter.Workbook(OUT / 'excel-practice.xlsx', {'strings_to_formulas': False})
book.set_properties({'title':'Учебный реестр поручений','author':'Бойко Кирилл','comments':'Синтетические данные. Не реальные сведения органа власти.'})
head = book.add_format({'bold':True,'bg_color':'#'+BLUE,'font_color':'white','text_wrap':True,'valign':'vcenter'})
normal = book.add_format({'font_name':'Calibri','font_size':12,'valign':'vcenter'})
@@ -61,7 +65,9 @@ def build_workbook():
inst.set_column('A:A',30); inst.set_column('B:B',100)
instructions = [
('Учебный материал','60 вымышленных поручений. Лекция 20.10.2026. Никаких реальных ПДн.'),
('Начало демонстрации','Сохраните копию книги. На листе Старт: Ctrl+T, заголовки, фильтр, заполнение формул H.'),
('Порядок демонстрации','Старт: простое условие (одно ЕСЛИ, без дат). Старт 2: вложенное условие (два ЕСЛИ, с датой среза). Реестр: готовый итог.'),
('Старт','Простое условие по статусу: =ЕСЛИ(F2="Выполнено";"Закрыто";"В работе"). Даты не используются.'),
('Старт 2','Вложенное условие: сначала статус, затем срок против Параметры!B2. Здесь появляется «Просрочено».'),
('Реестр','Готовая умная таблица и формулы H2:H61. Проверенный образец, не исходник для очистки.'),
('Параметры!B2','Фиксированная дата среза 20.10.2026. СЕГОДНЯ() намеренно не используется.'),
('Сводная','Вставка → Сводная таблица. Источник РеестрПоручений. Строки: Подразделение; столбцы: Контроль срока; значения: количество ID.'),
@@ -69,6 +75,7 @@ def build_workbook():
('Проверка','Ожидаемые итоги: 60 всего; 24 выполнено; 36 открыто; 18 открытых просрочено; 50% просроченных среди открытых.'),
('Смысл формулы H','Выполненные получают Завершено независимо от соблюдения срока. Проверка своевременности выполненных — отдельный расчёт.'),
('Границы примера','Выполненных с задержкой 12, без задержки 12. Учебный флаг просрочки считает только открытые.'),
('Выпадающие списки','Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе Параметры. Опечатки исключены.'),
('Не делать','Не перезаписывать оригинал при демонстрации, не отправлять реальные служебные данные внешней модели.'),
('Формулы в файле','Внутри XLSX функции сохранены на английском по стандарту. Excel показывает локализованные имена.'),
]
@@ -77,10 +84,32 @@ def build_workbook():
params = book.add_worksheet('Параметры');params.set_column('A:A',30);params.set_column('B:B',26)
params.write('A1','Параметр',head);params.write('B1','Значение',head)
params.write('A2','Дата среза',normal);params.write_datetime('B2',DAY,date)
params.write('A4','Допустимые статусы',head)
for j,v in enumerate(['Выполнено','В работе','Новое'],4):params.write(j,0,v,normal)
book.define_name('AllowedStatuses',"='Параметры'!$A$5:$A$7")
for name, ready in [('Старт',False),('Реестр',True)]:
# Источники выпадающих списков. Правятся здесь и сразу действуют на всех листах.
picklists=[('Допустимые статусы',STATUSES,3,'AllowedStatuses'),
('Допустимые подразделения',DEPTS,8,'AllowedDepartments'),
('Допустимые категории',CATEGORIES,15,'AllowedCategories')]
for label,values,head_row,name in picklists:
params.write(head_row,0,label,head)
params.set_row(head_row,22)
for k,v in enumerate(values):
params.write(head_row+1+k,0,v,normal)
params.set_row(head_row+k,20)
first,last=head_row+2,head_row+1+len(values)
book.define_name(name,f"='Параметры'!$A${first}:$A${last}")
def simple_formula(r): return f'=IF(F{r}="Выполнено","Закрыто","В работе")'
def nested_formula(r): return f'=IF(F{r}="Выполнено","Завершено",IF(E{r}<\'Параметры\'!$B$2,"Просрочено","В срок"))'
demo = [
('Старт', simple_formula, lambda row:'Закрыто' if row[5]=='Выполнено' else 'В работе',
'ШАГ 1. Простое условие: одно ЕСЛИ, только по статусу. Даты пока не используем.',
'=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")', False),
('Старт 2', nested_formula, lambda row:row[7],
'ШАГ 2. Вложенное условие: сначала статус, затем срок против даты среза из Параметры!B2.',
'=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))', False),
('Реестр', nested_formula, lambda row:row[7],
'ГОТОВЫЙ ИТОГ. Умная таблица с формулами H2:H61 и подсветкой значений.',
'=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))', True),
]
for name, formula, expected_value, note_text, formula_text, ready in demo:
sh=book.add_worksheet(name);sh.freeze_panes(1,2);sh.set_zoom(110)
for c,w in enumerate([12,29,19,19,19,18,19,20]):sh.set_column(c,c,w,normal)
sh.set_row(0,36)
@@ -90,14 +119,16 @@ def build_workbook():
for c,v in enumerate(row[:7]):
if isinstance(v,datetime):sh.write_datetime(n,c,v,date)
elif v is not None:sh.write(n,c,v,normal)
if ready:
formula=f'=IF(F{n+1}="Выполнено","Завершено",IF(E{n+1}<\'Параметры\'!$B$2,"Просрочено","В срок"))'
sh.write_formula(n,7,formula,normal,row[7])
sh.write_formula(n,7,formula(n+1),normal,expected_value(row))
if ready:
sh.add_table(0,0,60,7,{'name':'РеестрПоручений','style':'Table Style Medium 2','columns':[{'header':h} for h in headers]})
sh.conditional_format('H2:H61',{'type':'text','criteria':'containing','value':'Просрочено','format':alert})
sh.conditional_format('H2:H61',{'type':'text','criteria':'containing','value':'Завершено','format':ok})
sh.data_validation('F2:F1000',{'validate':'list','source':'=AllowedStatuses','error_title':'Выберите статус','error_message':'Используйте значение из списка.'})
sh.data_validation('B2:B1000',{'validate':'list','source':'=AllowedDepartments','error_title':'Выберите подразделение','error_message':'Используйте значение из списка.'})
sh.data_validation('C2:C1000',{'validate':'list','source':'=AllowedCategories','error_title':'Выберите категорию','error_message':'Используйте значение из списка.'})
sh.merge_range(62,0,62,7,note_text,wrap);sh.set_row(62,26)
sh.merge_range(63,0,63,7,'Формула: '+formula_text,wrap);sh.set_row(63,26)
sh.set_landscape();sh.fit_to_pages(1,0);sh.repeat_rows(0)
check=book.add_worksheet('Проверка');check.set_column('A:A',47);check.set_column('B:C',22)
check.write_row('A1',['Показатель','Расчёт','Ожидается'],head)
@@ -387,7 +418,13 @@ def build_slides():
if __name__=='__main__':
qr=qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M,box_size=16,border=4)
qr.add_data(URL);qr.make(fit=True);qr.make_image(fill_color='black',back_color='white').save(OUT/'telegram-qr.png')
build_workbook()
build_slides()
parser=argparse.ArgumentParser()
parser.add_argument('--only',choices=['all','workbook','slides'],default='all',
help='Rebuild everything, or only one artifact')
args=parser.parse_args()
if args.only in ('all','workbook'):
qr=qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M,box_size=16,border=4)
qr.add_data(URL);qr.make(fit=True);qr.make_image(fill_color='black',back_color='white').save(OUT/'telegram-qr.png')
build_workbook()
if args.only in ('all','slides'):
build_slides()
+213
View File
@@ -0,0 +1,213 @@
"""Build the small, human-readable Excel example used for the basic block.
Run: py scripts/build_simple_example.py
Optional check against an independent recalculation:
py scripts/build_simple_example.py --recalculated <path-to-recalculated.xlsx>
"""
from pathlib import Path
from datetime import datetime, timedelta
import argparse
import openpyxl
import xlsxwriter
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'materials' / 'excel-prostoy-primer.xlsx'
CUTOFF = datetime(2026, 10, 20)
BLUE, RED, GREEN = '1F3B73', 'FFE3EA', 'DDFAEC'
# (заявитель, тема, дата обращения, срок в днях, дата исполнения, статус)
RECORDS = [
('Иванов И. И.', 'Справка', datetime(2026, 10, 1), 10, datetime(2026, 10, 8), 'Выполнено'),
('Петрова А. С.', 'Консультация', datetime(2026, 10, 3), 15, datetime(2026, 10, 16), 'Выполнено'),
('Сидоров П. Н.', 'Жалоба', datetime(2026, 10, 5), 30, None, 'В работе'),
('Кузнецова М. В.', 'Справка', datetime(2026, 10, 6), 10, datetime(2026, 10, 14), 'Выполнено'),
('Смирнов Д. А.', 'Обращение', datetime(2026, 10, 8), 10, None, 'В работе'),
('Волкова Е. П.', 'Консультация', datetime(2026, 10, 9), 15, datetime(2026, 10, 19), 'Выполнено'),
('Морозов В. И.', 'Справка', datetime(2026, 10, 12), 7, None, 'В работе'),
('Новикова О. С.', 'Обращение', datetime(2026, 10, 13), 10, None, 'В работе'),
('Фёдоров К. М.', 'Жалоба', datetime(2026, 10, 14), 30, None, 'В работе'),
('Гусева Т. А.', 'Справка', datetime(2026, 10, 15), 7, None, 'В работе'),
]
HEADERS = ['№', 'Заявитель', 'Тема обращения', 'Дата обращения', 'Срок, дней',
'Срок исполнения', 'Дата исполнения', 'Статус', 'Проверка срока', 'Дней на исполнение']
FIRST, LAST = 3, 12 # Excel rows of the ten records
CUTOFF_CELL = 'C13' # holds the fixed cut-off date
FIRST_TOTAL = 16 # Excel row of the first summary line
EPOCH = datetime(1899, 12, 30) # Excel serial-number origin
def serial(value):
"""Excel serial number, required for a cached numeric result of a date formula."""
return (value - EPOCH).days
def as_date(value):
"""Normalise a cached or recalculated cell to a date for comparison."""
if value is None:
return None
return value if isinstance(value, datetime) else EPOCH + timedelta(days=int(value))
def expected():
"""Independent expectation for every calculated column and summary."""
deadline, control, days = [], [], []
for _, _, received, term, done, status in RECORDS:
end = received + timedelta(days=term)
deadline.append(end)
control.append('Закрыто' if status == 'Выполнено' else ('Просрочено' if end < CUTOFF else 'В срок'))
days.append((done - received).days if done else None)
totals = [
('Всего обращений', 10),
('Выполнено', 4),
('В работе', 6),
('Просрочено', 2),
('Доля просроченных среди открытых', 2 / 6),
('Среднее число дней на исполнение', 9.5),
('Суммарный срок, дней', 144),
]
return deadline, control, days, totals
def check(totals, recalculated=None):
deadline, control, days, _ = expected()
books = [(OUT, openpyxl.load_workbook(OUT, data_only=True)['Пример'], 'cached')]
if recalculated:
books.append((recalculated, openpyxl.load_workbook(recalculated, data_only=True)['Пример'], 'recalculated'))
for path, sheet, mode in books:
got_deadline = [as_date(sheet.cell(r, 6).value) for r in range(FIRST, LAST + 1)]
got_days = [v if v not in ('', None) else None for v in (sheet.cell(r, 10).value for r in range(FIRST, LAST + 1))]
assert got_deadline == deadline, (mode, path, 'срок исполнения', got_deadline, deadline)
assert got_days == days, (mode, path, 'дней на исполнение', got_days, days)
if mode == 'cached':
assert [sheet.cell(r, 9).value for r in range(FIRST, LAST + 1)] == control
for n, (label, value) in enumerate(totals):
got = sheet.cell(FIRST_TOTAL + n, 3).value
close = isinstance(got, (int, float)) and isinstance(value, (int, float)) and abs(got - value) < 1e-9
assert close or got == value, (mode, path, label, got, value)
if mode == 'cached':
assert as_date(sheet.cell(13, 3).value) == CUTOFF
# The reference sheet must keep formulas as readable text, never as live formulas.
ref = openpyxl.load_workbook(OUT, data_only=False)['Формулы']
expected_text = ['=D3+E3', '=ЕСЛИ(H3="Выполнено";"Закрыто";\nЕСЛИ(F3<$C$13;"Просрочено";"В срок"))',
'=ЕСЛИ(G3<>"";G3-D3;"")', '=СЧЁТ(A3:A12)', '=СЧЁТЕСЛИ(H3:H12;"Выполнено")',
'=СЧЁТЕСЛИ(I3:I12;"Просрочено")', '=СРЗНАЧ(J3:J12)']
got_text = [ref.cell(r, 3).value for r in range(3, 10)]
assert got_text == expected_text, got_text
return True
def build():
deadline, control, days, totals = expected()
book = xlsxwriter.Workbook(OUT, {'strings_to_formulas': False})
book.set_properties({
'title': 'Простой пример с формулами',
'author': 'Бойко Кирилл',
'comments': 'Учебные вымышленные данные. Не реальные обращения граждан.',
})
head = book.add_format({'bold': True, 'bg_color': '#' + BLUE, 'font_color': 'white',
'text_wrap': True, 'valign': 'vcenter', 'align': 'center'})
text = book.add_format({'font_name': 'Calibri', 'font_size': 12, 'valign': 'vcenter'})
date = book.add_format({'num_format': 'dd.mm.yyyy', 'font_size': 12})
num = book.add_format({'num_format': '0', 'font_size': 12, 'align': 'center'})
avg = book.add_format({'num_format': '0.0', 'font_size': 12})
share = book.add_format({'num_format': '0%', 'font_size': 12})
bold = book.add_format({'bold': True, 'font_size': 12})
note = book.add_format({'font_size': 11, 'text_wrap': True, 'valign': 'top', 'font_color': '#555566'})
mono = book.add_format({'font_name': 'Consolas', 'font_size': 11, 'valign': 'vcenter', 'text_wrap': True})
red = book.add_format({'bg_color': '#' + RED, 'font_color': '#9D123E'})
green = book.add_format({'bg_color': '#' + GREEN, 'font_color': '#086B40'})
sheet = book.add_worksheet('Пример')
sheet.set_zoom(115)
for col, width in zip('ABCDEFGHIJ', [5, 18, 15, 15, 10, 15, 15, 12, 15, 18]):
sheet.set_column(col + ':' + col, width)
sheet.freeze_panes(2, 0)
sheet.set_row(0, 24)
sheet.merge_range(0, 0, 0, 9, 'ЖУРНАЛ ОБРАЩЕНИЙ — УЧЕБНЫЙ ПРИМЕР НА 10 СТРОК', head)
sheet.write_row(1, 0, HEADERS, head)
sheet.set_row(1, 34)
for n, (person, topic, received, term, done, status) in enumerate(RECORDS):
row = FIRST + n
sheet.set_row(row - 1, 20)
sheet.write(row - 1, 0, n + 1, num)
sheet.write(row - 1, 1, person, text)
sheet.write(row - 1, 2, topic, text)
sheet.write_datetime(row - 1, 3, received, date)
sheet.write(row - 1, 4, term, num)
sheet.write_formula(row - 1, 5, f'=D{row}+E{row}', date, serial(deadline[n]))
if done:
sheet.write_datetime(row - 1, 6, done, date)
else:
sheet.write(row - 1, 6, '', text)
sheet.write(row - 1, 7, status, text)
sheet.write_formula(row - 1, 8,
f'=IF(H{row}="Выполнено","Закрыто",IF(F{row}<${CUTOFF_CELL[0]}${CUTOFF_CELL[1:]},'
f'"Просрочено","В срок"))', text, control[n])
if days[n] is None:
sheet.write_formula(row - 1, 9, f'=IF(G{row}<>"",G{row}-D{row},"")', text, '')
else:
sheet.write_formula(row - 1, 9, f'=IF(G{row}<>"",G{row}-D{row},"")', avg, days[n])
sheet.data_validation(f'H{FIRST}:H{LAST}',
{'validate': 'list', 'source': ['Выполнено', 'В работе'],
'error_title': 'Выберите значение',
'error_message': 'Только «Выполнено» или «В работе».'})
sheet.set_row(12, 22)
sheet.merge_range(12, 0, 12, 1, 'Дата среза →', bold)
sheet.write_datetime(12, 2, CUTOFF, date)
sheet.set_row(14, 22)
sheet.write(14, 0, 'ИТОГИ', bold)
for n, (label, value) in enumerate(totals):
row = FIRST_TOTAL + n
sheet.set_row(row - 1, 20)
sheet.merge_range(row - 1, 0, row - 1, 1, label, text)
fmt = share if 'Доля' in label else (avg if 'Среднее' in label else num)
sheet.write(row - 1, 2, value, fmt)
sheet.conditional_format(f'I{FIRST}:I{LAST}', {'type': 'text', 'criteria': 'containing', 'value': 'Просрочено', 'format': red})
sheet.conditional_format(f'I{FIRST}:I{LAST}', {'type': 'text', 'criteria': 'containing', 'value': 'Закрыто', 'format': green})
sheet.merge_range(23, 0, 23, 9, 'Все записи вымышленные. Формулы в файле записаны по стандарту (IF, COUNTIF, AVERAGE); '
'в русском Excel они отображаются как ЕСЛИ, СЧЁТЕСЛИ, СРЗНАЧ.', note)
sheet.set_row(23, 30)
ref = book.add_worksheet('Формулы')
for col, width in zip('ABCD', [4, 30, 34, 52]):
ref.set_column(col + ':' + col, width)
ref.merge_range('A1:D1', 'ТРИ ФОРМУЛЫ И ЧЕТЫРЕ ПОДСЧЁТА — ШПАРГАЛКА', head)
ref.set_row(0, 26)
ref.write_row(1, 0, ['№', 'Что считаем', 'Формула', 'Словами'], head)
ref.set_row(1, 24)
rows = [
('Срок исполнения', '=D3+E3', 'Дата обращения плюс срок в днях.'),
('Проверка срока', '=ЕСЛИ(H3="Выполнено";"Закрыто";\nЕСЛИ(F3<$C$13;"Просрочено";"В срок"))',
'Если выполнено — «Закрыто». Иначе сравнить срок с датой среза.'),
('Дней на исполнение', '=ЕСЛИ(G3<>"";G3-D3;"")', 'Если есть дата исполнения, посчитать разницу. Иначе пусто.'),
('Сколько всего', '=СЧЁТ(A3:A12)', 'Число обращений в списке.'),
('Сколько выполнено', '=СЧЁТЕСЛИ(H3:H12;"Выполнено")', 'Считает строки со статусом «Выполнено».'),
('Сколько просрочено', '=СЧЁТЕСЛИ(I3:I12;"Просрочено")', 'Считает строки со значением «Просрочено».'),
('Среднее время', '=СРЗНАЧ(J3:J12)', 'Среднее по заполненным значениям. Пустые строки не учитываются.'),
]
for n, (what, formula, words) in enumerate(rows, start=2):
ref.set_row(n, 44 if '\n' in formula else 22)
ref.write(n, 0, n - 1, num)
ref.write(n, 1, what, text)
ref.write(n, 2, formula, mono)
ref.write(n, 3, words, note)
ref.merge_range('A11:D12', 'Правило: сначала формулируем словами, потом пишем формулу. Если словами объяснить нельзя — '
'формула будет считать «как-то не так».', note)
ref.set_row(10, 30)
ref.set_row(11, 30)
book.close()
return totals
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--recalculated', type=Path)
args = parser.parse_args()
result = build()
check(result, args.recalculated)
state = 'cached values + independent recalculation' if args.recalculated else 'cached values'
print(f'OK: 10 rows and 7 summary values verified ({state}).')
+27 -2
View File
@@ -40,9 +40,32 @@ 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
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
# 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),'статус вне списка'
for r in rows:
expected='Завершено' if r[5]=='Выполнено' else ('Просрочено' if r[4]<book['Параметры']['B2'].value else 'В срок')
assert expected==r[7]
@@ -54,7 +77,9 @@ if args.recalculated_workbook:
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))
recalc_status='independent recalculation matches all 60 rows, check totals and four boundary tests'
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_status='independent recalculation matches 60 rows, both teaching sheets, totals and boundary tests'
pdf=pymupdf.open(OUT/'excel-ai-lecture.pdf')
assert len(pdf)==26