Source code for src.pre_recebimento.ficha_tecnica.api.relatorio_fichas_tecnicas_excel

import io
from datetime import datetime

import xlsxwriter

from src.pre_recebimento.ficha_tecnica.models import FichaTecnicaDoProduto

COLUNAS = [
    "Nome do Produto",
    "Empresa",
    "Categoria",
    "Programa",
    "Nº de Pregão / Chamada Pública",
    "Fabricante / Produtor, Envasador ou Distribuidor",
    "Data de Validade",
    "Produto é orgânico",
    "Qual o mecanismo de controle?",
    "O produto contém ou pode conter ingredientes/aditivos alergênicos?",
    "O Produto contém glúten?",
    "O produto contém lactose",
    "O produto é líquido?",
    "Peso líquido do Produto na embalagem primária",
    "Unidade de Medida (primária)",
    "Peso líquido do Produto na embalagem secundária",
    "Unidade de Medida (secundária)",
    "Status",
]

# Índices (0-based) das colunas numéricas de peso: 13 e 15
# (colunas 14 e 16 na planilha, 1-based).
INDICES_COLUNAS_NUMERICAS = (13, 15)

TITULO_RELATORIO = "Relatório de Fichas Técnicas"
MENSAGEM_SEM_REGISTROS = "Nenhum registro encontrado"

TAMANHO_MAXIMO_COLUNA = 40


def _sim_nao(valor):
    """True → "Sim"; False/None → "Não"."""
    return "Sim" if valor is True else "Não"


def _nome_fabricante_envasador(ficha):
    """Concatena fabricante e envasador/distribuidor com " / ", ignorando nulos."""
    nomes = []
    if ficha.fabricante and ficha.fabricante.fabricante:
        nomes.append(ficha.fabricante.fabricante.nome)
    if ficha.envasador_distribuidor and ficha.envasador_distribuidor.fabricante:
        nomes.append(ficha.envasador_distribuidor.fabricante.nome)
    return " / ".join(nomes)


def _sanitiza_texto(valor):
    """Previne Excel formula injection prefixando valores que começam com =, +, - ou @."""
    if valor and isinstance(valor, str) and valor[0] in ("=", "+", "-", "@"):
        return "'" + valor
    return valor or ""


def _montar_valores_dados(ficha):
    """Prepara os valores de uma linha de dados (mesma ordem de COLUNAS)."""
    return [
        _sanitiza_texto(ficha.produto.nome) if ficha.produto else "",
        _sanitiza_texto(ficha.empresa.nome_fantasia) if ficha.empresa else "",
        _sanitiza_texto(ficha.get_categoria_display()),
        _sanitiza_texto(ficha.get_programa_display()),
        _sanitiza_texto(ficha.pregao_chamada_publica),
        _sanitiza_texto(_nome_fabricante_envasador(ficha)),
        _sanitiza_texto(ficha.prazo_validade),
        _sim_nao(ficha.organico),
        _sanitiza_texto(ficha.get_mecanismo_controle_display() or ""),
        _sim_nao(ficha.alergenicos),
        _sim_nao(ficha.gluten),
        _sim_nao(ficha.lactose),
        _sim_nao(ficha.produto_eh_liquido),
        ficha.peso_liquido_embalagem_primaria,
        _sanitiza_texto(ficha.unidade_medida_primaria.abreviacao)
        if ficha.unidade_medida_primaria
        else "",
        ficha.peso_liquido_embalagem_secundaria,
        _sanitiza_texto(ficha.unidade_medida_secundaria.abreviacao)
        if ficha.unidade_medida_secundaria
        else "",
        _sanitiza_texto(ficha.get_status_display()),
    ]


