Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -100,7 +100,7 @@ TAM_HEADER = Pt(9)
|
|
| 100 |
TAM_TINY = Pt(8)
|
| 101 |
|
| 102 |
# Página A4
|
| 103 |
-
MARGEM_SUP = Cm(
|
| 104 |
MARGEM_INF = Cm(3.0)
|
| 105 |
MARGEM_ESQ = Cm(2.5)
|
| 106 |
MARGEM_DIR = Cm(2.0)
|
|
@@ -153,6 +153,35 @@ def fmt(p, text, bold=False, italic=False, size=None, color=None, font=FONTE, ca
|
|
| 153 |
if caps: run.font.all_caps = True
|
| 154 |
return run
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
def spacing(p, before=0, after=0, line=1.15):
|
| 157 |
pf = p.paragraph_format
|
| 158 |
pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line
|
|
@@ -260,17 +289,26 @@ def build_footer(section, meta, pal):
|
|
| 260 |
else:
|
| 261 |
fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA")
|
| 262 |
|
| 263 |
-
# Página
|
| 264 |
pp = footer.add_paragraph()
|
| 265 |
pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
| 266 |
spacing(pp, 2, 0)
|
| 267 |
fmt(pp, "Página ", size=TAM_TINY, color="888888")
|
|
|
|
| 268 |
for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
|
| 269 |
f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>',
|
| 270 |
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
|
| 271 |
pp.add_run()._r.append(parse_xml(x))
|
| 272 |
r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888")
|
| 273 |
pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}",
|
| 275 |
size=TAM_TINY, color="888888")
|
| 276 |
|
|
@@ -308,13 +346,18 @@ def add_bullet(doc, text, bold_prefix=None, indent=IND_ITEM, pal=None):
|
|
| 308 |
else:
|
| 309 |
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
|
| 310 |
|
| 311 |
-
def add_num(doc, n, text, indent=IND_SUBITEM, pal=None):
|
| 312 |
c = pal["primary"] if pal else DEFAULT_PRIMARY
|
| 313 |
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 314 |
spacing(p, 1, 4, 1.15)
|
| 315 |
p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5)
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
def add_def_item(doc, termo, defn, pal):
|
| 320 |
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
@@ -520,27 +563,46 @@ def gerar_diagrama_png(tipo, conteudo, titulo, pal):
|
|
| 520 |
edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'}
|
| 521 |
|
| 522 |
if tipo in ("processo", "fluxograma"):
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
n = len(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
if n <= 3:
|
| 527 |
-
# Simple horizontal
|
| 528 |
dot.attr(rankdir='LR', size='8,2!')
|
| 529 |
-
for i, et in enumerate(
|
| 530 |
-
|
| 531 |
-
|
|
|
|
| 532 |
if i > 0:
|
| 533 |
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
|
| 534 |
else:
|
| 535 |
-
# Zigzag: rows of 3, TB layout
|
| 536 |
dot.attr(rankdir='TB', size='8,6!')
|
| 537 |
per_row = 3
|
| 538 |
-
for i, et in enumerate(
|
| 539 |
-
|
| 540 |
-
|
|
|
|
| 541 |
if i > 0:
|
| 542 |
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
|
| 543 |
-
# Group nodes into ranks (rows of 3)
|
| 544 |
for row_start in range(0, n, per_row):
|
| 545 |
row_nodes = ' '.join(f'n{i}' for i in range(row_start, min(row_start + per_row, n)))
|
| 546 |
dot.body.append(f' {{ rank=same; {row_nodes} }}')
|
|
@@ -631,19 +693,29 @@ def _inserir_diagrama_no_corpo(doc, img_path, legenda="", pal=None):
|
|
| 631 |
|
| 632 |
|
| 633 |
def _render_passo(doc, num, item, pal):
|
| 634 |
-
"""Renderiza um item de procedimento: string
|
| 635 |
-
|
| 636 |
-
|
|
|
|
|
|
|
|
|
|
| 637 |
"""
|
| 638 |
if isinstance(item, str):
|
| 639 |
add_num(doc, num, item, pal=pal)
|
| 640 |
elif isinstance(item, dict):
|
| 641 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
diag = item.get("diagrama", None)
|
| 643 |
if diag is None and "tipo" in item:
|
| 644 |
-
diag = item
|
| 645 |
if diag is None:
|
| 646 |
-
# Dict
|
| 647 |
add_num(doc, num, str(item), pal=pal)
|
| 648 |
return
|
| 649 |
tipo = diag.get("tipo", "processo")
|
|
@@ -653,7 +725,6 @@ def _render_passo(doc, num, item, pal):
|
|
| 653 |
if img_path:
|
| 654 |
_inserir_diagrama_no_corpo(doc, img_path, titulo, pal)
|
| 655 |
else:
|
| 656 |
-
# Fallback: texto descritivo
|
| 657 |
items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo)
|
| 658 |
add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal)
|
| 659 |
|
|
@@ -961,6 +1032,8 @@ def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overri
|
|
| 961 |
# Seções
|
| 962 |
sn = 1
|
| 963 |
add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal)
|
|
|
|
|
|
|
| 964 |
add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]"))
|
| 965 |
|
| 966 |
sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal)
|
|
@@ -998,7 +1071,25 @@ def gerar_pop_docx(json_str, primary_color=None, logo_bytes=None, palette_overri
|
|
| 998 |
if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal)
|
| 999 |
else: add_bullet(doc, str(it), pal=pal)
|
| 1000 |
add_h2(doc, f"{sn}.2", "Plano de Contingência", pal)
|
| 1001 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1002 |
|
| 1003 |
sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal)
|
| 1004 |
add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser "
|
|
|
|
| 100 |
TAM_TINY = Pt(8)
|
| 101 |
|
| 102 |
# Página A4
|
| 103 |
+
MARGEM_SUP = Cm(6.0) # 6cm — espaço uniforme header↔corpo em todas as páginas
|
| 104 |
MARGEM_INF = Cm(3.0)
|
| 105 |
MARGEM_ESQ = Cm(2.5)
|
| 106 |
MARGEM_DIR = Cm(2.0)
|
|
|
|
| 153 |
if caps: run.font.all_caps = True
|
| 154 |
return run
|
| 155 |
|
| 156 |
+
|
| 157 |
+
def _add_highlight_to_run(run, highlight_color="cyan"):
|
| 158 |
+
"""Aplica highlight (fundo colorido) a um run via XML."""
|
| 159 |
+
rPr = run._r.get_or_add_rPr()
|
| 160 |
+
rPr.append(parse_xml(f'<w:highlight {nsdecls("w")} w:val="{highlight_color}"/>'))
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def fmt_with_meta_badges(p, text, size=None, color="1A1A1A", font=FONTE):
|
| 164 |
+
"""Renderiza texto com badges visuais para Metas OMS (Meta 1..Meta 6).
|
| 165 |
+
Metas ficam em bold + highlight cyan para destaque visual em auditoria."""
|
| 166 |
+
import re
|
| 167 |
+
pattern = re.compile(r'(Meta\s+[1-6])', re.IGNORECASE)
|
| 168 |
+
parts = pattern.split(text)
|
| 169 |
+
for part in parts:
|
| 170 |
+
if pattern.match(part):
|
| 171 |
+
# Badge: bold + highlight
|
| 172 |
+
run = p.add_run(part)
|
| 173 |
+
run.bold = True
|
| 174 |
+
run.font.name = font
|
| 175 |
+
if size: run.font.size = size
|
| 176 |
+
run.font.color.rgb = RGBColor.from_string("1A3A6E")
|
| 177 |
+
_add_highlight_to_run(run, "cyan")
|
| 178 |
+
else:
|
| 179 |
+
if part:
|
| 180 |
+
run = p.add_run(part)
|
| 181 |
+
run.font.name = font
|
| 182 |
+
if size: run.font.size = size
|
| 183 |
+
if color: run.font.color.rgb = RGBColor.from_string(color)
|
| 184 |
+
|
| 185 |
def spacing(p, before=0, after=0, line=1.15):
|
| 186 |
pf = p.paragraph_format
|
| 187 |
pf.space_before = Pt(before); pf.space_after = Pt(after); pf.line_spacing = line
|
|
|
|
| 289 |
else:
|
| 290 |
fmt(c.paragraphs[0], "________________", size=TAM_SMALL, color="AAAAAA")
|
| 291 |
|
| 292 |
+
# Página X de Y
|
| 293 |
pp = footer.add_paragraph()
|
| 294 |
pp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
| 295 |
spacing(pp, 2, 0)
|
| 296 |
fmt(pp, "Página ", size=TAM_TINY, color="888888")
|
| 297 |
+
# Campo PAGE (número atual)
|
| 298 |
for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
|
| 299 |
f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>',
|
| 300 |
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
|
| 301 |
pp.add_run()._r.append(parse_xml(x))
|
| 302 |
r = pp.add_run("1"); r.font.size = TAM_TINY; r.font.color.rgb = RGBColor.from_string("888888")
|
| 303 |
pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
|
| 304 |
+
# " de " + NUMPAGES (total)
|
| 305 |
+
fmt(pp, " de ", size=TAM_TINY, color="888888")
|
| 306 |
+
for x in [f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>',
|
| 307 |
+
f'<w:instrText {nsdecls("w")} xml:space="preserve"> NUMPAGES </w:instrText>',
|
| 308 |
+
f'<w:fldChar {nsdecls("w")} w:fldCharType="separate"/>']:
|
| 309 |
+
pp.add_run()._r.append(parse_xml(x))
|
| 310 |
+
r2 = pp.add_run("1"); r2.font.size = TAM_TINY; r2.font.color.rgb = RGBColor.from_string("888888")
|
| 311 |
+
pp.add_run()._r.append(parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>'))
|
| 312 |
fmt(pp, f" | {meta.get('codigo','POP-XXX-001')} | v{meta.get('versao','01')}",
|
| 313 |
size=TAM_TINY, color="888888")
|
| 314 |
|
|
|
|
| 346 |
else:
|
| 347 |
fmt(p, text, size=TAM_CORPO, color="1A1A1A")
|
| 348 |
|
| 349 |
+
def add_num(doc, n, text, indent=IND_SUBITEM, pal=None, critico=False):
|
| 350 |
c = pal["primary"] if pal else DEFAULT_PRIMARY
|
| 351 |
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 352 |
spacing(p, 1, 4, 1.15)
|
| 353 |
p.paragraph_format.left_indent = Cm(indent); p.paragraph_format.first_line_indent = Cm(-0.5)
|
| 354 |
+
if critico:
|
| 355 |
+
fmt(p, f"\u26a0 ", bold=True, size=TAM_CORPO, color="C05000")
|
| 356 |
+
fmt(p, f"{n}. [PASSO CR\u00cdTICO] ", bold=True, size=TAM_CORPO, color="C05000")
|
| 357 |
+
else:
|
| 358 |
+
fmt(p, f"{n}. ", bold=True, size=TAM_CORPO, color=c)
|
| 359 |
+
# Texto com badges de Meta OMS
|
| 360 |
+
fmt_with_meta_badges(p, text, size=TAM_CORPO)
|
| 361 |
|
| 362 |
def add_def_item(doc, termo, defn, pal):
|
| 363 |
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
|
|
| 563 |
edge_style = {'color': f'#{pal["primary"]}', 'penwidth': '1.0', 'arrowsize': '0.6'}
|
| 564 |
|
| 565 |
if tipo in ("processo", "fluxograma"):
|
| 566 |
+
etapas_raw = conteudo if isinstance(conteudo, list) else [conteudo]
|
| 567 |
+
etapas_raw = etapas_raw[:9]
|
| 568 |
+
n = len(etapas_raw)
|
| 569 |
+
|
| 570 |
+
# Mapear formas: elipse→ellipse, losango→diamond, retangulo→box
|
| 571 |
+
forma_map = {'elipse': 'ellipse', 'losango': 'diamond', 'retangulo': 'box',
|
| 572 |
+
'ellipse': 'ellipse', 'diamond': 'diamond', 'box': 'box'}
|
| 573 |
+
|
| 574 |
+
def _parse_etapa(et, idx, total):
|
| 575 |
+
"""Retorna (texto, forma) de um item do fluxograma."""
|
| 576 |
+
if isinstance(et, dict):
|
| 577 |
+
txt = et.get('texto', et.get('titulo', str(et)))[:30]
|
| 578 |
+
frm = forma_map.get(et.get('forma', 'retangulo'), 'box')
|
| 579 |
+
return txt, frm
|
| 580 |
+
txt = str(et)[:30]
|
| 581 |
+
# Auto-forma: primeiro=elipse, último=elipse, demais=box
|
| 582 |
+
if idx == 0 or idx == total - 1:
|
| 583 |
+
return txt, 'ellipse'
|
| 584 |
+
# Detectar decisões por "?" no texto
|
| 585 |
+
if '?' in str(et):
|
| 586 |
+
return txt, 'diamond'
|
| 587 |
+
return txt, 'box'
|
| 588 |
+
|
| 589 |
if n <= 3:
|
|
|
|
| 590 |
dot.attr(rankdir='LR', size='8,2!')
|
| 591 |
+
for i, et in enumerate(etapas_raw):
|
| 592 |
+
txt, frm = _parse_etapa(et, i, n)
|
| 593 |
+
st = {**(node_style if i % 2 == 0 else node_light), 'shape': frm}
|
| 594 |
+
dot.node(f'n{i}', txt, **st)
|
| 595 |
if i > 0:
|
| 596 |
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
|
| 597 |
else:
|
|
|
|
| 598 |
dot.attr(rankdir='TB', size='8,6!')
|
| 599 |
per_row = 3
|
| 600 |
+
for i, et in enumerate(etapas_raw):
|
| 601 |
+
txt, frm = _parse_etapa(et, i, n)
|
| 602 |
+
st = {**(node_style if i % 2 == 0 else node_light), 'shape': frm}
|
| 603 |
+
dot.node(f'n{i}', txt, **st)
|
| 604 |
if i > 0:
|
| 605 |
dot.edge(f'n{i-1}', f'n{i}', **edge_style)
|
|
|
|
| 606 |
for row_start in range(0, n, per_row):
|
| 607 |
row_nodes = ' '.join(f'n{i}' for i in range(row_start, min(row_start + per_row, n)))
|
| 608 |
dot.body.append(f' {{ rank=same; {row_nodes} }}')
|
|
|
|
| 693 |
|
| 694 |
|
| 695 |
def _render_passo(doc, num, item, pal):
|
| 696 |
+
"""Renderiza um item de procedimento: string, dict com texto/prioridade, ou dict com diagrama.
|
| 697 |
+
Formatos aceitos:
|
| 698 |
+
- "texto simples"
|
| 699 |
+
- {"texto": "...", "prioridade": "critica"}
|
| 700 |
+
- {"diagrama": {"tipo":..., "titulo":..., "conteudo":...}}
|
| 701 |
+
- {"tipo":..., "titulo":..., "conteudo":...}
|
| 702 |
"""
|
| 703 |
if isinstance(item, str):
|
| 704 |
add_num(doc, num, item, pal=pal)
|
| 705 |
elif isinstance(item, dict):
|
| 706 |
+
# Formato 1: {"texto": "...", "prioridade": "critica"}
|
| 707 |
+
if "texto" in item and "diagrama" not in item and "tipo" not in item:
|
| 708 |
+
texto = item.get("texto", "")
|
| 709 |
+
critico = item.get("prioridade", "").lower() == "critica"
|
| 710 |
+
add_num(doc, num, texto, pal=pal, critico=critico)
|
| 711 |
+
return
|
| 712 |
+
|
| 713 |
+
# Formato 2: diagrama (com ou sem wrapper "diagrama")
|
| 714 |
diag = item.get("diagrama", None)
|
| 715 |
if diag is None and "tipo" in item:
|
| 716 |
+
diag = item
|
| 717 |
if diag is None:
|
| 718 |
+
# Dict não reconhecido — texto fallback
|
| 719 |
add_num(doc, num, str(item), pal=pal)
|
| 720 |
return
|
| 721 |
tipo = diag.get("tipo", "processo")
|
|
|
|
| 725 |
if img_path:
|
| 726 |
_inserir_diagrama_no_corpo(doc, img_path, titulo, pal)
|
| 727 |
else:
|
|
|
|
| 728 |
items_txt = ', '.join(str(c) for c in conteudo) if isinstance(conteudo, list) else str(conteudo)
|
| 729 |
add_num(doc, num, f"[{titulo}]: {items_txt}", pal=pal)
|
| 730 |
|
|
|
|
| 1032 |
# Seções
|
| 1033 |
sn = 1
|
| 1034 |
add_h1(doc, sn, "OBJETIVO / FINALIDADE", pal)
|
| 1035 |
+
# Remover space_before do primeiro H1 (margem superior já cuida do espaço)
|
| 1036 |
+
doc.paragraphs[0].paragraph_format.space_before = Pt(0)
|
| 1037 |
add_body(doc, secoes.get("objetivo", "[Objetivo não fornecido]"))
|
| 1038 |
|
| 1039 |
sn += 1; add_h1(doc, sn, "CAMPO DE APLICAÇÃO / ÁREA", pal)
|
|
|
|
| 1071 |
if isinstance(it, dict): add_risk(doc, it.get("risco",""), it.get("barreira",""), pal)
|
| 1072 |
else: add_bullet(doc, str(it), pal=pal)
|
| 1073 |
add_h2(doc, f"{sn}.2", "Plano de Contingência", pal)
|
| 1074 |
+
# Caixa de destaque com borda lateral vermelha/laranja
|
| 1075 |
+
cont_text = riscos.get("contingencia","[Contingência não fornecida]")
|
| 1076 |
+
cont_tbl = doc.add_table(rows=1, cols=1)
|
| 1077 |
+
cont_tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 1078 |
+
cc = cont_tbl.cell(0, 0)
|
| 1079 |
+
set_width(cc, LARGURA_DXA - 200)
|
| 1080 |
+
set_margins(cc, 80, 80, 200, 200)
|
| 1081 |
+
# Borda esquerda laranja grossa, demais finas cinza
|
| 1082 |
+
set_borders(cc, ("E87D00","12"), (pal["gray_border"],"1"), (pal["gray_border"],"1"), (pal["gray_border"],"1"))
|
| 1083 |
+
set_shading(cc, "FFF8F0")
|
| 1084 |
+
# Ícone + título
|
| 1085 |
+
pt = cc.paragraphs[0]
|
| 1086 |
+
pt.paragraph_format.space_after = Pt(6)
|
| 1087 |
+
fmt(pt, "\u26a0 ATENÇÃO — Plano de Contingência", bold=True, size=TAM_CORPO, color="C05000")
|
| 1088 |
+
# Texto
|
| 1089 |
+
pb = cc.add_paragraph()
|
| 1090 |
+
pb.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 1091 |
+
pb.paragraph_format.line_spacing = 1.15
|
| 1092 |
+
fmt(pb, cont_text, size=TAM_SMALL, color="1A1A1A")
|
| 1093 |
|
| 1094 |
sn += 1; add_h1(doc, sn, "REGISTROS DA QUALIDADE (EVIDÊNCIAS)", pal)
|
| 1095 |
add_body(doc, "A execução deste procedimento e eventuais intercorrências devem ser "
|