Files

755 lines
25 KiB
Python
Raw Permalink Normal View History

2026-03-15 18:24:18 +00:00
import os
import sys
import tkinter as tk
from tkinter import ttk
from datetime import datetime
from docx import Document
from tkinter import filedialog
from PIL import Image
import tempfile
from docx.shared import Inches, Pt, RGBColor
import io
TEMPLATE_DOCX = "Corpus.docx"
def resource_path(relative_path: str) -> str:
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def image_to_png_stream(image_path: str) -> io.BytesIO:
with Image.open(image_path) as img:
if img.mode not in ("RGB", "RGBA"):
img = img.convert("RGB")
bio = io.BytesIO()
img.save(bio, format="PNG")
bio.seek(0)
return bio
def insert_image_at_placeholder(doc: Document, placeholder: str, image_path: str, width_inches: float = 6.20) -> bool:
if not image_path or not os.path.exists(image_path):
return False
try:
img_stream = image_to_png_stream(image_path)
except Exception as e:
raise ValueError(f"Não consegui abrir/convert a imagem:\n{image_path}\n\nDetalhes:\n{e}")
def replace_in_paragraph(p) -> bool:
full_text = "".join(run.text for run in p.runs)
if placeholder not in full_text:
return False
for r in p.runs:
r.text = ""
run = p.add_run()
run.add_picture(img_stream, width=Inches(width_inches))
return True
for p in doc.paragraphs:
if replace_in_paragraph(p):
return True
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for p in cell.paragraphs:
if replace_in_paragraph(p):
return True
for section in doc.sections:
for p in section.header.paragraphs:
if replace_in_paragraph(p):
return True
for p in section.footer.paragraphs:
if replace_in_paragraph(p):
return True
return False
def insert_images_at_placeholders(doc: Document, placeholders: dict, width_inches: float = 6.20) -> list:
missing = []
for placeholder, image_path in placeholders.items():
if not image_path:
continue
ok = insert_image_at_placeholder(doc, placeholder, image_path, width_inches=width_inches)
if not ok:
missing.append(placeholder)
return missing
def insert_tecnicas_utilizadas(doc: Document, placeholder: str, tecnicas: list) -> bool:
def replace_in_paragraph(p) -> bool:
full_text = "".join(run.text for run in p.runs)
if placeholder not in full_text:
return False
for r in p.runs:
r.text = ""
first_block = True
for t in tecnicas:
if not t.get("selected"):
continue
nome = (t.get("name") or "").strip()
desc = (t.get("desc") or "").strip()
fotos = t.get("photos") or []
if not nome:
continue
if not first_block:
p.add_run().add_break()
p.add_run().add_break()
run_title = p.add_run(nome)
run_title.font.name = "Verdana"
run_title.font.size = Pt(11)
run_title.font.color.rgb = RGBColor(0x4A, 0x66, 0xAC)
p.add_run().add_break()
if desc:
run_desc = p.add_run(desc)
run_desc.font.name = "Verdana"
run_desc.font.size = Pt(10)
run_desc.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
p.add_run().add_break()
if fotos:
for i, image_path in enumerate(fotos):
if not image_path or not os.path.exists(image_path):
continue
try:
img_stream = image_to_png_stream(image_path)
except Exception as e:
raise ValueError(f"Não consegui abrir/convert a imagem:\n{image_path}\n\nDetalhes:\n{e}")
run_img = p.add_run()
run_img.add_picture(img_stream, width=Inches(2.80))
is_last = i == len(fotos) - 1
if not is_last:
if i % 2 == 0:
p.add_run(" ")
else:
p.add_run().add_break()
p.add_run().add_break()
first_block = False
return True
for p in doc.paragraphs:
if replace_in_paragraph(p):
return True
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for p in cell.paragraphs:
if replace_in_paragraph(p):
return True
for section in doc.sections:
for p in section.header.paragraphs:
if replace_in_paragraph(p):
return True
for p in section.footer.paragraphs:
if replace_in_paragraph(p):
return True
return False
def insert_metodos_utilizados(doc: Document, placeholder: str, values: dict) -> bool:
items = []
seen = set()
for group_key in ("inicio_pesquisa", "tecnicas"):
for t in values.get(group_key) or []:
if not t.get("selected"):
continue
name = (t.get("name") or "").strip()
if not name or name in seen:
continue
seen.add(name)
items.append(name)
if not items:
return False
def replace_in_paragraph(p) -> bool:
full_text = "".join(run.text for run in p.runs)
if placeholder not in full_text:
return False
for r in p.runs:
r.text = ""
for i, name in enumerate(items):
run = p.add_run(f"- {name}")
run.font.name = "Verdana"
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
if i < len(items) - 1:
p.add_run().add_break()
return True
for p in doc.paragraphs:
if replace_in_paragraph(p):
return True
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for p in cell.paragraphs:
if replace_in_paragraph(p):
return True
for section in doc.sections:
for p in section.header.paragraphs:
if replace_in_paragraph(p):
return True
for p in section.footer.paragraphs:
if replace_in_paragraph(p):
return True
return False
def insert_multiple_images_at_placeholder(doc: Document, placeholder: str, image_paths: list, width_inches: float = 2.80) -> bool:
if not image_paths:
return False
def replace_in_paragraph(p) -> bool:
full_text = "".join(run.text for run in p.runs)
if placeholder not in full_text:
return False
for r in p.runs:
r.text = ""
for i, image_path in enumerate(image_paths):
if not image_path or not os.path.exists(image_path):
continue
try:
img_stream = image_to_png_stream(image_path)
except Exception as e:
raise ValueError(f"Não consegui abrir/convert a imagem:\n{image_path}\n\nDetalhes:\n{e}")
run = p.add_run()
run.add_picture(img_stream, width=Inches(width_inches))
is_last = i == len(image_paths) - 1
if not is_last:
if i % 2 == 0:
p.add_run(" ")
else:
p.add_run().add_break()
return True
for p in doc.paragraphs:
if replace_in_paragraph(p):
return True
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for p in cell.paragraphs:
if replace_in_paragraph(p):
return True
for section in doc.sections:
for p in section.header.paragraphs:
if replace_in_paragraph(p):
return True
for p in section.footer.paragraphs:
if replace_in_paragraph(p):
return True
return False
def fatal_popup_and_exit(title: str, msg: str, exit_code: int = 1):
win = tk.Tk()
win.title(title)
win.geometry("520x260")
win.resizable(False, False)
frm = ttk.Frame(win, padding=14)
frm.pack(fill="both", expand=True)
ttk.Label(frm, text=title, font=("Segoe UI", 12, "bold")).pack(anchor="w", pady=(0, 8))
txt = tk.Text(frm, height=8, wrap="word")
txt.pack(fill="both", expand=True)
txt.insert("1.0", msg)
txt.config(state="disabled")
def close_and_exit():
win.destroy()
raise SystemExit(exit_code)
ttk.Button(frm, text="OK", command=close_and_exit).pack(anchor="e", pady=(10, 0))
win.protocol("WM_DELETE_WINDOW", close_and_exit)
win.mainloop()
def info_popup(title: str, msg: str):
win = tk.Toplevel()
win.title(title)
win.geometry("520x240")
win.resizable(False, False)
frm = ttk.Frame(win, padding=14)
frm.pack(fill="both", expand=True)
ttk.Label(frm, text=title, font=("Segoe UI", 12, "bold")).pack(anchor="w", pady=(0, 8))
txt = tk.Text(frm, height=7, wrap="word")
txt.pack(fill="both", expand=True)
txt.insert("1.0", msg)
txt.config(state="disabled")
def close():
win.destroy()
ttk.Button(frm, text="OK", command=close).pack(anchor="e", pady=(10, 0))
win.grab_set()
win.focus_force()
def replace_placeholders_in_doc(doc: Document, mapping: dict) -> None:
for p in doc.paragraphs:
text = p.text
new_text = text
for k, v in mapping.items():
new_text = new_text.replace(k, v)
if new_text != text:
p.text = new_text
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for p in cell.paragraphs:
text = p.text
new_text = text
for k, v in mapping.items():
new_text = new_text.replace(k, v)
if new_text != text:
p.text = new_text
def generate_output_filename() -> str:
stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
return f"{stamp}.docx"
def build_presence_text(values: dict) -> str:
parts = []
present_people = []
presencas = values.get("presencas") or []
if not presencas:
if values.get("segurado_presente"):
presencas.append(
{
"tipo": "segurado",
"nome": values.get("segurado_nome"),
"info": values.get("segurado_info"),
}
)
if values.get("lesado_presente"):
presencas.append(
{
"tipo": "lesado",
"nome": values.get("lesado_nome"),
"info": values.get("lesado_info"),
}
)
if values.get("outro_presente"):
presencas.append(
{
"tipo": "outro",
"nome": values.get("outro_nome"),
"info": values.get("outro_info"),
}
)
def join_people(items: list) -> str:
if not items:
return ""
if len(items) == 1:
return items[0]
return ", ".join(items[:-1]) + " e " + items[-1]
for item in presencas:
tipo = (item.get("tipo") or "").strip().lower()
nome = (item.get("nome") or "").strip()
info = (item.get("info") or "").strip()
if not nome:
continue
if tipo == "segurado":
present_people.append(f"o segurado, {nome}")
if info:
parts.append(f"O segurado, {nome} informou que {info}.")
elif tipo == "lesado":
present_people.append(f"o lesado, {nome}")
if info:
parts.append(f"O lesado, {nome} informou que {info}.")
else:
present_people.append(nome)
if info:
parts.append(f"{nome} informou que {info}.")
header = ""
if present_people:
joined = join_people(present_people)
if len(present_people) == 1:
header = f"{joined} esteve presente aquando a pesquisa."
else:
header = f"{joined} estiveram presentes aquando a pesquisa."
if header and parts:
return header + "\n\n" + "\n".join(parts)
if header:
return header
if parts:
return "\n".join(parts)
return ""
def run_generation(values: dict, root: tk.Tk):
mapping = {
"{{nproc}}": (values.get("nproc") or "").strip(),
"{{segurado}}": (values.get("segurado") or "").strip(),
"{{comp}}": (values.get("comp") or "").strip(),
"{{terceiro}}": (values.get("terceiro") or "").strip(),
"{{data}}": (values.get("data") or "").strip(),
"{{local da visita}}": (values.get("local") or "").strip(),
"{{descriçãoimovel}}": (values.get("descriçãoimovel") or "").strip(),
"{{anoconstr}}": (values.get("anoconstr") or "").strip(),
"{{presencas}}": build_presence_text(values),
"{{descricao}}": (values.get("descricao") or "").strip(),
"{{area}}": (values.get("area") or "").strip(),
}
template_path = resource_path(TEMPLATE_DOCX)
if not os.path.exists(template_path):
try:
root.destroy()
except Exception:
pass
fatal_popup_and_exit(
"Erro: template não encontrado",
f"Não encontrei o template '{TEMPLATE_DOCX}'.\n\n"
f"Coloca o ficheiro '{TEMPLATE_DOCX}' na mesma pasta do executável e volta a executar."
)
return
output_docx = generate_output_filename()
try:
doc = Document(template_path)
replace_placeholders_in_doc(doc, mapping)
foto1_path = (values.get("foto1_path") or "").strip()
fotos2_paths = values.get("fotos2_paths") or []
placeholders = {
"{{foto1}}": foto1_path,
}
missing = insert_images_at_placeholders(doc, placeholders, width_inches=6.20)
if missing:
info_popup(
"Aviso",
"Não encontrei os placeholders no template (corpo/cabeçalho/rodapé):\n"
+ ", ".join(missing)
)
if fotos2_paths:
ok_multi = insert_multiple_images_at_placeholder(doc, "{{foto2}}", fotos2_paths, width_inches=2.80)
if not ok_multi:
info_popup(
"Aviso",
"Não encontrei o placeholder {{foto2}} no template (corpo/cabeçalho/rodapé)."
)
tecnicas = values.get("tecnicas") or []
if tecnicas:
ok_tecnicas = insert_tecnicas_utilizadas(doc, "{{tecnicasutilizadas}}", tecnicas)
if not ok_tecnicas:
info_popup(
"Aviso",
"Não encontrei o placeholder {{tecnicasutilizadas}} no template (corpo/cabeçalho/rodapé)."
)
inicio_pesquisa = values.get("inicio_pesquisa") or []
if inicio_pesquisa:
ok_inicio = insert_tecnicas_utilizadas(doc, "{{iniciopesquisa}}", inicio_pesquisa)
if not ok_inicio:
info_popup(
"Aviso",
"Não encontrei o placeholder {{iniciopesquisa}} no template (corpo/cabeçalho/rodapé)."
)
if values.get("inicio_pesquisa") or values.get("tecnicas"):
ok_metodos = insert_metodos_utilizados(doc, "{{metodosutilizados}}", values)
if not ok_metodos:
info_popup(
"Aviso",
"Não encontrei o placeholder {{metodosutilizados}} no template (corpo/cabeçalho/rodapé)."
)
doc.save(output_docx)
info_popup("Sucesso", f"Documento gerado com sucesso:\n\n{output_docx}")
except Exception as e:
try:
root.destroy()
except Exception:
pass
fatal_popup_and_exit(
"Erro ao gerar documento",
f"Falhou ao gerar o documento.\n\nDetalhes:\n{e}"
)
def choose_image(foto_var):
path = filedialog.askopenfilename(
title="Selecionar fotografia",
filetypes=[
("Imagens", "*.png *.jpg *.jpeg *.bmp"),
("Todos os ficheiros", "*.*"),
],
)
if path:
foto_var.set(path)
def choose_images(fotos_var, label_var):
paths = filedialog.askopenfilenames(
title="Selecionar fotografias",
filetypes=[
("Imagens", "*.png *.jpg *.jpeg *.bmp"),
("Todos os ficheiros", "*.*"),
],
)
if paths:
fotos_var.set(list(paths))
label_var.set(f"{len(paths)} imagem(ns)")
def main():
root = tk.Tk()
root.title("Preencher Corpus (Word)")
root.geometry("1120x640")
root.resizable(False, False)
frm = ttk.Frame(root, padding=16)
frm.pack(fill="both", expand=True)
fields = [("Nº do Processo:", "nproc"),("Segurado:", "segurado"),
("Companhia:", "comp"),
("Terceiro:", "terceiro"),
("Data da visita:", "data"),
("Local da visita:", "local"),
("Descrição do imóvel:", "descriçãoimovel"),
("Ano de Construção:", "anoconstr"),
("Descrição da pesquisa:", "descricao"),
("Área do do espaço:", "area")
]
entries = {}
foto_var = tk.StringVar(value="")
fotos2_var = tk.Variable(value=[])
fotos2_label = tk.StringVar(value="")
tecnicas_data = []
inicio_data = []
for i, (label, key) in enumerate(fields):
ttk.Label(frm, text=label).grid(row=i, column=0, sticky="w", pady=6)
ent = ttk.Entry(frm, width=80)
ent.grid(row=i, column=1, sticky="w", pady=6)
entries[key] = ent
if key == "descriçãoimovel":
ttk.Button(
frm,
text="Escolher imagem…",
command=lambda: choose_image(foto_var)
).grid(row=i, column=2, sticky="w", padx=8)
ttk.Label(
frm,
textvariable=foto_var,
width=28
).grid(row=i, column=4, sticky="w")
if key == "descricao":
ttk.Button(
frm,
text="Escolher imagens…",
command=lambda: choose_images(fotos2_var, fotos2_label)
).grid(row=i, column=2, sticky="w", padx=8)
ttk.Label(
frm,
textvariable=fotos2_label,
width=14
).grid(row=i, column=4, sticky="w")
def build_tecnicas_window(title: str, data_list: list):
win = tk.Toplevel(root)
win.title(title)
win.geometry("980x520")
win.resizable(False, False)
container = ttk.Frame(win, padding=16)
container.pack(fill="both", expand=True)
if not data_list:
tecnicas_list = [
"Controlo Visual",
"Medição de humidade",
"Câmara térmica",
"Câmara endoscópica",
"Obturação",
"Teste de pressão",
"Teste com corantes",
]
for nome in tecnicas_list:
data_list.append({
"name": nome,
"var": tk.BooleanVar(value=False),
"desc": tk.StringVar(value=""),
"photos": tk.Variable(value=[]),
"label": tk.StringVar(value=""),
})
def set_row_state(row_widgets, enabled: bool):
state = "normal" if enabled else "disabled"
for w in row_widgets:
w.configure(state=state)
for idx, item in enumerate(data_list):
row = ttk.Frame(container)
row.pack(fill="x", pady=6)
chk = ttk.Checkbutton(
row,
text=item["name"],
variable=item["var"],
command=lambda it=item: set_row_state(it["widgets"], it["var"].get())
)
chk.grid(row=0, column=0, sticky="w")
ttk.Label(row, text="Descrição:").grid(row=0, column=1, sticky="w", padx=(12, 4))
entry = ttk.Entry(row, textvariable=item["desc"], width=50)
entry.grid(row=0, column=2, sticky="w")
btn = ttk.Button(
row,
text="Escolher fotos…",
command=lambda it=item: choose_images(it["photos"], it["label"])
)
btn.grid(row=0, column=3, sticky="w", padx=10)
lbl = ttk.Label(row, textvariable=item["label"], width=18)
lbl.grid(row=0, column=4, sticky="w")
item["widgets"] = [entry, btn]
set_row_state(item["widgets"], item["var"].get())
ttk.Button(container, text="Fechar", command=win.destroy).pack(anchor="e", pady=(12, 0))
win.grab_set()
win.focus_force()
ttk.Button(
frm,
text="Técnicas utilizadas…",
command=lambda: build_tecnicas_window("Técnicas utilizadas", tecnicas_data)
).grid(row=len(fields), column=0, sticky="w", pady=(14, 0))
ttk.Button(
frm,
text="Início da pesquisa…",
command=lambda: build_tecnicas_window("Início da pesquisa", inicio_data)
).grid(row=len(fields), column=1, sticky="w", pady=(14, 0), padx=(12, 0))
presence_frame = ttk.LabelFrame(frm, text="Presencas na pesquisa", padding=12)
presence_frame.grid(row=len(fields) + 1, column=0, columnspan=4, sticky="w", pady=(12, 6))
presenca_rows = {
"segurado": [],
"lesado": [],
"outro": [],
}
type_frames = {}
type_labels = {
"segurado": "segurado",
"lesado": "lesado",
"outro": "outro",
}
def remove_presence_row(tipo: str, row_data: dict):
row_data["frame"].destroy()
presenca_rows[tipo] = [r for r in presenca_rows[tipo] if r is not row_data]
def add_presence_row(tipo: str):
container = type_frames[tipo]
row = ttk.Frame(container)
row.pack(fill="x", pady=3)
nome_var = tk.StringVar(value="")
info_var = tk.StringVar(value="")
ttk.Label(row, text=f"Nome do {type_labels[tipo]}:").grid(row=0, column=0, sticky="w")
ttk.Entry(row, textvariable=nome_var, width=30).grid(row=0, column=1, sticky="w", padx=6)
ttk.Label(row, text="Declaracoes:").grid(row=0, column=2, sticky="w", padx=(12, 0))
ttk.Entry(row, textvariable=info_var, width=42).grid(row=0, column=3, sticky="w", padx=6)
row_data = {
"frame": row,
"nome": nome_var,
"info": info_var,
}
ttk.Button(
row,
text="Remover",
command=lambda t=tipo, d=row_data: remove_presence_row(t, d)
).grid(row=0, column=4, sticky="w", padx=(8, 0))
presenca_rows[tipo].append(row_data)
def build_presence_section(parent, row_index: int, tipo: str, titulo: str):
sec = ttk.Frame(parent)
sec.grid(row=row_index, column=0, sticky="w", pady=4)
ttk.Label(sec, text=titulo).grid(row=0, column=0, sticky="w")
ttk.Button(
sec,
text=f"Adicionar {type_labels[tipo]}",
command=lambda t=tipo: add_presence_row(t)
).grid(row=0, column=1, sticky="w", padx=(10, 0))
container = ttk.Frame(sec)
container.grid(row=1, column=0, columnspan=2, sticky="w", pady=(6, 0))
type_frames[tipo] = container
build_presence_section(presence_frame, 0, "segurado", "Segurados")
build_presence_section(presence_frame, 1, "lesado", "Lesados")
build_presence_section(presence_frame, 2, "outro", "Outros")
add_presence_row("segurado")
def on_generate():
values = {k: e.get() for k, e in entries.items()}
values["foto1_path"] = foto_var.get()
values["fotos2_paths"] = list(fotos2_var.get()) if fotos2_var.get() else []
values["tecnicas"] = [
{
"name": t["name"],
"selected": t["var"].get(),
"desc": t["desc"].get(),
"photos": list(t["photos"].get()) if t["photos"].get() else [],
}
for t in tecnicas_data
]
values["inicio_pesquisa"] = [
{
"name": t["name"],
"selected": t["var"].get(),
"desc": t["desc"].get(),
"photos": list(t["photos"].get()) if t["photos"].get() else [],
}
for t in inicio_data
]
values["presencas"] = []
for tipo in ("segurado", "lesado", "outro"):
for row in presenca_rows[tipo]:
values["presencas"].append(
{
"tipo": tipo,
"nome": row["nome"].get(),
"info": row["info"].get(),
}
)
run_generation(values, root)
btns = ttk.Frame(frm)
btns.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="w", pady=(18, 0))
ttk.Button(btns, text="Gerar Word", command=on_generate).pack(side="left")
ttk.Button(btns, text="Sair", command=root.destroy).pack(side="left", padx=10)
root.mainloop()
if __name__ == "__main__":
main()