mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-02-25 17:04:51 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e159166b | ||
|
|
6c7594ad14 | ||
|
|
d3ea0e43da | ||
|
|
dde75416ca | ||
|
|
c9b346b791 | ||
|
|
9896044a15 | ||
|
|
eb65eb4590 | ||
|
|
017c70e8b2 | ||
|
|
64b0830909 | ||
|
|
25d99cbece | ||
|
|
033f0e1b0d | ||
|
|
35027ee0ae |
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
165
app/apps/insights/utils/category_overview.py
Normal file
165
app/apps/insights/utils/category_overview.py
Normal 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
|
||||
@@ -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"])
|
||||
|
||||
@@ -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-24 16:30-0300\n"
|
||||
"PO-Revision-Date: 2025-02-24 22:33+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"POT-Creation-Date: 2025-02-27 23:32-0300\n"
|
||||
"PO-Revision-Date: 2025-03-02 01:59+0000\n"
|
||||
"Last-Translator: Schmitz Schmitz <stefanschmitz@t-online.de>\n"
|
||||
"Language-Team: German <https://translations.herculino.com/projects/wygiwyh/"
|
||||
"app/de/>\n"
|
||||
"Language: de\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 "Neuer 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 "Kategorie"
|
||||
|
||||
@@ -722,7 +723,7 @@ msgstr "Entitäten"
|
||||
#: templates/recurring_transactions/fragments/list.html:5
|
||||
#: templates/recurring_transactions/pages/index.html:4
|
||||
msgid "Recurring Transactions"
|
||||
msgstr "Wiederkehrende Transaktion"
|
||||
msgstr "Wiederkehrende Transaktionen"
|
||||
|
||||
#: apps/export_app/forms.py:56 apps/export_app/forms.py:130
|
||||
#: apps/transactions/models.py:391 templates/includes/navbar.html:72
|
||||
@@ -880,6 +881,7 @@ msgstr "Vorgang erfolgreich gelöscht"
|
||||
|
||||
#: 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 "Unkategorisiert"
|
||||
|
||||
@@ -1252,6 +1254,7 @@ msgstr "Entität"
|
||||
#: 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 "Einnahme"
|
||||
@@ -1262,6 +1265,7 @@ msgstr "Einnahme"
|
||||
#: 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 "Ausgabe"
|
||||
|
||||
@@ -1370,7 +1374,7 @@ msgstr "Regelmäßigkeit"
|
||||
|
||||
#: apps/transactions/models.py:580
|
||||
msgid "Recurrence Interval"
|
||||
msgstr "Intervall"
|
||||
msgstr "Wiederholungsintervall"
|
||||
|
||||
#: apps/transactions/models.py:584
|
||||
msgid "Last Generated Date"
|
||||
@@ -1888,6 +1892,7 @@ msgid "Muted"
|
||||
msgstr "Ausgeblendet"
|
||||
|
||||
#: templates/categories/fragments/table.html:57
|
||||
#: templates/insights/fragments/category_overview/index.html:67
|
||||
msgid "No categories"
|
||||
msgstr "Keine Kategorien"
|
||||
|
||||
@@ -2451,6 +2456,11 @@ msgstr "Einnahmen/Ausgaben nach Konto"
|
||||
msgid "Income/Expense by Currency"
|
||||
msgstr "Einnahmen/Ausgaben nach Währung"
|
||||
|
||||
#: templates/insights/fragments/category_overview/index.html:12
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:167
|
||||
msgid "Total"
|
||||
msgstr "Gesamt"
|
||||
|
||||
#: templates/insights/fragments/late_transactions.html:15
|
||||
msgid "All good!"
|
||||
msgstr "Alles gut!"
|
||||
@@ -2506,10 +2516,14 @@ msgid "Category Explorer"
|
||||
msgstr "Kategorien-Explorer"
|
||||
|
||||
#: templates/insights/pages/index.html:102
|
||||
msgid "Categories Overview"
|
||||
msgstr "Kategorien-Übersicht"
|
||||
|
||||
#: templates/insights/pages/index.html:109
|
||||
msgid "Late Transactions"
|
||||
msgstr "Verspätete Transaktionen"
|
||||
|
||||
#: templates/insights/pages/index.html:108
|
||||
#: templates/insights/pages/index.html:115
|
||||
msgid "Latest Transactions"
|
||||
msgstr "Letzte Transaktionen"
|
||||
|
||||
@@ -2607,10 +2621,6 @@ msgstr "erwartet"
|
||||
msgid "Expenses"
|
||||
msgstr "Ausgaben"
|
||||
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:167
|
||||
msgid "Total"
|
||||
msgstr "Gesamt"
|
||||
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:257
|
||||
msgid "Distribution"
|
||||
msgstr "Verteilung"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-24 23:04-0300\n"
|
||||
"POT-Creation-Date: 2025-02-27 23:32-0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -75,6 +75,7 @@ msgstr ""
|
||||
#: 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 ""
|
||||
|
||||
@@ -862,6 +863,7 @@ msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
@@ -1221,6 +1223,7 @@ msgstr ""
|
||||
#: 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 ""
|
||||
@@ -1231,6 +1234,7 @@ msgstr ""
|
||||
#: 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 ""
|
||||
|
||||
@@ -1856,6 +1860,7 @@ msgid "Muted"
|
||||
msgstr ""
|
||||
|
||||
#: templates/categories/fragments/table.html:57
|
||||
#: templates/insights/fragments/category_overview/index.html:67
|
||||
msgid "No categories"
|
||||
msgstr ""
|
||||
|
||||
@@ -2414,6 +2419,11 @@ msgstr ""
|
||||
msgid "Income/Expense by Currency"
|
||||
msgstr ""
|
||||
|
||||
#: templates/insights/fragments/category_overview/index.html:12
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:167
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: templates/insights/fragments/late_transactions.html:15
|
||||
msgid "All good!"
|
||||
msgstr ""
|
||||
@@ -2469,10 +2479,14 @@ msgid "Category Explorer"
|
||||
msgstr ""
|
||||
|
||||
#: templates/insights/pages/index.html:102
|
||||
msgid "Categories Overview"
|
||||
msgstr ""
|
||||
|
||||
#: templates/insights/pages/index.html:109
|
||||
msgid "Late Transactions"
|
||||
msgstr ""
|
||||
|
||||
#: templates/insights/pages/index.html:108
|
||||
#: templates/insights/pages/index.html:115
|
||||
msgid "Latest Transactions"
|
||||
msgstr ""
|
||||
|
||||
@@ -2567,10 +2581,6 @@ msgstr ""
|
||||
msgid "Expenses"
|
||||
msgstr ""
|
||||
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:167
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: templates/monthly_overview/fragments/monthly_summary.html:257
|
||||
msgid "Distribution"
|
||||
msgstr ""
|
||||
|
||||
@@ -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-24 16:30-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"
|
||||
|
||||
@@ -2443,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!"
|
||||
@@ -2498,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"
|
||||
|
||||
@@ -2598,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"
|
||||
|
||||
@@ -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-24 16:30-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"
|
||||
|
||||
@@ -2441,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!"
|
||||
@@ -2496,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"
|
||||
|
||||
@@ -2596,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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user