Учебные примеры Excel: простой журнал, шаги ЕСЛИ и выпадающие списки
This commit is contained in:
1 parent
401b02b680
commit
63c4a5ff40
8 files changed
+310
-19
No files matched your search
@@ -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.
@@ -264,7 +264,7 @@
|
||||
|
||||
### Действие / демо
|
||||
|
||||
Первые 40 секунд разобрать дерево из двух условий. Следующие 50 секунд ввести в `Реестр!H2` точную формулу и распространить на H2:H61:
|
||||
Первые 40 секунд разобрать дерево из двух условий. Затем показать два шага в книге: на листе `Старт` простое условие `=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")`, на следующем листе `Старт 2` — вложенное условие. Следующие 50 секунд ввести в `Старт 2!H2` точную формулу и распространить на H2:H61:
|
||||
|
||||
```excel
|
||||
=ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок"))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+47
-10
@@ -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__':
|
||||
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()
|
||||
@@ -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}).')
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user