commit b305173b9daab2b17bf702334a1df13255530c3b Author: Christoph Date: Sun Nov 9 12:26:46 2025 -0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/RicoToGhostfolio.iml b/.idea/RicoToGhostfolio.iml new file mode 100644 index 0000000..6f7c1f5 --- /dev/null +++ b/.idea/RicoToGhostfolio.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e899fe4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0de4795 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea862cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# Use the official Python image +FROM python:3.12-slim + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY . /code/app + +CMD ["fastapi", "run", "app/main.py", "--port", "8000"] \ No newline at end of file diff --git a/converter.py b/converter.py new file mode 100644 index 0000000..6d8feb1 --- /dev/null +++ b/converter.py @@ -0,0 +1,112 @@ +import io +import logging +import simplejson as json +import pandas as pd +import mappings +import ghostfolio_service +import datetime +from correpy.domain.enums import TransactionType +from correpy.parsers.brokerage_notes.parser_factory import ParserFactory +from fastapi import UploadFile + +account_id = '1590f1bb-b3ee-4dc8-ac46-bd1f15a83e87' + +logger = logging.getLogger(__name__) + + +def read_brokerage_pdf(file: UploadFile): + content = io.BytesIO(file.file.read()) + content.seek(0) + + logger.info(f'Processing {file.filename} ') + + try: + brokerage_notes = ParserFactory(brokerage_note=content, password='052').parse() + return convert_to_ghostfolio(brokerage_notes, file.filename) + except Exception as e: + print(f'Failed to parse {file.filename}: {e}') + logger.error(f'Failed to parse {file.filename}: {e}') + return 500 + + +def convert_to_ghostfolio(brokerage_list: list, filename: str): + activities = {} + + for brokerage_note in brokerage_list: + reference_date = brokerage_note.reference_date.isoformat() + 'T00:00:00.000Z' + fees = (brokerage_note.settlement_fee + brokerage_note.emoluments + brokerage_note.others) / len( + brokerage_note.transactions) + + # activities['meta'] = {'date': datetime.datetime.now().isoformat(), 'version': 'v0'} + activities['activities'] = [] + # activities['updateCashBalance'] = 'false' + + for transaction in brokerage_note.transactions: + + # activity['fee'] = (brokerage_note.settlement_fee + brokerage_note.emoluments) + + transaction_type: str = '' + + if transaction.transaction_type == TransactionType.BUY: + transaction_type = 'BUY' + elif transaction.transaction_type == TransactionType.SELL: + transaction_type = 'SELL' + + activity = {'accountId': account_id, 'comment': filename, 'date': reference_date, + 'type': transaction_type, + 'fee': fees, 'unitPrice': transaction.unit_price, 'quantity': transaction.amount, + 'symbol': mappings.name_to_tickers[transaction.security.name.strip()].strip(), + 'currency': 'BRL', + 'dataSource': 'YAHOO'} + activities['activities'].append(activity) + + if len(activity['symbol']) < 1: + logger.info(f'No symbol for {transaction.security.name}') + + result = ghostfolio_service.import_activities(activities, filename) + + return result + + +def read_dividends_xlsx(file: UploadFile): + dividends_xlsx = file.file + df = pd.read_excel(dividends_xlsx, engine='openpyxl') + path = f'./{file.filename}' + df.to_csv(path, encoding='utf-8') + df = pd.read_csv(path, encoding='utf-8') + df = df[df['Produto'].notnull()] + df = df[df['Tipo de Evento'] != 'PAGAMENTO DE JUROS'] + + return dividends_df_to_ghostfolio(df, file.filename) + + +def dividends_df_to_ghostfolio(dividends_df: pd.DataFrame, filename: str): + activities = {"activities": []} + + for index, row in dividends_df.iterrows(): + payment_date = datetime.datetime.strptime(row['Pagamento'], "%d/%m/%Y").isoformat() + name = row['Produto'].split('-')[0].strip() + '.SA' + + if name in mappings.ticker_changes: + symbol = mappings.ticker_changes[name] + else: + symbol = name + + activity = { + "accountId": account_id, + "date": payment_date, + "comment": filename, + "currency": "BRL", + "dataSource": "YAHOO", + "quantity": row['Quantidade'], + "type": "DIVIDEND", + "symbol": symbol, + "unitPrice": row['Preço unitário'], + "fee": 0 + } + + activities["activities"].append(activity) + + print(activities) + + return ghostfolio_service.import_activities(activities, filename) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5b14fa4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + rico-to-ghostfolio: + ports: + - "8001:8000" + build: + dockerfile: Dockerfile \ No newline at end of file diff --git a/ghostfolio_service.py b/ghostfolio_service.py new file mode 100644 index 0000000..fb6ecf1 --- /dev/null +++ b/ghostfolio_service.py @@ -0,0 +1,40 @@ +import logging + +import requests +import simplejson as json +import ntfy_service + +logger = logging.getLogger(__name__) + + +def get_bearer_token(): + response = requests.post('http://192.168.1.202:3333/api/v1/auth/anonymous', data={ + "accessToken": "cab07a6b0a87711013ee5457411a26c7c7dd2787830b64b914d5678d4dc54af911752975380467fd73c3798b043855520ab148a372d8fd859860703833e96cba"} + ) + + token = response.json()['authToken'] + + return token + + +def import_activities(activities_json, filename: str): + bearer_token = get_bearer_token() + + data = json.dumps(activities_json) + + response = requests.post('http://192.168.1.202:3333/api/v1/import', data=data, + headers={"Authorization": "Bearer " + bearer_token, "Content-Type": "application/json"}) + + print(response.json()) + + if len(response.json()['activities']) < 1: + logger.info(f'No activity imported for {filename}') + print(f'No activity imported for {filename}') + ntfy_service.send_notification(f'No activity imported for {filename}') + else: + ntfy_service.send_notification(f'Imported {len(response.json()['activities'])} activities from {filename}') + print(f'Imported {len(response.json()['activities'])} activities from {filename}') + + # print(json.dumps(response.json(), indent=4)) + + return response.status_code diff --git a/main.py b/main.py new file mode 100644 index 0000000..3e3a90c --- /dev/null +++ b/main.py @@ -0,0 +1,30 @@ +import logging +from starlette.requests import Request +from starlette.responses import Response +import uvicorn +from fastapi import FastAPI, Form, UploadFile, File +import ntfy_service + +from converter import read_brokerage_pdf, read_dividends_xlsx +import io +app = FastAPI() + +logger = logging.getLogger(__name__) +logging.basicConfig(filename='rico-to-ghostfolio.log', level=logging.INFO) + +@app.post("/brokerage") +async def convert_pdf(file: UploadFile = File(...)): + return read_brokerage_pdf(file) + +@app.post("/b3dividends") +async def convert_pdf(file: UploadFile = File(...)): + return read_dividends_xlsx(file) + +@app.post("/notification") +async def notification(request: Request): + message = await request.body() + return ntfy_service.send_notification(message.decode('utf8')) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/mappings.py b/mappings.py new file mode 100644 index 0000000..6388413 --- /dev/null +++ b/mappings.py @@ -0,0 +1,16 @@ +name_to_tickers = { + "BRASIL ON NM" : 'BBAS3.SA', + "BRASIL ON EJ NM": 'BBAS3.SA', + "METAL LEVE ON NM": 'LEVE3.SA', + "METAL LEVE ON ED NM": 'LEVE3.SA', + "TRAN PAULIST PN N1": 'ISAE4.SA', + "ISA ENERGIA PN N1": 'ISAE4.SA', + "TAESA PN N2": 'TAEE4.SA', + "VALE ON NM": 'VALE3.SA', + "OI ON N1": 'OIBR3.SA' +} + + +ticker_changes = { + "TRPL4.SA": 'ISAE4.SA', +} \ No newline at end of file diff --git a/notas-teste/XPINC_NOTA_NEGOCIACAO_B3_1_10_2024.pdf b/notas-teste/XPINC_NOTA_NEGOCIACAO_B3_1_10_2024.pdf new file mode 100644 index 0000000..ea48675 Binary files /dev/null and b/notas-teste/XPINC_NOTA_NEGOCIACAO_B3_1_10_2024.pdf differ diff --git a/ntfy_service.py b/ntfy_service.py new file mode 100644 index 0000000..b0f441b --- /dev/null +++ b/ntfy_service.py @@ -0,0 +1,6 @@ +import requests + + +def send_notification(message: str): + response = requests.post('https://ntfy.ccalifice.com/ghostfolio', data=message, headers={"Authorization": "Bearer tk_dl5jaui6j3pc2dx9rx8qj9sdwh23k"}) + return response.status_code \ No newline at end of file diff --git a/playwright_t.py b/playwright_t.py new file mode 100644 index 0000000..b98e35e --- /dev/null +++ b/playwright_t.py @@ -0,0 +1,36 @@ +from playwright.sync_api import sync_playwright +import time + +with sync_playwright() as p: + # Launch a browser + browser = p.chromium.launch(headless=False) # Set headless=True if you don't want to see the browser + page = browser.new_page() + + # Go to the login page + page.goto("https://www.investidor.b3.com.br/login") + + # Fill in the username and password + page.fill('input[name="investidor"]', '01783945052') + page.click('button[type="submit"]') + + time.sleep(2) + + page.fill('input[type="password"]', 'malvado6696') + + time.sleep(200) + page.click('input[type="checkbox"]') + page.click('button[type="submit"]') + + + # Wait for navigation or any element that confirms login success + page.wait_for_load_state('networkidle') + + # Go to the protected page + page.goto("https://example.com/secret-page") + + # Get the page content + content = page.content() + print(content) + + # Close the browser + #browser.close() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55d5600 Binary files /dev/null and b/requirements.txt differ diff --git a/test_main.http b/test_main.http new file mode 100644 index 0000000..a2d81a9 --- /dev/null +++ b/test_main.http @@ -0,0 +1,11 @@ +# Test your FastAPI endpoints + +GET http://127.0.0.1:8000/ +Accept: application/json + +### + +GET http://127.0.0.1:8000/hello/User +Accept: application/json + +###