Planilhas (.xlsx)¶
PDF é o que você envia quando os números estão fechados. Planilha é o que
você envia quando quem recebe precisa trabalhar com eles: ordenar,
filtrar, refazer o total, conferir linha a linha. Orçamento, tabela de
preços, conciliação, exportação de relatório — tudo isso chega como
.xlsx, e quase sempre é montado com openpyxl na mão.
Montar na mão custa três coisas, sempre as mesmas:
- Aritmética de linha. Cada escrita é um par
(linha, coluna)que você controla. Insira uma linha no topo e todas as constantes abaixo mudam. - Estilo que escorre. Quatro atribuições (
font,fill,alignment,border) repetidas em cada célula, e a milésima linha não parece com a primeira. - O formato numérico errado.
"#,##0.00"parece certo e é uma armadilha: o Excel resolve essa máscara com o locale de quem abre. A planilha que você gerou em São Paulo mostra1.234,56aqui e1,234.56no notebook en-US do colega. O valor é o mesmo; o documento está errado, e ninguém percebe.
tempest_fastapi_sdk.spreadsheet resolve os três: um cursor de linha,
colunas declaradas uma vez, e máscaras fixadas em pt-BR.
Extra necessário
Trazopenpyxl. O motor é importado no primeiro uso, então importar o
módulo — e definir as colunas e o tema do projeto — funciona sem ele.
Sua primeira planilha¶
# scripts/orcamento.py
from decimal import Decimal
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
Column,
SheetWriter,
new_workbook,
workbook_to_bytes,
)
def main() -> None:
"""Write a two-item price table to disk."""
workbook = new_workbook("Orçamento")
writer = SheetWriter(
workbook["Orçamento"],
columns=[
Column("Item", width=48, wrap=True),
Column("Qtd.", width=12, horizontal="center"),
Column("Valor unitário", width=20, number_format=BR_CURRENCY_FORMAT),
],
)
writer.title_block(["PREFEITURA MUNICIPAL DE EXEMPLO", "Pregão 1/2026"])
writer.header_row()
writer.write_row(["Serviço de instalação", 2, Decimal("2930.00")])
writer.write_row(["Manutenção mensal", 12, Decimal("450.50")])
writer.total_row(["Total", None, Decimal("11266.00")])
writer.apply_widths()
with open("orcamento.xlsx", "wb") as handle:
handle.write(workbook_to_bytes(workbook))
if __name__ == "__main__":
main()
Abra o arquivo: título centralizado nas três colunas, cabeçalho azul-marinho
com texto branco, valores alinhados à direita como R$ 2.930,00, e a linha
de total destacada em âmbar.
O que você não escreveu
Nenhum par (linha, coluna). Nenhuma Font, PatternFill ou Border.
Nenhuma máscara repetida por célula. O cursor é do SheetWriter, o
estilo vem do tema e o formato vem da coluna.
new_workbook e a aba fantasma¶
O openpyxl sempre cria a pasta de trabalho com uma aba chamada Sheet.
Esquecer de removê-la entrega um documento com uma aba vazia sobrando — o
tipo de detalhe que denuncia que o arquivo foi gerado por script.
from tempest_fastapi_sdk.spreadsheet import new_workbook
workbook = new_workbook("Análise", "Orçamento", "Exequibilidade")
print(workbook.sheetnames) # ['Análise', 'Orçamento', 'Exequibilidade']
Sem argumento nenhum, a aba padrão é mantida — útil quando você vai nomeá-la depois.
Colunas: declare uma vez¶
Column é a especificação da coluna, não de uma célula. Ela vale para
todas as linhas do corpo, e é por isso que o formato não pode divergir entre
a primeira linha e a milésima.
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
BR_PERCENT_FORMAT,
Column,
TEXT_FORMAT,
)
columns = [
Column("Processo", width=18, number_format=TEXT_FORMAT),
Column("Descrição", width=52, wrap=True),
Column("Deságio", width=12, number_format=BR_PERCENT_FORMAT),
Column("Valor", width=18, number_format=BR_CURRENCY_FORMAT),
]
| Campo | Para quê |
|---|---|
title |
Texto do cabeçalho, usado por header_row() |
width |
Largura em caracteres; None deixa o padrão (que corta texto) |
number_format |
Máscara aplicada a toda célula do corpo |
horizontal |
"left", "center", "right"; None deixa o Excel decidir |
wrap |
Quebra de linha — ligue na descrição, deixe desligada no resto |
wrap=True em coluna curta deixa a linha alta à toa
A altura da linha é a da célula mais alta. Uma coluna de duas palavras com quebra ligada estica a linha inteira sem ganhar nada.
Números, não strings¶
A tentação é formatar em Python e escrever o texto pronto. A célula fica
com "R$ 2.930,00", que é texto: quem recebe não consegue somar,
ordenar nem filtrar por ela, e o SOMA do Excel devolve zero para a coluna
inteira.
from decimal import Decimal
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
Column,
SheetWriter,
new_workbook,
)
from tempest_fastapi_sdk.utils import format_currency_br
workbook = new_workbook("Orçamento")
writer = SheetWriter(
workbook["Orçamento"],
columns=[
Column("Item", width=48),
Column("Valor", width=18, number_format=BR_CURRENCY_FORMAT),
],
)
# ❌ vira texto: nada de soma, ordenação ou filtro
writer.write_row(["Serviço", format_currency_br(Decimal("2930.00"))])
# ✅ escreva o número e deixe a máscara apresentar
writer.write_row(["Serviço", Decimal("2930.00")])
Na tela as duas linhas dão exatamente o mesmo R$ 2.930,00; no arquivo só
a segunda é um número.
Use format_currency_br para prosa
tempest_fastapi_sdk.utils.format_currency_br
existe para o texto que vai para um PDF, um e-mail ou uma página. Célula
de planilha recebe número.
Formatos brasileiros¶
| Constante | Renderiza | Para |
|---|---|---|
BR_CURRENCY_FORMAT |
R$ 1.234,56 |
Dinheiro com símbolo |
BR_CURRENCY_FORMAT_NO_SYMBOL |
1.234,56 |
Coluna cujo cabeçalho já diz (R$) |
BR_QUANTITY_FORMAT |
1.234,56 |
Quantidade não monetária |
BR_INTEGER_FORMAT |
1.234 |
Contagem, número inteiro |
BR_PERCENT_FORMAT |
30,00% |
Percentual |
BR_DATE_FORMAT |
14/08/2026 |
Data |
BR_DATETIME_FORMAT |
14/08/2026 19:30 |
Data e hora |
TEXT_FORMAT |
o que você escreveu | Identificador que parece número |
O que faz essas máscaras funcionarem é o código de idioma embutido —
[$R$-416] para moeda, [$-416] para o resto. Ele fixa o ponto como
separador de milhar e a vírgula como decimal dentro do arquivo, então o
documento lê igual em qualquer máquina.
Percentual guarda a razão, não o percentual
O Excel multiplica por 100 sozinho. Uma célula com
BR_PERCENT_FORMAT tem que receber Decimal("0.30"), não 30 —
escrever 30 mostra 3000,00%. Parece erro de digitação, mas é erro de
unidade.
TEXT_FORMAT salva zeros à esquerda
CPF, número de processo (0001/2026), agência bancária. Sem ele o Excel
normaliza para número e os zeros somem sem volta.
As linhas que um documento tem¶
from decimal import Decimal
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
Column,
SheetWriter,
new_workbook,
)
workbook = new_workbook("Orçamento")
writer = SheetWriter(
workbook["Orçamento"],
columns=[
Column("Item", width=48),
Column("Qtd.", width=10, horizontal="center"),
Column("Valor", width=18, number_format=BR_CURRENCY_FORMAT),
],
)
writer.title_block(["ÓRGÃO", "Pregão 1/2026", "Anexo I"]) # mesclado, centralizado
first_item_row = writer.header_row() # cabeçalho da tabela
writer.group_row(["GRUPO 1 — ARTESANATO"]) # subtítulo dentro da tabela
writer.write_row(["Item", 2, Decimal("10.00")]) # corpo
writer.total_row(["Total", None, Decimal("20.00")]) # destaque
writer.blank_rows(2) # respiro
Todos devolvem a próxima linha livre — foi assim que first_item_row
ficou com a posição do primeiro item sem ninguém contar linha.
Uma célula None no meio da linha continua estilizada: é assim que a linha
de total pula as colunas do meio sem perder o preenchimento.
Fórmulas vivas¶
Uma string começando com = vira fórmula de verdade:
from tempest_fastapi_sdk.spreadsheet import Column, SheetWriter, new_workbook
workbook = new_workbook("Orçamento")
writer = SheetWriter(workbook["Orçamento"], [Column("Item"), Column("Valor")])
writer.write_row(["Soma conferida", "=SUM(B5:B24)"])
Vale a pena para as linhas de conferência. Um auditor que edita um valor vê o número reagir, em vez de ler uma constante que era verdade só no instante em que o arquivo foi gerado.
Tema¶
SheetStyle é dado puro — cores em hexadecimal, tamanhos em inteiros,
nenhum objeto do openpyxl. Por isso o tema do seu projeto é definível,
testável e comparável sem o extra instalado.
from tempest_fastapi_sdk.spreadsheet import SheetStyle, SheetWriter
CORPORATE = SheetStyle(
header_background="0B3D2E",
header_foreground="FFFFFF",
group_background="D6E9DF",
total_background="F3E5AB",
border_color="C0C0C0",
font_name="Calibri",
)
Passe no construtor: SheetWriter(sheet, columns, style=CORPORATE).
Cores seguem a convenção do openpyxl
RRGGBB ou AARRGGBB, sem # na frente.
Servindo como download¶
Nada disso toca o disco: workbook_to_bytes devolve os bytes, e o handler
os entrega.
from fastapi import APIRouter
from fastapi.responses import Response
from tempest_fastapi_sdk.spreadsheet import new_workbook, workbook_to_bytes
from tempest_fastapi_sdk.utils import build_content_disposition
XLSX_MEDIA_TYPE = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
router = APIRouter()
@router.get("/orcamentos/{budget_id}/planilha")
async def download_budget(budget_id: int) -> Response:
"""Stream the budget as an .xlsx download."""
workbook = new_workbook("Orçamento")
return Response(
content=workbook_to_bytes(workbook),
media_type=XLSX_MEDIA_TYPE,
headers={
"Content-Disposition": build_content_disposition(
f"orcamento-{budget_id}.xlsx",
),
},
)
Sem arquivo temporário, sem corrida
Duas requisições simultâneas escreveriam o mesmo caminho temporário. Em memória o problema não existe — e não sobra nada para limpar.
Recapitulando¶
new_workbook("Aba")cria a pasta sem a aba fantasma doopenpyxl.Columndeclara título, largura, máscara e alinhamento uma vez.SheetWritersegura o cursor:title_block,header_row,group_row,write_row,total_row,blank_rows— todos devolvem a próxima linha livre.- Escreva números; a máscara apresenta. Texto pronto mata soma e filtro.
- As máscaras
BR_*embutem o código416, então o arquivo lê igual em qualquer locale. SheetStyleé dado puro, então o tema não precisa do extra para existir.workbook_to_bytesentrega bytes — resposta HTTP, storage, e-mail.
Para gerar o mesmo conteúdo como documento fechado, veja Geração de PDF. Para os utilitários de moeda que formatam a prosa do documento, veja Helpers brasileiros.