Compare commits

...

21 Commits

Author SHA1 Message Date
Schmitz Schmitz
e0e159166b locale(German): update translation
Currently translated at 100.0% (585 of 585 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/de/
2025-03-02 01:59:16 +00:00
Herculino Trotta
6c7594ad14 Merge pull request #197 from eitchtee/dev
feat(automatic-exchange-rates): add Transitive rate provider
2025-03-01 22:59:00 -03:00
Herculino Trotta
d3ea0e43da feat(automatic-exchange-rates): add Transitive rate provider 2025-03-01 22:58:33 -03:00
Herculino Trotta
dde75416ca Merge pull request #196
feat(automatic-exchange-rates): add Synth Finance Stock
2025-03-01 22:41:12 -03:00
Herculino Trotta
c9b346b791 feat(automatic-exchange-rates): add Synth Finance Stock 2025-03-01 22:40:50 -03:00
Dimitri Decrock
9896044a15 locale(Dutch): update translation
Currently translated at 100.0% (585 of 585 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-03-01 03:01:20 +00:00
Herculino Trotta
eb65eb4590 add translation info on readme 2025-02-28 00:30:00 -03:00
Herculino Trotta
017c70e8b2 locale((Portuguese)): deleted translation using Weblate 2025-02-28 03:04:29 +00:00
Herculino Trotta
64b0830909 locale((Portuguese)): added translation using Weblate 2025-02-28 03:03:27 +00:00
Herculino Trotta
25d99cbece Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (585 of 585 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-02-28 02:37:39 +00:00
Herculino Trotta
033f0e1b0d Merge pull request #195
feat(insights): add Categories Overview
2025-02-27 23:33:25 -03:00
Herculino Trotta
35027ee0ae feat(insights): add Categories Overview
Closes #94
2025-02-27 23:33:05 -03:00
Herculino Trotta
91904e959b Merge pull request #194
locale(de): update translation - thanks to @CocaCola2701
2025-02-25 10:53:25 -03:00
Herculino Trotta
a6a85ae3a2 locale(de): update translation - thanks to @CocaCola2701 2025-02-25 10:53:08 -03:00
Herculino Trotta
b0f53f45f9 Merge pull request #193
fix(rules): Update or Create Transaction rule unable to match againt dates and other types
2025-02-25 10:49:01 -03:00
Herculino Trotta
0f60f8d486 fix(rules): Update or Create Transaction rule unable to match againt dates and other types 2025-02-25 10:48:43 -03:00
Herculino Trotta
efb207a109 Merge pull request #191 from eitchtee/dev
locale: add en
2025-02-24 23:07:14 -03:00
Herculino Trotta
95b1481dd5 locale: add en 2025-02-24 23:06:15 -03:00
Herculino Trotta
8de340b68b Merge pull request #190
locale(de): enable Deutsch
2025-02-24 16:34:50 -03:00
Herculino Trotta
ef15b85386 fix(locale): transactions quick search placeholder is not translatable 2025-02-24 16:34:05 -03:00
Herculino Trotta
45d939237d locale(de): enable Deutsch 2025-02-24 16:33:14 -03:00
21 changed files with 4095 additions and 641 deletions

View File

@@ -13,6 +13,7 @@
<a href="#key-features">Features</a> •
<a href="#how-to-use">Usage</a> •
<a href="#how-it-works">How</a> •
<a href="#help-us-translate-wygiwyh">Translate</a> •
<a href="#caveats-and-warnings">Caveats and Warnings</a> •
<a href="#built-with">Built with</a>
</p>
@@ -133,6 +134,14 @@ To create the first user, open the container's console using Unraid's UI, by cli
Check out our [Wiki](https://github.com/eitchtee/WYGIWYH/wiki) for more information.
# Help us translate WYGIWYH!
<a href="https://translations.herculino.com/engage/wygiwyh/">
<img src="https://translations.herculino.com/widget/wygiwyh/open-graph.png" alt="Translation status" />
</a>
> [!NOTE]
> Login with your github account
# Caveats and Warnings
- I'm not an accountant, some terms and even calculations might be wrong. Make sure to open an issue if you see anything that could be improved.

View File

@@ -163,6 +163,7 @@ AUTH_USER_MODEL = "users.User"
LANGUAGE_CODE = "en"
LANGUAGES = (
("de", "Deutsch"),
("en", "English"),
("nl", "Nederlands"),
("pt-br", "Português (Brasil)"),

View File

@@ -6,8 +6,10 @@ from django.utils import timezone
from apps.currencies.exchange_rates.providers import (
SynthFinanceProvider,
SynthFinanceStockProvider,
CoinGeckoFreeProvider,
CoinGeckoProProvider,
TransitiveRateProvider,
)
from apps.currencies.models import ExchangeRateService, ExchangeRate, Currency
@@ -17,8 +19,10 @@ logger = logging.getLogger(__name__)
# Map service types to provider classes
PROVIDER_MAPPING = {
"synth_finance": SynthFinanceProvider,
"synth_finance_stock": SynthFinanceStockProvider,
"coingecko_free": CoinGeckoFreeProvider,
"coingecko_pro": CoinGeckoProProvider,
"transitive": TransitiveRateProvider,
}

View File

@@ -3,11 +3,11 @@ import time
import requests
from decimal import Decimal
from typing import Tuple, List
from typing import Tuple, List, Optional, Dict
from django.db.models import QuerySet
from apps.currencies.models import Currency
from apps.currencies.models import Currency, ExchangeRate
from apps.currencies.exchange_rates.base import ExchangeRateProvider
logger = logging.getLogger(__name__)
@@ -150,3 +150,159 @@ class CoinGeckoProProvider(CoinGeckoFreeProvider):
super().__init__(api_key)
self.session = requests.Session()
self.session.headers.update({"x-cg-pro-api-key": api_key})
class SynthFinanceStockProvider(ExchangeRateProvider):
"""Implementation for Synth Finance API Real-Time Prices endpoint (synthfinance.com)"""
BASE_URL = "https://api.synthfinance.com/tickers"
rates_inverted = True
def __init__(self, api_key: str = None):
super().__init__(api_key)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {self.api_key}", "accept": "application/json"}
)
def get_rates(
self, target_currencies: QuerySet, exchange_currencies: set
) -> List[Tuple[Currency, Currency, Decimal]]:
results = []
for currency in target_currencies:
if currency.exchange_currency not in exchange_currencies:
continue
try:
# Same currency has rate of 1
if currency.code == currency.exchange_currency.code:
rate = Decimal("1")
results.append((currency.exchange_currency, currency, rate))
continue
# Fetch real-time price for this ticker
response = self.session.get(
f"{self.BASE_URL}/{currency.code}/real-time"
)
response.raise_for_status()
data = response.json()
# Use fair market value as the rate
rate = Decimal(data["data"]["fair_market_value"])
results.append((currency.exchange_currency, currency, rate))
# Log API usage
credits_used = data["meta"]["credits_used"]
credits_remaining = data["meta"]["credits_remaining"]
logger.info(
f"Synth Finance API call for {currency.code}: {credits_used} credits used, {credits_remaining} remaining"
)
except requests.RequestException as e:
logger.error(
f"Error fetching rate from Synth Finance API for ticker {currency.code}: {e}",
exc_info=True,
)
except KeyError as e:
logger.error(
f"Unexpected response structure from Synth Finance API for ticker {currency.code}: {e}",
exc_info=True,
)
except Exception as e:
logger.error(
f"Unexpected error processing Synth Finance data for ticker {currency.code}: {e}",
exc_info=True,
)
return results
class TransitiveRateProvider(ExchangeRateProvider):
"""Calculates exchange rates through paths of existing rates"""
rates_inverted = True
def __init__(self, api_key: str = None):
super().__init__(api_key) # API key not needed but maintaining interface
@classmethod
def requires_api_key(cls) -> bool:
return False
def get_rates(
self, target_currencies: QuerySet, exchange_currencies: set
) -> List[Tuple[Currency, Currency, Decimal]]:
results = []
# Get recent rates for building the graph
recent_rates = ExchangeRate.objects.all()
# Build currency graph
currency_graph = self._build_currency_graph(recent_rates)
for target in target_currencies:
if (
not target.exchange_currency
or target.exchange_currency not in exchange_currencies
):
continue
# Find path and calculate rate
from_id = target.exchange_currency.id
to_id = target.id
path, rate = self._find_conversion_path(currency_graph, from_id, to_id)
if path and rate:
path_codes = [Currency.objects.get(id=cid).code for cid in path]
logger.info(
f"Found conversion path: {' -> '.join(path_codes)}, rate: {rate}"
)
results.append((target.exchange_currency, target, rate))
else:
logger.debug(
f"No conversion path found for {target.exchange_currency.code}->{target.code}"
)
return results
@staticmethod
def _build_currency_graph(rates) -> Dict[int, Dict[int, Decimal]]:
"""Build a graph representation of currency relationships"""
graph = {}
for rate in rates:
# Add both directions to make the graph bidirectional
if rate.from_currency_id not in graph:
graph[rate.from_currency_id] = {}
graph[rate.from_currency_id][rate.to_currency_id] = rate.rate
if rate.to_currency_id not in graph:
graph[rate.to_currency_id] = {}
graph[rate.to_currency_id][rate.from_currency_id] = Decimal("1") / rate.rate
return graph
@staticmethod
def _find_conversion_path(
graph, from_id, to_id
) -> Tuple[Optional[list], Optional[Decimal]]:
"""Find the shortest path between currencies using breadth-first search"""
if from_id not in graph or to_id not in graph:
return None, None
queue = [(from_id, [from_id], Decimal("1"))]
visited = {from_id}
while queue:
current, path, current_rate = queue.pop(0)
if current == to_id:
return path, current_rate
for neighbor, rate in graph.get(current, {}).items():
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor], current_rate * rate))
return None, None

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-02 01:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('currencies', '0011_remove_exchangerateservice_fetch_interval_hours_and_more'),
]
operations = [
migrations.AlterField(
model_name='exchangerateservice',
name='service_type',
field=models.CharField(choices=[('synth_finance', 'Synth Finance'), ('synth_finance_stock', 'Synth Finance Stock'), ('coingecko_free', 'CoinGecko (Demo/Free)'), ('coingecko_pro', 'CoinGecko (Pro)')], max_length=255, verbose_name='Service Type'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-02 01:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('currencies', '0012_alter_exchangerateservice_service_type'),
]
operations = [
migrations.AlterField(
model_name='exchangerateservice',
name='service_type',
field=models.CharField(choices=[('synth_finance', 'Synth Finance'), ('synth_finance_stock', 'Synth Finance Stock'), ('coingecko_free', 'CoinGecko (Demo/Free)'), ('coingecko_pro', 'CoinGecko (Pro)'), ('transitive', 'Transitive (Calculated from Existing Rates)')], max_length=255, verbose_name='Service Type'),
),
]

View File

@@ -92,8 +92,10 @@ class ExchangeRateService(models.Model):
class ServiceType(models.TextChoices):
SYNTH_FINANCE = "synth_finance", "Synth Finance"
SYNTH_FINANCE_STOCK = "synth_finance_stock", "Synth Finance Stock"
COINGECKO_FREE = "coingecko_free", "CoinGecko (Demo/Free)"
COINGECKO_PRO = "coingecko_pro", "CoinGecko (Pro)"
TRANSITIVE = "transitive", "Transitive (Calculated from Existing Rates)"
class IntervalType(models.TextChoices):
ON = "on", _("On")

View File

@@ -29,6 +29,11 @@ urlpatterns = [
views.category_sum_by_currency,
name="category_sum_by_currency",
),
path(
"insights/category-overview/",
views.category_overview,
name="category_overview",
),
path(
"insights/late-transactions/",
views.late_transactions,

View File

@@ -0,0 +1,165 @@
from decimal import Decimal
from django.db import models
from django.db.models import Sum, Case, When, Value, DecimalField
from django.db.models.functions import Coalesce
from apps.transactions.models import Transaction
from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert
def get_categories_totals(transactions_queryset, ignore_empty=False):
# Get metrics for each category and currency in a single query
category_currency_metrics = (
transactions_queryset.values(
"category",
"category__name",
"account__currency",
"account__currency__code",
"account__currency__name",
"account__currency__decimal_places",
"account__currency__prefix",
"account__currency__suffix",
"account__currency__exchange_currency",
)
.annotate(
expense_current=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE, is_paid=True, then="amount"
),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
expense_projected=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE, is_paid=False, then="amount"
),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
income_current=Coalesce(
Sum(
Case(
When(type=Transaction.Type.INCOME, is_paid=True, then="amount"),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
income_projected=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME, is_paid=False, then="amount"
),
default=Value(0),
output_field=models.DecimalField(),
)
),
Decimal("0"),
),
)
.order_by("category__name")
)
# Process the results to structure by category
result = {}
for metric in category_currency_metrics:
# Skip empty categories if ignore_empty is True
if ignore_empty and all(
metric[field] == Decimal("0")
for field in [
"expense_current",
"expense_projected",
"income_current",
"income_projected",
]
):
continue
# Calculate derived totals
total_current = metric["income_current"] - metric["expense_current"]
total_projected = metric["income_projected"] - metric["expense_projected"]
total_income = metric["income_current"] + metric["income_projected"]
total_expense = metric["expense_current"] + metric["expense_projected"]
total_final = total_current + total_projected
category_id = metric["category"]
currency_id = metric["account__currency"]
if category_id not in result:
result[category_id] = {"name": metric["category__name"], "currencies": {}}
# Add currency data
currency_data = {
"currency": {
"code": metric["account__currency__code"],
"name": metric["account__currency__name"],
"decimal_places": metric["account__currency__decimal_places"],
"prefix": metric["account__currency__prefix"],
"suffix": metric["account__currency__suffix"],
},
"expense_current": metric["expense_current"],
"expense_projected": metric["expense_projected"],
"total_expense": total_expense,
"income_current": metric["income_current"],
"income_projected": metric["income_projected"],
"total_income": total_income,
"total_current": total_current,
"total_projected": total_projected,
"total_final": total_final,
}
# Add exchanged values if exchange_currency exists
if metric["account__currency__exchange_currency"]:
from_currency = Currency.objects.get(id=currency_id)
exchange_currency = Currency.objects.get(
id=metric["account__currency__exchange_currency"]
)
exchanged = {}
for field in [
"expense_current",
"expense_projected",
"income_current",
"income_projected",
"total_income",
"total_expense",
"total_current",
"total_projected",
"total_final",
]:
amount, prefix, suffix, decimal_places = convert(
amount=currency_data[field],
from_currency=from_currency,
to_currency=exchange_currency,
)
if amount is not None:
exchanged[field] = amount
if "currency" not in exchanged:
exchanged["currency"] = {
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
"code": exchange_currency.code,
"name": exchange_currency.name,
}
if exchanged:
currency_data["exchanged"] = exchanged
result[category_id]["currencies"][currency_id] = currency_data
return result

