Учебные примеры 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

+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