Add two-week changes summary PDF

This commit is contained in:
Cauê Faleiros
2026-08-12 15:07:02 -03:00
parent 743dc55687
commit 0bfd5a7e6f
2 changed files with 297 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (Nexstar Graphs) /CreationDate (D:20260812150606-03'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260812150606-03'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Resumo de Altera\347\365es - Nexstar Graphs) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2000
>>
stream
GauHL9lo&I&A@sBi95>`L**-<V'j<+3hG'5@k0S8!f8UbPQVhR<&FFDI1)2NA]\LrP%a3U-isNphnIop:r%JV"n&rQ(je2SDr(^M#hf-D"_2Jpju;XAIeMm)*LXG=hj_%VR*ki$N2?aFHK-:AU1q]CaI?XuJt;on2R_V"rFpMeZG3X>B/]dT<qq3a]@W4u_q(%9`!aVi#:$r%fjENIDTOj+6P`A;ShMF*c6'215Mk8%5H`YXc2?kci[OG^V^RM-*r!*)ZJg&;+he3OAB):'O/MK_)^jZ*33XMr[O1bZP/rR'Lu3U+-Zd#-o2!du7H6qF9GjA9AVs^c'I@0oaTAqcNJeMTa7Bnl/MKl"F^E7[E@+[31;g@dd]Be)"WlR_dT6!,R<'Z<o0TBJG7OIek>YilWPf(k1L[?HFZe2$(!=s1BlmNsQQ@kcQU\Q_?ijXJRo1+!'ZHbL9$T9-nYO`mkM?c;5tE_K_4:ALN'JX>_36,E!:^)YOV_oo*.&.jP(nAIf.UQO!5Q[?Vtu3&"6RSEbaZ!%okXnG/=:[B=2//ZE(h^X!L_kjml@7fFCaQ16'Aa[cIoI`l_;T,iQ5#Y?mr9oNPDEm'rXsN2/`SCEIZ4s7<#.4Xa=jkr_9=<\tDVq)]-=_s!-$lo<GXog^'b.4cFVkklS=[-'nla1SSML.<<]`\mW?@.p6T8Cbuj\3"A[ahPH>;S!@`&@,AAa@pQU$VpBa!i*P[s$d_L_AM\B?$esZCeND_nJJN)HPE15%E5GodJFX>R6l$H365IMt4uUMo+Y,iJ)"FGgrU$*U]V;rPOgh$,BsZWA&D]$$"hGMLFIAYN-CI\EbH[;%#n6aMVf,]LX;VA`7P84)!ZVdpE&K+S(_k/B*C4X",HSk$>5*D/V!L]O@@BRi+XZA5Z`MZRM_;eok4o?1;RKbPaY<qWGSZns]@A[?.Fc:!A6*8`%LSHUE-F$sOK+JD`r0m($RW\l;sfeGmN0$,b&K?$%4%:$T+9hTaJ:#^b8_tX>I>Trp(o&r%`1+q:;T-:oYLsAm[;)99CeNaW']iR)^Ug>U:j.++`^C4>Ngc_]b6a"2jM-#MGSS^$a`bXNn)+UBktI>65*_<SjqA0fEh1I[j/[.i7m8drs0DgD7;S^(<sM^&+"uDB_[T5Y.;0EhK`ahbk-)dk9gK/6"lI%jg`KW4_AL)V),bU'3#m&M;*t20oEHJ&q+&iSV0R+,,P2k"&`i+Xh'*7OCY<_]1hE[Q4`@4TN-kA)DluoFU^-s(XXf'U8+qYhX7(,\h?T$,^-s%T[ElsMR6H0WM#%VoeTYa*rl'r@V][bp:"=_A-@%0SCn@O7Q/\VC?g$`X<hd8cP@!<,<OL`_^:dfg&9g\I$YNI]1lK=R8[#01@$lbIQgjJ^0Ek6k`S1h6E'!<[f(%r?Q=aF]HF"kah=&-HHk^EoG+9RX8p\ga@nK``&Cla6LqVPh9G'A#hM.^]tL6-r@K"=GOocHr!&uhfG&/D$Y`&KnM%"bNZ+qL_H?V.*9L+Lh?=XIhX)4B]tu9`Ql;ZJ$7)dD<ebkmoto*O8QEa\3,BbYGEI=dBI9[-TV.D)l.(h,br^Gff7][f(/(&*Wji>4j2iel+e48"KBDoPKY(=2Es!bDn]^TSIdCr@^-YV>/FJdl[bm:!H!-`#_BKeC\IH4]&#j0E-'$1^rP'VO8hW*fIY:'#l*+G,gr9k_API\$`bT0DEM?[e8LWFd3'+49/.;\@N.OtNpE4Ei2g&,Ek\WI0ibsHu^DHMh")?#9X>o%;+Kci'\?]<E85&OX](1Z6[B(bc%PTED<9U5?YNTU,3bLO6fk\d5g`=>UO%t8VgqZ9&C=*`D>CdKSFDtM>7([=HB$74MrC!"N,9&+B=#mF&?<[\&3eVC_A'n_qI(udD^ugf\-/_H%b@Q3b'7d>NgOV48Llp+=95"CB"krYkU3]DimG);Y^X-\:QkN5s.u3It!Bub%mJ8Z+mn#ORs&7Gj;NnVmOWVm$ftXoMNH"~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1422
>>
stream
GauI69lo#Z&;KZQ'n(24Lk6nj7rAWuCi+$PF^d6V?cGtGj,u7AN'pP=om(m04q+6o#;9WsPVeinI_,FD-DVga6N<Vm7.1rak$pqj7#N@FUL6KJ:\8(8oAc^[1\3P[_6gh*(%'2IA4bG@2k%ZN4H1bLK.6LBkk5C&?*,[VBiVQaYA!GoMa(a-YLn6sfH^cBoQW9YVpYWjJoEc0B+[m0VUGeu9u;,f,q$_B.1RO^k0:q;b\k[t[-$9&2jMnsQaLf88ki/cjbSb!D14>>b`iLT,a2Wk)09n/32:F4Gh683eW:OaKlI[HJY&&:>5H3*(e_=KiFLr/\tr%Ah`,,dj]Z5M%'?!hb*\1tVb1)\N`V4K=Kp#=Xe@D<Z?&'*=Gted]/H=o>a;2"?u,PJo5J@*m6<Mhd`IA.CU3(G*_l,G)c)6T-8ea(;.Uj/EP0g*^8lG)3+3Wq&W:8Q+EjrH.b@=a[`DVrE9SR2[q*$')dF[XE$_lmjDLY@<h;"t$=YPodUel"8s3`MN#cPWc\jPq\].*p:SDO3>%qA#QK7#SPhaGs(g_jFkIkkq$Z[`C\lej5<:WZ3H39+c^KNfkkh'dcdfrt=*%2Ppf\d,hF^A9sJ"C/K/gXa\g,3n)BK5BoTOo`.CWiLp<GsW=&<\ODRY))8Ugf_;,2T2E8GTFb"I@fkJsb?nrGRTn<5lABb[<bD.OVk(B2^7u_&9(9JObQ\_&HAYS9WZIdb$n3n2dD79\5H6)qOu#7`dN4]gM%@K.*dcop+%+.8%HWVNh#9E;oPf_)<cUqT/sl4#(XKGa@]X.iC_:c>^2')66iM<-c*2a+I$b(JUEAD<8VtBbiASEopZs`O/;p()#J6hIqd@Xq8-;VqR@J]1NCL6dA<gLI9Lt1^?m_8nGHE-1)gFq5Qg6nD/0r@9EnaEV(2')=*Kb9XS5P(;DjtTIEQAUdiK$)CYSGaS`@KkL96/k'`gA>TN/3>]Q8o-O6FYH0Lp!mrK-="Hbr?F\=9GiV#TqVUp@jRIdhk?HU5h[(C_A,.aFm$Al&SXWLj?9#"6]p)J$Q'eNY>Di&@PhJK*)1c\`U+[)W<\%Q2-DL<$)Y.nen36<*_U/EG'iu+M4EBbNRi&XM(Q+R+%S9<YDL>oIA4`Z#$'R,<2cI62A,em3`G`%Ou<4!1j$MNp$*O.ebFAG`m,Re;bJ%"3"YtV-H[POVL4n=e.HE\?H:PN40ItnjLG-fGD*`]XXYN.'_q':*0QEqEeUfu-erbEt';1&g4n"(F8b%(WE]+s:-W^=!J7mPs]Rbnd-2OsbkfF?-q-rm]n1]90d(_VUN=]8qTcZBuar]<RgcA_/jbn/-_Wt:@D0XbP3.TO2VrV5BZTC3$l,K>'9916K-_;/cT)Q526T^EU!RcG?<]sN-/@%3#Vo[9+e]A!406T;a[0K^d&~>endstream
endobj
xref
0 11
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000514 00000 n
0000000708 00000 n
0000000776 00000 n
0000001087 00000 n
0000001152 00000 n
0000003243 00000 n
trailer
<<
/ID
[<b4e24a5fe488c0cf59be880df192eae1><b4e24a5fe488c0cf59be880df192eae1>]
% ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R
/Root 6 0 R
/Size 11
>>
startxref
4757
%%EOF

