from __future__ import annotations import re from datetime import date from decimal import Decimal from enum import Enum from typing import Optional from pydantic import BaseModel, Field, field_validator class RegimeTributario(str, Enum): LUCRO_REAL = "1" LUCRO_PRESUMIDO = "2" SIMPLES_NACIONAL = "3" ARBITRADO = "4" IMUNE = "5" OPTANTE_SIMPLES = "6" LUCRO_REAL_TRIMESTRAL = "7" class TipoEmpresa(str, Enum): INDUSTRIA = "industria" COMERCIO = "comercio" SERVICOS = "servicos" MISTO = "misto" class PerfilSPED(str, Enum): A = "A" # Escrituração completa B = "B" # Escrituração com dados simplificados C = "C" # Escrituração com dados simplificados (ME/EPP Simples) class UF(str, Enum): AC = "AC"; AL = "AL"; AP = "AP"; AM = "AM" BA = "BA"; CE = "CE"; DF = "DF"; ES = "ES" GO = "GO"; MA = "MA"; MT = "MT"; MS = "MS" MG = "MG"; PA = "PA"; PB = "PB"; PR = "PR" PE = "PE"; PI = "PI"; RJ = "RJ"; RN = "RN" RS = "RS"; RO = "RO"; RR = "RR"; SC = "SC" SP = "SP"; SE = "SE"; TO = "TO" def _limpar_cnpj(cnpj: str) -> str: return re.sub(r"\D", "", cnpj) def _validar_cnpj(cnpj: str) -> bool: d = _limpar_cnpj(cnpj) if len(d) != 14 or d == d[0] * 14: return False for i, j in [(12, 13), (13, 14)]: s = sum(int(d[k]) * ((i - k - 1) % 8 + 2) for k in range(i)) c = 0 if (r := s % 11) < 2 else 11 - r if c != int(d[j - 1]): return False return True def _validar_cpf(cpf: str) -> bool: d = re.sub(r"\D", "", cpf) if len(d) != 11 or d == d[0] * 11: return False for i in range(9, 11): s = sum(int(d[k]) * (i + 1 - k) for k in range(i)) if (0 if (r := (s * 10 % 11)) >= 10 else r) != int(d[i]): return False return True class Endereco(BaseModel): logradouro: str numero: str complemento: str = "" bairro: str municipio: str uf: UF cep: str cod_municipio: str = "" @field_validator("cep") @classmethod def normalizar_cep(cls, v: str) -> str: return re.sub(r"\D", "", v).zfill(8) class Contato(BaseModel): nome: str = "" telefone: str = "" fax: str = "" email: str = "" class Empresa(BaseModel): cnpj: str razao_social: str nome_fantasia: str = "" ie: str = "" im: str = "" suframa: str = "" regime_tributario: RegimeTributario tipo_empresa: TipoEmpresa endereco: Endereco contato: Contato = Field(default_factory=Contato) perfil_sped: PerfilSPED = PerfilSPED.A atividade_industrial: bool = False data_inicio_atividade: Optional[date] = None @field_validator("cnpj") @classmethod def validar_cnpj(cls, v: str) -> str: d = _limpar_cnpj(v) if not _validar_cnpj(d): raise ValueError(f"CNPJ inválido: {v}") return d @property def cnpj_formatado(self) -> str: d = self.cnpj return f"{d[:2]}.{d[2:5]}.{d[5:8]}/{d[8:12]}-{d[12:]}" class Contador(BaseModel): nome: str cpf: str crc: str cnpj_escritorio: str = "" endereco: Optional[Endereco] = None contato: Contato = Field(default_factory=Contato) @field_validator("cpf") @classmethod def validar_cpf(cls, v: str) -> str: d = re.sub(r"\D", "", v) if not _validar_cpf(d): raise ValueError(f"CPF inválido: {v}") return d class Produto(BaseModel): codigo: str descricao: str ncm: str ex_ipi: str = "" unidade: str tipo_item: str = "00" cod_gen: str = "" aliq_icms: Decimal = Decimal("0") aliq_ipi: Decimal = Decimal("0") cst_icms: str = "000" cst_ipi: str = "50" cst_pis: str = "01" cst_cofins: str = "01" cest: str = "" cod_barras: str = "" peso_bruto: Decimal = Decimal("0") peso_liquido: Decimal = Decimal("0") class ItemNotaFiscal(BaseModel): numero_item: int produto: Produto cfop: str quantidade: Decimal valor_unitario: Decimal desconto: Decimal = Decimal("0") outras_despesas: Decimal = Decimal("0") frete: Decimal = Decimal("0") seguro: Decimal = Decimal("0") @property def valor_produtos(self) -> Decimal: return (self.quantidade * self.valor_unitario) - self.desconto @property def base_icms(self) -> Decimal: return self.valor_produtos + self.frete + self.seguro + self.outras_despesas @property def valor_icms(self) -> Decimal: return (self.base_icms * self.produto.aliq_icms / 100).quantize(Decimal("0.01")) @property def base_ipi(self) -> Decimal: return self.valor_produtos @property def valor_ipi(self) -> Decimal: return (self.base_ipi * self.produto.aliq_ipi / 100).quantize(Decimal("0.01")) @property def valor_total(self) -> Decimal: return self.valor_produtos + self.valor_ipi + self.frete + self.seguro + self.outras_despesas class NotaFiscal(BaseModel): numero: str serie: str modelo: str = "55" data_emissao: date data_saida_entrada: date hora_saida_entrada: str = "00:00:00" emitente: Empresa destinatario: Empresa natureza_operacao: str tipo_operacao: str = "1" ind_pagamento: str = "0" chave_acesso: str = "" protocolo: str = "" itens: list[ItemNotaFiscal] = Field(default_factory=list) info_complementar: str = "" @property def valor_produtos(self) -> Decimal: return sum(i.valor_produtos for i in self.itens) @property def valor_frete(self) -> Decimal: return sum(i.frete for i in self.itens) @property def valor_seguro(self) -> Decimal: return sum(i.seguro for i in self.itens) @property def valor_desconto(self) -> Decimal: return sum(i.desconto for i in self.itens) @property def valor_outras_despesas(self) -> Decimal: return sum(i.outras_despesas for i in self.itens) @property def valor_icms(self) -> Decimal: return sum(i.valor_icms for i in self.itens) @property def valor_ipi(self) -> Decimal: return sum(i.valor_ipi for i in self.itens) @property def valor_total(self) -> Decimal: return sum(i.valor_total for i in self.itens) class LancamentoContabil(BaseModel): data: date numero_lancamento: str historico: str debito_conta: str credito_conta: str valor: Decimal complemento: str = "" class MovimentacaoEstoque(BaseModel): data: date produto: Produto tipo_movimento: str quantidade: Decimal valor_unitario: Decimal documento: str = "" @property def valor_total(self) -> Decimal: return self.quantidade * self.valor_unitario class PeriodoApuracao(BaseModel): data_inicio: date data_fim: date empresa: Empresa notas_saida: list[NotaFiscal] = Field(default_factory=list) notas_entrada: list[NotaFiscal] = Field(default_factory=list) lancamentos: list[LancamentoContabil] = Field(default_factory=list) movimentacoes: list[MovimentacaoEstoque] = Field(default_factory=list) @property def total_vendas(self) -> Decimal: return sum(nf.valor_total for nf in self.notas_saida) @property def total_compras(self) -> Decimal: return sum(nf.valor_total for nf in self.notas_entrada)