|
|
| 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" |
| B = "B" |
| C = "C" |
|
|
|
|
| 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) |
|
|