Формульный отчёт на листе эталона; лист диаграмм по флагу

This commit is contained in:
apuc committed 2026-10-20 00:26:55 +03:00
1 parent 63c4a5ff40
commit f33c0fbe34
5 files changed
+163 -8

No files matched your search

+11 -1
View File
@@ -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.
+2 -1
View File
@@ -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"
} }
+97 -4
View File
@@ -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()
+53 -2
View File
@@ -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')