View File

@@ -0,0 +1,204 @@
from pathlib import Path
from reportlab.lib.colors import HexColor
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import LETTER
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import (
BaseDocTemplate,
Frame,
PageBreak,
PageTemplate,
Paragraph,
Spacer,
)
OUTPUT = Path("output/pdf/resumo-alteracoes-ultimas-duas-semanas.pdf")
BLUE = HexColor("#2E75B6")
NAVY = HexColor("#1F4E79")
BLACK = HexColor("#000000")
GRAY = HexColor("#666666")
def footer(canvas, doc):
canvas.saveState()
canvas.setFont("Helvetica", 9)
canvas.setFillColor(GRAY)
canvas.drawRightString(7.45 * inch, 0.47 * inch, "Nexstar Graphs")
canvas.restoreState()
def make_styles():
styles = getSampleStyleSheet()
return {
"title": ParagraphStyle(
"SummaryTitle",
parent=styles["Normal"],
fontName="Helvetica-Bold",
fontSize=18,
leading=22,
textColor=BLACK,
spaceAfter=13,
alignment=TA_LEFT,
),
"overview": ParagraphStyle(
"Overview",
parent=styles["Normal"],
fontName="Helvetica-Bold",
fontSize=10,
leading=13.5,
textColor=NAVY,
spaceAfter=16,
),
"section": ParagraphStyle(
"Section",
parent=styles["Normal"],
fontName="Helvetica-Bold",
fontSize=14.2,
leading=17.4,
textColor=BLUE,
spaceBefore=1,
spaceAfter=7,
),
"bullet": ParagraphStyle(
"Bullet",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=9.8,
leading=13.1,
textColor=BLACK,
leftIndent=19,
firstLineIndent=-12,
spaceAfter=1.8,
),
}
def bullet(text, styles):
return Paragraph(f"•&nbsp;&nbsp;&nbsp;{text}", styles["bullet"])
def section(title, items, styles):
flowables = [Paragraph(title, styles["section"])]
flowables.extend(bullet(item, styles) for item in items)
flowables.append(Spacer(1, 8))
return flowables
def main():
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
document = BaseDocTemplate(
str(OUTPUT),
pagesize=LETTER,
leftMargin=0.96 * inch,
rightMargin=0.84 * inch,
topMargin=1.08 * inch,
bottomMargin=0.75 * inch,
title="Resumo de Alterações - Nexstar Graphs",
author="Nexstar Graphs",
)
frame = Frame(
document.leftMargin,
document.bottomMargin,
document.width,
document.height,
id="content",
)
document.addPageTemplates([PageTemplate(id="main", frames=[frame], onPage=footer)])
styles = make_styles()
story = [
Paragraph("RESUMO DE ALTERAÇÕES", styles["title"]),
Paragraph(
"Visão geral: nas últimas duas semanas, o Graphs evoluiu a integração com a Olist, "
"o planejamento de corte, a gestão de ordens de produção, suprimentos e a experiência operacional.",
styles["overview"],
),
]
story.extend(
section(
"Integração Olist e dados de produção",
[
"Implementada conexão OAuth nativa com a API V3 da Olist, com armazenamento seguro de token, renovação automática e reconexão pela tela de monitoramento.",
"Criada sincronização de produtos fabricados e composições, com atualização incremental, retomada após reinício e limite de requisições compatível com a operação.",
"Adicionado agendamento diário às 11h, com exibição da próxima execução, progresso, duração, histórico, logs por produto e falhas na tela Olist.",
"Incluídos controles para parar e continuar a sincronização e importação de OPs finalizadas por CSV, com deduplicação em reimportações.",
"As observações das OPs agora fornecem fornecedor, lote, bobinas, quilos de malha e ribana e rendimento; as pendências podem ser exportadas pela tela Olist.",
],
styles,
)
)
story.extend(
section(
"Plano de corte e rendimentos",
[
"Reorganizada a tela de Plano de Corte para priorizar produto, necessidade de corte, demanda, disponível e situação operacional, sem repetir indicadores.",
"Incluídas edição contextual por SKU, visualização de produto e paginação no plano e na prévia de OPs.",
"Centralizada a configuração de corte e simplificado o preenchimento de rendimentos por família.",
"Corrigida a taxonomia: BLCS, BLOS, BLMC, BLPM e BLRM representam camiseta regular, oversize, moletom, pima e regata.",
"Importado histórico de OPs finalizadas para sugerir rendimento por família e, quando disponível, por fornecedor de tecido.",
],
styles,
)
)
story.extend(
section(
"Ordens de produção",
[
"Melhorada a prévia das OPs com paginação e seleção de itens individuais, página atual, todos os elegíveis ou somente itens com dados completos.",
"Reestruturada a página de Ordens de Produção para usar detalhes expansíveis na tabela, evitando painel lateral e duplicidade de controles.",
"Transformado o status em um menu compacto com opções claras para Em aberto, Em andamento, Finalizada e Cancelada.",
"Adicionada ação para devolver uma OP local em aberto ao Plano de Corte, preservando o histórico e devolvendo a necessidade ao planejamento.",
],
styles,
)
)
story.append(PageBreak())
story.extend(
section(
"Suprimentos, compras e estoque",
[
"Redesenhado o hub de Suprimentos para seguir o fluxo Necessidade de Compra, Plano de Corte e Ordens de Produção, com foco nas próximas ações.",
"Simplificado o fluxo de Necessidade de Compra: removida a área de mapeamento separada e incluída edição direta dos materiais por ícone.",
"Materiais sem saldo, com saldo negativo ou com necessidade de serviço passaram a usar filtros e estados visuais consistentes entre abas.",
"Criado centro de exceções para saldos negativos, SKUs sem composição e cadastro para corte, mantendo essas filas informativas e auditáveis.",
"Aplicada regra de estoque disponível: saldo negativo vale zero para compras e corte, sem esconder o saldo original para auditoria.",
],
styles,
)
)
story.extend(
section(
"Cadastros e produtos",
[
"Adicionada paginação com 10 itens por padrão nas listas de Produtos, Categorias e Referências de Consumo.",
"Incluídas ações de edição para Categorias, Produtos e Referências de Consumo, aproximando Cadastros da página de Produtos.",
"Melhorados os detalhes de composição e a navegação entre variações de produto para tornar a consulta mais direta.",
"Criadas ações compactas por ícone para materiais, preservando a leitura das tabelas.",
],
styles,
)
)
story.extend(
section(
"Experiência e navegação",
[
"Padronizado o visual de Suprimentos, Corte e Produção com as tabelas, filtros, badges e paginação já usados em Produtos.",
"Ajustada a cor da família BLCS no Plano de Corte e removidos cartões-resumo redundantes de Vendedores.",
"Aprimorada a navegação para dispositivos menores e refinados os estados de carregamento nas páginas comerciais.",
],
styles,
)
)
document.build(story)
print(OUTPUT)
if __name__ == "__main__":
main()