456 lines
17 KiB
Python
456 lines
17 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
Gerador do "Plano de Ataque" (PPTX) por quinzena.
|
||
|
|
|
||
|
|
Uso: coloque este executavel na MESMA PASTA de:
|
||
|
|
- um arquivo .json de cores (ex.: cores.json)
|
||
|
|
- um arquivo .pptx modelo (com o slide 2 = layout de uma quinzena)
|
||
|
|
- um arquivo .xlsx (planilha de planejamento)
|
||
|
|
|
||
|
|
e execute. O programa detecta esses 3 arquivos automaticamente na pasta
|
||
|
|
e gera "<nome-do-modelo>_ATUALIZADO.pptx" na mesma pasta.
|
||
|
|
|
||
|
|
Nao precisa de Python nem de nenhuma instalacao no computador que for
|
||
|
|
rodar o executavel -- tudo ja vai embutido nele.
|
||
|
|
"""
|
||
|
|
import glob
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
import copy
|
||
|
|
import traceback
|
||
|
|
from datetime import date, timedelta
|
||
|
|
|
||
|
|
|
||
|
|
def base_dir():
|
||
|
|
"""Pasta onde o executavel (ou este script) esta localizado."""
|
||
|
|
if getattr(sys, 'frozen', False):
|
||
|
|
return os.path.dirname(os.path.abspath(sys.executable))
|
||
|
|
return os.path.dirname(os.path.abspath(__file__))
|
||
|
|
|
||
|
|
|
||
|
|
def pause_on_exit(frozen_only=True):
|
||
|
|
if not frozen_only or getattr(sys, 'frozen', False):
|
||
|
|
try:
|
||
|
|
input('\nPressione ENTER para sair...')
|
||
|
|
except EOFError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def find_one(patterns, folder, label, exclude_suffix=None):
|
||
|
|
candidates = []
|
||
|
|
for pat in patterns:
|
||
|
|
for f in glob.glob(os.path.join(folder, pat)):
|
||
|
|
name = os.path.basename(f)
|
||
|
|
if name.startswith('~$'):
|
||
|
|
continue
|
||
|
|
if exclude_suffix and name.endswith(exclude_suffix):
|
||
|
|
continue
|
||
|
|
candidates.append(f)
|
||
|
|
candidates = sorted(set(candidates))
|
||
|
|
if not candidates:
|
||
|
|
raise SystemExit(
|
||
|
|
f'ERRO: nenhum arquivo {label} encontrado na pasta:\n {folder}\n'
|
||
|
|
f'Coloque um arquivo {label} nessa pasta e execute novamente.')
|
||
|
|
if len(candidates) > 1:
|
||
|
|
lista = '\n '.join(candidates)
|
||
|
|
raise SystemExit(
|
||
|
|
f'ERRO: mais de um arquivo {label} encontrado na pasta:\n {lista}\n'
|
||
|
|
f'Deixe apenas UM arquivo {label} na pasta e execute novamente.')
|
||
|
|
return candidates[0]
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
folder = base_dir()
|
||
|
|
print(f'Pasta de trabalho: {folder}')
|
||
|
|
|
||
|
|
json_path = find_one(['cores.json', '*.json'], folder, '.json (cores)')
|
||
|
|
xlsx_path = find_one(['*.xlsx'], folder, '.xlsx (planilha)')
|
||
|
|
pptx_path = find_one(['*.pptx'], folder, '.pptx (modelo)', exclude_suffix='_ATUALIZADO.pptx')
|
||
|
|
|
||
|
|
print(f' cores : {os.path.basename(json_path)}')
|
||
|
|
print(f' xlsx : {os.path.basename(xlsx_path)}')
|
||
|
|
print(f' modelo : {os.path.basename(pptx_path)}')
|
||
|
|
|
||
|
|
with open(json_path, 'r', encoding='utf-8') as f:
|
||
|
|
cfg = json.load(f)
|
||
|
|
|
||
|
|
import openpyxl
|
||
|
|
from pptx import Presentation
|
||
|
|
from pptx.oxml.ns import qn
|
||
|
|
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
|
||
|
|
from lxml import etree
|
||
|
|
|
||
|
|
out_name = os.path.splitext(os.path.basename(pptx_path))[0] + '_ATUALIZADO.pptx'
|
||
|
|
out_path = os.path.join(folder, out_name)
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 1) Coordenadas de pixel dos eixos, medidas nas imagens do modelo
|
||
|
|
# original (colunas 1-26 na "Imagem 6", colunas 26-48 na "Imagem 3").
|
||
|
|
# Isso e' fixo porque descreve a planta em si, nao a cor.
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
im1_cols_list = [102.1, 152.3, 203.3, 253.2, 303.9, 353.7, 404.8, 455.0, 505.3, 556.4,
|
||
|
|
606.4, 657.1, 707.2, 757.9, 808.1, 858.6, 909.4, 959.4, 1010.6, 1059.7,
|
||
|
|
1110.7, 1160.8, 1211.5, 1262.3, 1312.4, 1363.6]
|
||
|
|
im2_cols_list = [83.0, 132.8, 183.7, 234.0, 284.5, 335.4, 385.2, 435.9, 486.0, 536.7,
|
||
|
|
587.0, 637.4, 688.4, 738.3, 789.0, 839.0, 889.8, 939.8, 990.6, 1041.1,
|
||
|
|
1091.4, 1142.1, 1192.1]
|
||
|
|
|
||
|
|
IM1_COLPX = {i + 1: v for i, v in enumerate(im1_cols_list)}
|
||
|
|
IM2_COLPX = {26 + i: v for i, v in enumerate(im2_cols_list)}
|
||
|
|
IM1_ROWPX = {'D': 88.8, 'C': 178.3, 'B1': 237.7, 'B': 305.3, 'A1': 359.2, 'A': 434.9}
|
||
|
|
IM2_ROWPX = {'D': 72.5, 'C': 161.5, 'B1': 221.5, 'B': 288.5, 'A1': 342.5, 'A': 418.5}
|
||
|
|
IM1_PXSIZE = (1442, 500)
|
||
|
|
IM2_PXSIZE = (1267, 485)
|
||
|
|
|
||
|
|
def which_image(c1, c2):
|
||
|
|
return 'im1' if max(c1, c2) <= 26 else 'im2'
|
||
|
|
|
||
|
|
def quad_pixel_rect(c1, c2, r1, r2):
|
||
|
|
img = which_image(c1, c2)
|
||
|
|
colpx = IM1_COLPX if img == 'im1' else IM2_COLPX
|
||
|
|
rowpx = IM1_ROWPX if img == 'im1' else IM2_ROWPX
|
||
|
|
x0, x1 = sorted([colpx[c1], colpx[c2]])
|
||
|
|
y0, y1 = sorted([rowpx[r1], rowpx[r2]])
|
||
|
|
return img, x0, y0, x1, y1
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 2) Transform pixel -> EMU, usando a posicao/tamanho reais das
|
||
|
|
# imagens 'Imagem 6' (im1) e 'Imagem 3' (im2) no slide-modelo.
|
||
|
|
# 'Imagem 3' tem um corte (srcRect) aplicado no PowerPoint que
|
||
|
|
# precisa ser descontado, senao o desenho fica deslocado.
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
IM1_OFF = (16846, 989588)
|
||
|
|
IM1_EXT = (8200608, 2841514)
|
||
|
|
IM2_OFF = (4073548, 3846576)
|
||
|
|
IM2_EXT = (8118452, 3006806)
|
||
|
|
|
||
|
|
IM1_SX = IM1_EXT[0] / IM1_PXSIZE[0]
|
||
|
|
IM1_SY = IM1_EXT[1] / IM1_PXSIZE[1]
|
||
|
|
|
||
|
|
IM2_CROP_T = 3246 / 100000
|
||
|
|
IM2_CROP_B = 1 / 100000
|
||
|
|
IM2_VIS_Y0 = IM2_CROP_T * IM2_PXSIZE[1]
|
||
|
|
IM2_VIS_Y1 = (1 - IM2_CROP_B) * IM2_PXSIZE[1]
|
||
|
|
IM2_VIS_X0 = 0.0
|
||
|
|
IM2_VIS_X1 = IM2_PXSIZE[0]
|
||
|
|
|
||
|
|
IM2_SX = IM2_EXT[0] / (IM2_VIS_X1 - IM2_VIS_X0)
|
||
|
|
IM2_SY = IM2_EXT[1] / (IM2_VIS_Y1 - IM2_VIS_Y0)
|
||
|
|
|
||
|
|
def to_emu(img, px, py):
|
||
|
|
if img == 'im1':
|
||
|
|
return IM1_OFF[0] + px * IM1_SX, IM1_OFF[1] + py * IM1_SY
|
||
|
|
return (IM2_OFF[0] + (px - IM2_VIS_X0) * IM2_SX,
|
||
|
|
IM2_OFF[1] + (py - IM2_VIS_Y0) * IM2_SY)
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 3) Ler planilha de planejamento
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
pat = re.compile(r'^(\d+)-(\d+)/([A-Z0-9]+)-([A-Z0-9]+)$')
|
||
|
|
|
||
|
|
def to_date(v):
|
||
|
|
if v is None:
|
||
|
|
return None
|
||
|
|
return v.date() if hasattr(v, 'date') else v
|
||
|
|
|
||
|
|
def parse_quads(path):
|
||
|
|
wb = openpyxl.load_workbook(path, data_only=True)
|
||
|
|
ws = wb['Planejamento']
|
||
|
|
quads = []
|
||
|
|
for row in ws.iter_rows(min_row=4, max_row=ws.max_row, values_only=True):
|
||
|
|
if row[1] is None:
|
||
|
|
continue
|
||
|
|
m = pat.match(str(row[1]).strip())
|
||
|
|
if not m:
|
||
|
|
continue
|
||
|
|
c1, c2, r1, r2 = int(m.group(1)), int(m.group(2)), m.group(3), m.group(4)
|
||
|
|
quads.append(dict(
|
||
|
|
c1=c1, c2=c2, r1=r1, r2=r2,
|
||
|
|
geo=(to_date(row[2]), to_date(row[3])),
|
||
|
|
inf=(to_date(row[4]), to_date(row[5])),
|
||
|
|
sup=(to_date(row[6]), to_date(row[7])),
|
||
|
|
))
|
||
|
|
return quads
|
||
|
|
|
||
|
|
QUADS = parse_quads(xlsx_path)
|
||
|
|
if not QUADS:
|
||
|
|
raise SystemExit('ERRO: nenhum quadrante valido foi lido da planilha (aba "Planejamento").')
|
||
|
|
|
||
|
|
def status(inicio, termino, p_end):
|
||
|
|
if inicio is None or inicio > p_end:
|
||
|
|
return None
|
||
|
|
if termino is not None and termino <= p_end:
|
||
|
|
return 'concluido'
|
||
|
|
return 'andamento'
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 4) Quinzenas
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
qz_cfg = cfg.get('quinzena', {})
|
||
|
|
PERIOD_DAYS = int(qz_cfg.get('duracao_dias', 14))
|
||
|
|
START = date.fromisoformat(qz_cfg.get('data_inicio', '2026-12-14'))
|
||
|
|
FIRST_NUM = int(qz_cfg.get('numero_inicial', 2))
|
||
|
|
|
||
|
|
all_dates = []
|
||
|
|
for q in QUADS:
|
||
|
|
for k in ('geo', 'inf', 'sup'):
|
||
|
|
i, f = q[k]
|
||
|
|
if i:
|
||
|
|
all_dates.append(i)
|
||
|
|
if f:
|
||
|
|
all_dates.append(f)
|
||
|
|
if not all_dates:
|
||
|
|
raise SystemExit('ERRO: a planilha nao tem nenhuma data preenchida.')
|
||
|
|
DATA_FIM = max(all_dates)
|
||
|
|
|
||
|
|
PERIODS = []
|
||
|
|
cur = START
|
||
|
|
num = FIRST_NUM
|
||
|
|
while cur <= DATA_FIM:
|
||
|
|
p_end = cur + timedelta(days=PERIOD_DAYS - 1)
|
||
|
|
PERIODS.append((num, cur, p_end))
|
||
|
|
cur = p_end + timedelta(days=1)
|
||
|
|
num += 1
|
||
|
|
|
||
|
|
def fmt(d):
|
||
|
|
return f'{d.day:02d}/{d.month:02d}/{d.year}'
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 5) Cores (lidas do JSON)
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
JSON_KEY = {'geo': 'geotecnia', 'sup': 'superestrutura', 'inf': 'infraestrutura'}
|
||
|
|
COLOR_HEX, BORDER_HEX, ALPHA_DRAWING_ST = {}, {}, {}
|
||
|
|
for short, jkey in JSON_KEY.items():
|
||
|
|
try:
|
||
|
|
section = cfg[jkey]
|
||
|
|
except KeyError:
|
||
|
|
raise SystemExit(f'ERRO: o JSON de cores nao tem a secao "{jkey}".')
|
||
|
|
for st in ('andamento', 'concluido'):
|
||
|
|
try:
|
||
|
|
entry = section[st]
|
||
|
|
COLOR_HEX[(short, st)] = entry['preenchimento'].lstrip('#').upper()
|
||
|
|
BORDER_HEX[(short, st)] = entry['borda'].lstrip('#').upper()
|
||
|
|
ALPHA_DRAWING_ST[(short, st)] = int(round(float(entry.get('alpha_preenchimento', 100)) * 1000))
|
||
|
|
except KeyError as e:
|
||
|
|
raise SystemExit(f'ERRO: falta o campo {e} em "{jkey}.{st}" no JSON de cores.')
|
||
|
|
|
||
|
|
ALPHA_LEGEND = 100000
|
||
|
|
MARKER_FRAC = float(cfg.get('marcador_fracao', 0.42))
|
||
|
|
|
||
|
|
def _solid_fill_xml(hexcol, alpha):
|
||
|
|
return (f'<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
||
|
|
f'<a:srgbClr val="{hexcol}"><a:alpha val="{alpha}"/></a:srgbClr></a:solidFill>')
|
||
|
|
|
||
|
|
def _fill_xml(key):
|
||
|
|
return _solid_fill_xml(COLOR_HEX[key], ALPHA_DRAWING_ST[key])
|
||
|
|
|
||
|
|
def _line_xml(key):
|
||
|
|
return (f'<a:ln xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" w="9525">'
|
||
|
|
f'<a:solidFill><a:srgbClr val="{BORDER_HEX[key]}"/></a:solidFill></a:ln>')
|
||
|
|
|
||
|
|
FILL_XML = {k: _fill_xml(k) for k in COLOR_HEX}
|
||
|
|
LINE_XML = {k: _line_xml(k) for k in COLOR_HEX}
|
||
|
|
|
||
|
|
ROUND_ADJ_QUAD = 12000
|
||
|
|
ROUND_ADJ_MARK = 30000
|
||
|
|
STATUS_RANK = {'andamento': 1, 'concluido': 2}
|
||
|
|
|
||
|
|
_shape_id_counter = [9000]
|
||
|
|
|
||
|
|
def next_id():
|
||
|
|
_shape_id_counter[0] += 1
|
||
|
|
return _shape_id_counter[0]
|
||
|
|
|
||
|
|
def make_roundrect_xml(name, x_emu, y_emu, w_emu, h_emu, fill_xml, adj, line_xml=None):
|
||
|
|
sid = next_id()
|
||
|
|
line = line_xml if line_xml else '<a:ln><a:noFill/></a:ln>'
|
||
|
|
return f'''<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||
|
|
<p:nvSpPr>
|
||
|
|
<p:cNvPr id="{sid}" name="{name} {sid}"/>
|
||
|
|
<p:cNvSpPr/>
|
||
|
|
<p:nvPr/>
|
||
|
|
</p:nvSpPr>
|
||
|
|
<p:spPr>
|
||
|
|
<a:xfrm><a:off x="{int(x_emu)}" y="{int(y_emu)}"/><a:ext cx="{int(w_emu)}" cy="{int(h_emu)}"/></a:xfrm>
|
||
|
|
<a:prstGeom prst="roundRect"><a:avLst><a:gd name="adj" fmla="val {adj}"/></a:avLst></a:prstGeom>
|
||
|
|
{fill_xml}
|
||
|
|
{line}
|
||
|
|
</p:spPr>
|
||
|
|
<p:txBody>
|
||
|
|
<a:bodyPr/><a:lstStyle/>
|
||
|
|
<a:p><a:endParaRPr lang="pt-BR"/></a:p>
|
||
|
|
</p:txBody>
|
||
|
|
</p:sp>'''
|
||
|
|
|
||
|
|
IM1_COL_STEP = (max(im1_cols_list) - min(im1_cols_list)) / (len(im1_cols_list) - 1)
|
||
|
|
IM2_COL_STEP = (max(im2_cols_list) - min(im2_cols_list)) / (len(im2_cols_list) - 1)
|
||
|
|
COL_STEP_PX = {'im1': IM1_COL_STEP, 'im2': IM2_COL_STEP}
|
||
|
|
|
||
|
|
def add_quadrant_overlays(slide, p_end):
|
||
|
|
spTree = slide.shapes._spTree
|
||
|
|
|
||
|
|
for q in QUADS:
|
||
|
|
st = status(*q['geo'], p_end)
|
||
|
|
if st is None:
|
||
|
|
continue
|
||
|
|
img, x0, y0, x1, y1 = quad_pixel_rect(q['c1'], q['c2'], q['r1'], q['r2'])
|
||
|
|
ex0, ey0 = to_emu(img, x0, y0)
|
||
|
|
ex1, ey1 = to_emu(img, x1, y1)
|
||
|
|
xml = make_roundrect_xml('Estaqueamento', ex0, ey0, ex1 - ex0, ey1 - ey0,
|
||
|
|
FILL_XML[('geo', st)], ROUND_ADJ_QUAD, LINE_XML[('geo', st)])
|
||
|
|
spTree.append(etree.fromstring(xml))
|
||
|
|
|
||
|
|
for key in ('inf', 'sup'):
|
||
|
|
best = {}
|
||
|
|
for q in QUADS:
|
||
|
|
st = status(*q[key], p_end)
|
||
|
|
if st is None:
|
||
|
|
continue
|
||
|
|
img = which_image(q['c1'], q['c2'])
|
||
|
|
for col in (q['c1'], q['c2']):
|
||
|
|
for row in (q['r1'], q['r2']):
|
||
|
|
pt = (img, col, row)
|
||
|
|
cur_st = best.get(pt)
|
||
|
|
if cur_st is None or STATUS_RANK[st] > STATUS_RANK[cur_st]:
|
||
|
|
best[pt] = st
|
||
|
|
|
||
|
|
name = 'Superestrutura' if key == 'sup' else 'Infraestrutura'
|
||
|
|
for (img, col, row), st in best.items():
|
||
|
|
colpx = IM1_COLPX if img == 'im1' else IM2_COLPX
|
||
|
|
rowpx = IM1_ROWPX if img == 'im1' else IM2_ROWPX
|
||
|
|
px, py = colpx[col], rowpx[row]
|
||
|
|
ex, ey = to_emu(img, px, py)
|
||
|
|
step = COL_STEP_PX[img]
|
||
|
|
sx = IM1_SX if img == 'im1' else IM2_SX
|
||
|
|
sy = IM1_SY if img == 'im1' else IM2_SY
|
||
|
|
mw, mh = step * MARKER_FRAC * sx, step * MARKER_FRAC * sy
|
||
|
|
xml = make_roundrect_xml(name, ex - mw / 2, ey - mh / 2, mw, mh,
|
||
|
|
FILL_XML[(key, st)], ROUND_ADJ_MARK, LINE_XML[(key, st)])
|
||
|
|
spTree.append(etree.fromstring(xml))
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 6) Duplicar slide (clona shapes + relacionamentos de imagem)
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
def duplicate_slide(prs, source_index):
|
||
|
|
source = prs.slides[source_index]
|
||
|
|
dest = prs.slides.add_slide(source.slide_layout)
|
||
|
|
for shp in list(dest.shapes):
|
||
|
|
shp._element.getparent().remove(shp._element)
|
||
|
|
|
||
|
|
rid_map = {}
|
||
|
|
for rId, rel in source.part.rels.items():
|
||
|
|
if rel.reltype == RT.SLIDE_LAYOUT:
|
||
|
|
continue
|
||
|
|
if rel.is_external:
|
||
|
|
continue
|
||
|
|
new_rId = dest.part.relate_to(rel.target_part, rel.reltype)
|
||
|
|
rid_map[rId] = new_rId
|
||
|
|
|
||
|
|
r_embed = qn('r:embed')
|
||
|
|
r_link = qn('r:link')
|
||
|
|
for shp in source.shapes:
|
||
|
|
new_el = copy.deepcopy(shp._element)
|
||
|
|
for el in new_el.iter():
|
||
|
|
old = el.get(r_embed)
|
||
|
|
if old is not None and old in rid_map:
|
||
|
|
el.set(r_embed, rid_map[old])
|
||
|
|
old = el.get(r_link)
|
||
|
|
if old is not None and old in rid_map:
|
||
|
|
el.set(r_link, rid_map[old])
|
||
|
|
dest.shapes._spTree.append(new_el)
|
||
|
|
return dest
|
||
|
|
|
||
|
|
def set_quinzena_title(slide, num, p_start, p_end):
|
||
|
|
for sh in slide.shapes:
|
||
|
|
if sh.name == 'CaixaDeTexto 5':
|
||
|
|
runs = sh.text_frame.paragraphs[0].runs
|
||
|
|
runs[-1].text = f'Quinzena {num:02d} ({fmt(p_start)} a {fmt(p_end)})'
|
||
|
|
return
|
||
|
|
raise RuntimeError('titulo (CaixaDeTexto 5) nao encontrado no slide-modelo')
|
||
|
|
|
||
|
|
LEGEND_Y_TO_KEY = {
|
||
|
|
4200000: ('geo', 'andamento'),
|
||
|
|
4597600: ('geo', 'concluido'),
|
||
|
|
4993578: ('sup', 'andamento'),
|
||
|
|
5381498: ('sup', 'concluido'),
|
||
|
|
5802852: ('inf', 'andamento'),
|
||
|
|
6190772: ('inf', 'concluido'),
|
||
|
|
}
|
||
|
|
|
||
|
|
def update_legend_colors(slide):
|
||
|
|
for sh in slide.shapes:
|
||
|
|
if sh.shape_type != 6: # GROUP
|
||
|
|
continue
|
||
|
|
for sub in sh.shapes:
|
||
|
|
if not sub.name.startswith('Rounded'):
|
||
|
|
continue
|
||
|
|
key = None
|
||
|
|
for y, k in LEGEND_Y_TO_KEY.items():
|
||
|
|
if abs(sub.top - y) < 5000:
|
||
|
|
key = k
|
||
|
|
break
|
||
|
|
if key is None:
|
||
|
|
continue
|
||
|
|
sp = sub._element.spPr
|
||
|
|
for el in sp.findall(qn('a:solidFill')):
|
||
|
|
sp.remove(el)
|
||
|
|
fill_el = etree.fromstring(_solid_fill_xml(COLOR_HEX[key], ALPHA_LEGEND))
|
||
|
|
ln = sp.find(qn('a:ln'))
|
||
|
|
sp.insert(list(sp).index(ln), fill_el)
|
||
|
|
for c in ln.findall(qn('a:solidFill')):
|
||
|
|
ln.remove(c)
|
||
|
|
ln_fill = etree.fromstring(
|
||
|
|
f'<a:solidFill xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
||
|
|
f'<a:srgbClr val="{BORDER_HEX[key]}"/></a:solidFill>')
|
||
|
|
ln.append(ln_fill)
|
||
|
|
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
# 7) Montagem final
|
||
|
|
# -----------------------------------------------------------
|
||
|
|
prs = Presentation(pptx_path)
|
||
|
|
if len(prs.slides) < 3:
|
||
|
|
raise SystemExit('ERRO: o modelo .pptx precisa ter pelo menos 3 slides '
|
||
|
|
'(capa, slide de quinzena, encerramento).')
|
||
|
|
|
||
|
|
template_idx = 1
|
||
|
|
update_legend_colors(prs.slides[template_idx])
|
||
|
|
|
||
|
|
targets = [prs.slides[template_idx]]
|
||
|
|
for _ in range(len(PERIODS) - 1):
|
||
|
|
targets.append(duplicate_slide(prs, template_idx))
|
||
|
|
|
||
|
|
for slide, (num, p_start, p_end) in zip(targets, PERIODS):
|
||
|
|
set_quinzena_title(slide, num, p_start, p_end)
|
||
|
|
add_quadrant_overlays(slide, p_end)
|
||
|
|
|
||
|
|
sldIdLst = prs.slides._sldIdLst
|
||
|
|
ids = list(sldIdLst)
|
||
|
|
thankyou_el = ids[2]
|
||
|
|
sldIdLst.remove(thankyou_el)
|
||
|
|
sldIdLst.append(thankyou_el)
|
||
|
|
|
||
|
|
prs.save(out_path)
|
||
|
|
print(f'\nOK! Gerado: {out_path}')
|
||
|
|
print(f'Total de slides: {len(prs.slides._sldIdLst)} | Quinzenas: {len(PERIODS)}')
|
||
|
|
print(f'Primeira quinzena: {PERIODS[0][0]:02d} ({fmt(PERIODS[0][1])} a {fmt(PERIODS[0][2])})')
|
||
|
|
print(f'Ultima quinzena: {PERIODS[-1][0]:02d} ({fmt(PERIODS[-1][1])} a {fmt(PERIODS[-1][2])})')
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
try:
|
||
|
|
main()
|
||
|
|
except SystemExit as e:
|
||
|
|
print(str(e))
|
||
|
|
pause_on_exit()
|
||
|
|
sys.exit(1)
|
||
|
|
except Exception:
|
||
|
|
print('ERRO INESPERADO:')
|
||
|
|
traceback.print_exc()
|
||
|
|
pause_on_exit()
|
||
|
|
sys.exit(1)
|
||
|
|
else:
|
||
|
|
pause_on_exit()
|