597 lines
25 KiB
Python
597 lines
25 KiB
Python
"""
|
|
Arcádia Contábil - Motor de Contabilidade
|
|
Serviço FastAPI para processamento contábil
|
|
"""
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
from datetime import datetime, date
|
|
from decimal import Decimal
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
app = FastAPI(
|
|
title="Arcádia Contábil",
|
|
description="Motor de Contabilidade - DRE, Balanço, Balancete, SPED ECD",
|
|
version="1.0.0"
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ========== Models ==========
|
|
|
|
class ContaContabil(BaseModel):
|
|
codigo: str
|
|
descricao: str
|
|
tipo: str # ativo, passivo, patrimonio, receita, despesa
|
|
natureza: str # devedora, credora
|
|
nivel: int = 1
|
|
contaPai: Optional[int] = None
|
|
aceitaLancamento: bool = True
|
|
|
|
class Partida(BaseModel):
|
|
contaId: int
|
|
tipo: str # debito, credito
|
|
valor: float
|
|
historico: Optional[str] = None
|
|
centroCustoId: Optional[int] = None
|
|
|
|
class Lancamento(BaseModel):
|
|
dataLancamento: str
|
|
dataCompetencia: Optional[str] = None
|
|
tipoDocumento: Optional[str] = None
|
|
numeroDocumento: Optional[str] = None
|
|
historico: str
|
|
partidas: List[Partida]
|
|
|
|
class PeriodoRequest(BaseModel):
|
|
ano: int
|
|
mes: int
|
|
tenantId: Optional[int] = None
|
|
|
|
class RelatorioRequest(BaseModel):
|
|
tenantId: Optional[int] = None
|
|
dataInicio: str
|
|
dataFim: str
|
|
centroCustoId: Optional[int] = None
|
|
|
|
# ========== Plano de Contas Padrão (Simplificado) ==========
|
|
|
|
PLANO_CONTAS_PADRAO = [
|
|
# ATIVO
|
|
{"codigo": "1", "descricao": "ATIVO", "tipo": "ativo", "natureza": "devedora", "nivel": 1},
|
|
{"codigo": "1.1", "descricao": "ATIVO CIRCULANTE", "tipo": "ativo", "natureza": "devedora", "nivel": 2},
|
|
{"codigo": "1.1.01", "descricao": "Caixa e Equivalentes", "tipo": "ativo", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "1.1.01.001", "descricao": "Caixa Geral", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "1.1.01.002", "descricao": "Bancos Conta Movimento", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "1.1.02", "descricao": "Clientes", "tipo": "ativo", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "1.1.02.001", "descricao": "Duplicatas a Receber", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "1.1.03", "descricao": "Estoques", "tipo": "ativo", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "1.1.03.001", "descricao": "Mercadorias para Revenda", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "1.2", "descricao": "ATIVO NÃO CIRCULANTE", "tipo": "ativo", "natureza": "devedora", "nivel": 2},
|
|
{"codigo": "1.2.01", "descricao": "Imobilizado", "tipo": "ativo", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "1.2.01.001", "descricao": "Máquinas e Equipamentos", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "1.2.01.002", "descricao": "Móveis e Utensílios", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "1.2.01.003", "descricao": "Veículos", "tipo": "ativo", "natureza": "devedora", "nivel": 4},
|
|
|
|
# PASSIVO
|
|
{"codigo": "2", "descricao": "PASSIVO", "tipo": "passivo", "natureza": "credora", "nivel": 1},
|
|
{"codigo": "2.1", "descricao": "PASSIVO CIRCULANTE", "tipo": "passivo", "natureza": "credora", "nivel": 2},
|
|
{"codigo": "2.1.01", "descricao": "Fornecedores", "tipo": "passivo", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "2.1.01.001", "descricao": "Fornecedores Nacionais", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.02", "descricao": "Obrigações Trabalhistas", "tipo": "passivo", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "2.1.02.001", "descricao": "Salários a Pagar", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.02.002", "descricao": "INSS a Recolher", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.02.003", "descricao": "FGTS a Recolher", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.02.004", "descricao": "IRRF a Recolher", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.03", "descricao": "Obrigações Fiscais", "tipo": "passivo", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "2.1.03.001", "descricao": "ICMS a Recolher", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.03.002", "descricao": "PIS a Recolher", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
{"codigo": "2.1.03.003", "descricao": "COFINS a Recolher", "tipo": "passivo", "natureza": "credora", "nivel": 4},
|
|
|
|
# PATRIMÔNIO LÍQUIDO
|
|
{"codigo": "3", "descricao": "PATRIMÔNIO LÍQUIDO", "tipo": "patrimonio", "natureza": "credora", "nivel": 1},
|
|
{"codigo": "3.1", "descricao": "Capital Social", "tipo": "patrimonio", "natureza": "credora", "nivel": 2},
|
|
{"codigo": "3.1.01", "descricao": "Capital Social Integralizado", "tipo": "patrimonio", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "3.2", "descricao": "Lucros ou Prejuízos Acumulados", "tipo": "patrimonio", "natureza": "credora", "nivel": 2},
|
|
{"codigo": "3.2.01", "descricao": "Lucros Acumulados", "tipo": "patrimonio", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "3.2.02", "descricao": "Prejuízos Acumulados", "tipo": "patrimonio", "natureza": "devedora", "nivel": 3},
|
|
|
|
# RECEITAS
|
|
{"codigo": "4", "descricao": "RECEITAS", "tipo": "receita", "natureza": "credora", "nivel": 1},
|
|
{"codigo": "4.1", "descricao": "Receita Operacional Bruta", "tipo": "receita", "natureza": "credora", "nivel": 2},
|
|
{"codigo": "4.1.01", "descricao": "Vendas de Mercadorias", "tipo": "receita", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "4.1.02", "descricao": "Prestação de Serviços", "tipo": "receita", "natureza": "credora", "nivel": 3},
|
|
{"codigo": "4.2", "descricao": "Deduções da Receita", "tipo": "receita", "natureza": "devedora", "nivel": 2},
|
|
{"codigo": "4.2.01", "descricao": "Devoluções de Vendas", "tipo": "receita", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "4.2.02", "descricao": "Impostos sobre Vendas", "tipo": "receita", "natureza": "devedora", "nivel": 3},
|
|
|
|
# DESPESAS
|
|
{"codigo": "5", "descricao": "DESPESAS", "tipo": "despesa", "natureza": "devedora", "nivel": 1},
|
|
{"codigo": "5.1", "descricao": "Custos das Vendas", "tipo": "despesa", "natureza": "devedora", "nivel": 2},
|
|
{"codigo": "5.1.01", "descricao": "CMV - Custo das Mercadorias Vendidas", "tipo": "despesa", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "5.2", "descricao": "Despesas Operacionais", "tipo": "despesa", "natureza": "devedora", "nivel": 2},
|
|
{"codigo": "5.2.01", "descricao": "Despesas com Pessoal", "tipo": "despesa", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "5.2.01.001", "descricao": "Salários e Ordenados", "tipo": "despesa", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "5.2.01.002", "descricao": "INSS Patronal", "tipo": "despesa", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "5.2.01.003", "descricao": "FGTS", "tipo": "despesa", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "5.2.02", "descricao": "Despesas Administrativas", "tipo": "despesa", "natureza": "devedora", "nivel": 3},
|
|
{"codigo": "5.2.02.001", "descricao": "Aluguel", "tipo": "despesa", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "5.2.02.002", "descricao": "Energia Elétrica", "tipo": "despesa", "natureza": "devedora", "nivel": 4},
|
|
{"codigo": "5.2.02.003", "descricao": "Telefone e Internet", "tipo": "despesa", "natureza": "devedora", "nivel": 4},
|
|
]
|
|
|
|
# ========== Endpoints ==========
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {
|
|
"service": "Arcádia Contábil",
|
|
"version": "1.0.0",
|
|
"status": "running",
|
|
"modules": ["plano_contas", "lancamentos", "dre", "balanco", "balancete", "sped_ecd"]
|
|
}
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
|
|
|
@app.get("/plano-contas/padrao")
|
|
async def get_plano_contas_padrao():
|
|
"""Retorna o plano de contas padrão para importação"""
|
|
return {"planoContas": PLANO_CONTAS_PADRAO, "total": len(PLANO_CONTAS_PADRAO)}
|
|
|
|
@app.post("/validar-lancamento")
|
|
async def validar_lancamento(lancamento: Lancamento):
|
|
"""Valida se um lançamento está balanceado (débitos = créditos)"""
|
|
total_debitos = sum(p.valor for p in lancamento.partidas if p.tipo == "debito")
|
|
total_creditos = sum(p.valor for p in lancamento.partidas if p.tipo == "credito")
|
|
|
|
balanceado = abs(total_debitos - total_creditos) < 0.01
|
|
|
|
return {
|
|
"valido": balanceado,
|
|
"totalDebitos": round(total_debitos, 2),
|
|
"totalCreditos": round(total_creditos, 2),
|
|
"diferenca": round(total_debitos - total_creditos, 2),
|
|
"mensagem": "Lançamento balanceado" if balanceado else "Lançamento desbalanceado"
|
|
}
|
|
|
|
@app.post("/calcular-dre")
|
|
async def calcular_dre(request: RelatorioRequest):
|
|
"""Calcula a DRE (Demonstração do Resultado do Exercício)"""
|
|
# Estrutura padrão da DRE
|
|
dre = {
|
|
"periodo": {"inicio": request.dataInicio, "fim": request.dataFim},
|
|
"receitaBruta": 0,
|
|
"deducoes": {
|
|
"devolucoes": 0,
|
|
"impostos": 0,
|
|
"total": 0
|
|
},
|
|
"receitaLiquida": 0,
|
|
"custoVendas": 0,
|
|
"lucroBruto": 0,
|
|
"despesasOperacionais": {
|
|
"vendas": 0,
|
|
"administrativas": 0,
|
|
"pessoal": 0,
|
|
"total": 0
|
|
},
|
|
"resultadoOperacional": 0,
|
|
"receitasFinanceiras": 0,
|
|
"despesasFinanceiras": 0,
|
|
"resultadoAntesIR": 0,
|
|
"provisaoIR": 0,
|
|
"lucroLiquido": 0
|
|
}
|
|
|
|
return {
|
|
"dre": dre,
|
|
"observacao": "Valores serão calculados com base nos lançamentos do período"
|
|
}
|
|
|
|
@app.post("/calcular-balanco")
|
|
async def calcular_balanco(request: RelatorioRequest):
|
|
"""Calcula o Balanço Patrimonial"""
|
|
balanco = {
|
|
"dataBase": request.dataFim,
|
|
"ativo": {
|
|
"circulante": {
|
|
"caixaEquivalentes": 0,
|
|
"contasReceber": 0,
|
|
"estoques": 0,
|
|
"total": 0
|
|
},
|
|
"naoCirculante": {
|
|
"imobilizado": 0,
|
|
"intangivel": 0,
|
|
"total": 0
|
|
},
|
|
"total": 0
|
|
},
|
|
"passivo": {
|
|
"circulante": {
|
|
"fornecedores": 0,
|
|
"obrigacoesTrabalhistas": 0,
|
|
"obrigacoesFiscais": 0,
|
|
"total": 0
|
|
},
|
|
"naoCirculante": {
|
|
"emprestimos": 0,
|
|
"total": 0
|
|
},
|
|
"total": 0
|
|
},
|
|
"patrimonioLiquido": {
|
|
"capitalSocial": 0,
|
|
"reservas": 0,
|
|
"lucrosAcumulados": 0,
|
|
"total": 0
|
|
},
|
|
"totalPassivoPL": 0
|
|
}
|
|
|
|
return {
|
|
"balanco": balanco,
|
|
"equilibrado": balanco["ativo"]["total"] == balanco["totalPassivoPL"]
|
|
}
|
|
|
|
@app.post("/calcular-balancete")
|
|
async def calcular_balancete(request: PeriodoRequest):
|
|
"""Calcula o Balancete de Verificação"""
|
|
balancete = {
|
|
"periodo": {"ano": request.ano, "mes": request.mes},
|
|
"contas": [],
|
|
"totais": {
|
|
"saldoAnteriorDebito": 0,
|
|
"saldoAnteriorCredito": 0,
|
|
"movimentoDebito": 0,
|
|
"movimentoCredito": 0,
|
|
"saldoAtualDebito": 0,
|
|
"saldoAtualCredito": 0
|
|
}
|
|
}
|
|
|
|
return {"balancete": balancete}
|
|
|
|
@app.post("/gerar-razao")
|
|
async def gerar_razao(request: RelatorioRequest):
|
|
"""Gera o Livro Razão para uma conta específica"""
|
|
razao = {
|
|
"periodo": {"inicio": request.dataInicio, "fim": request.dataFim},
|
|
"conta": None,
|
|
"saldoAnterior": 0,
|
|
"lancamentos": [],
|
|
"saldoFinal": 0
|
|
}
|
|
|
|
return {"razao": razao}
|
|
|
|
@app.post("/gerar-diario")
|
|
async def gerar_diario(request: RelatorioRequest):
|
|
"""Gera o Livro Diário"""
|
|
diario = {
|
|
"periodo": {"inicio": request.dataInicio, "fim": request.dataFim},
|
|
"lancamentos": [],
|
|
"totalDebitos": 0,
|
|
"totalCreditos": 0
|
|
}
|
|
|
|
return {"diario": diario}
|
|
|
|
@app.get("/tabela-contas-referenciais")
|
|
async def get_tabela_contas_referenciais():
|
|
"""Retorna a tabela de contas referenciais do SPED ECD"""
|
|
# Tabela simplificada - L100 (Balanço) e L300 (DRE)
|
|
contas_referenciais = {
|
|
"L100": [ # Balanço Patrimonial
|
|
{"codigo": "1", "descricao": "ATIVO TOTAL"},
|
|
{"codigo": "1.01", "descricao": "Ativo Circulante"},
|
|
{"codigo": "1.02", "descricao": "Ativo Não Circulante"},
|
|
{"codigo": "2", "descricao": "PASSIVO TOTAL"},
|
|
{"codigo": "2.01", "descricao": "Passivo Circulante"},
|
|
{"codigo": "2.02", "descricao": "Passivo Não Circulante"},
|
|
{"codigo": "2.03", "descricao": "Patrimônio Líquido"},
|
|
],
|
|
"L300": [ # DRE
|
|
{"codigo": "3.01", "descricao": "Receita Líquida"},
|
|
{"codigo": "3.02", "descricao": "Custo dos Bens e Serviços Vendidos"},
|
|
{"codigo": "3.03", "descricao": "Resultado Bruto"},
|
|
{"codigo": "3.04", "descricao": "Despesas Operacionais"},
|
|
{"codigo": "3.05", "descricao": "Resultado Antes do IR"},
|
|
{"codigo": "3.06", "descricao": "Lucro Líquido do Exercício"},
|
|
]
|
|
}
|
|
|
|
return contas_referenciais
|
|
|
|
@app.post("/exportar-sped-ecd")
|
|
async def exportar_sped_ecd(request: PeriodoRequest):
|
|
"""Gera arquivo SPED ECD (Escrituração Contábil Digital)"""
|
|
# Estrutura do arquivo SPED ECD
|
|
sped_ecd = {
|
|
"registro0000": {
|
|
"tipo_escrit": "G", # G - Livro Diário Geral
|
|
"ind_sit_esp": "0", # 0 - Normal
|
|
"dt_ini": f"{request.ano}-01-01",
|
|
"dt_fin": f"{request.ano}-12-31",
|
|
},
|
|
"registroI010": {
|
|
"ind_esc": "G", # G - Diário Geral
|
|
},
|
|
"registroI050": [], # Plano de Contas
|
|
"registroI150": [], # Saldos Periódicos
|
|
"registroI200": [], # Lançamentos
|
|
"registroJ100": [], # Balanço
|
|
"registroJ150": [], # DRE
|
|
}
|
|
|
|
return {
|
|
"status": "estrutura_gerada",
|
|
"arquivo": f"SPED_ECD_{request.ano}.txt",
|
|
"estrutura": sped_ecd,
|
|
"observacao": "Arquivo será gerado com base nos lançamentos contábeis"
|
|
}
|
|
|
|
# ========== Integrações ==========
|
|
|
|
@app.post("/integrar-nfe")
|
|
async def integrar_nfe(nota: dict):
|
|
"""Gera lançamentos contábeis automáticos para NF-e"""
|
|
tipo_operacao = nota.get("tipoOperacao", "saida")
|
|
valor_total = float(nota.get("valorTotal", 0))
|
|
|
|
if tipo_operacao == "saida":
|
|
lancamento = {
|
|
"historico": f"NF-e {nota.get('numero')} - Venda de mercadorias",
|
|
"partidas": [
|
|
{"conta": "1.1.01.001", "tipo": "debito", "valor": valor_total, "descricao": "Caixa"},
|
|
{"conta": "4.1.01", "tipo": "credito", "valor": valor_total, "descricao": "Vendas"}
|
|
]
|
|
}
|
|
else:
|
|
lancamento = {
|
|
"historico": f"NF-e {nota.get('numero')} - Compra de mercadorias",
|
|
"partidas": [
|
|
{"conta": "1.1.03.001", "tipo": "debito", "valor": valor_total, "descricao": "Estoques"},
|
|
{"conta": "2.1.01.001", "tipo": "credito", "valor": valor_total, "descricao": "Fornecedores"}
|
|
]
|
|
}
|
|
|
|
return {"lancamentoSugerido": lancamento}
|
|
|
|
@app.post("/integrar-folha")
|
|
async def integrar_folha(folha: dict):
|
|
"""Gera lançamentos contábeis automáticos para folha de pagamento"""
|
|
total_bruto = float(folha.get("totalBruto", 0))
|
|
total_inss = float(folha.get("totalInss", 0))
|
|
total_irrf = float(folha.get("totalIrrf", 0))
|
|
total_fgts = float(folha.get("totalFgts", 0))
|
|
total_liquido = float(folha.get("totalLiquido", 0))
|
|
|
|
lancamento = {
|
|
"historico": f"Folha de Pagamento - Competência {folha.get('competencia')}",
|
|
"partidas": [
|
|
{"conta": "5.2.01.001", "tipo": "debito", "valor": total_bruto, "descricao": "Salários e Ordenados"},
|
|
{"conta": "5.2.01.002", "tipo": "debito", "valor": total_inss * 0.2, "descricao": "INSS Patronal"},
|
|
{"conta": "5.2.01.003", "tipo": "debito", "valor": total_fgts, "descricao": "FGTS"},
|
|
{"conta": "2.1.02.001", "tipo": "credito", "valor": total_liquido, "descricao": "Salários a Pagar"},
|
|
{"conta": "2.1.02.002", "tipo": "credito", "valor": total_inss, "descricao": "INSS a Recolher"},
|
|
{"conta": "2.1.02.003", "tipo": "credito", "valor": total_fgts, "descricao": "FGTS a Recolher"},
|
|
{"conta": "2.1.02.004", "tipo": "credito", "valor": total_irrf, "descricao": "IRRF a Recolher"},
|
|
]
|
|
}
|
|
|
|
return {"lancamentoSugerido": lancamento}
|
|
|
|
# ========== Importação de Balanço Patrimonial ==========
|
|
|
|
class BalancoImportRequest(BaseModel):
|
|
texto: str
|
|
dataBalanco: str
|
|
criarContas: bool = True
|
|
criarAbertura: bool = True
|
|
|
|
def parse_valor_contabil(valor_str: str) -> tuple:
|
|
"""Parseia valor contábil no formato brasileiro (ex: 1.234.567,89D ou 1.234.567,89C)"""
|
|
if not valor_str or valor_str.strip() == "0,00" or valor_str.strip() == "0":
|
|
return 0.0, None
|
|
|
|
valor_str = valor_str.strip()
|
|
natureza = None
|
|
|
|
if valor_str.endswith("D"):
|
|
natureza = "devedora"
|
|
valor_str = valor_str[:-1]
|
|
elif valor_str.endswith("C"):
|
|
natureza = "credora"
|
|
valor_str = valor_str[:-1]
|
|
|
|
valor_str = valor_str.replace(".", "").replace(",", ".").strip()
|
|
|
|
try:
|
|
valor = float(valor_str)
|
|
return valor, natureza
|
|
except:
|
|
return 0.0, None
|
|
|
|
def extrair_contas_balanco(texto: str) -> list:
|
|
"""Extrai estrutura hierárquica de contas do balanço patrimonial"""
|
|
linhas = texto.split("\n")
|
|
contas = []
|
|
|
|
nivel_map = {
|
|
"ATIVO": 1, "PASSIVO": 1, "PATRIMÔNIO LÍQUIDO": 1,
|
|
"ATIVO CIRCULANTE": 2, "ATIVO NÃO-CIRCULANTE": 2, "ATIVO NÃO CIRCULANTE": 2,
|
|
"PASSIVO CIRCULANTE": 2, "PASSIVO NÃO-CIRCULANTE": 2, "PASSIVO NÃO CIRCULANTE": 2,
|
|
}
|
|
|
|
tipo_map = {
|
|
"ATIVO": "ativo", "ATIVO CIRCULANTE": "ativo", "ATIVO NÃO-CIRCULANTE": "ativo",
|
|
"PASSIVO": "passivo", "PASSIVO CIRCULANTE": "passivo",
|
|
"PATRIMÔNIO LÍQUIDO": "patrimonio", "CAPITAL SOCIAL": "patrimonio",
|
|
"LUCROS OU PREJUÍZOS ACUMULADOS": "patrimonio",
|
|
}
|
|
|
|
codigo_contador = {"1": 0, "2": 0, "3": 0}
|
|
tipo_atual = "ativo"
|
|
nivel_atual = 1
|
|
codigo_pai = ""
|
|
|
|
for linha in linhas:
|
|
linha = linha.strip()
|
|
if not linha or "Folha:" in linha or "C.N.P.J" in linha or "Balanço encerrado" in linha:
|
|
continue
|
|
if "BALANÇO PATRIMONIAL" in linha or "Descrição" in linha or "______" in linha:
|
|
continue
|
|
if "Assinado digitalmente" in linha or "SERGIO SZWEC" in linha or "ELOIR" in linha:
|
|
continue
|
|
|
|
partes = linha.split()
|
|
if len(partes) < 2:
|
|
continue
|
|
|
|
descricao_partes = []
|
|
valor_2024 = None
|
|
valor_2023 = None
|
|
|
|
for i, parte in enumerate(partes):
|
|
if any(c.isdigit() for c in parte) and ("," in parte or parte.endswith("D") or parte.endswith("C")):
|
|
valor_2024, natureza = parse_valor_contabil(parte)
|
|
if i + 1 < len(partes):
|
|
valor_2023, _ = parse_valor_contabil(partes[i + 1])
|
|
break
|
|
else:
|
|
descricao_partes.append(parte)
|
|
|
|
descricao = " ".join(descricao_partes).strip()
|
|
if not descricao or len(descricao) < 2:
|
|
continue
|
|
|
|
descricao_upper = descricao.upper()
|
|
|
|
if descricao_upper in ["ATIVO", "PASSIVO"]:
|
|
tipo_atual = "ativo" if descricao_upper == "ATIVO" else "passivo"
|
|
nivel_atual = 1
|
|
base_codigo = "1" if tipo_atual == "ativo" else "2"
|
|
codigo = base_codigo
|
|
elif "PATRIMÔNIO LÍQUIDO" in descricao_upper:
|
|
tipo_atual = "patrimonio"
|
|
nivel_atual = 1
|
|
codigo = "3"
|
|
elif descricao_upper in nivel_map:
|
|
nivel_atual = nivel_map[descricao_upper]
|
|
if tipo_atual == "ativo":
|
|
codigo = f"1.{codigo_contador['1'] + 1}"
|
|
codigo_contador['1'] += 1
|
|
elif tipo_atual == "passivo":
|
|
codigo = f"2.{codigo_contador['2'] + 1}"
|
|
codigo_contador['2'] += 1
|
|
else:
|
|
codigo = f"3.{codigo_contador['3'] + 1}"
|
|
codigo_contador['3'] += 1
|
|
else:
|
|
espacos = len(linha) - len(linha.lstrip())
|
|
nivel = min(max(espacos // 2, 1), 5)
|
|
if nivel <= 2:
|
|
nivel = 3
|
|
|
|
base = "1" if tipo_atual == "ativo" else ("2" if tipo_atual == "passivo" else "3")
|
|
codigo_contador[base] = codigo_contador.get(base, 0) + 1
|
|
codigo = f"{base}.{nivel}.{codigo_contador[base]:02d}"
|
|
|
|
natureza_conta = "devedora" if tipo_atual == "ativo" else "credora"
|
|
if "(-)" in descricao or "DEPRECIAÇÃO" in descricao_upper or "AMORTIZAÇÃO" in descricao_upper:
|
|
natureza_conta = "credora" if tipo_atual == "ativo" else "devedora"
|
|
|
|
if valor_2024 and valor_2024 > 0:
|
|
contas.append({
|
|
"codigo": codigo,
|
|
"descricao": descricao,
|
|
"tipo": tipo_atual,
|
|
"natureza": natureza_conta,
|
|
"nivel": nivel_atual if descricao_upper in nivel_map else 4,
|
|
"saldo2024": valor_2024,
|
|
"saldo2023": valor_2023 or 0,
|
|
"aceitaLancamento": True
|
|
})
|
|
|
|
return contas
|
|
|
|
@app.post("/importar-balanco")
|
|
async def importar_balanco(request: BalancoImportRequest):
|
|
"""Importa balanço patrimonial e gera plano de contas + lançamentos de abertura"""
|
|
try:
|
|
contas = extrair_contas_balanco(request.texto)
|
|
|
|
if not contas:
|
|
texto_upper = request.texto.upper()
|
|
has_structure = any(x in texto_upper for x in ["ATIVO", "PASSIVO", "PATRIMÔNIO"])
|
|
has_values = bool(re.search(r'\d{1,3}(?:\.\d{3})*,\d{2}', request.texto))
|
|
|
|
error_msg = "Não foi possível extrair contas do balanço. "
|
|
if not has_structure:
|
|
error_msg += "O texto não contém estrutura de balanço (ATIVO, PASSIVO, PATRIMÔNIO LÍQUIDO). "
|
|
if not has_values:
|
|
error_msg += "Valores não encontrados no formato brasileiro (ex: 1.234,56). "
|
|
error_msg += "Verifique se o PDF contém os dados do balanço patrimonial."
|
|
|
|
raise HTTPException(status_code=400, detail=error_msg)
|
|
|
|
plano_contas = []
|
|
lancamentos_abertura = []
|
|
|
|
for conta in contas:
|
|
plano_contas.append({
|
|
"codigo": conta["codigo"],
|
|
"descricao": conta["descricao"],
|
|
"tipo": conta["tipo"],
|
|
"natureza": conta["natureza"],
|
|
"nivel": conta["nivel"],
|
|
"aceitaLancamento": conta["aceitaLancamento"],
|
|
"status": "ativo"
|
|
})
|
|
|
|
if request.criarAbertura and conta.get("saldo2024", 0) > 0:
|
|
tipo_lancamento = "debito" if conta["natureza"] == "devedora" else "credito"
|
|
lancamentos_abertura.append({
|
|
"conta": conta["codigo"],
|
|
"descricao": conta["descricao"],
|
|
"tipo": tipo_lancamento,
|
|
"valor": conta["saldo2024"],
|
|
"historico": f"Saldo de abertura - {conta['descricao']}"
|
|
})
|
|
|
|
total_debito = sum(l["valor"] for l in lancamentos_abertura if l["tipo"] == "debito")
|
|
total_credito = sum(l["valor"] for l in lancamentos_abertura if l["tipo"] == "credito")
|
|
|
|
return {
|
|
"success": True,
|
|
"planoContas": plano_contas,
|
|
"lancamentosAbertura": lancamentos_abertura,
|
|
"resumo": {
|
|
"totalContas": len(plano_contas),
|
|
"totalLancamentos": len(lancamentos_abertura),
|
|
"totalDebito": total_debito,
|
|
"totalCredito": total_credito,
|
|
"diferenca": abs(total_debito - total_credito)
|
|
}
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
port = int(os.environ.get("CONTABIL_PORT", 8003))
|
|
uvicorn.run(app, host="0.0.0.0", port=port)
|