From 63c4a5ff40a16f41cac69f34d691a01a05f1fa38 Mon Sep 17 00:00:00 2001 From: Kavalar Date: Mon, 19 Oct 2026 23:20:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=87=D0=B5=D0=B1=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20Excel:=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D1=82=D0=BE=D0=B9=20=D0=B6=D1=83=D1=80=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB,=20=D1=88=D0=B0=D0=B3=D0=B8=20=D0=95=D0=A1=D0=9B?= =?UTF-8?q?=D0=98=20=D0=B8=20=D0=B2=D1=8B=D0=BF=D0=B0=D0=B4=D0=B0=D1=8E?= =?UTF-8?q?=D1=89=D0=B8=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +++- materials/excel-practice.xlsx | Bin 18694 -> 23929 bytes materials/excel-prostoy-primer.xlsx | Bin 0 -> 9644 bytes materials/speaker-notes.md | 2 +- materials/validation.json | 2 +- scripts/build_materials.py | 65 +++++++-- scripts/build_simple_example.py | 213 ++++++++++++++++++++++++++++ scripts/verify_materials.py | 29 +++- 8 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 materials/excel-prostoy-primer.xlsx create mode 100644 scripts/build_simple_example.py diff --git a/README.md b/README.md index 2f91d4f..5544db3 100644 --- a/README.md +++ b/README.md @@ -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 таймингов; ручные изменения в этих файлах следует сохранить отдельно. diff --git a/materials/excel-practice.xlsx b/materials/excel-practice.xlsx index 5efcbf4d3bd7a17966b5aefa983194318599bc2e..e66ecb340d92d253f8a0bb4ba6eb3cfeb1ec934b 100644 GIT binary patch delta 19951 zcmagGWmH_t*0znidyq8l?(S~E9THrEO9%v5xLfcb!QBJFNeJ#5+}+*%ZIXS?_v}~3 z_oI6>JtniN?t8B4bxpbmj)S~yf&^3)pkZ(zARrLH4-p8|u#Q(=kPr~P;2*KU@90Z7 z*t?qByS~-%a5Q%@Wd3MpJCtPjrkxch_*m%yUBO{UOH|5)z~IKIm~s!D_VCsFowO$!2D3RckNPjN<^ExmI+sB!AqD4ID0kbFn_9 z$H8ct%pQEOtJ?d7s<561_*!+Q1IAKCIqfH(IqZf2>jx7$ z11INb)E?|n$issc&GWWWb=Es;U0n4-0rulrkrfH6AD=IahFZjt)72{Ae>W>U`!jCV z^u~I@)pqb=p(BZnF6B4;jdqu8spH*m5i#yN8E03*cBe&*=(J!*&IRtk+z1QqFP%nPP`$`hl^gu#GqGcO; z#o&YeN``}gAcTltmLvvRClzcuSTW#MytCagYAVeI!q$^|XA{Kh>^vciV?dork<#8b zSGb5u*a$huxrr&5=ll9m-1EoTmYG&X6Y`>1U2Q6fO?w&?E8fp%vCtF@npB_4RoVh! z=GUI=&%;}5?L+76DYw*c5Ut8dGIQ->!4h&A=F@tjt` z8VMs6xU|>L#4Ti-yMHPhNYb;e@~r88q)Lt)xT*|(>rK;z{_je6Ep?HWfr5abK#V7& zc?Gb&ly+j0QcM~vM(}as9f)z6s0xuR%y+UvtMp2xcyD_a5_=R%x-Q=K+1pMw=QnoA zoxiOh$H+{Jr_$ZY)c2jhG3<8KITYu;r21V45fZGD~8gr8Er3eQ9<#8`kyL#1Qi2v%Yo>RspLCKFpbhM zaGq4^BJ8kcd$Dw+GS_{dL3~gfK)ysY1mBN0Mt7f+eOsJ+6-1Sf;!Q=IQF`Bp5JR7B z343hH7(uc-QPuflrH$#V^(d3hS)a`4aJRP)3hBH2ODa>!F; z7v=}X9)%wgpX2*T+jmZnW#y}`SdaeRiChaUEdMO(bD`bq?lS@|LMaom{Q-*hm?!;nRZwS-K>epojr@s?%N%f zH1!&3%epUW#2!Ib%lzc^@oPsY)vryzfAR?wmvU7Jcwnwg5-#_nz;WYvRKKYR2Vo_Y z<%CE-;}lj#9>2GntE-C>=j?yw@CLVOTuo#>J5ieY&7#YBuv}H zXRMfUkz~E2Jve6UuugX;@*p@Ky5QJezFgkUeQTeKXmr~eIY;aEnI|@<4cymj)+e68 ztB;4rp^5?P<|{MsArmV@W~Dqogj1(z29OjUw1!(Kh#4uGZ_-lNUvV0Rb_;uQ1W+T7 zVTX7M1fb#PNPvVm9C%Z|?)Fkh{z5Tch3u+!$H@8erF~ik2Y@&PEm>KVoO~ej+|RY# zLZUeyr64;+td9?>I9m96H0w@(-J+f)Zk&kuW&Sl3pT^dd-i5t6b3A^w(jtB-ccv6| zx%s%J_)dhe!}X~|+B5XO%jhA|VT%eC0%FYSPZ@Fi`8dva_$zo<}EGjd7ny)YNzNh^aoXw?v6~W|bF$ z$BcY-tcPSo9v4)UtPaio_FzPzIL6=&9J9Q`?|TkzPl-Q-GLtb z6CiOFh%n(40x6wYe;D=t%+z#D{B(wYUw``$-=&#^oAE7VcP;CaCQePOk%QBRM8g=o zm@{2&rsc-o?djZ{2N$1N{lTr-otCH3%e+GO4{=MER)U8Wo-=9nyA(eg2r|ukZfA$w zxS6yv25b6?XxHBwL|H~0t@^YE6`prI&H_8^pOSDv!(WF5@YmM@9;|0oj?C-b8@6?%BQryuNV`Yu1x zBk=i7~GYk-XW z-HZ)|CdNF-s|+Np^W(zD@q1rLi&k^%?VWHy$6Wt-^!b5_D+k@uh{KzjyFiDP_KB zLVg?#T0%W&FKc#m$U!(H*LDBgvTK4dBLyrqi?bj#CvzGXqQQR2pGow8xCLO!8z?gx)mZ zaP-6I>HY1*Sd_dN-|0PnR zhxi8SIFnbF-qwUx((lJRd=>gP89n&;9m$k~$${Q}=eGaEVV%G>@y_}z(-yf%kRi=< zTCeqeI(xhA-E64rCBk^>tt=RMoIsWEF-awRPhu?_BjaM(hZ8-B_8oSi_nWnQRV)z% zg$G}3^yc($fmkB|R*59Etf1MU*~!(JFiuVzh0luI1F8I)dk5A}h5Pt6(l@%|VU~!A zsf+a^9B)>HkIzeHS z@`rz_v*>{Mj@<(Z8NR&dYXZ;7-$wYsmrKKaijbuwBTMuW7=t9kB8Hq#Rf5+bhKCZ< zW6PqJ9rq6=!O$T@j_t=UNZTT7{oNlF}zYpm{UwJyAUo*ttCH^A1vlgVm^%&)Py zpUjow-p_aSO?cpziSUt@Q=2|)T)jz-5=JXH2WcCRCR?6EplHxd6b`MFxC1Eo{o5y+_#AwJvQ)iNpKDV7NyMhH_!$myV6k?1N;!+^i zzNKyPV^H%D)lv8Rg{6oE)f5AG_C|~TLyCB0!Vj|VoJ-V{6w#n8oawNfi^BTq1!i4o zk^VIrqN1{9Sy-PNcD%ypSP3P?3U9@VT-HW3Gv}d%c^PVa&V$&t4UNsz6ayZ%MjS-G z-<=yTQ~r|#_-D?ha65m3V*8x9%RYmj82<2&=3fXro$AYUFdK|*Q}d{%EWofg8uyco&bjyn z0)1qDeZ}%zYHOKaJO49^^u>juE`HaE&JA#`KNbA%JpV+5|NlnJxdwn$ zvx@EejF2yD#k^(ej2!~>DUrq#6)ZrqH)a{b>oKbF^lorU$M#uA*1mIIe~I`TV31Cp z@85lp1I?sWN+fB|KM^20_T>j`JP{~9jaShg>dOM_<{Azy?#rUh_u;;Pi`a;!kp8!S zUsOz=5;1kXpQ8O2XQ%LZBtmD=(G!O|a0NH(;*0|Ff8?F&+2BTtpBJUBS8UYzstzvU zTAgNla2@1BAz9`7zZCGksWVYXpB-tuet|)HP9FGDv8w*C$j#PhX96zGJL~t@@%o_n zd0It#=r0TGn`=QC8lzPbV6PKxOq?-T|EbL%i+u$WU<-nuSkvk7lRJO{a3Y(RL{Gde zr_-x;KChQ}3j=j;me^Kgh z!F=DxtBjv8K>3osW%BPN+N;%HkjU$@y4KtxRHBjHRYj5A!8T&tWG!_2IpZU2Mzd+J z;t1nP;>Ed8TJJZIW<<{{`jtnoh$%KZl*Oj zf@)k~+okVZ1?Q3S4@T0&P6_b+WiW%J{~Km&KEqZI2X@eqG(|w#Fi0>RJn5n?qz}`Q zTNPo0LFH9aN@ho1W=A=(jmWJY08vdvdzcD+=u8ft!i?cM$pWFn>eSY2`zRi#^QA5( zq||0nPIr`MBpUf*Qj-Y1GGq79C5@dEdwY!?y{g9Z>c)tUE#}xEl8K7;&zXPpA^#A9 zKWNSG>!>{8hv#_Y<#@E#)`%SF($i4tXjbSS%4Ag?zDSm_z8FF>-Uvt|>y0*^k2OX} zY%|9WXNYcpQV_(D=E2MR1Mo5W$tS6^jTuKpf{|))$3j(#NP1&m}z_^S}xj z#zuTxat7U*1p#8715kPA@T$jTt254uOf6QlYA}(_!_PXP51v4GEEi^me*K<65#Jy{Q=7%4GR&TZ8ZMTmi&H$FkGubI?@$Tuz6)k0!Qg?fiGadX!!J~M$oYM+OF96t3t!-o}JUpITQ{?zl) zK3a>pZoYLXpql(tOvBVOYs<+}Leul`(|jvr4}$Lc=-B*oDdF>(i^YcQF^LDeGbjDG z0WV>DUX3i{YR|giPr0|74a5fV2RX$00x|NhQ!4unR3DpqEiX&s61CC88lU&+i83tD z_bR{b7;bntJzfm56j$9O-v zxRb84`D~+ndIX;ASj0lDy9aE&(mNjO3GJ+@K2d!*{6%mYL7nm60q)~WU3gw*GcJAxyH&4*VH8-lF_!^h?mu4=@50~YulEjMmg4MLg2 zmnSy|RHGh+(QcivU< zcXZ7D0+w8Fc)b(0KuVxtJ!j}&Z3SM{o#FyI*{Oq^u*B$u>3+XbK7v+ZFnZIqBP z^lfBZ4%eJt`Az?TgA#+A>ZE*AnW?6wQ{3%wDYBr(!h8}9UBHajy0`az_gemS)PVX@ z-FuJO{YnQtM-^S&8K=~*s4P9V@UxRndOyBf!D|0_q$C zF#=k~ula2XE%_^KBP{b#sckY0h9gLQ>f_4j^|Z3JHFxIeUEk;g>1wWmcSJ4B%97CI zk{zVdowAahhhtS&ZB5tXIg>&o+{1jf#4wo-ua8wc0~LBT{a-vz#b~j7=a@>EGB{6_ zQ0tfQGN0nryniOJh@9T5&T3~8>(o#Cr~zN!QOOa=wb1dIQD!LhVN&ec%il%j)ziJo z>O8cSf6a>ZosQ3f@=j}nSySW~|>yDVBwB=oSUg6pW|%c#p66J`>LniS@W ze|jt&#L~hk;%wlcTjADX@|5&i1|1!&wEkj&^AyqZ@_w9L6Nv!fi!eU5-c;ONn*tXD zR;S5b5napp%Z7l@V^3PU`0}&}X#1znr_-UeuFLJ9wYNcQ9@)2hhncO3E%_1G-s%a@ zA0MSI&(Yl{y%+V8n0~}8d019@YjuZhuh?<1>!Z%ugq71m>Z_WBG09j=OufFfy+%C^ zlw^CClT|lP-+OAhUOkCm$Avm$V-D2m)h2EUC-|1rR(B>*DA-aN7CVH|P=-Td!+uZH z5_8_KzMQcMs+)tebz|2jnUfb(P=Tt;fr+c4sEb?omJuW?Bw$rl(YMB>903W$lq*}6 zzHgh-v3MU@7sn>;k5z?B6&q-3!cd$8mWDPi5B^1(p6Z@D9V`6@YCBV)w#A9$ixUZF zv{@kx)jMK&(xDHbPLxqs0+q*#(9}rI;p@>o4bh_w$`svh=G|^%yyIqw@@>2cqU48I z>$0WS&6Us2M69Z#uct2wrf~_U?K&I24mP~5l~Ih-@W*0Bql%^fRQXe8Cw@H~%{GPB zDrVm!avw2jzg2Gi2+89oAiqY*RyK~^Bdbpx3CC#48B3*2cuXRiiG)K^@A!H2sjgV} zTBnd7#zX9cnW9Tj|=(nm`Np5Nz zzkIJ7&RJZFKZ!H|9@Yx}8>hJd4kiX9I}|e(!TsGyble%qB+81aczbSadMC~)D`9P{ zpI~e--DY-`sOq4EH6%VFiobW3jGSDI4csLy*}iJ8iV4yu0#v=;mDjyB3D<4jD6qng zAoYM7TF{xQ+E#9=pRl~FH_mthT9WZ^UO-<7(KQ!2pJCSXp`d7-r@F2i3?<2t%8`kL_AAxWROo|q_f7eZ$ieh zTu`2}t<}{ZB%WENU{jyW!y^K(_zpWn^umD)t<#t!!ualb}>6j#V?f(MD3d^F{0 z#Np{;4#1d(*R)5#TqYcF#r{{y4wNUqGQxHdN#C14Dz9b^$xo3at$`L{p2oxEE&8(+-P4%T3W%vwgwRhuWDWFU?hX7mSA^#sjHgv{ z$PkGenfsF%a5CWE*$Na$EKbnPtiek*j3~GXS^Dnyxou`T1gOP%-my4|!CM1GiRMe3 zqUVYszj}bSPFVx>qj7eLB#?mPNN4_EaUS52dTVf=MZcE&{bzndf93~(Cz$*)K|J;w zcr3au5fU3ROe-m-vj4{EcZo=c0w_?4%v~apaOtL;4eh>bApXdFsFa$n7eh{0K@?pg z7Ns{m2&XY%ne+c^$b`y+!~`{NTMH92*4%a^`ZEFD6WhP@0U4#$Is<_xF^Dhn>C9la zi*$zokvPvO(o?pGfAR%s>Vs_y0WVjVD-a0v)os*-eio&w`Y`Y}koqJBa}5+Any>I8 zOUQf1Rd%umQ}KUX1Fd(6NQVQ8*#DYd2bY%17mO!1GT@;@3JNXcB%Pkzesr|7QZgf~4m zCo!MbKoclWY~l~57)7IEHtkl5F?u?t)ydhe@ZIu~S?|g;pq~jqPXL?RgK6rozY^o} zBPoF|Asy4%GPlTqzgu@^cDhpB~s8G5zM z-^iZE*g^eg8TO2bxxkBIPv5|XN&LsK{SFbWaKIL9*q>F5=f2ca2Nxco8xFJ?gCDD4 zh5wlv%0E-Pc)1jZ0rayD=&2fPL)tG9tx$jw3RmirOnc~P2Vv0b3Iw02qC9)8-;IbP`R)ud(ffYUAJomd=L11Ft3hnkA^E0b zK=4V@hc6{jMIw}!RzD4>LP;Kr6+sl(vswBXZ3A8~y>f`L^YoC;;!qp0#k5F9Ski%0 zlR6Mv;t0J#7#J>i3yhqBzUktY;W-;r#*$WSxqh95CZdwQD4~4F zdc-E8X^Ca}jOL;hh;SQ$D7|EwsyMBbkqAr`)i8rWq_N0iq+5(uP6$wZAQ)#ggbl*k zAwah&gTn+@3O*f?LOkMq8KlcEp^Shu{^{9CzJ~G|_qUd(-NQ;Mq%?V=XpP}H&=Y}} z*k(Bah_sFaSbbXbt<3ba@?4osImtXMS$Ocx45&MiGyJj1BOcWeNxm@5`O zZW|@@7USr?2?!xPpOkiq9wD4h{v?$qt7|~OqSctBHK+ymyF*kCm5j=GNE~E>pl{sz zW!w8+63v0$^O1AFV>@|*4RNePD1_9xqrcr!4 z971-;A}iT{tm}tk!C%HpzUde;|Iue5cVWL~os{d!pT4=tq42z{w{?F8t!A(&5^kV_ z_jIdN*PFd0bWz*8%K8QbSk|bnVE&}^fPf%?h^L~#1-Slq2Gt?ABF^ICdQM89r#q3) zGtZJknU|}4qB_BX zkxoJI^6=M;7#H6AiNWEc`;lRpsKP#1ZpEHYN^b6@v$EYiyYF~~7vK9l0D0H@$~9~Q zck5z6LUerV*16e~7x26-GT*9Lm^m~YNBRDSu)C&pOE*dBgI@XUsjl-<=Asf!-^@>! zIOUQ1FVAkCbE!%Na)1i4tFX;(d3llB5-2(?g78Cu=AP^u<=~z^V^1%quaPxUR1;>u zN4y?tYF5pr7M7WYPaKwO*8>t-oJa-cgXAaQ8w$^LylA&R%=?Ilk^ zU!T$W*Ezm$TV^_(OqBS6+4AYAk%^0kQzsxnJI~9d+~=7U-KCasC~bR!^N~?xv5sS2R(UF*Lcxb<@9xy3aQiCT-1jH|qQ3KEy_P!NNbaJ~?rF0jYuq+jdXZsph-4 zxvRUnS}C7zzr|bMd9X9*C`$J^`P@dyWnXDB6Qh&5_ELx*KFQO_P)P#L%u>{Xe9~Q$OG8=U*{*w$oUjGz$Sxy(xWZ9M!Ihu4LFB9 zo^?;3HyvX$Znf|10ew5Ww7P-$H4pm$YwlrlCv}% zH>aH6k9#e@Fx9jnOrg3TMR>`itytw*b>=`?gPt+taSKR#cW0qX? zB89e>?r4JEilRdEvr_Ak*|XTO@|$C>?PUXRq34l@!rPd0VC3iZvyaVF3E%U;!{!5U ze12T{qv3AdEqmJil0|0LVJQO;06Z>(v>yf-j6M!cA3v>S+>ji`+)pJxyIz`~o}=?j zdQa#i3;|X@u9xihDPyole$CsM)e+WCV(F`b0kIQHA8n|qe14;5k*gCMODZPJe@?!X z^ceKTw_Mt)wD^}6L(ODPIp=jYj4dD)MpZ|wXTq>6Z>S(h_OgVc@>--g{TSOeEr#03 zsduIEvwNdjYr2ttd6J|$v1Qq<6Wclb^-yV8vFGP(rx6Ti_ z&M&^s|DB+oiNl^~j)Gi{{%>I7yu%Nfow#dUl9?AqttVl%*SA`@n&&(!h`nZ@QO4>( z5^@3}Yt09(+SsL`b)Ab5mmpn>MaP_w@evOl_EYtL6fi z+98G(u}U?t@pr#8q*t-qBgRHNblgv!AEqmIvB@1q)Qu8{!&21&-|Xxsjj5ZeCw z1!EfE&lop_96=I(=nFj>q{8%pUI_`eMV?tPSZbgk&U~fnKk#${9hMLW+6lsW6a`qy z+I%z+o@nVFRf)Pp^g;nns6_66XouGYOgneDvasPku+E=HhYP{zrf?dwMEef}*3!K| zAaMX-HTN6mc~nGE)vo~fvPPcmtqi6LFn6`8KIG=ByI~a78dsn}8@BPmJ&)QJD-`Z0 zfccZ%xX`s5PNfA~)656HX#&v%q(49K+?uxvniF4K0NHwKfX+keP1?Z0H{ z@ak#I{Lc+EbB72&m_h&V>6%cUci8_BPx2~=r2PetT}f_$e@Gh4n`0?xZYjS+V)Mbk z;Len~2J)P4mx3}~#&{Z2d&u4Y^*_a7GCKiV@&ZPqIL|5{{#x??h}!_)yOWNnJwSWZ zK|PI;ffc!tnGOJIF`lhp=mC;&;7{iOB%6oxTw5F@nT7kk>c4RgIL`!RPi+6AC4dAC z=3~|*q<6!3;H$)oI`9uLMy>tBWPgBrnC&K{KnYCBN@*Gp=XDLTR+$`N%6Q?Zf8}$n zXa`J~)Pn)VS1;TXBxHWm1IAG>zNrtSGW{L5$|k~(dF2K4OOsYXDKvnSvHjPNf940a_b;dZ!S@h`vMqM-SP82j;IF+YU>*fi$X?q& zyp8r!fg}}GRRds*p7jIwIle)^v}eG*hL_o){{xqQx)4^zK~zS;YoHylfTA?zzJMaa zAK1S9hf|SA^136tZPk$(7_Zn9j@op>5O5+B=~qBt-j)Wycr5e}9*fIlvw?dH42<{e z;7sKCH2~>ntu~SWPhy5n5j;O662tC4y?`4^D6kc8MgKv|VM~d>Xi57YWMs)p0e5F` z|G?W$3z&~Q^bII$M`khp5EoB4P=@gg=)lmsauRS5g1uN{(GLb}|5yR<3)Y6H{SOS1 z{0C{D7l^=dFD0ruaDt9W`lp*Ra5CaaRsYhP(I1%jloxX>mL)#z()mZdn}HYyI3li?P{YAn=yO$E> zH!wa;&cA^tE&x9&d;u^m>(D(_gGK(MO8=idgQ=|QWQX>jfx{yI_9K&FwSHM8+jPL zz~mXHA-I7aDo|B6%A&+ z7>T$yh&MLg85{=hERMDjTL#dkru~hB+oDCtng7L$D_6IO0q5ab_VEEu* z#$4V!*sjb|uH~GSq?)V}|ih$AM{#^3Re*@F$0Zp+oW~z|`rnewPf5 zg4*e&Er1^+ zszM1XHYP2m(y8jtbKr}|bd++L>Q+_L`@QHpdw3GDxE#K`r;0O3KH5IH?CmRemxls| zn!&^|fx^U{t$wRMvIN)cJb0?>?2VldeQW)C_1skgZwd;#s6%SHojlowSt>kAT^ota z%s!mq0XA&MGy75G`aGJq>V@mSSeHT7B6EclGoKXJ%$q*3 z&f&f9csgp+f0hF{i5Wkjw0>CP+|?gICPB5l4Nl2b^?Vg`w>}bHnx2uLjNTm|aS&j} zt#41Khx4Iu!qu~xwGa6#N{!dsF7l8^rWoMqZm;X%Ua+#|gl?~EQd|2ibH35#bSVq| z7vN~`%{lmup=VvI?aBw(p9XUm{45`ZfJxfC%EuAV7RxbrrHGkO>x8<#zCO=k@zwFB zb}Y(k%f|zgq(LpE0x$LQw;xeaRSk~ar7C>5o`>JA#Y`vPa;H&E2{>ixqQ%Prsha}* zPd~p8qTj-IzFidY0u3QQKXB`VDVrDu(A&t$q$>QuknE))`^&a^`w0GVxA^!lO4WJo zern1Pcq?G&rHfNp(| zS=-cez{Y1mNzd3d>DLdSH6Oy(CGS|pxNFZQ5XloFwk<|-}J^8f+qt~=B^-dx(DQ_&PANN5t6#g} zrp%9;@HIoSAlfq)?7P=2F)D&@RRAsSfDjm7vPpG$QzHe^{3o|dqi!<82GM%Bq>@O^ z0SGlmIa+0rDL6M0Blvc=tUA=d69+$MAt%_|@xrGWx)U|}k}q;c=igQBq9&~ilG=1kRj%ehtP#PXOyf;!SFSn|_2Yex!m{Z1ybGJw59%R-a1 z%)6n)O3?e$2WNhdAKFih4g>R(+mp$YLrXP?wKN5H?Vl;W{?dSX2!UZTjnLokDHmFs z%JMA_o9W^B{0Z*xOYu>_+5MjWZF36?kmx?>qrDUmMNounQX8BSTumi?Oe1})1EU(X zq_X}C`i=g%bwOJp54it;mkEr;jO?sTzzDA|PYU~AscIgAO4H81MxAYIXD_YYrVyeE zp_(eh%)W`4&6d%zsJvpM7g7u_Z0`T@?fBKVLI;&ZKe8%enD6CGI;EhpNl1pleceU) zvfw#mTQ<~tHdKL>K<^#YF4Wc5ujws&4SW{pJRzaNwpqudpi2snTdcW0)NN7YdY}vM zBKS9oNs_DV4}#U2?TwvE2clw>k4DCbex~%i`izBBO>5qk)L4YAZ5MuC(p4bTlc3bR z%QOQ((tnKkEJZM(6ap;O0fg%!|1SAmK8g*uct^_D@hj4>Kw{EBbQ<^&)^*|?TGBOY zy`vmVB(*up@*upJ5Tpv3UH1nde zzO$3H*-4+32uYqyj})>%!bIkB%Wzpc!91J5f!0Pc+p)7N%Q{n_KWCd`GV&eQ1^@bn z#S>aC8={|$0|au1MlcHzl;sj^(iat@&T|UoifJN`ysEoMBF>{NL7|}(;BMI5ro@>J zjkwd*bwlYkHbPW_f((y95Z0Uj{Vf|a#Aa_9SGZc0NHz{%+{k=e|FxL9UR2n%oaU*b zSTwS0*)6*BU3jTPU)Q%rc1j8^^`BYA8+ziSS?-rW3E*rbJzA~(!`z;DIfzt%T{%_6 z@>f$}CiI~~mH>=iTE6JIU^P9v7;zUF6^-cRV3~xoLs>b)6jgjcjI)Myj$@;qL$$es z__`8x)5j03Jq;)amwu8USsCcy6o9t<3sd_Soa(Du!UDVjwo0LUz8JSdxh=t~?=%_b zyaC(cmVkE`^v2b^*iWVoKLXKd`WlY@gPp8asqoY~(EDh(x~zQDp>NxA3-_`MbYYdB z8;~iFp!)oins{PAUVr|m9w$n1DqT~-6<57wL%9SM<_;YOfWkzm#z5wPpP85fUhVIn zev}O{lqs=g-$T6d8Dn9P3mI-c{v2O{iyJy1vIRKK`$tRWE?*bVdG?u(jSs*5MpV5UF%lz-#jrg7D&ONX%PEu+lt72o*`@wX_! zXAu6L7yM}Xp&2HgnC3OW`_iXuO&V~Rc-e(zAJDr~3F5Onc&(e`%=AFm0;FV}>*@K* zL`-1}^M>{FD^TP1&5jE{S;-FDO&^eVfxz`z!~Hzb{4fdi`|ac7%dpLvSpg*zM}N8q z**)>x0tw#<--}$=mG*iEw69!b?-t(N`zWd%v~vuue7!#L131VoQc&5pL?vavAcv&K z=%sf1lPP;?QBb6ZC&rF;Zkx!%6B4N&T<5D}y)USk{@xj*rXy)J284;Vvm!Yr!>q#; zwjhW*9k{595J^+O2^E6EzPFHxI_x~p~s zlM-+!$6a^%v$?Pk4}HmPb>xf^;j@u4(KVai{;{pX4yj>bK_LS0NDtr{_%AeM>Tjmf z3KqJpy(&7gqVdT3QeZeWb61k5j$2S&va4mKNTpm!4=B52k4Z8__zWich$=7H4u>%= zb>*n4OG`6+c2ATZAPh{9Mh>IGFXv}x%bPh7NE zuF0H@x>;;5WWA3ww0v8WwtIVVzCe3Ha_%&k>0qfndwF3%>x0lGlifJuQ#-lD9<-cT zwX7`C3v3rQJ0@pUnJ=#lN9P84Bx=st99eN36`^#hEqOMVi6Kp+sWr?aU59h_YBc0> zEsO;(jCni%x)-q25f zb(HzNh6c$!OQF0JVczSEE5e1V;D*EdPQ!jxC7@Ne!2ZjAF^_ra7LuDeeEl||-LMXq ztEPlQUzJ)$1GBBh*Waa=x_dI3+Q^K$O;idFvueMKn7ntf72z8K)>Hg^BPYC$+o|>s zpnltiDS!XAIJ(V4amXCI9r*z1vpXS{z>cx50yFs@6~SukJFyFU9L2A#P{&_p!bIUj zuM2=%#e;!Z4B^ZTIXrjiW1fF+-Zd*6jW*!>m2p@I2z2nV+K;v@?helHO&lEFzg#xu z32(rc4Nlv!tXIfjNiFtJCe^MUS4KCS36{Sc!@LuF)V!X{L9dgwJA;HjG<;_i5pX1n zJx<9vBOGuZ^>KJ=NE0wVs8WdAzPTRwbmcPlK0cDKGL*4s1@UbEwKKwTSi+f@zmY3Q zL`O4t0x64X2H0D z&MSD0l)OyAfklyxe%oF4!E`eC!AUJq5*PQiGTc%Q378eklyLxS8R?^m>^YR%oOgfk zk(3nL@VO;#Kno2D9}51Ssywb7HjX%Lx0$_Y^jQc+zaoYQrn7*yl)VBCmA9Zt33Bn! zL6wmh0ZjE5Yl}lQcIFB)Z2owmeswlw6^!=r*Tdu@;=>J|OS#2_?V<-Z1B(+6W|>+2JHFE-xmM|6 z`XSi`&L$-n2|vhiZ|ckLm@6~`J4Rgl*ghl6(e#;`UF#N)-qxILbJ7;vHg#XM#ummR zD%%nFA<1^08mV$W+5*E#m}(xr_gFtf=|PlNCp}96KXY?%G>BQ zMU3l^XkL>j!I+5PW8S0?5I7L=u`+nTcU^r4B25e*nKcn&Enh6UsMhxaOdqf$>2MQ2 zLQ@*$PL0ZjsoUWH(q9{ekk^z@`we9yFHaSVVX|%(#xh&p$-;Y&e?v96S=O4yv0sZT zsFLoI;lR8+_$324KcB?C)P1yY@hGoeX{eEsnIzJlu3+L3)^!wqSUGD%j<%@-k_e}h zw5A9zVYeeZCp=tEHDzMilgc3b?mCD)!SNu*U1NRwG`?$S+n zU}hPv$Gc=X)#e|C=Z~y+d%KGb9QN3ebnZ75dJCnq)Z%R;HzXvF;3gim_s4$}c+x{R zwhR?mpO+VzRBd_fFz!Tla5O^yi2d-oQ@;t{JwZF&EuzL~EG-}`r0N|6KFYhjI{$g7 z7N#HZ#8Cztp+y3d4u*4X#GC^{RGIzczBpsbo zgEOS3cI!t%Xw+?avQMvfQBBmtMfD}7a%eu1r{q{}oc0-ocHOjQp4~CB9Ryq-?g^cI z4Y?aBw#G?f(SK9MEC6Vjp!~`fg$PE3nqlr@hI;QC{aZv|fWCP(;YzF>o>(c1I=3M~ z6;;e!&N0)1{I?~pA<8iKmgo~K2)Ld3bb5W!8(eN(-lK*U^X$xrp`m21b2v^040eV;$F%{c%mm@raSf@s^e|@7+{Nb{TsEOyv~h1%B#}t{JcCH5 z!8S@FBKSdqWvUVIsgK(OI)6e8QAl|LYc^**@s2HmU?!UW|ps{f=n3)A@|UePu{>sO)IqRc%a752DE zTCCT_(mm*x-VDC8rZO{p$Rc;OSQ8g_`%m;!pGBT20YGj#g7s6Un_dgwK;+PYpXMW5 zX<~ZH*`V(k>P<%jF{|QxlO>Uw@v5!k!t+euhp(E}2nyt^i_|qyWY3CxE#A}|l2_r1 zbc{E~o$1$#oDaAO&uNZ8qV+1|% zb&ZAd^3z0r#g}0T&2^B?wO1v^t!^ae*6!R2p8*Uo(-=%#q--lfhj6Ps$hqHiZiUSV zVy4lUoJ-n%4IRS%`=%CFb)uv#6{fyi=l}8298+JnQwy&;QOfrJc!^o9DQSy~sV~*3 z^}oM#;jUZ00O~!pqvizoT3brSKkT#L`&OlS?=oi;FP=3q+?kn>lsrBcIi)~@mQ|lW zT_5-EkWZ%N@I)@)Jyibw>|rSIeXPR0Wm;R0YxPK*<;UJHqwGy99Gg+Ti8}7;?^gs* z^J72vK{!(eCq2x zU^c7HWjuA@OCO$ye4aiLFy7LUc;J0cw0O=cd=<{|ZS zp;LypU`Az}_``O5<@8*w*gK8m7N~?(MzD0!=v3<)B48nTUSX}u|9ffc&H~bncI&sc zll-oJyof$mF@c(>dopaj@Nn~^dkOxb!)YP6wfSNgtznv@Gb8VLZ|Z1wey?_M=55q< zL;mtw{YJpeXC2|;WANAnXyIS)m5(Eq^`n<0StU zXfNZOQ3@$KjYh#BW3;fl@8dR8{K&y#qn*O!SRW;cSopzCvRrPI%Qt_*E==c@s=yPn zKD9Bn#w&olnm2X$}}s+ui7egOKNug6PML2%jWDPT$`GG2oTu=jVM*) z<#?ZVoN;--YF8;mnV-@o=c>oF(oo5XvQU*>N7d8LwAc^2j%ew&X!VXm=gmS}@eyT9 zcs&Gb;|n&*ê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 UIs~Yg5SS44;6}nL_M7zo0P<*T@Bjb+ diff --git a/materials/excel-prostoy-primer.xlsx b/materials/excel-prostoy-primer.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..887f4d714471adc079588c7c9de7eb42603cb7fd GIT binary patch literal 9644 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 nTg9K|e( literal 0 HcmV?d00001 diff --git a/materials/speaker-notes.md b/materials/speaker-notes.md index 4431a6f..1f047e7 100644 --- a/materials/speaker-notes.md +++ b/materials/speaker-notes.md @@ -264,7 +264,7 @@ ### Действие / демо -Первые 40 секунд разобрать дерево из двух условий. Следующие 50 секунд ввести в `Реестр!H2` точную формулу и распространить на H2:H61: +Первые 40 секунд разобрать дерево из двух условий. Затем показать два шага в книге: на листе `Старт` простое условие `=ЕСЛИ(F2="Выполнено";"Закрыто";"В работе")`, на следующем листе `Старт 2` — вложенное условие. Следующие 50 секунд ввести в `Старт 2!H2` точную формулу и распространить на H2:H61: ```excel =ЕСЛИ(F2="Выполнено";"Завершено";ЕСЛИ(E2<Параметры!$B$2;"Просрочено";"В срок")) diff --git a/materials/validation.json b/materials/validation.json index 081002d..636047a 100644 --- a/materials/validation.json +++ b/materials/validation.json @@ -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" } diff --git a/scripts/build_materials.py b/scripts/build_materials.py index 137c25c..2421a80 100644 --- a/scripts/build_materials.py +++ b/scripts/build_materials.py @@ -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() diff --git a/scripts/build_simple_example.py b/scripts/build_simple_example.py new file mode 100644 index 0000000..b3d1684 --- /dev/null +++ b/scripts/build_simple_example.py @@ -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 +""" +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}).') diff --git a/scripts/verify_materials.py b/scripts/verify_materials.py index 57be1d1..a106bda 100644 --- a/scripts/verify_materials.py +++ b/scripts/verify_materials.py @@ -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]