def _escreve_cabecalho(workbook, worksheet):
    """Escreve cabeçalho com logo, título, subtítulo e linha de colunas."""
    formato_titulo = workbook.add_format(
        {
            "bold": True,
            "bg_color": "#A9D18E",
            "align": "center",
            "valign": "vcenter",
            "font_size": 14,
        }
    )

    formato_subtitulo = workbook.add_format(
        {
            "bold": False,
            "bg_color": "#D9EAD3",
            "align": "center",
            "valign": "vcenter",
            "font_size": 10,
            "text_wrap": True,
        }
    )

    formato_header = workbook.add_format(
        {
            "bold": True,
            "bg_color": "#A9D18E",
            "align": "center",
            "valign": "vcenter",
            "border": 1,
            "text_wrap": True,
            "font_size": 9,
        }
    )

    # Logo + Título (linha 0)
    worksheet.merge_range(
        0, 0, 0, len(COLUNAS) - 1, TITULO_RELATORIO, formato_titulo
    )
    worksheet.set_row(0, 50)

    try:
        worksheet.insert_image(
            0,
            0,
            "src/relatorios/static/images/logo-sigpae.png",
            {
                "x_offset": 10,
                "y_offset": 5,
                "x_scale": 0.08,
                "y_scale": 0.08,
            },
        )
    except Exception:
        pass  # Segue sem a imagem se o arquivo não for encontrado

    # Subtítulo (linha 1) - data de extração
    worksheet.merge_range(
        1,
        0,
        1,
        len(COLUNAS) - 1,
        f"Data de extração: {datetime.now().strftime('%d/%m/%Y %H:%M')}",
        formato_subtitulo,
    )
    worksheet.set_row(1, 30)

    # Linha de cabeçalho das colunas (linha 2)
    for col_idx, coluna in enumerate(COLUNAS):
        worksheet.write(2, col_idx, coluna, formato_header)
    worksheet.set_row(2, 30)


def _ajusta_max_length(max_lengths, col_idx, valor):
    """Compara e atualiza o tamanho máximo de uma coluna."""
    if valor is None:
        return
    tamanho = len(str(valor))
    if tamanho > max_lengths[col_idx]:
        max_lengths[col_idx] = tamanho


def _ajusta_largura_colunas(workbook, worksheet, max_lengths):
    """Ajusta a largura das colunas dinamicamente baseado no conteúdo."""
    formato = workbook.add_format()
    formato.set_align("center")
    formato.set_align("vcenter")

    for col_idx, tamanho in enumerate(max_lengths):
        largura = min(tamanho + 2, TAMANHO_MAXIMO_COLUNA)
        if largura < 10:
            largura = 10
        worksheet.set_column(col_idx, col_idx, largura, formato)


[docs] def gera_relatorio_fichas_tecnicas_xlsx(fichas_ids): """Gera o arquivo Excel do Relatório de Fichas Técnicas. Args: fichas_ids: Lista de ids de FichaTecnicaDoProduto. Returns: bytes: Conteúdo do arquivo .xlsx. """ fichas = ( FichaTecnicaDoProduto.objects.filter(id__in=fichas_ids) .select_related( "produto", "empresa", "fabricante__fabricante", "envasador_distribuidor__fabricante", "unidade_medida_primaria", "unidade_medida_secundaria", ) .order_by("-criado_em") ) file = io.BytesIO() workbook = xlsxwriter.Workbook(file, {"in_memory": True}) worksheet = workbook.add_worksheet("Fichas Técnicas") # Desabilita a grade padrão para limpeza visual worksheet.hide_gridlines(2) _escreve_cabecalho(workbook, worksheet) max_lengths = [len(coluna) for coluna in COLUNAS] formato_dados = workbook.add_format( { "align": "center", "valign": "vcenter", "border": 1, "font_size": 9, "text_wrap": True, } ) formato_dados_numero = workbook.add_format( { "align": "center", "valign": "vcenter", "border": 1, "font_size": 9, "num_format": "#,##0.00", } ) linha_atual = 3 for ficha in fichas: valores = _montar_valores_dados(ficha) for col_idx, valor in enumerate(valores): if col_idx in INDICES_COLUNAS_NUMERICAS: if valor is not None: worksheet.write_number( linha_atual, col_idx, valor, formato_dados_numero ) else: worksheet.write(linha_atual, col_idx, "", formato_dados) else: worksheet.write(linha_atual, col_idx, valor, formato_dados) _ajusta_max_length(max_lengths, col_idx, valor) linha_atual += 1 if linha_atual == 3: worksheet.write(linha_atual, 0, MENSAGEM_SEM_REGISTROS, formato_dados) _ajusta_largura_colunas(workbook, worksheet, max_lengths) workbook.close() return file.getvalue()