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)
|