All checks were successful
Build and deploy / Validate source (push) Successful in 10s
Build and deploy / Integration suite on a real stack (push) Successful in 1m47s
Build and deploy / Secret scan and release gate (push) Successful in 7s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m38s
The previous commit used git add -A and swept in four binaries that were deliberately untracked: the week-1 client report as .docx and .pdf, a duplicate of it under output/documents, and imagem-teste.jpg, an input dropped in to test with. None of them are the repository's to version. They are untracked here, left on disk, and covered by .gitignore so the mistake cannot repeat. Three files had no sensible home. The meeting notes sat at the repository root under a 78-character name with spaces and accents, the roadmap generator lived in tmp/ — a directory otherwise ignored as scratch — and its output in output/, which is otherwise generated evidence. They are now docs/reuniao-2026-09-09- anotacoes.pdf, tools/generate_dtf_report.py and docs/roadmap-cliente.pdf, with CONTEXT.md and ROADMAP.md updated to match and a docs/README.md saying what each document is for. .gitignore no longer needs four rules to keep one generator out of an ignored directory; tmp/ is scratch again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
439 lines
27 KiB
Python
439 lines
27 KiB
Python
from pathlib import Path
|
|
import math
|
|
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
from reportlab.lib.units import cm
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
from reportlab.platypus import (
|
|
BaseDocTemplate, Frame, PageBreak, PageTemplate, Paragraph, Spacer,
|
|
Table, TableStyle, KeepTogether, Flowable,
|
|
)
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUT = ROOT / "output/pdf/dtf-plano-producao-e-roadmap.pdf"
|
|
FONT = "/usr/share/fonts/TTF/DejaVuSans.ttf"
|
|
FONT_BOLD = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
|
|
|
|
pdfmetrics.registerFont(TTFont("DejaVu", FONT))
|
|
pdfmetrics.registerFont(TTFont("DejaVu-Bold", FONT_BOLD))
|
|
|
|
NAVY = colors.HexColor("#071426")
|
|
BLUE = colors.HexColor("#1463D8")
|
|
SKY = colors.HexColor("#EAF2FF")
|
|
ORANGE = colors.HexColor("#FF8A1F")
|
|
GREEN = colors.HexColor("#16825B")
|
|
RED = colors.HexColor("#B83737")
|
|
INK = colors.HexColor("#162235")
|
|
MUTED = colors.HexColor("#607085")
|
|
LINE = colors.HexColor("#D9E1EA")
|
|
PALE = colors.HexColor("#F6F8FB")
|
|
WHITE = colors.white
|
|
|
|
|
|
class Rule(Flowable):
|
|
def __init__(self, color=ORANGE, width=4.0, length=54):
|
|
Flowable.__init__(self)
|
|
self.color, self.thickness, self.length = color, width, length
|
|
self.width, self.height = length, width
|
|
|
|
def draw(self):
|
|
self.canv.setStrokeColor(self.color)
|
|
self.canv.setLineWidth(self.thickness)
|
|
self.canv.line(0, self.thickness / 2, self.length, self.thickness / 2)
|
|
|
|
|
|
class ArchitectureDiagram(Flowable):
|
|
"""Small three-zone diagram drawn in vector primitives."""
|
|
def __init__(self):
|
|
Flowable.__init__(self)
|
|
self.width, self.height = 17.0 * cm, 7.2 * cm
|
|
|
|
def box(self, x, y, w, h, title, sub, fill, accent):
|
|
c = self.canv
|
|
c.setFillColor(fill)
|
|
c.setStrokeColor(accent)
|
|
c.setLineWidth(1)
|
|
c.roundRect(x, y, w, h, 7, fill=1, stroke=1)
|
|
c.setFillColor(INK)
|
|
c.setFont("DejaVu-Bold", 8.3)
|
|
c.drawString(x + 8, y + h - 16, title)
|
|
c.setFillColor(MUTED)
|
|
c.setFont("DejaVu", 6.6)
|
|
for i, line in enumerate(sub.split("\n")):
|
|
c.drawString(x + 8, y + h - 29 - 9 * i, line)
|
|
|
|
def _arrowhead(self, tip_x, tip_y, from_x, from_y):
|
|
c = self.canv
|
|
dx, dy = from_x - tip_x, from_y - tip_y
|
|
distance = math.hypot(dx, dy)
|
|
if not distance:
|
|
return
|
|
ux, uy = dx / distance, dy / distance
|
|
angle = math.radians(28)
|
|
arm = 5
|
|
for sign in (-1, 1):
|
|
vx = ux * math.cos(angle) - sign * uy * math.sin(angle)
|
|
vy = uy * math.cos(angle) + sign * ux * math.sin(angle)
|
|
c.line(tip_x, tip_y, tip_x + arm * vx, tip_y + arm * vy)
|
|
|
|
def arrow(self, x1, y1, x2, y2, label=None):
|
|
c = self.canv
|
|
c.setStrokeColor(colors.HexColor("#8BA0B8"))
|
|
c.setFillColor(colors.HexColor("#8BA0B8"))
|
|
c.setLineWidth(1.1)
|
|
c.line(x1, y1, x2, y2)
|
|
self._arrowhead(x2, y2, x1, y1)
|
|
if label:
|
|
c.setFillColor(MUTED)
|
|
c.setFont("DejaVu", 5.9)
|
|
c.drawCentredString((x1 + x2) / 2, (y1 + y2) / 2 + 4, label)
|
|
|
|
def double_arrow(self, x1, y1, x2, y2, label=None):
|
|
c = self.canv
|
|
c.setStrokeColor(colors.HexColor("#8BA0B8"))
|
|
c.setFillColor(colors.HexColor("#8BA0B8"))
|
|
c.setLineWidth(1.1)
|
|
c.line(x1, y1, x2, y2)
|
|
self._arrowhead(x1, y1, x2, y2)
|
|
self._arrowhead(x2, y2, x1, y1)
|
|
if label:
|
|
c.setFillColor(MUTED)
|
|
c.setFont("DejaVu", 5.9)
|
|
c.drawCentredString((x1 + x2) / 2, (y1 + y2) / 2 + 4, label)
|
|
|
|
def draw(self):
|
|
c = self.canv
|
|
zones = [(0, 0, 142, "CLIENTE E INTEGRAÇÕES"), (152, 0, 214, "NUVEM / VPS"),
|
|
(376, 0, 106, "EQUIPE DA FÁBRICA")]
|
|
for x, y, w, name in zones:
|
|
c.setFillColor(PALE)
|
|
c.setStrokeColor(LINE)
|
|
c.roundRect(x, 0, w, self.height, 8, fill=1, stroke=1)
|
|
c.setFillColor(MUTED)
|
|
c.setFont("DejaVu-Bold", 6.4)
|
|
c.drawString(x + 9, self.height - 13, name)
|
|
self.box(12, 117, 120, 40, "Site DTF", "arte, preço e checkout\nacompanhamento do pedido", WHITE, ORANGE)
|
|
self.box(12, 48, 120, 40, "Integrações externas", "Mercado Pago · Tiny/Olist\nWhatsApp Business", WHITE, BLUE)
|
|
self.box(162, 117, 88, 40, "API FastAPI", "pedido, upload\ne integrações", SKY, BLUE)
|
|
self.box(258, 117, 92, 40, "R2", "originais e\narte final privada", SKY, BLUE)
|
|
self.box(162, 48, 88, 40, "Worker", "antivírus e\npré-flight", SKY, GREEN)
|
|
self.box(258, 48, 92, 40, "PostgreSQL", "pedidos, jobs\ne histórico", SKY, BLUE)
|
|
self.box(388, 117, 82, 40, "Kanban online", "fila, status e\ndownload seguro", WHITE, colors.HexColor("#7650B8"))
|
|
self.box(388, 48, 82, 40, "Operador", "baixa e importa\nno FlexiPRINT", WHITE, colors.HexColor("#7650B8"))
|
|
self.arrow(132, 137, 162, 137)
|
|
self.double_arrow(132, 88, 162, 117)
|
|
self.arrow(250, 137, 258, 137)
|
|
self.arrow(206, 117, 206, 88)
|
|
self.double_arrow(250, 117, 258, 88)
|
|
self.double_arrow(250, 68, 258, 68)
|
|
self.arrow(350, 137, 388, 137)
|
|
self.arrow(429, 117, 429, 88)
|
|
|
|
|
|
def paragraph(text, style):
|
|
return Paragraph(text, style)
|
|
|
|
|
|
def bullet(text, styles):
|
|
return Paragraph(f"<font color='#FF8A1F'>•</font> {text}", styles["bullet"])
|
|
|
|
|
|
def section_title(text, styles, kicker=None):
|
|
blocks = []
|
|
if kicker:
|
|
blocks.append(Paragraph(kicker.upper(), styles["kicker"]))
|
|
blocks.append(Spacer(1, 3))
|
|
blocks += [Paragraph(text, styles["dtf_h1"]), Spacer(1, 7), Rule(), Spacer(1, 12)]
|
|
return blocks
|
|
|
|
|
|
def on_page(canvas, doc):
|
|
if doc.page == 1:
|
|
return
|
|
canvas.saveState()
|
|
canvas.setStrokeColor(LINE)
|
|
canvas.line(doc.leftMargin, A4[1] - 1.25 * cm, A4[0] - doc.rightMargin, A4[1] - 1.25 * cm)
|
|
canvas.setFont("DejaVu", 7.4)
|
|
canvas.setFillColor(MUTED)
|
|
canvas.drawString(doc.leftMargin, A4[1] - 1.05 * cm, "DTF Dropstar - plano de entrega")
|
|
canvas.drawRightString(A4[0] - doc.rightMargin, 0.82 * cm, f"Página {doc.page}")
|
|
canvas.restoreState()
|
|
|
|
|
|
def build_pdf():
|
|
styles = getSampleStyleSheet()
|
|
styles.add(ParagraphStyle(name="cover_kicker", fontName="DejaVu-Bold", fontSize=10,
|
|
leading=13, textColor=ORANGE, spaceAfter=10, letterSpacing=0.6))
|
|
styles.add(ParagraphStyle(name="cover_title", fontName="DejaVu-Bold", fontSize=30,
|
|
leading=35, textColor=WHITE, spaceAfter=11))
|
|
styles.add(ParagraphStyle(name="cover_text", fontName="DejaVu", fontSize=12,
|
|
leading=17, textColor=colors.HexColor("#D6E0EF")))
|
|
styles.add(ParagraphStyle(name="kicker", fontName="DejaVu-Bold", fontSize=8,
|
|
leading=10, textColor=ORANGE, letterSpacing=0.7))
|
|
styles.add(ParagraphStyle(name="dtf_h1", fontName="DejaVu-Bold", fontSize=20,
|
|
leading=24, textColor=NAVY, spaceAfter=0))
|
|
styles.add(ParagraphStyle(name="dtf_h2", fontName="DejaVu-Bold", fontSize=12,
|
|
leading=15, textColor=INK, spaceBefore=6, spaceAfter=5))
|
|
styles.add(ParagraphStyle(name="body", fontName="DejaVu", fontSize=9.3,
|
|
leading=14, textColor=INK, spaceAfter=8))
|
|
styles.add(ParagraphStyle(name="bullet", fontName="DejaVu", fontSize=9.1,
|
|
leading=13, textColor=INK, leftIndent=2, spaceAfter=4))
|
|
styles.add(ParagraphStyle(name="small", fontName="DejaVu", fontSize=7.6,
|
|
leading=10.5, textColor=MUTED))
|
|
styles.add(ParagraphStyle(name="cell", fontName="DejaVu", fontSize=7.7,
|
|
leading=10, textColor=INK))
|
|
styles.add(ParagraphStyle(name="cell_bold", fontName="DejaVu-Bold", fontSize=7.7,
|
|
leading=10, textColor=INK))
|
|
styles.add(ParagraphStyle(name="cell_white", fontName="DejaVu-Bold", fontSize=7.7,
|
|
leading=10, textColor=WHITE))
|
|
styles.add(ParagraphStyle(name="note", fontName="DejaVu", fontSize=8.2,
|
|
leading=12, textColor=INK, backColor=colors.HexColor("#FFF4E7"),
|
|
borderColor=colors.HexColor("#F7D2A5"), borderWidth=0.5,
|
|
borderPadding=8, borderRadius=4))
|
|
|
|
doc = BaseDocTemplate(str(OUT), pagesize=A4, leftMargin=1.65 * cm, rightMargin=1.65 * cm,
|
|
topMargin=1.7 * cm, bottomMargin=1.45 * cm)
|
|
frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="main")
|
|
doc.addPageTemplates([PageTemplate(id="main", frames=[frame], onPage=on_page)])
|
|
story = []
|
|
|
|
# Cover
|
|
story.append(Spacer(1, 2.4 * cm))
|
|
cover = [
|
|
Paragraph("PLANO DE ENTREGA", styles["cover_kicker"]),
|
|
Paragraph("Sistema DTF<br/>Plano de entrega", styles["cover_title"]),
|
|
Paragraph("Uma operação em que o cliente envia a arte, recebe análise, paga e acompanha a produção sem depender de WhatsApp, pastas ou repasse manual.", styles["cover_text"]),
|
|
Spacer(1, 28),
|
|
Table([[paragraph("OBJETIVO", styles["cover_kicker"]), paragraph("DECISÃO DE ARQUITETURA", styles["cover_kicker"])],
|
|
[paragraph("Transformar capacidade ociosa em atendimento e produção 24 horas.", styles["cover_text"]),
|
|
paragraph("Portal e Kanban online + Cloudflare R2 + VPS existente. A impressão continua manual na fábrica.", styles["cover_text"])]],
|
|
colWidths=[8.1 * cm, 8.1 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, -1), NAVY),
|
|
("BOX", (0, 0), (-1, -1), 0.6, colors.HexColor("#40516B")),
|
|
("INNERGRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#40516B")),
|
|
("LEFTPADDING", (0, 0), (-1, -1), 13), ("RIGHTPADDING", (0, 0), (-1, -1), 13),
|
|
("TOPPADDING", (0, 0), (-1, -1), 11), ("BOTTOMPADDING", (0, 0), (-1, -1), 12),
|
|
])),
|
|
Spacer(1, 0.7 * cm),
|
|
Paragraph("Consolidação das discussões técnicas e comerciais - setembro de 2026", styles["cover_text"]),
|
|
]
|
|
cover_table = Table([[cover]], colWidths=[17.7 * cm], rowHeights=[23.6 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, -1), NAVY),
|
|
("LEFTPADDING", (0, 0), (-1, -1), 1.15 * cm),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 1.15 * cm),
|
|
("TOPPADDING", (0, 0), (-1, -1), 1.05 * cm),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 1.05 * cm),
|
|
]))
|
|
story.append(cover_table)
|
|
story.append(PageBreak())
|
|
|
|
# Context
|
|
story += section_title("O problema que vamos resolver", styles, "Contexto")
|
|
story.append(paragraph(
|
|
"A sala de DTF tem capacidade instalada muito maior que a produção atual. O gargalo não é a máquina: é o fluxo entre o pedido, a arte, o designer e a sala. Hoje o cliente envia arquivo no WhatsApp, o atendimento repassa, o designer descobre problemas tarde e o status fica escondido em pastas de rede.",
|
|
styles["body"]))
|
|
story.append(paragraph("O projeto resolve quatro falhas do processo:", styles["dtf_h2"]))
|
|
for item in [
|
|
"<b>Recebimento limitado pelo horário:</b> um cliente que chega no fim do dia espera até o turno seguinte.",
|
|
"<b>Arquivo sem rastreabilidade:</b> WhatsApp e nomes digitados manualmente facilitam perda e erro.",
|
|
"<b>Arquivo ruim chega tarde ao designer:</b> qualidade, dimensão e transparência são verificadas depois de consumir tempo humano.",
|
|
"<b>Produção sem números:</b> não há registro confiável de espera, velocidade real, fila ou retrabalho.",
|
|
]:
|
|
story.append(bullet(item, styles))
|
|
story.append(Spacer(1, 7))
|
|
decision = Table([
|
|
[paragraph("ANTES", styles["cell_bold"]), paragraph("DEPOIS", styles["cell_bold"])],
|
|
[paragraph("Pedido e arte circulam por WhatsApp e pastas.", styles["cell"]), paragraph("Site DTF recebe a arte, analisa, precifica e registra o fluxo.", styles["cell"])],
|
|
[paragraph("Designer corrige indiscriminadamente.", styles["cell"]), paragraph("Arte pronta segue rápido; exceções vão para atendimento humano.", styles["cell"])],
|
|
[paragraph("Sala não enxerga fila real nem tempos.", styles["cell"]), paragraph("Kanban online organiza a fila e registra cada movimento manualmente.", styles["cell"])],
|
|
], colWidths=[8.6 * cm, 8.6 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, 0), SKY), ("GRID", (0, 0), (-1, -1), 0.45, LINE),
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 9),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
]))
|
|
story.append(decision)
|
|
story.append(PageBreak())
|
|
|
|
# Flow
|
|
story += section_title("Como será a jornada do pedido", styles, "Modelo operacional")
|
|
steps = [
|
|
("1", "Cliente entra no site DTF", "Escolhe o produto, envia a arte e vê o valor conforme as regras comerciais já configuradas."),
|
|
("2", "Sistema registra a arte", "Calcula a metragem e prepara o pedido. O pré-flight automático será validado depois da entrega, com arquivos reais."),
|
|
("3", "Cliente paga", "O checkout é do Mercado Pago; os dados do cartão não passam pelo nosso servidor."),
|
|
("4", "Pedido entra no sistema", "Depois da confirmação do pagamento, o pedido é criado no Tiny e aparece no Kanban."),
|
|
("5", "Equipe baixa e imprime", "O operador baixa o arquivo final no Kanban, importa no FlexiPRINT e atualiza o card."),
|
|
]
|
|
rows = []
|
|
for n, title, text in steps:
|
|
rows.append([
|
|
paragraph(n, ParagraphStyle("num" + n, parent=styles["cell_bold"], fontSize=13, textColor=ORANGE, alignment=TA_CENTER)),
|
|
paragraph(title, styles["cell_bold"]),
|
|
paragraph(text, styles["cell"]),
|
|
])
|
|
story.append(Table(rows, colWidths=[1.0 * cm, 5.3 * cm, 10.9 * cm], style=TableStyle([
|
|
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
|
("BACKGROUND", (0, 0), (0, -1), PALE), ("LEFTPADDING", (0, 0), (-1, -1), 9),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, -1), 10),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 10),
|
|
])))
|
|
story.append(Spacer(1, 15))
|
|
story.append(paragraph("Onde o WhatsApp entra", styles["dtf_h2"]))
|
|
story.append(paragraph(
|
|
"O WhatsApp deixa de ser o lugar onde a arte é enviada. Ele serve para avisar o cliente: pagamento aprovado, pedido em produção, pedido pronto ou necessidade de correção. Quando houver problema, a mensagem leva o cliente de volta para o site.", styles["body"]))
|
|
story.append(paragraph(
|
|
"<b>Cuidados na automação:</b> se Mercado Pago, Tiny ou WhatsApp reenviar um aviso por falha, o sistema confere antes de agir. Assim, não cria dois pedidos nem manda a mesma mensagem duas vezes.", styles["note"]))
|
|
story.append(PageBreak())
|
|
|
|
# Architecture
|
|
story += section_title("Arquitetura de software e infraestrutura", styles, "Produção")
|
|
story.append(paragraph(
|
|
"Todo o sistema roda na nuvem: recebe, processa, integra e disponibiliza o arquivo final. A fábrica acessa o Kanban pelo navegador e baixa o arquivo quando for produzir; não há componente instalado na rede interna.", styles["body"]))
|
|
story.append(ArchitectureDiagram())
|
|
story.append(Spacer(1, 12))
|
|
columns = [
|
|
[paragraph("NUVEM", styles["cell_bold"]), paragraph("Site DTF, Kanban online, API FastAPI, worker, ClamAV, PostgreSQL e Cloudflare R2.", styles["cell"])],
|
|
[paragraph("FÁBRICA", styles["cell_bold"]), paragraph("Navegador, operador e FlexiPRINT atual. O arquivo é baixado e importado manualmente.", styles["cell"])],
|
|
[paragraph("TERCEIROS", styles["cell_bold"]), paragraph("Mercado Pago, Tiny/Olist e WhatsApp Business API.", styles["cell"])],
|
|
]
|
|
story.append(Table(columns, colWidths=[3.0 * cm, 14.2 * cm], style=TableStyle([
|
|
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("BACKGROUND", (0, 0), (0, -1), SKY), ("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 8), ("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
])))
|
|
story.append(Spacer(1, 10))
|
|
for item in [
|
|
"<b>Upload grande:</b> o navegador manda o arquivo direto para o R2, em partes. O VPS não precisa receber 5 GB de uma vez.",
|
|
"<b>Fila:</b> o banco guarda o que precisa ser analisado, enviado ao Tiny, avisado no WhatsApp ou apagado no prazo certo.",
|
|
"<b>Segurança:</b> o arquivo entra em quarentena, passa pelo antivírus e só depois é analisado e liberado no Kanban.",
|
|
"<b>Se a fábrica ficar sem internet:</b> o site continua no ar. A equipe apenas não consegue baixar arquivo novo ou atualizar o Kanban até a conexão voltar.",
|
|
]:
|
|
story.append(bullet(item, styles))
|
|
story.append(PageBreak())
|
|
|
|
# Storage/costs
|
|
story += section_title("R2: custos e retenção", styles, "Cloudflare")
|
|
story.append(paragraph(
|
|
"O R2 é onde os arquivos ficam guardados. Ele é privado: o operador só baixa pelo Kanban, com link temporário. O custo vem principalmente da média de GB guardados no mês; leitura e gravação costumam pesar bem menos. O download não tem cobrança de saída de dados.", styles["body"]))
|
|
cost = [
|
|
[paragraph("MÉDIA ARMAZENADA", styles["cell_white"]), paragraph("R2 / MÊS", styles["cell_white"]), paragraph("APROX. EM R$*", styles["cell_white"])],
|
|
[paragraph("100 GB", styles["cell"]), paragraph("US$ 1,35", styles["cell"]), paragraph("R$ 7", styles["cell"])],
|
|
[paragraph("500 GB", styles["cell"]), paragraph("US$ 7,35", styles["cell"]), paragraph("R$ 38", styles["cell"])],
|
|
[paragraph("1 TB", styles["cell"]), paragraph("US$ 14,85", styles["cell"]), paragraph("R$ 76", styles["cell"])],
|
|
[paragraph("3 TB", styles["cell"]), paragraph("US$ 44,85", styles["cell"]), paragraph("R$ 230", styles["cell"])],
|
|
]
|
|
story.append(Table(cost, colWidths=[6.0 * cm, 5.2 * cm, 6.0 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
|
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
|
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("LEFTPADDING", (0, 0), (-1, -1), 9),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
])))
|
|
story.append(Spacer(1, 5))
|
|
story.append(paragraph("* Referência de US$ 1 = R$ 5,13; câmbio, impostos e tarifas do meio de pagamento podem variar.", styles["small"]))
|
|
story.append(Spacer(1, 10))
|
|
retention = [
|
|
[paragraph("ARTEFATO", styles["cell_bold"]), paragraph("PRAZO", styles["cell_bold"]), paragraph("REGRA", styles["cell_bold"])],
|
|
[paragraph("Upload multipart incompleto", styles["cell"]), paragraph("1 dia", styles["cell"]), paragraph("Abortado automaticamente.", styles["cell"])],
|
|
[paragraph("Recusado ou infectado", styles["cell"]), paragraph("3 dias", styles["cell"]), paragraph("Suporte pode conferir; depois apaga.", styles["cell"])],
|
|
[paragraph("Original do cliente", styles["cell"]), paragraph("até 7 dias", styles["cell"]), paragraph("Após aprovação, não fica guardado sem necessidade.", styles["cell"])],
|
|
[paragraph("Arte pronta para impressão", styles["cell"]), paragraph("máx. 30 dias", styles["cell"]), paragraph("Prazo conta do primeiro upload; permite correção ou reimpressão recente.", styles["cell"])],
|
|
[paragraph("Histórico e métricas", styles["cell"]), paragraph("banco", styles["cell"]), paragraph("Sem manter a imagem.", styles["cell"])],
|
|
]
|
|
story.append(Table(retention, colWidths=[5.2 * cm, 2.8 * cm, 9.2 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, 0), SKY), ("GRID", (0, 0), (-1, -1), 0.45, LINE),
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 8), ("TOPPADDING", (0, 0), (-1, -1), 7),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
|
|
])))
|
|
story.append(Spacer(1, 9))
|
|
story.append(paragraph("Decisão: não haverá biblioteca infinita de artes. Após o prazo, o cliente reenvia o arquivo; o pedido e sua rastreabilidade permanecem registrados.", styles["note"]))
|
|
story.append(PageBreak())
|
|
|
|
# Production / CI CD
|
|
story += section_title("Como fica em produção", styles, "Operação, publicação e continuidade")
|
|
story.append(paragraph(
|
|
"A primeira entrega será operada como uma única aplicação na VPS. Portal, Kanban, processamento e banco seguem o mesmo ciclo de publicação, com acesso da fábrica exclusivamente pelo navegador. Isso reduz pontos de manutenção e mantém a operação independente da rede interna.", styles["body"]))
|
|
stacks = [
|
|
[paragraph("AMBIENTE", styles["cell_white"]), paragraph("COMPONENTES", styles["cell_white"]), paragraph("OPERAÇÃO", styles["cell_white"])],
|
|
[paragraph("Sistema DTF", styles["cell_bold"]), paragraph("site DTF, Kanban online, API, processamento, antivírus, banco de dados e cópia diária de segurança", styles["cell"]), paragraph("VPS com armazenamento persistente; operação e acompanhamento centralizados", styles["cell"])],
|
|
]
|
|
story.append(Table(stacks, colWidths=[3.7 * cm, 9.1 * cm, 4.4 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
|
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("LEFTPADDING", (0, 0), (-1, -1), 8), ("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
|
("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
])))
|
|
story.append(Spacer(1, 13))
|
|
story.append(paragraph("Publicação com controle", styles["dtf_h2"]))
|
|
for item in [
|
|
"Toda alteração passa por testes automáticos antes de poder chegar ao ambiente de produção.",
|
|
"Cada publicação fica identificada pela versão do código, permitindo restaurar rapidamente a versão anterior se houver qualquer problema.",
|
|
"As mudanças são conferidas primeiro em ambiente de validação e só então liberadas para a operação.",
|
|
"Ao final da publicação, uma verificação confirma que os serviços essenciais voltaram a responder antes de encerrar o processo.",
|
|
"Credenciais de storage, pagamentos, integrações e banco permanecem fora do código e são tratadas como dados restritos do ambiente.",
|
|
]:
|
|
story.append(bullet(item, styles))
|
|
story.append(Spacer(1, 9))
|
|
story.append(paragraph("Acompanhamento operacional", styles["dtf_h2"]))
|
|
story.append(Spacer(1, 5))
|
|
story.append(paragraph(
|
|
"A rotina de operação acompanha disponibilidade do site e do Kanban, andamento da fila de processamento, falhas de integração e resultado das cópias de segurança. Assim, qualquer desvio é identificado antes de interromper o atendimento ou a produção.", styles["note"]))
|
|
story.append(PageBreak())
|
|
|
|
# Roadmap
|
|
story += section_title("Roadmap de implementação", styles, "MVP em até 3 semanas")
|
|
story.append(paragraph("O cronograma considera a base de código existente e dedicação integral. A prioridade é concluir o fluxo completo - envio, pagamento, frete, pedido e acompanhamento - antes de qualquer integração com as máquinas. As regras comerciais seguem exatamente o que já está configurado no Site DTF.", styles["body"]))
|
|
roadmap = [
|
|
[paragraph("QUANDO", styles["cell_white"]), paragraph("ENTREGA", styles["cell_white"]), paragraph("RESULTADO", styles["cell_white"])],
|
|
[paragraph("Sem. 1", styles["cell_bold"]), paragraph("Infraestrutura e upload", styles["cell"]), paragraph("VPS, domínio, banco, R2, upload multipart, antivírus e base do serviço.", styles["cell"])],
|
|
[paragraph("Sem. 2", styles["cell_bold"]), paragraph("Pagamento, frete e pedido", styles["cell"]), paragraph("Mercado Pago, cotação de frete, criação idempotente no Tiny, geração do arquivo final e estados principais do Kanban.", styles["cell"])],
|
|
[paragraph("Sem. 3", styles["cell_bold"]), paragraph("Kanban e entrega", styles["cell"]), paragraph("Kanban online, download seguro, mensagens de status no WhatsApp, retenção de 30 dias e orientação à operação.", styles["cell"])],
|
|
]
|
|
story.append(Table(roadmap, colWidths=[2.1 * cm, 5.0 * cm, 10.1 * cm], style=TableStyle([
|
|
("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
|
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("BACKGROUND", (0, 1), (0, -1), PALE), ("LEFTPADDING", (0, 0), (-1, -1), 8),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 8), ("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
])))
|
|
story.append(Spacer(1, 14))
|
|
story.append(paragraph("Fora da primeira entrega", styles["dtf_h2"]))
|
|
story.append(Spacer(1, 5))
|
|
story.append(paragraph("A primeira entrega termina no download do arquivo final pelo Kanban e na orientação de uso à operação. Integração direta com FlexiPRINT, agente dentro da fábrica, pasta monitorada, painel de sala e relatórios avançados não fazem parte deste prazo. O pré-flight automático será validado depois da entrega, com arquivos e impressão reais. A fábrica mantém esses recursos no ambiente atual; após a entrega, damos suporte e avaliamos mudanças conforme a necessidade.", styles["note"]))
|
|
story.append(Spacer(1, 10))
|
|
story.append(paragraph("O que fica com a operação", styles["dtf_h2"]))
|
|
for item in [
|
|
"Acessar o Kanban, baixar o arquivo final e importá-lo manualmente no FlexiPRINT atual.",
|
|
"Realizar os testes de impressão e informar qualquer diferença encontrada no arquivo ou no resultado impresso.",
|
|
"Manter computadores, FlexiPRINT, perfis de impressão, impressoras e eventuais pastas internas funcionando.",
|
|
"A conexão local é necessária apenas para a equipe acessar o Kanban e baixar o arquivo na fábrica. Se ela cair, portal, pagamentos, uploads e fila continuam ativos na nuvem; a produção retoma quando o acesso local voltar.",
|
|
"Comunicar mudanças de preços, frete ou regras operacionais que precisem ser refletidas no sistema.",
|
|
]:
|
|
story.append(bullet(item, styles))
|
|
story.append(Spacer(1, 7))
|
|
story.append(paragraph("O que precisamos para começar", styles["dtf_h2"]))
|
|
for item in [
|
|
"Credenciais do Mercado Pago, incluindo a configuração do webhook para confirmação de pagamento.",
|
|
"Dados da integração de frete: plataforma(s) já utilizada(s), acesso à API, CEP de origem, serviços oferecidos, regra de cobrança e referência de peso e dimensões por pacote.",
|
|
]:
|
|
story.append(bullet(item, styles))
|
|
story.append(Spacer(1, 7))
|
|
story.append(paragraph("Antes de começar", styles["dtf_h2"]))
|
|
story.append(Spacer(1, 5))
|
|
story.append(paragraph("Com esses acessos e dados liberados, a primeira semana pode começar. A tabela, os descontos e as regras comerciais já existentes no Site DTF serão mantidos. Tiny/Olist e WhatsApp já estão disponíveis; durante o desenvolvimento será definida a ligação técnica dessas integrações com os eventos do pedido. A fábrica valida a impressão depois, baixando o arquivo final pelo Kanban.", styles["note"]))
|
|
|
|
doc.build(story)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
build_pdf()
|
|
print(OUT)
|