Формульный отчёт на листе эталона; лист диаграмм по флагу
This commit is contained in:
1 parent
63c4a5ff40
commit
f33c0fbe34
5 files changed
+163
-8
No files matched your search
@@ -36,8 +36,18 @@
|
|||||||
4. На листе **Реестр** — готовый итог: умная таблица и формулы H2:H61.
|
4. На листе **Реестр** — готовый итог: умная таблица и формулы H2:H61.
|
||||||
5. Создать настоящую сводную в Excel: строки — подразделения, столбцы — контроль срока, значения — количество ID.
|
5. Создать настоящую сводную в Excel: строки — подразделения, столбцы — контроль срока, значения — количество ID.
|
||||||
6. Сверить с листами **Проверка** и **Эталон отчёта**. Последний — статический эталон, а не объект сводной таблицы.
|
6. Сверить с листами **Проверка** и **Эталон отчёта**. Последний — статический эталон, а не объект сводной таблицы.
|
||||||
|
Ниже, с 15-й строки, там же лежит **отчёт на формулах** (СЧЁТЕСЛИ и СЧЁТЕСЛИМН по подразделениям): он считается прямо из «Реестра»
|
||||||
|
и пересчитывается сам, а две строки «Сверка» показывают «Совпадает». Это удобный способ показать разницу между формулой и сводной.
|
||||||
Граничные случаи формулы вынесены в лист **Тесты** и не включены в 60 исходных поручений.
|
Граничные случаи формулы вынесены в лист **Тесты** и не включены в 60 исходных поручений.
|
||||||
7. Показать работу OpenWork только после репетиции с выбранными моделью, инструментами и разрешениями. Слайды не утверждают, что конкретная конфигурация уже настроена. Полные задания — в сценарии.
|
7. Построить диаграммы вживую: данные берутся из сводной (столбцы — «Контроль срока») либо через
|
||||||
|
СЧЁТЕСЛИМН по подразделениям, как в отчёте на формулах. Готового листа с диаграммами в книге нет
|
||||||
|
намеренно — слушатели не должны видеть ответ заранее.
|
||||||
|
8. Показать работу OpenWork только после репетиции с выбранными моделью, инструментами и разрешениями. Слайды не утверждают, что конкретная конфигурация уже настроена. Полные задания — в сценарии.
|
||||||
|
|
||||||
|
**Лист с диаграммами.** Если нужен готовый образец для проверки, сборка добавит его отдельным флагом:
|
||||||
|
`py scripts/build_materials.py --only workbook --with-chart-sheet`. Тогда в книге появится лист «Диаграмма»
|
||||||
|
с блоком СЧЁТЕСЛИМН и двумя диаграммами (столбчатая с накоплением по подразделениям и круговая по контролю срока).
|
||||||
|
Обычная сборка без флага возвращает книгу к восьми листам без диаграмм.
|
||||||
|
|
||||||
**Выпадающие списки.** Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе **Параметры**:
|
**Выпадающие списки.** Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе **Параметры**:
|
||||||
`A5:A7` — статусы, `A10:A14` — подразделения, `A17:A19` — категории. Значения защищены от опечаток,
|
`A5:A7` — статусы, `A10:A14` — подразделения, `A17:A19` — категории. Значения защищены от опечаток,
|
||||||
|
|||||||
Binary file not shown.
@@ -10,5 +10,6 @@
|
|||||||
"overdue_share_open": 0.5,
|
"overdue_share_open": 0.5,
|
||||||
"qr": "decoded from PNG and PDF slide 25",
|
"qr": "decoded from PNG and PDF slide 25",
|
||||||
"render_text_warnings": [],
|
"render_text_warnings": [],
|
||||||
"recalculated_workbook": "independent recalculation matches 60 rows, both teaching sheets, totals and boundary tests"
|
"chart_sheet": "лист «Диаграмма»: блок данных и две диаграммы совпадают с эталоном",
|
||||||
|
"recalculated_workbook": "not performed in this check"
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,54 @@ assert counts == {'Завершено':24,'Просрочено':18,'В срок
|
|||||||
assert all(r[1] in DEPTS and r[2] in CATEGORIES and r[5] in STATUSES for r in rows), 'значение вне выпадающего списка'
|
assert all(r[1] in DEPTS and r[2] in CATEGORIES and r[5] in STATUSES for r in rows), 'значение вне выпадающего списка'
|
||||||
|
|
||||||
|
|
||||||
def build_workbook():
|
def build_chart_sheet(book,head,normal,wrap):
|
||||||
|
"""Лист с диаграммами: данные считаются из «Реестра», диаграммы строятся по этому блоку.
|
||||||
|
|
||||||
|
На лекции лист не используется: диаграммы спикер строит вживую.
|
||||||
|
Включается флагом --with-chart-sheet.
|
||||||
|
"""
|
||||||
|
charts=book.add_worksheet('Диаграмма')
|
||||||
|
charts.set_column('A:A',32);charts.set_column('B:D',13);charts.set_column('E:E',2)
|
||||||
|
charts.write_row(0,0,['Подразделение','Завершено','Просрочено','В срок'],head)
|
||||||
|
charts.set_row(0,34)
|
||||||
|
for n,dept in enumerate(DEPTS):
|
||||||
|
r=n+2
|
||||||
|
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]=='В срок')
|
||||||
|
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.set_landscape();charts.set_paper(9);charts.fit_to_pages(1,0)
|
||||||
|
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],
|
||||||
|
'fill':{'color':'#'+color},'border':{'color':'#FFFFFF'}})
|
||||||
|
column.set_title({'name':'Поручения по подразделениям','name_font':{'name':'Segoe UI Semibold','size':14}})
|
||||||
|
column.set_x_axis({'name':'Подразделение'})
|
||||||
|
column.set_y_axis({'name':'Количество поручений','major_gridlines':{'visible':True}})
|
||||||
|
column.set_legend({'position':'bottom'})
|
||||||
|
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],
|
||||||
|
'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}})
|
||||||
|
pie.set_legend({'position':'bottom'})
|
||||||
|
pie.set_size({'width':420,'height':340})
|
||||||
|
charts.insert_chart('F21',pie)
|
||||||
|
|
||||||
|
|
||||||
|
def build_workbook(with_charts=False):
|
||||||
book = xlsxwriter.Workbook(OUT / 'excel-practice.xlsx', {'strings_to_formulas': False})
|
book = xlsxwriter.Workbook(OUT / 'excel-practice.xlsx', {'strings_to_formulas': False})
|
||||||
book.set_properties({'title':'Учебный реестр поручений','author':'Бойко Кирилл','comments':'Синтетические данные. Не реальные сведения органа власти.'})
|
book.set_properties({'title':'Учебный реестр поручений','author':'Бойко Кирилл','comments':'Синтетические данные. Не реальные сведения органа власти.'})
|
||||||
head = book.add_format({'bold':True,'bg_color':'#'+BLUE,'font_color':'white','text_wrap':True,'valign':'vcenter'})
|
head = book.add_format({'bold':True,'bg_color':'#'+BLUE,'font_color':'white','text_wrap':True,'valign':'vcenter'})
|
||||||
@@ -71,11 +118,12 @@ def build_workbook():
|
|||||||
('Реестр','Готовая умная таблица и формулы H2:H61. Проверенный образец, не исходник для очистки.'),
|
('Реестр','Готовая умная таблица и формулы H2:H61. Проверенный образец, не исходник для очистки.'),
|
||||||
('Параметры!B2','Фиксированная дата среза 20.10.2026. СЕГОДНЯ() намеренно не используется.'),
|
('Параметры!B2','Фиксированная дата среза 20.10.2026. СЕГОДНЯ() намеренно не используется.'),
|
||||||
('Сводная','Вставка → Сводная таблица. Источник РеестрПоручений. Строки: Подразделение; столбцы: Контроль срока; значения: количество ID.'),
|
('Сводная','Вставка → Сводная таблица. Источник РеестрПоручений. Строки: Подразделение; столбцы: Контроль срока; значения: количество ID.'),
|
||||||
('Эталон отчёта','Статическая контрольная агрегация, НЕ настоящая сводная таблица Excel. Сравните с созданной вручную.'),
|
('Эталон отчёта','Строки 1–9: статический эталон, НЕ настоящая сводная. С 15-й строки — отчёт на формулах, который считается из Реестра и обновляется сам.'),
|
||||||
('Проверка','Ожидаемые итоги: 60 всего; 24 выполнено; 36 открыто; 18 открытых просрочено; 50% просроченных среди открытых.'),
|
('Проверка','Ожидаемые итоги: 60 всего; 24 выполнено; 36 открыто; 18 открытых просрочено; 50% просроченных среди открытых.'),
|
||||||
('Смысл формулы H','Выполненные получают Завершено независимо от соблюдения срока. Проверка своевременности выполненных — отдельный расчёт.'),
|
('Смысл формулы H','Выполненные получают Завершено независимо от соблюдения срока. Проверка своевременности выполненных — отдельный расчёт.'),
|
||||||
('Границы примера','Выполненных с задержкой 12, без задержки 12. Учебный флаг просрочки считает только открытые.'),
|
('Границы примера','Выполненных с задержкой 12, без задержки 12. Учебный флаг просрочки считает только открытые.'),
|
||||||
('Выпадающие списки','Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе Параметры. Опечатки исключены.'),
|
('Выпадающие списки','Подразделение (B), Категория (C) и Статус (F) выбираются из списков на листе Параметры. Опечатки исключены.'),
|
||||||
|
('Диаграмма','Лист намеренно не включён: диаграммы строим вживую. Данные проще всего взять из сводной (Контроль срока) или получить СЧЁТЕСЛИМН по подразделениям, как в отчёте на формулах.'),
|
||||||
('Не делать','Не перезаписывать оригинал при демонстрации, не отправлять реальные служебные данные внешней модели.'),
|
('Не делать','Не перезаписывать оригинал при демонстрации, не отправлять реальные служебные данные внешней модели.'),
|
||||||
('Формулы в файле','Внутри XLSX функции сохранены на английском по стандарту. Excel показывает локализованные имена.'),
|
('Формулы в файле','Внутри XLSX функции сохранены на английском по стандарту. Excel показывает локализованные имена.'),
|
||||||
]
|
]
|
||||||
@@ -150,7 +198,7 @@ def build_workbook():
|
|||||||
check.write(n,2,expected,fmt)
|
check.write(n,2,expected,fmt)
|
||||||
check.write('A12','Статические проверки нужно повторить после изменения данных.',wrap)
|
check.write('A12','Статические проверки нужно повторить после изменения данных.',wrap)
|
||||||
check.set_row(11,45)
|
check.set_row(11,45)
|
||||||
report=book.add_worksheet('Эталон отчёта');report.set_column('A:A',32);report.set_column('B:E',19)
|
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.merge_range('A1:E1','СТАТИЧЕСКИЙ ЭТАЛОН — не сводная таблица Excel',head)
|
||||||
report.write_row('A3',['Подразделение','Завершено','Просрочено','В срок','Всего'],head)
|
report.write_row('A3',['Подразделение','Завершено','Просрочено','В срок','Всего'],head)
|
||||||
for n,dept in enumerate(DEPTS,3):
|
for n,dept in enumerate(DEPTS,3):
|
||||||
@@ -160,6 +208,49 @@ def build_workbook():
|
|||||||
report.write_row('A9',['ИТОГО',24,18,18,60],head)
|
report.write_row('A9',['ИТОГО',24,18,18,60],head)
|
||||||
report.write('A12','Для обновляемого отчёта создайте сводную через Вставка → Сводная таблица.',wrap)
|
report.write('A12','Для обновляемого отчёта создайте сводную через Вставка → Сводная таблица.',wrap)
|
||||||
report.set_row(11,70)
|
report.set_row(11,70)
|
||||||
|
|
||||||
|
# Отчёт на формулах: считается из листа «Реестр» и обновляется сам. Начинается с 15-й строки.
|
||||||
|
report.merge_range('A15:F15','ОТЧЁТ НА ФОРМУЛАХ — считается из листа «Реестр» и обновляется сам',head)
|
||||||
|
report.set_row(14,26)
|
||||||
|
report.write_row(15,0,['Подразделение','Всего','Завершено','Просрочено','В срок',
|
||||||
|
'Доля просроченных среди открытых'],head)
|
||||||
|
report.set_row(15,40)
|
||||||
|
rep_first,rep_last=17,21 # Excel rows of the five departments
|
||||||
|
for n,dept in enumerate(DEPTS):
|
||||||
|
r=rep_first+n
|
||||||
|
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,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
|
||||||
|
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,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
|
||||||
|
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},'
|
||||||
|
f'SUM(E{rep_first}:E{rep_last})=E{total_row}),"Совпадает","Расхождение")',normal,'Совпадает')
|
||||||
|
checks_check=sums_check+1 # Excel 25
|
||||||
|
report.write(checks_check-1,0,'Сверка: итог совпадает с листом «Проверка»',normal)
|
||||||
|
report.write_formula(checks_check-1,1,
|
||||||
|
f'=IF(AND(B{total_row}=Проверка!B2,C{total_row}=Проверка!B3,D{total_row}=Проверка!B5),'
|
||||||
|
f'"Совпадает","Расхождение")',normal,'Совпадает')
|
||||||
|
report.write(checks_check+1,0,'Правите данные в «Реестре» — этот отчёт пересчитывается сам. Сводная требует обновления.',wrap)
|
||||||
|
report.set_row(checks_check,30)
|
||||||
|
# Лист с диаграммами строится только по запросу: на лекции диаграммы показываются вживую.
|
||||||
|
if with_charts:
|
||||||
|
build_chart_sheet(book, head, normal, wrap)
|
||||||
tests=book.add_worksheet('Тесты')
|
tests=book.add_worksheet('Тесты')
|
||||||
tests.set_column('A:A',36);tests.set_column('B:E',21)
|
tests.set_column('A:A',36);tests.set_column('B:E',21)
|
||||||
tests.write_row('A1',['Случай','Статус','Срок','Ожидается','Формула'],head)
|
tests.write_row('A1',['Случай','Статус','Срок','Ожидается','Формула'],head)
|
||||||
@@ -421,10 +512,12 @@ if __name__=='__main__':
|
|||||||
parser=argparse.ArgumentParser()
|
parser=argparse.ArgumentParser()
|
||||||
parser.add_argument('--only',choices=['all','workbook','slides'],default='all',
|
parser.add_argument('--only',choices=['all','workbook','slides'],default='all',
|
||||||
help='Rebuild everything, or only one artifact')
|
help='Rebuild everything, or only one artifact')
|
||||||
|
parser.add_argument('--with-chart-sheet',action='store_true',
|
||||||
|
help='Добавить лист «Диаграмма». По умолчанию его нет: диаграммы строятся на лекции вживую.')
|
||||||
args=parser.parse_args()
|
args=parser.parse_args()
|
||||||
if args.only in ('all','workbook'):
|
if args.only in ('all','workbook'):
|
||||||
qr=qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M,box_size=16,border=4)
|
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')
|
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_workbook(with_charts=args.with_chart_sheet)
|
||||||
if args.only in ('all','slides'):
|
if args.only in ('all','slides'):
|
||||||
build_slides()
|
build_slides()
|
||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import argparse
|
import argparse
|
||||||
|
import zipfile
|
||||||
import pymupdf
|
import pymupdf
|
||||||
import openpyxl
|
import openpyxl
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
@@ -66,6 +67,44 @@ for (sheet,column),name in expected_dv.items():
|
|||||||
assert all(r[1] in depts for r in rows),( 'подразделение вне списка')
|
assert all(r[1] in depts for r in rows),( 'подразделение вне списка')
|
||||||
assert all(r[2] in cats 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 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
|
||||||
|
# Лист с диаграммами включается флагом --with-chart-sheet. Проверяем его только если он есть.
|
||||||
|
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/')],'найдены диаграммы без листа'
|
||||||
|
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='лист «Диаграмма»: блок данных и две диаграммы совпадают с эталоном'
|
||||||
for r in rows:
|
for r in rows:
|
||||||
expected='Завершено' if r[5]=='Выполнено' else ('Просрочено' if r[4]<book['Параметры']['B2'].value else 'В срок')
|
expected='Завершено' if r[5]=='Выполнено' else ('Просрочено' if r[4]<book['Параметры']['B2'].value else 'В срок')
|
||||||
assert expected==r[7]
|
assert expected==r[7]
|
||||||
@@ -79,7 +118,19 @@ if args.recalculated_workbook:
|
|||||||
assert all(recalculated['Тесты'].cell(n,4).value==recalculated['Тесты'].cell(n,5).value for n in range(2,6))
|
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['Старт'].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
|
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'
|
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=='Совпадает'
|
||||||
|
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')
|
pdf=pymupdf.open(OUT/'excel-ai-lecture.pdf')
|
||||||
assert len(pdf)==26
|
assert len(pdf)==26
|
||||||
|
|
||||||
@@ -130,7 +181,7 @@ except ImportError:
|
|||||||
report={
|
report={
|
||||||
'slides':26,'total_seconds':3600,'notes_embedded':True,'pdf_pages':len(pdf),
|
'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,
|
'synthetic_rows':60,'completed':24,'open':36,'overdue_open':18,'overdue_share_open':.5,
|
||||||
'qr':qr_status,'render_text_warnings':warnings,
|
'qr':qr_status,'render_text_warnings':warnings,'chart_sheet':chart_status,
|
||||||
'recalculated_workbook':recalc_status,
|
'recalculated_workbook':recalc_status,
|
||||||
}
|
}
|
||||||
(OUT/'validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
(OUT/'validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||||
|
|||||||
Reference in new issue
Block a user