|
#!/usr/bin/env python3 |
|
""" |
|
Script de conciliação de extratos OFX via Telegram |
|
Processa arquivos OFX diretamente do Nubank |
|
""" |
|
|
|
import sys |
|
import re |
|
from pathlib import Path |
|
from typing import List, Set, Tuple, Optional |
|
from beancount.loader import load_file |
|
from beancount.parser import printer |
|
from beancount.core import data, amount |
|
from ofxparse import OfxParser |
|
|
|
# Dicionário de categorização de transações |
|
# Copiado de contabilidade-pessoal/importers/nubank/nubank_ofx_importer.py |
|
_prefix_account_dct = { |
|
'Desconto Antecipação': 'Income:Business:Nubank:Desconto', |
|
'Desconto Antecipação': 'Income:Business:Nubank:Desconto', # Encoding issue |
|
'Ederson':'Expenses:Mercado', |
|
'Muniz':'Expenses:Mercado', |
|
'Jim.Com':'Expenses:Mercado', |
|
'Ricardo':'Expenses:Mercado', |
|
'Alvino':'Expenses:Mercado', |
|
'Banca do Silvio':'Expenses:Mercado', |
|
'Bdm Mercado':'Expenses:Mercado', |
|
'Jessianetobias':'Expenses:Mercado', |
|
'Armazemda':'Expenses:Mercado', |
|
'Karina Aparecida':'Expenses:Mercado', |
|
'Juliocesarde':'Expenses:Mercado', |
|
'Diego':'Expenses:Mercado', |
|
'Mp *Thais':'Expenses:Mercado', |
|
'Mp *Aguadecocosjc':'Expenses:Mercado', |
|
'Lilianepaivasilva':'Expenses:Mercado', |
|
'Teresinha':'Expenses:Mercado', |
|
'Gabriel Figueiredo':'Expenses:Mercado', |
|
'Cafe do Joao':'Expenses:Mercado', |
|
'Vandersondinisda':'Expenses:Mercado', |
|
'Jnt Legumes e Verduras':'Expenses:Mercado', |
|
'31.529.460 Leandro':'Expenses:Mercado', |
|
'Almir Faria':'Expenses:Mercado', |
|
'Assai':'Expenses:Mercado', |
|
'46.398.651 Francesca':'Expenses:Mercado', |
|
'Paygo*Joao':'Expenses:Mercado', |
|
'Gigiomaster':'Expenses:Mercado', |
|
'Orgair Joaquim ':'Expenses:Mercado', |
|
'Rigonan Paes':'Expenses:Mercado', |
|
'Miyaguis':'Expenses:Mercado', |
|
'Nagumo':'Expenses:Mercado', |
|
'Andrezapaulo':'Expenses:Mercado', |
|
'Ademirlemospontes':'Expenses:Mercado', |
|
'Familho':'Expenses:Mercado', |
|
'Casa de Carnes Majesta':'Expenses:Mercado', |
|
'Diegohenrique':'Expenses:Mercado', |
|
'P. M Furuiama Sandova':'Expenses:Mercado', |
|
'Janeaparecida':'Expenses:Mercado', |
|
'Deboraalexandre':'Expenses:Mercado', |
|
'Leandroaparecido':'Expenses:Mercado', |
|
'Paygo*Joao Aparecido':'Expenses:Mercado', |
|
'Mp *Almir':'Expenses:Mercado', |
|
'N S A Comercio de Alim':'Expenses:Mercado', |
|
'Leonardomartinsde':'Expenses:Mercado', |
|
'Rubensrosella':'Expenses:Mercado', |
|
'Marcia da Rocha Onari':'Expenses:Mercado', |
|
'Amandamartins':'Expenses:Mercado', |
|
'Lojas Americanas': 'Expenses:Mercado', |
|
'Minuto Pa': 'Expenses:Mercado', |
|
'Pao de Acucar': 'Expenses:Mercado', |
|
'N S A Foods': 'Expenses:Mercado', |
|
'Supermercado Maximo': 'Expenses:Mercado', |
|
'Emporio de Paes Integr': 'Expenses:Mercado', |
|
'Oba Hortifruti': 'Expenses:Mercado', |
|
'Carrefour': 'Expenses:Mercado', |
|
'Urbanova Carnes': 'Expenses:Mercado', |
|
'Alvino\'S Confeitaria': 'Expenses:Mercado', |
|
'Almirfaria': 'Expenses:Mercado', |
|
'Devid': 'Expenses:Mercado', |
|
'Fruit City': 'Expenses:Mercado', |
|
'Padaria e Confeitaria': 'Expenses:Mercado', |
|
'Cleyton G. Montuan': 'Expenses:Mercado', |
|
'la Sardina': 'Expenses:Mercado', |
|
'Sams': 'Expenses:Mercado', |
|
'Villa Hortifruti': 'Expenses:Mercado', |
|
'Sebastiaocarlosde': 'Expenses:Mercado', |
|
'Jonathanmarcelo': 'Expenses:Mercado', |
|
'Rogeralvesdemelo': 'Expenses:Mercado', |
|
'Marlyarnautpena': 'Expenses:Mercado', |
|
'Fsf Comercio de Alime': 'Expenses:Mercado', |
|
'Tentacao': 'Expenses:Mercado', |
|
'Granovie Mercado Nat': 'Expenses:Mercado', |
|
'Tercioluizcaldas': 'Expenses:Mercado', |
|
'Ricardopescados': 'Expenses:Mercado', |
|
'Villarreal': 'Expenses:Mercado', |
|
'Alexcasaltavieira': 'Expenses:Mercado', |
|
'Mp *Fabiotourrj': 'Expenses:Mercado', |
|
'Tauste': 'Expenses:Mercado', |
|
'Emporio Santa Gula': 'Expenses:Mercado', |
|
'Guilherme da S Leal': 'Expenses:Mercado', |
|
'Padaria': 'Expenses:Mercado', |
|
'Aparecidadefatima': 'Expenses:Mercado', |
|
'Carol Alimentos Aparec': 'Expenses:Mercado', |
|
'Zelainepescados': 'Expenses:Mercado', |
|
'David': 'Expenses:Mercado', |
|
'Pg *Mada Comercio de R': 'Expenses:Mercado', |
|
'Ldonizettidefaria': 'Expenses:Mercado', |
|
'Sitio Verde': 'Expenses:Mercado', |
|
'Josericardopralon': 'Expenses:Mercado', |
|
'Mercadinho Saturno': 'Expenses:Mercado', |
|
'39452045tania': 'Expenses:Mercado', |
|
'Marcelodefaria':'Expenses:Mercado', |
|
'Supermercado Jk':'Expenses:Mercado', |
|
'Marcelavitoriano':'Expenses:Mercado', |
|
'Leandro':'Expenses:Mercado', |
|
'Diallalopesdias':'Expenses:Restaurante', |
|
'Golden':'Expenses:Restaurante', |
|
'Estacao Coronel':'Expenses:Restaurante', |
|
'Fbr':'Expenses:Restaurante', |
|
'Segretini':'Expenses:Restaurante', |
|
'Fairy Cake':'Expenses:Restaurante', |
|
'Empanada':'Expenses:Restaurante', |
|
'Kfc':'Expenses:Restaurante', |
|
'Sapore':'Expenses:Restaurante', |
|
'Gopag S*Ponto da Parri':'Expenses:Restaurante', |
|
'Ifd':'Expenses:Restaurante', |
|
'Grand Parrilla':'Expenses:Restaurante', |
|
'Bigjohnburguer':'Expenses:Restaurante', |
|
'Sjkbarerestaurant':'Expenses:Restaurante', |
|
'Divino Fogao':'Expenses:Restaurante', |
|
'Elianeferreirade':'Expenses:Restaurante', |
|
'Loja Nova - Bullguer':'Expenses:Restaurante', |
|
'Frangoefilegrill':'Expenses:Restaurante', |
|
'Pointdoacai':'Expenses:Restaurante', |
|
'Nsa Comercio Alimentos':'Expenses:Restaurante', |
|
'Churraskilo':'Expenses:Restaurante', |
|
'Cristiane Tieko':'Expenses:Restaurante', |
|
'Leandro Aparecido ':'Expenses:Restaurante', |
|
'Aguadecocos':'Expenses:Restaurante', |
|
'Fazenda da Comadre':'Expenses:Restaurante', |
|
'Rrpgelateria':'Expenses:Restaurante', |
|
'Chiquinho':'Expenses:Restaurante', |
|
'Xun Barbaresco':'Expenses:Restaurante', |
|
'Mp *Helena':'Expenses:Restaurante', |
|
'Mara Cakes':'Expenses:Restaurante', |
|
'Hokkaido':'Expenses:Restaurante', |
|
'Pointdoacai"':'Expenses:Restaurante', |
|
'Cacau Show':'Expenses:Restaurante', |
|
'Subway':'Expenses:Restaurante', |
|
'Toquedenut':'Expenses:Restaurante', |
|
'Zp*Barbaresco Express':'Expenses:Restaurante', |
|
'Pastel da Neni':'Expenses:Restaurante', |
|
'Hoken Sushi':'Expenses:Restaurante', |
|
'Gelateria Borelli':'Expenses:Restaurante', |
|
'Cafe Donuts':'Expenses:Restaurante', |
|
'Reaiche':'Expenses:Restaurante', |
|
'Melt':'Expenses:Restaurante', |
|
'Restaurante':'Expenses:Restaurante', |
|
'Xttbarbaresco':'Expenses:Restaurante', |
|
'Ternopil':'Expenses:Restaurante', |
|
'Rei do Mate Vale Sul':'Expenses:Restaurante', |
|
'Troina':'Expenses:Restaurante', |
|
'Lanchonete':'Expenses:Restaurante', |
|
'Container Burger':'Expenses:Restaurante', |
|
'Maicon Douglas de Oliv':'Expenses:Restaurante', |
|
'Bom Pastel':'Expenses:Restaurante', |
|
'Lcs Vale Restaurante':'Expenses:Restaurante', |
|
'Orient Express':'Expenses:Restaurante', |
|
'Boali - Colinas Shop':'Expenses:Restaurante', |
|
'Fwl Alimentos':'Expenses:Restaurante', |
|
'Bacio Di Latte': 'Expenses:Restaurante', |
|
'Oakberry Acai': 'Expenses:Restaurante', |
|
'Espresso Latte': 'Expenses:Restaurante', |
|
'Mc Donalds': 'Expenses:Restaurante', |
|
'Ifood': 'Expenses:Restaurante', |
|
'Zig*Fazenda da Vovo': 'Expenses:Restaurante', |
|
'Demoiselle Restaurante': 'Expenses:Restaurante', |
|
'Sheriff Beer Bbq': 'Expenses:Restaurante', |
|
'Kiosque Kalango': 'Expenses:Restaurante', |
|
'Mais1 Cafe Sp': 'Expenses:Restaurante', |
|
'Pizza': 'Expenses:Restaurante', |
|
'Outback': 'Expenses:Restaurante', |
|
'Ifd*': 'Expenses:Restaurante', |
|
'Apa Vale': 'Expenses:Restaurante', |
|
'Chale da Pamonha': 'Expenses:Restaurante', |
|
'Spetinho': 'Expenses:Restaurante', |
|
'Boteco': 'Expenses:Restaurante', |
|
'Asami Sushi': 'Expenses:Restaurante', |
|
'Patia': 'Expenses:Restaurante', |
|
'Barbaresco': 'Expenses:Restaurante', |
|
'Al Badah': 'Expenses:Restaurante', |
|
'Coco Bambu': 'Expenses:Restaurante', |
|
'Jardim do Cafe': 'Expenses:Restaurante', |
|
'Fuxiqueira Santos': 'Expenses:Restaurante', |
|
'Sandra Uema Sao Vicent': 'Expenses:Restaurante', |
|
'Capodarte Oscar': 'Expenses:Restaurante', |
|
'Acqio*Up Urbapizza': 'Expenses:Restaurante', |
|
'Posto 012 e Esporte': 'Expenses:Restaurante', |
|
'Cap Quiero': 'Expenses:Restaurante', |
|
'Toca do Mineiro':'Expenses:Restaurante', |
|
'Lig Lig':'Expenses:Restaurante', |
|
'Hospital Policlin':'Expenses:Familia:Saude', |
|
'Drogasil':'Expenses:Familia:Saude', |
|
'Byoformula Sjc':'Expenses:Familia:Saude', |
|
'Mercadopago *Growthsu':'Expenses:Familia:Saude', |
|
'Pg *Pp Reggia':'Expenses:Familia:Saude', |
|
'Hosp Vivalle':'Expenses:Familia:Saude', |
|
'Pg *Pp Terapeutica':'Expenses:Familia:Saude', |
|
'Farma':'Expenses:Familia:Saude', |
|
'Cipax':'Expenses:Familia:Saude', |
|
'Original Nutri':'Expenses:Familia:Saude', |
|
'Hakim Farma':'Expenses:Familia:Saude', |
|
'Raia': 'Expenses:Familia:Saude', |
|
'Elisvacinas': 'Expenses:Familia:Saude', |
|
'Drogaria': 'Expenses:Familia:Saude', |
|
'Pg *Medmanipulado': 'Expenses:Familia:Saude', |
|
'Mp *Growthsupplements': 'Expenses:Familia:Saude', |
|
'Terapeutica Farmacia': 'Expenses:Familia:Saude', |
|
'Bp Crs':'Expenses:Transporte:Carro', |
|
'Urentcar':'Expenses:Transporte:Carro', |
|
'Gp Negocios Lava Rapid':'Expenses:Transporte:Carro', |
|
'Maxx Jato':'Expenses:Transporte:Carro', |
|
'Postosete':'Expenses:Transporte:Carro', |
|
'Allpark':'Expenses:Transporte:Carro', |
|
'Auto Posto':'Expenses:Transporte:Carro', |
|
'Vallparking':'Expenses:Transporte:Carro', |
|
'Mss Park Estacionament':'Expenses:Transporte:Carro', |
|
'Mobiciti':'Expenses:Transporte:Carro', |
|
'Cpark':'Expenses:Transporte:Carro', |
|
'Ayuni Centro Automot':'Expenses:Transporte:Carro', |
|
'NuTag':'Expenses:Transporte:Carro', |
|
'Posto-2504-Colinas':'Expenses:Transporte:Carro', |
|
'Mhh':'Expenses:Transporte:Carro', |
|
'MOBILICIDADE':'Expenses:Transporte:Carro', |
|
'Zae':'Expenses:Transporte:Carro', |
|
'Estacionamento': 'Expenses:Transporte:Carro', |
|
'Autoposto': 'Expenses:Transporte:Carro', |
|
'Ancar Parking': 'Expenses:Transporte:Carro', |
|
'Transação de NuTag': 'Expenses:Transporte:Carro', |
|
'Executive Parking': 'Expenses:Transporte:Carro', |
|
'Colinas Pay': 'Expenses:Transporte:Carro', |
|
'Yelum': 'Expenses:Transporte:Carro', |
|
'Liberty Seguros': 'Expenses:Transporte:Carro', |
|
'Buser': 'Expenses:Transporte:Uber', |
|
'Uber': 'Expenses:Transporte:Uber', |
|
'Movida': 'Expenses:Transporte:Uber', |
|
'Vale Eventos':'Expenses:Lazer', |
|
'Mp *Camilashesebr':'Expenses:Lazer', |
|
'Senhor Smart':'Expenses:Lazer', |
|
'Pg *Pedido Pago Intern':'Expenses:Lazer', |
|
'Swsjcroupase':'Expenses:Lazer', |
|
'Nagata Shoes':'Expenses:Lazer', |
|
'Eccere Centro':'Expenses:Lazer', |
|
'On Mart ':'Expenses:Lazer', |
|
'Julliekelly':'Expenses:Lazer', |
|
'Asa*Gabriel':'Expenses:Lazer', |
|
'Stb*Buscacenter':'Expenses:Lazer', |
|
'Angel Presentes':'Expenses:Lazer', |
|
'Trailler':'Expenses:Lazer', |
|
'Du Chapeu':'Expenses:Lazer', |
|
'Aqualanches':'Expenses:Lazer', |
|
'Wdn':'Expenses:Lazer', |
|
'Bazar Baby Kids':'Expenses:Lazer', |
|
'Surbanova':'Expenses:Lazer', |
|
'00355 Sh Colinas':'Expenses:Lazer', |
|
'00030 Sh Centervale':'Expenses:Lazer', |
|
'Empreendimentos Turist':'Expenses:Lazer', |
|
'Lxj Presentes':'Expenses:Lazer', |
|
'Reve Perfumes':'Expenses:Lazer', |
|
'Animalia':'Expenses:Lazer', |
|
'Glow Park':'Expenses:Lazer', |
|
'Casa de Bolos Urbanova':'Expenses:Lazer', |
|
'Pesqueiro':'Expenses:Lazer', |
|
'Mio':'Expenses:Lazer', |
|
'Hillarius':'Expenses:Lazer', |
|
'Cinemark':'Expenses:Lazer', |
|
'Fontanezi Franquias':'Expenses:Lazer', |
|
'Tcketnamao':'Expenses:Lazer', |
|
'Stafeba*Lanchdinossa':'Expenses:Lazer', |
|
'Georgesseven':'Expenses:Lazer', |
|
'Idealizzare':'Expenses:Lazer', |
|
'Cervejaria':'Expenses:Lazer', |
|
'Domis Milho':'Expenses:Lazer', |
|
'Sorveteira':'Expenses:Lazer', |
|
'Zig*Sea Lounge':'Expenses:Lazer', |
|
'Dibran Turismo':'Expenses:Lazer', |
|
'Zp *Elo7 Elo7':'Expenses:Lazer', |
|
'Shein':'Expenses:Lazer', |
|
'Vivara':'Expenses:Lazer', |
|
'Shx':'Expenses:Lazer', |
|
'Saborqueromais':'Expenses:Lazer', |
|
'Paroquia':'Expenses:Lazer', |
|
'Tecplant':'Expenses:Lazer', |
|
'Jian Presentes':'Expenses:Lazer', |
|
'King Bike':'Expenses:Lazer', |
|
'Sp Sao Jose Campos Val':'Expenses:Lazer', |
|
'Vmt Telecomunicacoes':'Expenses:Lazer', |
|
'8gs Diversoes':'Expenses:Lazer', |
|
'Oitopeia':'Expenses:Lazer', |
|
'Pousada':'Expenses:Lazer', |
|
'Redecine-Valesul Cine':'Expenses:Lazer', |
|
'Sorveteria':'Expenses:Lazer', |
|
'Mambuca Village':'Expenses:Lazer', |
|
'Acqualanches':'Expenses:Lazer', |
|
'Zp *Thermas':'Expenses:Lazer', |
|
'Redecine-Valesul ':'Expenses:Lazer', |
|
'Kopenhagen':'Expenses:Lazer', |
|
'Lvmlojade':'Expenses:Lazer', |
|
'Shopping Certer Valle':'Expenses:Lazer', |
|
'Daiso':'Expenses:Lazer', |
|
'Easyjet':'Expenses:Lazer', |
|
'7071 Sao Jose dos Ca':'Expenses:Lazer', |
|
'Quintal Bar Food Skate':'Expenses:Lazer', |
|
'Lojas Renascer':'Expenses:Lazer', |
|
'Sympla':'Expenses:Lazer', |
|
'Toykidzmachine': 'Expenses:Lazer', |
|
'Festou': 'Expenses:Lazer', |
|
'Netflix': 'Expenses:Lazer', |
|
'S J': 'Expenses:Lazer', |
|
'Pg *Ton Maria Eduard': 'Expenses:Lazer', |
|
'Jetshr': 'Expenses:Lazer', |
|
'Sonia Batistela':'Expenses:Lazer', |
|
'Pe de Moleque da Barra':'Expenses:Lazer', |
|
'Botiques':'Expenses:Familia:Priscila', |
|
'Mundo do Cabeleireiro':'Expenses:Familia:Priscila', |
|
'Otica Urbanova':'Expenses:Familia:Priscila', |
|
'Fcs Diagnostico Odont':'Expenses:Familia:Priscila', |
|
'Julio Cesar':'Expenses:Familia:Priscila', |
|
'Polyotica':'Expenses:Familia:Priscila', |
|
'Mp *Dceatariesmal':'Expenses:Familia:Priscila', |
|
'Fashion Biju':'Expenses:Familia:Priscila', |
|
'We Pink':'Expenses:Familia:Priscila', |
|
'Masterformula':'Expenses:Familia:Priscila', |
|
'Toy Life':'Expenses:Familia:Priscila', |
|
'Studio Velocity':'Expenses:Familia:Priscila', |
|
'Jefferson Fernando':'Expenses:Familia:Priscila', |
|
'Trhs Amis Boutique':'Expenses:Familia:Priscila', |
|
'Polyotica Colinas':'Expenses:Familia:Priscila', |
|
'Studio Francesca Bian':'Expenses:Familia:Priscila', |
|
'D. Cestari Esmalteria':'Expenses:Familia:Priscila', |
|
'Matsunos':'Expenses:Familia:Priscila', |
|
'Urba Cell':'Expenses:Familia:Priscila', |
|
'Loungerie':'Expenses:Familia:Priscila', |
|
'Lojas Renner':'Expenses:Familia:Priscila', |
|
'Youcom':'Expenses:Familia:Priscila', |
|
'Accv Calcados':'Expenses:Familia:Priscila', |
|
'Apple.Com': 'Expenses:Familia:Priscila', |
|
'Lemoa Cosmeticos': 'Expenses:Familia:Priscila', |
|
'Uni Esmalteria': 'Expenses:Familia:Priscila', |
|
'Applecombill': 'Expenses:Familia:Priscila', # Apple subscription |
|
'Manavet Servicos Vete':'Expenses:Cachorro', |
|
'Popular Pet': 'Expenses:Cachorro', |
|
'Gift Mix Papelaria':'Expenses:Familia:Filhos', |
|
'Stb*Fofo Papelaria':'Expenses:Familia:Filhos', |
|
'Big Jump Park Rio':'Expenses:Familia:Filhos', |
|
'Gigio':'Expenses:Familia:Filhos', |
|
'Livestorebrasi':'Expenses:Familia:Filhos', |
|
'Gigafesta':'Expenses:Familia:Filhos', |
|
'Franco Bolfarini':'Expenses:Familia:Filhos', |
|
'Leitura Colinas':'Expenses:Familia:Filhos', |
|
'Nutrebem':'Expenses:Familia:Filhos', |
|
'Facilivro':'Expenses:Familia:Filhos', |
|
'D.D.K.S Confeccoes':'Expenses:Familia:Filhos', |
|
'Mercadolivre*Trwindus':'Expenses:Familia:Filhos', |
|
'Kalunga':'Expenses:Familia:Filhos', |
|
'Inovathi':'Expenses:Familia:Filhos', |
|
'Cea Vsu 203':'Expenses:Familia:Filhos', |
|
'Decathlon':'Expenses:Familia:Filhos', |
|
'Fr4 Fitness': 'Expenses:Familia:Filhos', |
|
'Rm Confeccoes': 'Expenses:Familia:Filhos', |
|
'Gracie Barra': 'Expenses:Familia:Filhos', |
|
'Papelaria': 'Expenses:Familia:Filhos', |
|
'Arena': 'Expenses:Familia:Filhos', |
|
'Tribo do Papel': 'Expenses:Familia:Filhos', |
|
'Puket': 'Expenses:Familia:Filhos', |
|
'Don Guillermo': 'Expenses:Familia:Filhos', |
|
'Pbkids Brinquedos':'Expenses:Familia:Filhos', |
|
'Baby Hair':'Expenses:Familia:Filhos', |
|
'Ri Happy Brinquedos':'Expenses:Familia:Filhos', |
|
'Granovie':'Expenses:Familia:Renzo', |
|
'Prainha':'Expenses:Familia:Renzo', |
|
'Greenn**Educacao Real':'Expenses:Familia:Renzo', |
|
'Asaas *Tudo':'Expenses:Familia:Renzo', |
|
'Fisia Nfs2050':'Expenses:Familia:Renzo', |
|
'Farma Reggia Farm':'Expenses:Familia:Renzo', |
|
'Xtrack Sjc':'Expenses:Familia:Renzo', |
|
'Centauro':'Expenses:Familia:Renzo', |
|
'Hering':'Expenses:Familia:Renzo', |
|
'Fisia Nike Ecommer':'Expenses:Familia:Renzo', |
|
'Ppro *Microsoft': 'Expenses:Familia:Renzo', |
|
'Penaareiasports': 'Expenses:Familia:Renzo', |
|
'Abraao Barbearia': 'Expenses:Familia:Renzo', |
|
'Casa das Cuecas': 'Expenses:Familia:Renzo', |
|
'Democrata': 'Expenses:Familia:Renzo', |
|
'Centauro Ce85': 'Expenses:Familia:Renzo', |
|
'Insider Come': 'Expenses:Familia:Renzo', |
|
'Cyclex': 'Expenses:Familia:Renzo', |
|
'S Stein Joalheiros': 'Expenses:Familia:Renzo', |
|
'Quiosquepenaareia':'Expenses:Familia:Renzo', |
|
'Pgz*Oreidamot':'Assets:Bens:Moto', |
|
'Pagamento recebido': 'Assets:Caixa:Nubank:Renzo', |
|
'Prudent*Apol': 'Expenses:SeguroDeVida', |
|
'Microsoft': 'Equity:PythonPro:Estorno', |
|
'Arranjos': 'Expenses:Casa', |
|
'Mp *4produtos': 'Expenses:Casa', |
|
'Emporio Verde Garden': 'Expenses:Casa', |
|
'Hvn': 'Expenses:Casa', |
|
'Rw Negocios Comerciai': 'Expenses:Casa', |
|
'Condominio': 'Expenses:Casa', |
|
'Sodimac': 'Expenses:Casa', |
|
'Belcenter': 'Expenses:Casa', |
|
'Havan': 'Expenses:Casa', |
|
'Conta Vivo': 'Expenses:Casa', |
|
'Americanas *Electrolu': 'Expenses:Casa', |
|
'Leroy Merlin': 'Expenses:Casa', |
|
'Construdecor S A': 'Expenses:Casa', |
|
'Bradesco Auto Re': 'Expenses:Casa', |
|
'Lavtop': 'Expenses:Casa', |
|
'Varejao': 'Expenses:Casa', |
|
'Ajuste a crédito': 'Expenses:Casa', # Crédito de ajuste vai para Casa |
|
} |
|
|
|
|
|
def extract_existing_keys(entries: List) -> Set[str]: |
|
""" |
|
Extrai chaves únicas de transações existentes para detecção de duplicatas. |
|
Chave: "{data.isoformat()}-{nubank_id}" |
|
""" |
|
keys = set() |
|
for entry in entries: |
|
if isinstance(entry, data.Transaction): |
|
if hasattr(entry, 'meta') and 'nubank_id' in entry.meta: |
|
key = f"{entry.date.isoformat()}-{entry.meta['nubank_id']}" |
|
keys.add(key) |
|
return keys |
|
|
|
|
|
def categorize_transaction(memo: str, prefix_dict: dict) -> str: |
|
""" |
|
Categoriza transação baseado no memo usando dicionário de prefixos |
|
""" |
|
for prefix, account in prefix_dict.items(): |
|
if memo.startswith(prefix): |
|
return account |
|
return 'Expenses:Unknown' |
|
|
|
|
|
def categorize_entries(entries: List) -> dict: |
|
""" |
|
Agrupa transações por categoria para resumo |
|
""" |
|
categories = {} |
|
for entry in entries: |
|
if isinstance(entry, data.Transaction): |
|
for posting in entry.postings: |
|
account = posting.account |
|
if account.startswith('Expenses:') or account.startswith('Income:'): |
|
if account not in categories: |
|
categories[account] = {'count': 0, 'total': 0} |
|
categories[account]['count'] += 1 |
|
if posting.units: |
|
categories[account]['total'] += float(posting.units.number) |
|
return categories |
|
|
|
|
|
def parse_nubank_ofx(ofx_path: str, existing_entries: List = None) -> Tuple[List, str]: |
|
""" |
|
Parse arquivo OFX do Nubank e retorna lista de entries beancount |
|
|
|
Args: |
|
ofx_path: Caminho para o arquivo OFX |
|
existing_entries: Entries existentes do ledger (para calcular balance correto) |
|
|
|
Returns: |
|
(entries, account_type): Lista de entries e tipo de conta ('credit' ou 'account') |
|
""" |
|
entries = [] |
|
|
|
with open(ofx_path, 'rb') as f: |
|
ofx = OfxParser.parse(f) |
|
|
|
# Determinar tipo de conta baseado na estrutura OFX |
|
if hasattr(ofx, 'account'): |
|
statement = ofx.account.statement |
|
|
|
# Detectar tipo: creditcard tem routing_number None, bank account tem routing_number |
|
if hasattr(ofx.account, 'routing_number') and ofx.account.routing_number: |
|
account_type = 'account' |
|
base_account = 'Assets:Caixa:Nubank:Renzo' |
|
else: |
|
# Cartão de crédito (ou checar se tem account_type == 'CREDITLINE') |
|
account_type = 'credit' |
|
base_account = 'Liabilities:Cartoes:Nubank' |
|
else: |
|
raise ValueError("Formato OFX não reconhecido") |
|
|
|
# Processar transações |
|
for idx, transaction in enumerate(statement.transactions): |
|
meta = data.new_metadata(ofx_path, idx) |
|
meta['nubank_id'] = transaction.id |
|
|
|
# Categorizar transação |
|
if account_type == 'credit': |
|
expense_account = categorize_transaction(transaction.memo, _prefix_account_dct) |
|
|
|
# Criar transação de cartão de crédito |
|
txn = data.Transaction( |
|
meta, |
|
transaction.date.date(), |
|
'*', |
|
None, |
|
transaction.memo, |
|
data.EMPTY_SET, |
|
data.EMPTY_SET, |
|
[ |
|
data.Posting( |
|
'Liabilities:Cartoes:Nubank', |
|
amount.Amount(transaction.amount, 'BRL'), |
|
None, None, None, None |
|
), |
|
data.Posting( |
|
expense_account, |
|
amount.Amount(-transaction.amount, 'BRL'), |
|
None, None, None, None |
|
), |
|
] |
|
) |
|
else: |
|
# Conta corrente - categorizar baseado em patterns |
|
if any(pattern.search(transaction.memo) for pattern in _renzo_memo_patterns): |
|
contra_account = 'Expenses:Unknown' |
|
else: |
|
contra_account = 'Expenses:Unknown' |
|
|
|
txn = data.Transaction( |
|
meta, |
|
transaction.date.date(), |
|
'*', |
|
None, |
|
transaction.memo, |
|
data.EMPTY_SET, |
|
data.EMPTY_SET, |
|
[ |
|
data.Posting( |
|
base_account, |
|
amount.Amount(transaction.amount, 'BRL'), |
|
None, None, None, None |
|
), |
|
data.Posting( |
|
contra_account, |
|
amount.Amount(-transaction.amount, 'BRL'), |
|
None, None, None, None |
|
), |
|
] |
|
) |
|
|
|
entries.append(txn) |
|
|
|
# NÃO adicionar balance entry automaticamente |
|
# O saldo do OFX é apenas do mês, não inclui saldo anterior |
|
# Para cartões de crédito, o saldo do OFX não considera faturas passadas |
|
|
|
return entries, account_type |
|
|
|
|
|
def reconcile_ofx(ofx_path: str, ledger_path: str, auto_commit: bool = False) -> Tuple[str, Optional[List]]: |
|
""" |
|
Processa arquivo OFX e retorna transações novas. |
|
|
|
Args: |
|
ofx_path: Caminho para o arquivo OFX |
|
ledger_path: Caminho para transactions.beancount |
|
auto_commit: Se True, adiciona automaticamente ao ledger |
|
|
|
Returns: |
|
(mensagem, entries): Mensagem de resultado e lista de entries (se houver) |
|
""" |
|
try: |
|
# Verificar se arquivo existe |
|
if not Path(ofx_path).exists(): |
|
return f"❌ Arquivo não encontrado: {ofx_path}", None |
|
|
|
# Verificar formato do arquivo |
|
file_name = Path(ofx_path).name |
|
if not (file_name.endswith('.ofx') or file_name.endswith('.OFX')): |
|
return "❌ Arquivo deve ser .ofx", None |
|
|
|
# 1. Carregar ledger existente |
|
entries, errors, options = load_file(ledger_path) |
|
|
|
# Ignorar erros de validação do ledger existente - não afetam a conciliação |
|
# if errors: |
|
# error_msgs = [str(e) for e in errors[:3]] |
|
# return f"⚠️ Avisos ao carregar ledger:\n" + "\n".join(error_msgs), None |
|
|
|
# 2. Extrair chaves existentes para deduplicação |
|
existing_keys = extract_existing_keys(entries) |
|
|
|
# 3. Parsear arquivo OFX |
|
try: |
|
new_entries, account_type = parse_nubank_ofx(ofx_path, entries) |
|
except Exception as e: |
|
return f"❌ Erro ao processar OFX:\n{str(e)}", None |
|
|
|
if not new_entries: |
|
return "ℹ️ Nenhuma transação encontrada no arquivo.", None |
|
|
|
total_entries = len([e for e in new_entries if isinstance(e, data.Transaction)]) |
|
|
|
# 4. Filtrar duplicatas |
|
filtered_entries = [] |
|
duplicates_count = 0 |
|
|
|
for entry in new_entries: |
|
if isinstance(entry, data.Transaction): |
|
if hasattr(entry, 'meta') and 'nubank_id' in entry.meta: |
|
key = f"{entry.date.isoformat()}-{entry.meta['nubank_id']}" |
|
if key in existing_keys: |
|
duplicates_count += 1 |
|
continue |
|
filtered_entries.append(entry) |
|
else: |
|
# Balance entries sempre incluir |
|
filtered_entries.append(entry) |
|
|
|
new_transactions_count = len([e for e in filtered_entries if isinstance(e, data.Transaction)]) |
|
|
|
if new_transactions_count == 0: |
|
return f"✅ Todas as {total_entries} transações deste extrato já estão no ledger (duplicatas).", None |
|
|
|
# 5. Categorizar para resumo |
|
categories = categorize_entries(filtered_entries) |
|
unknown_count = categories.get('Expenses:Unknown', {}).get('count', 0) |
|
|
|
# 6. Formatar saída |
|
output_lines = [] |
|
for entry in filtered_entries: |
|
output_lines.append(printer.format_entry(entry)) |
|
|
|
output = '\n'.join(output_lines) |
|
|
|
# 7. Criar mensagem de resumo |
|
summary = f"📊 *Extrato processado:* {file_name}\n\n" |
|
summary += f"✅ {total_entries} transações encontradas\n" |
|
summary += f"🆕 {new_transactions_count} transações novas\n" |
|
|
|
if duplicates_count > 0: |
|
summary += f"🔄 {duplicates_count} duplicatas removidas\n" |
|
|
|
summary += f"\n*Categorias:*\n" |
|
for account, stats in sorted(categories.items(), key=lambda x: -x[1]['count'])[:5]: |
|
account_name = account.split(':')[-1] |
|
summary += f"• {account_name}: {stats['count']} transações (R$ {abs(stats['total']):.2f})\n" |
|
|
|
if unknown_count > 0: |
|
summary += f"\n⚠️ {unknown_count} transações precisam de categorização manual\n" |
|
|
|
# 8. Auto-commit se solicitado |
|
if auto_commit: |
|
with open(ledger_path, 'a', encoding='utf-8') as f: |
|
f.write('\n' + output + '\n') |
|
|
|
summary += f"\n✅ Transações adicionadas ao ledger!" |
|
return summary, filtered_entries |
|
|
|
summary += f"\n_Digite 'confirmar' para adicionar ao ledger ou 'cancelar' para descartar._" |
|
|
|
return summary, filtered_entries |
|
|
|
except Exception as e: |
|
import traceback |
|
return f"❌ Erro inesperado:\n{str(e)}\n\n{traceback.format_exc()}", None |
|
|
|
|
|
if __name__ == "__main__": |
|
if len(sys.argv) < 2: |
|
print("Uso: python reconcile_ofx.py <arquivo.ofx> [--commit]") |
|
sys.exit(1) |
|
|
|
ofx_path = sys.argv[1] |
|
auto_commit = '--commit' in sys.argv |
|
ledger_path = '/workspace/extra/projects/contabilidade-pessoal/transactions.beancount' |
|
|
|
result, entries = reconcile_ofx(ofx_path, ledger_path, auto_commit) |
|
print(result) |
|
|
|
if entries and not auto_commit: |
|
print("\n" + "="*60) |
|
print("TRANSAÇÕES:") |
|
print("="*60) |
|
for entry in entries: |
|
if isinstance(entry, data.Transaction): |
|
print(printer.format_entry(entry)) |