View File

@@ -1,3 +1,6 @@
import decimal
import json
from dateutil.relativedelta import relativedelta
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
@@ -23,6 +26,7 @@ from apps.insights.utils.sankey import (
)
from apps.insights.utils.transactions import get_transactions
from apps.transactions.models import TransactionCategory, Transaction
from apps.insights.utils.category_overview import get_categories_totals
@login_required
@@ -159,6 +163,24 @@ def category_sum_by_currency(request):
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def category_overview(request):
# Get filtered transactions
transactions = get_transactions(request, include_silent=True)
total_table = get_categories_totals(
transactions_queryset=transactions, ignore_empty=False
)
return render(
request,
"insights/fragments/category_overview/index.html",
{"total_table": total_table},
)
@only_htmx
@login_required
@require_http_methods(["GET"])

View File

@@ -297,10 +297,8 @@ class UpdateOrCreateTransactionRuleAction(models.Model):
search_query = Q()
def add_to_query(field_name, value, operator):
if isinstance(value, (int, str)):
lookup = f"{field_name}__{operator}"
return Q(**{lookup: value})
return Q()
lookup = f"{field_name}__{operator}"
return Q(**{lookup: value})
if self.search_account:
value = simple.eval(self.search_account)

View File

@@ -131,14 +131,16 @@ def _process_update_or_create_transaction_action(action, simple_eval):
# Build search query using the helper method
search_query = action.build_search_query(simple_eval)
logger.info("Searching transactions using: %s", search_query)
# Find latest matching transaction or create new
if search_query:
transaction = (
Transaction.objects.filter(search_query).order_by("-date", "-id").first()
)
transactions = Transaction.objects.filter(search_query).order_by("-date", "-id")
transaction = transactions.first()
logger.info("Found at least one matching transaction, using latest")
else:
transaction = None
logger.info("No matching transaction found, creating a new transaction")
if not transaction:
transaction = Transaction()

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-02-24 19:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0018_alter_usersettings_start_page'),
]
operations = [
migrations.AlterField(
model_name='usersettings',
name='language',
field=models.CharField(choices=[('auto', 'Auto'), ('de', 'Deutsch'), ('en', 'English'), ('nl', 'Nederlands'), ('pt-br', 'Português (Brasil)')], default='auto', max_length=10, verbose_name='Language'),
),
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,21 +3,21 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-19 23:05-0300\n"
"PO-Revision-Date: 2025-02-22 15:03+0100\n"
"Last-Translator: Dimitri Decrock <dimitri@fam-decrock.eu>\n"
"Language-Team: \n"
"POT-Creation-Date: 2025-02-27 23:32-0300\n"
"PO-Revision-Date: 2025-03-01 03:01+0000\n"
"Last-Translator: Dimitri Decrock <dj.flashpower@gmail.com>\n"
"Language-Team: Dutch <https://translations.herculino.com/projects/wygiwyh/"
"app/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.5\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.1\n"
#: apps/accounts/forms.py:24
msgid "Group name"
@@ -76,6 +76,7 @@ msgstr "Nieuw saldo"
#: apps/transactions/forms.py:299 apps/transactions/forms.py:479
#: apps/transactions/forms.py:724 apps/transactions/models.py:203
#: apps/transactions/models.py:378 apps/transactions/models.py:558
#: templates/insights/fragments/category_overview/index.html:9
msgid "Category"
msgstr "Categorie"
@@ -156,7 +157,7 @@ msgstr "Gearchiveerd"
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Gearchiveerde rekeningen worden niet weergegeven en tellen niet mee voor je "
"\"Netto Waarde\"."
"\"Netto Waarde\""
#: apps/accounts/models.py:59 apps/rules/forms.py:160 apps/rules/forms.py:173
#: apps/rules/models.py:24 apps/rules/models.py:236
@@ -309,8 +310,8 @@ msgstr[1] "over %(years)s jaren"
#, python-format
msgid "in %(months)s month"
msgid_plural "in %(months)s months"
msgstr[0] "over %(months)s maand"
msgstr[1] "over %(months)s maanden"
msgstr[0] "over %(months)s maand"
msgstr[1] "over %(months)s maanden"
#: apps/common/templatetags/natural.py:56
#, python-format
@@ -772,7 +773,7 @@ msgstr "Verrichtingsregels"
#: apps/export_app/forms.py:140 apps/rules/models.py:53
msgid "Edit transaction action"
msgstr "Bewerk verrichtingsregel actie"
msgstr "Bewerk verrichtingsactie"
#: apps/export_app/forms.py:143 apps/rules/models.py:290
msgid "Update or create transaction actions"
@@ -880,6 +881,7 @@ msgstr "Run met succes verwijderd"
#: apps/insights/forms.py:119 apps/insights/utils/sankey.py:36
#: apps/insights/utils/sankey.py:167
#: templates/insights/fragments/category_overview/index.html:18
msgid "Uncategorized"
msgstr "Ongecategoriseerd"
@@ -1200,7 +1202,7 @@ msgstr "De einddatum moet na de begindatum vallen"
#: apps/transactions/models.py:112
msgid "Mute"
msgstr "Gedempt"
msgstr "Dempen"
#: apps/transactions/models.py:117
msgid ""
@@ -1247,6 +1249,7 @@ msgstr "Bedrijf"
#: templates/calendar_view/fragments/list.html:52
#: templates/calendar_view/fragments/list.html:54
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/insights/fragments/category_overview/index.html:10
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr "Ontvangsten Transactie"
@@ -1257,8 +1260,9 @@ msgstr "Ontvangsten Transactie"
#: templates/calendar_view/fragments/list.html:56
#: templates/calendar_view/fragments/list.html:58
#: templates/cotton/ui/quick_transactions_buttons.html:18
#: templates/insights/fragments/category_overview/index.html:11
msgid "Expense"
msgstr "Uitgave Transactie"
msgstr "Uitgave"
#: apps/transactions/models.py:225 apps/transactions/models.py:390
msgid "Installment Plan"
@@ -1882,6 +1886,7 @@ msgid "Muted"
msgstr "Gedempt"
#: templates/categories/fragments/table.html:57
#: templates/insights/fragments/category_overview/index.html:67
msgid "No categories"
msgstr "Geen categorieën"
@@ -1895,6 +1900,7 @@ msgstr "Sluiten"
#: templates/cotton/config/search.html:6
#: templates/import_app/fragments/profiles/list_presets.html:13
#: templates/monthly_overview/pages/overview.html:177
msgid "Search"
msgstr "Zoeken"
@@ -2442,6 +2448,11 @@ msgstr "Inkomsten/uitgaven per rekening"
msgid "Income/Expense by Currency"
msgstr "Inkomsten/uitgaven per Munteenheid"
#: templates/insights/fragments/category_overview/index.html:12
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr "Totaal"
#: templates/insights/fragments/late_transactions.html:15
msgid "All good!"
msgstr "Allemaal goed!"
@@ -2497,10 +2508,14 @@ msgid "Category Explorer"
msgstr "Categorie Verkenner"
#: templates/insights/pages/index.html:102
msgid "Categories Overview"
msgstr "Categorieën Overzicht"
#: templates/insights/pages/index.html:109
msgid "Late Transactions"
msgstr "Betalingsachterstanden"
#: templates/insights/pages/index.html:108
#: templates/insights/pages/index.html:115
msgid "Latest Transactions"
msgstr "Laatste Verrichtingen"
@@ -2597,17 +2612,13 @@ msgstr "verwacht"
msgid "Expenses"
msgstr "Uitgaven"
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr "Totaal"
#: templates/monthly_overview/fragments/monthly_summary.html:257
msgid "Distribution"
msgstr "Verdeling"
#: templates/monthly_overview/pages/overview.html:68
msgid "Summary"
msgstr "Overzicht"
msgstr "Samenvatting"
#: templates/monthly_overview/pages/overview.html:142
msgid "Filter transactions"
@@ -2866,6 +2877,26 @@ msgstr "Bedragen tonen"
msgid "Yearly Overview"
msgstr "Jaaroverzicht"
#, fuzzy
#~| msgid "From Amount"
#~ msgid "Principal Amount"
#~ msgstr "Van Bedrag"
#, fuzzy
#~| msgid "Interval"
#~ msgid "Interest"
#~ msgstr "Interval"
#, fuzzy
#~| msgid "Management"
#~ msgid "Loan Payment"
#~ msgstr "Beheer"
#, fuzzy
#~| msgid "Management"
#~ msgid "Loan Payments"
#~ msgstr "Beheer"
#, fuzzy
#~| msgid "Installment Plans"
#~ msgid "Installment Planss"
@@ -2926,11 +2957,6 @@ msgstr "Jaaroverzicht"
#~ msgid "Reference Date Operator"
#~ msgstr "Referentiedatum vanaf"
#, fuzzy
#~| msgid "From Amount"
#~ msgid "Search Amount"
#~ msgstr "Van Bedrag"
#, fuzzy
#~| msgid "Amount max"
#~ msgid "Amount Operator"

View File

@@ -3,21 +3,21 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-19 23:05-0300\n"
"PO-Revision-Date: 2025-02-19 23:06-0300\n"
"Last-Translator: Herculino Trotta\n"
"Language-Team: \n"
"POT-Creation-Date: 2025-02-27 23:32-0300\n"
"PO-Revision-Date: 2025-02-28 02:37+0000\n"
"Last-Translator: Herculino Trotta <netotrotta@gmail.com>\n"
"Language-Team: Portuguese (Brazil) <https://translations.herculino.com/"
"projects/wygiwyh/app/pt_BR/>\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 3.5\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.10.1\n"
#: apps/accounts/forms.py:24
msgid "Group name"
@@ -76,6 +76,7 @@ msgstr "Novo saldo"
#: apps/transactions/forms.py:299 apps/transactions/forms.py:479
#: apps/transactions/forms.py:724 apps/transactions/models.py:203
#: apps/transactions/models.py:378 apps/transactions/models.py:558
#: templates/insights/fragments/category_overview/index.html:9
msgid "Category"
msgstr "Categoria"
@@ -529,8 +530,8 @@ msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
msgstr ""
"Formato inválido de hora. Use uma lista de horas separada por vírgulas "
"(0-23) e/ou uma faixa (ex.: '1-5,8,10-12')"
"Formato inválido de hora. Use uma lista de horas separada por vírgulas (0-23)"
" e/ou uma faixa (ex.: '1-5,8,10-12')."
#: apps/currencies/models.py:236
msgid ""
@@ -878,6 +879,7 @@ msgstr "Importação apagada com sucesso"
#: apps/insights/forms.py:119 apps/insights/utils/sankey.py:36
#: apps/insights/utils/sankey.py:167
#: templates/insights/fragments/category_overview/index.html:18
msgid "Uncategorized"
msgstr "Sem categoria"
@@ -1244,6 +1246,7 @@ msgstr "Entidade"
#: templates/calendar_view/fragments/list.html:52
#: templates/calendar_view/fragments/list.html:54
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/insights/fragments/category_overview/index.html:10
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr "Renda"
@@ -1254,6 +1257,7 @@ msgstr "Renda"
#: templates/calendar_view/fragments/list.html:56
#: templates/calendar_view/fragments/list.html:58
#: templates/cotton/ui/quick_transactions_buttons.html:18
#: templates/insights/fragments/category_overview/index.html:11
msgid "Expense"
msgstr "Despesa"
@@ -1879,6 +1883,7 @@ msgid "Muted"
msgstr "Silenciada"
#: templates/categories/fragments/table.html:57
#: templates/insights/fragments/category_overview/index.html:67
msgid "No categories"
msgstr "Nenhum categoria"
@@ -1892,6 +1897,7 @@ msgstr "Fechar"
#: templates/cotton/config/search.html:6
#: templates/import_app/fragments/profiles/list_presets.html:13
#: templates/monthly_overview/pages/overview.html:177
msgid "Search"
msgstr "Buscar"
@@ -2440,6 +2446,11 @@ msgstr "Gasto/Despesa por Conta"
msgid "Income/Expense by Currency"
msgstr "Gasto/Despesa por Moeda"
#: templates/insights/fragments/category_overview/index.html:12
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr "Total"
#: templates/insights/fragments/late_transactions.html:15
msgid "All good!"
msgstr "Tudo certo!"
@@ -2495,10 +2506,14 @@ msgid "Category Explorer"
msgstr "Explorador de Categoria"
#: templates/insights/pages/index.html:102
msgid "Categories Overview"
msgstr "Visão geral das categorias"
#: templates/insights/pages/index.html:109
msgid "Late Transactions"
msgstr "Transações Atrasadas"
#: templates/insights/pages/index.html:108
#: templates/insights/pages/index.html:115
msgid "Latest Transactions"
msgstr "Últimas Transações"
@@ -2595,10 +2610,6 @@ msgstr "previsto"
msgid "Expenses"
msgstr "Despesas"
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr "Total"
#: templates/monthly_overview/fragments/monthly_summary.html:257
msgid "Distribution"
msgstr "Distribuição"
@@ -2861,6 +2872,26 @@ msgstr "Mostrar valores"
msgid "Yearly Overview"
msgstr "Visão Anual"
#, fuzzy
#~| msgid "From Amount"
#~ msgid "Principal Amount"
#~ msgstr "Quantia de origem"
#, fuzzy
#~| msgid "Interval"
#~ msgid "Interest"
#~ msgstr "Intervalo"
#, fuzzy
#~| msgid "Management"
#~ msgid "Loan Payment"
#~ msgstr "Gerenciar"
#, fuzzy
#~| msgid "Management"
#~ msgid "Loan Payments"
#~ msgstr "Gerenciar"
#, fuzzy
#~| msgid "Installment Plans"
#~ msgid "Installment Planss"
@@ -2926,11 +2957,6 @@ msgstr "Visão Anual"
#~ msgid "Reference Date Operator"
#~ msgstr "Data de Referência de"
#, fuzzy
#~| msgid "From Amount"
#~ msgid "Search Amount"
#~ msgstr "Quantia de origem"
#, fuzzy
#~| msgid "Amount max"
#~ msgid "Amount Operator"

View File

@@ -0,0 +1,69 @@
{% load i18n %}
<div hx-get="{% url 'category_overview' %}" hx-trigger="updated from:window" class="show-loading" hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
{% if total_table %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{% trans 'Category' %}</th>
<th scope="col">{% trans 'Income' %}</th>
<th scope="col">{% trans 'Expense' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr>
</thead>
<tbody>
{% for category in total_table.values %}
<tr>
<th>{% if category.name %}{{ category.name }}{% else %}{% trans 'Uncategorized' %}{% endif %}</th>
<td>
{% for currency in category.currencies.values %}
{% if currency.total_income != 0 %}
<c-amount.display
:amount="currency.total_income"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="green"></c-amount.display>
{% else %}
<div>-</div>
{% endif %}
{% endfor %}
</td>
<td>
{% for currency in category.currencies.values %}
{% if currency.total_expense != 0 %}
<c-amount.display
:amount="currency.total_expense"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="red"></c-amount.display>
{% else %}
<div>-</div>
{% endif %}
{% endfor %}
</td>
<td>
{% for currency in category.currencies.values %}
{% if currency.total_final != 0 %}
<c-amount.display
:amount="currency.total_final"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
{% else %}
<div>-</div>
{% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<c-msg.empty title="{% translate "No categories" %}"></c-msg.empty>
{% endif %}
</div>

View File

@@ -94,6 +94,13 @@
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Category Explorer' %}
</button>
<button class="nav-link" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'category_overview' %}"
hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Categories Overview' %}
</button>
<hr>
<button class="nav-link" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"

View File

@@ -174,7 +174,7 @@
</div>
<div id="search" class="my-3">
<label class="w-100">
<input type="search" class="form-control" placeholder="Buscar" hx-preserve id="quick-search"
<input type="search" class="form-control" placeholder="{% translate 'Search' %}" hx-preserve id="quick-search"
_="on input or search or htmx:afterSwap from window
if my value is empty
trigger toggle on <.transactions-divider-collapse/>

View File

@@ -2,12 +2,14 @@ import AirDatepicker from 'air-datepicker';
import en from 'air-datepicker/locale/en';
import ptBr from 'air-datepicker/locale/pt-BR';
import nl from 'air-datepicker/locale/nl';
import de from 'air-datepicker/locale/de';
import {createPopper} from '@popperjs/core';
const locales = {
'pt': ptBr,
'en': en,
'nl': nl
'nl': nl,
'de': de
};
function isMobileDevice() {
@@ -43,7 +45,7 @@ window.DatePicker = function createDynamicDatePicker(element) {
toggleSelected: element.dataset.toggleSelected === 'true',
autoClose: element.dataset.autoClose === 'true',
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
locale: locales[element.dataset.language],
locale: locales[element.dataset.language] || locales['en'],
onSelect: ({date, formattedDate, datepicker}) => {
const _event = new CustomEvent("change", {
bubbles: true,
@@ -117,7 +119,7 @@ window.MonthYearPicker = function createDynamicDatePicker(element) {
toggleSelected: element.dataset.toggleSelected === 'true',
autoClose: element.dataset.autoClose === 'true',
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
locale: locales[element.dataset.language],
locale: locales[element.dataset.language] || locales['en'],
onSelect: ({date, formattedDate, datepicker}) => {
const _event = new CustomEvent("change", {
bubbles: true,
@@ -190,7 +192,7 @@ window.YearPicker = function createDynamicDatePicker(element) {
toggleSelected: element.dataset.toggleSelected === 'true',
autoClose: element.dataset.autoClose === 'true',
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
locale: locales[element.dataset.language],
locale: locales[element.dataset.language] || locales['en'],
onSelect: ({date, formattedDate, datepicker}) => {
const _event = new CustomEvent("change", {
bubbles: true,