Compare commits

...

73 Commits
0.7.1 ... 0.7.8

Author SHA1 Message Date
Herculino Trotta
27d448afd6 feat: add locale files for de (german) 2025-01-28 14:03:38 -03:00
Herculino Trotta
1dd90974bd Merge pull request #93
refactor: remove toasts from login screen
2025-01-28 13:54:20 -03:00
Herculino Trotta
31cc8db3ac refactor: remove toasts from login screen
Fixes #91
2025-01-28 13:53:47 -03:00
Herculino Trotta
3d85a15ec9 Merge pull request #90
feat: enable bulk actions on specific transactions list (calendar, recurring and installment)
2025-01-27 22:46:19 -03:00
Herculino Trotta
90f98c2d15 feat: enable bulk actions on specific transactions list (calendar, recurring and installment) 2025-01-27 22:45:40 -03:00
Herculino Trotta
643855e60e Merge pull request #89 from eitchtee/dev
fix(calendar): tooltip error when transaction has no description and wrong color
2025-01-27 22:44:43 -03:00
Herculino Trotta
e0f7b532f8 fix(calendar): tooltip error when transaction has no description and wrong color 2025-01-27 22:44:05 -03:00
Herculino Trotta
b4d3e4b42f Merge pull request #88 from eitchtee/dev
feat: add "Clear cache" button to user menu
2025-01-27 21:50:48 -03:00
Herculino Trotta
9a7ccb0973 feat: add "Clear cache" button to user menu 2025-01-27 21:49:32 -03:00
Herculino Trotta
a9b67ff272 Merge pull request #87
fix(security): toasts and month_year_picker accessible without login
2025-01-27 21:42:36 -03:00
Herculino Trotta
233b9629a2 fix(security): toasts and month_year_picker accessible without login 2025-01-27 21:41:55 -03:00
Herculino Trotta
4180c177f1 Merge pull request #86
fix: cleanup_deleted_transactions task couldn't trigger
2025-01-27 21:34:15 -03:00
Herculino Trotta
f1bc04756f fix: cleanup_deleted_transactions task couldn't trigger 2025-01-27 21:33:46 -03:00
Herculino Trotta
13795c797f Merge pull request #85
feat: add number format user setting and improve date format handling
2025-01-27 13:31:28 -03:00
Herculino Trotta
331a7d5b18 locale: update translations 2025-01-27 13:30:06 -03:00
Herculino Trotta
81b8da30d6 feat: add number_format to user_settings form 2025-01-27 13:26:08 -03:00
Herculino Trotta
80bad240e7 refactor: remove custom_date filter 2025-01-27 13:25:47 -03:00
Herculino Trotta
187c56c96c refactor: remove user attr from datepicker
since monkey patched get_format already does what we want
2025-01-27 13:25:06 -03:00
Herculino Trotta
3796112d77 feat: monkey patch get_format to return usersettings 2025-01-27 13:22:21 -03:00
Herculino Trotta
958940089a feat: add number_format user setting 2025-01-27 13:20:12 -03:00
Herculino Trotta
a08548bb13 feat: add local access to user and request from anywhere 2025-01-27 13:19:28 -03:00
Herculino Trotta
7fe446e510 refactor: remove custom_date filter 2025-01-27 13:18:57 -03:00
Herculino Trotta
eccb0d15ee Merge pull request #83 from eitchtee/eitchtee-patch-1
Update README.md
2025-01-26 21:03:45 -03:00
Herculino Trotta
7ebd329706 Update README.md 2025-01-26 21:03:14 -03:00
Herculino Trotta
d3fcd5fe7e Merge pull request #82
fix datepicker datetime handling and action-bar
2025-01-26 20:56:53 -03:00
Herculino Trotta
b0a3acbdde fix: transactions action bar error on page change 2025-01-26 20:56:03 -03:00
Herculino Trotta
33ce38d74c feat(datepicker): improve value handling 2025-01-26 20:54:29 -03:00
Herculino Trotta
fa51a7fef9 fix(datepicker): wrong datetime format 2025-01-26 20:53:16 -03:00
Herculino Trotta
d7c072a35c fix(currencies): don't error out if from_currency or to_currency isn't set 2025-01-26 20:52:47 -03:00
Herculino Trotta
c88a6dcf3a Update README.md 2025-01-26 11:49:28 -03:00
Herculino Trotta
fcb54a0af2 Merge pull request #79 from DragonHeart69/main
Add new Dutch translations for v0.7.2
2025-01-26 11:20:35 -03:00
Herculino Trotta
eec2ced481 refactor(settings): drop SQL_ENGINE env variable as only postgres is supported 2025-01-26 11:19:38 -03:00
Herculino Trotta
58a6048857 fix(settings): respect SQL_PORT env variable, defaulting to 5432 if not available 2025-01-26 11:17:38 -03:00
Herculino Trotta
93774cca64 docker: update python image from slim-buster to slim-bookworm 2025-01-26 11:16:39 -03:00
Dimitri Decrock
679f49badc Add new Dutch translations for v0.7.2 2025-01-26 13:37:06 +01:00
Herculino Trotta
b535a12014 feat: enable Dutch (Nederlands) language choice 2025-01-25 15:55:42 -03:00
Herculino Trotta
72876bff43 Merge pull request #76 from DragonHeart69/main
1st edition of the Dutch translation
2025-01-25 15:36:38 -03:00
Dimitri Decrock
4411022027 delete merge 2025-01-25 19:36:51 +01:00
Dimitri Decrock
086210b39d Merge branch 'eitchtee-main' 2025-01-25 19:29:07 +01:00
Dimitri Decrock
73cb2d861b update 2025-01-25 19:26:37 +01:00
Dimitri Decrock
1c479ef85a Merge branch 'main' of https://github.com/eitchtee/WYGIWYH into eitchtee-main 2025-01-25 19:25:56 +01:00
Dimitri Decrock
51b2b11825 final translation Dutch 1st publication 2025-01-25 18:44:53 +01:00
Herculino Trotta
c9d1b5b5f3 Merge pull request #75
locale: update locales
2025-01-25 13:55:09 -03:00
Herculino Trotta
a22a95cb9f locale: update locales 2025-01-25 13:54:10 -03:00
Herculino Trotta
5c46a2c15e feat: pluralize toast for bulk edit 2025-01-25 13:48:32 -03:00
Herculino Trotta
4f091c601e Merge pull request #73
feat: add bulk duplicate action and toasts for existing actions
2025-01-25 13:44:55 -03:00
Herculino Trotta
0fac78d15a feat: add bulk duplicate action and toasts for existing actions 2025-01-25 13:44:39 -03:00
Herculino Trotta
aa171c0e76 Merge pull request #72
fix: clear internal_id when duplicating
2025-01-25 13:42:54 -03:00
Herculino Trotta
73ca418dc8 fix: clear internal_id when duplicating 2025-01-25 13:42:23 -03:00
Herculino Trotta
7c34f36ffb Merge pull request #71 from eitchtee/dev
feat: tidy up transactions action bar
2025-01-25 12:44:48 -03:00
Herculino Trotta
2b6be8c6ac feat: tidy up transactions action bar 2025-01-25 12:43:53 -03:00
Herculino Trotta
f643c41cf1 Merge pull request #70
feat: bulk edit selected transactions
2025-01-25 12:42:36 -03:00
Herculino Trotta
1ef7a780fb feat: bulk edit selected transactions 2025-01-25 12:41:55 -03:00
Herculino Trotta
c3a753d221 Merge pull request #69 from eitchtee/dev
feat: add new animation to transactions action bar
2025-01-25 12:39:51 -03:00
Herculino Trotta
c474b6cda9 feat: add new animation to transactions action bar 2025-01-25 12:37:30 -03:00
Herculino Trotta
aff3aa7ed2 feat: add new animation to transactions action bar 2025-01-25 12:37:24 -03:00
Dimitri Decrock
414a9bb88a 4d part Dutch translation 2025-01-25 14:23:23 +01:00
Herculino Trotta
5f202a3820 Merge pull request #68
feat(transactions): proper clear button for filters
2025-01-25 01:30:43 -03:00
Herculino Trotta
e71775292a feat(transactions): proper clear button for filters 2025-01-25 01:30:24 -03:00
Herculino Trotta
01aa8acb71 Merge pull request #67 from eitchtee/dev
refactor: add end slashes for some urls without
2025-01-24 22:56:20 -03:00
Herculino Trotta
d030f9686b refactor: add end slashes for some urls without 2025-01-24 22:55:36 -03:00
Herculino Trotta
56d7e41bc5 Merge pull request #66
feat: add new /add/ endpoint for quickly adding new transactions
2025-01-24 22:52:17 -03:00
Herculino Trotta
0857b44fc3 feat: add new /add/ endpoint for quickly adding new transactions 2025-01-24 22:50:39 -03:00
Herculino Trotta
d4b5afd8b2 Merge pull request #65
fix(transactions): unaligned type button
2025-01-24 22:49:42 -03:00
Herculino Trotta
9c4ba3a6de fix(transactions): unaligned type button 2025-01-24 22:48:24 -03:00
Herculino Trotta
ec8b0e21d8 Merge pull request #63
feat(transactions): new is_paid switch
2025-01-24 22:47:20 -03:00
Herculino Trotta
6c60c3659c feat(transactions): new is_paid switch 2025-01-24 22:47:00 -03:00
Herculino Trotta
a040b8acd2 Merge pull request #62
fix(transactions:filter): unaligned filter buttons
2025-01-24 22:42:20 -03:00
Herculino Trotta
e72d6cd1ea fix(transactions:filter): unaligned filter buttons 2025-01-24 22:42:01 -03:00
Dimitri Decrock
f6d1a42b35 Merge branch 'eitchtee:main' into main 2025-01-24 19:22:03 +01:00
Dimitri Decrock
eb25f8aeb3 3d part Dutch translation 2025-01-24 19:22:01 +01:00
Dimitri Decrock
2ee64a534e 2nd part Dutch translation 2025-01-23 07:13:15 +01:00
Dimitri Decrock
14073d3555 Start with Dutch translation 2025-01-22 19:36:13 +01:00
60 changed files with 4148 additions and 1059 deletions

View File

@@ -9,7 +9,6 @@ SECRET_KEY=<GENERATE A SAFE SECRET KEY AND PLACE IT HERE>
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
OUTBOUND_PORT=9005
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=wygiwyh
SQL_USER=wygiwyh
SQL_PASSWORD=<INSERT A SAFE PASSWORD HERE>

View File

@@ -95,31 +95,16 @@ You can now access localhost:OUTBOUND_PORT
> [!NOTE]
> If you're going to use another IP that isn't localhost, add it to `DJANGO_ALLOWED_HOSTS`, without `http://`
## Building from source
Features are only added to main when ready, if you want to run the latest version, you must build from source.
Features are only added to `main` when ready, if you want to run the latest version, you must build from source.
All the required Dockerfiles are [here](https://github.com/eitchtee/WYGIWYH/tree/main/docker/prod).
```bash
# Create a folder for WYGIWYH (optional)
$ mkdir WYGIWYH
## Unraid
# Go into the folder
$ cd WYGIWYH
[nwithan8](https://github.com/nwithan8) has kindly provided a Unraid template for WYGIWYH, have a look at the [unraid_templates](https://github.com/nwithan8/unraid_templates) repo.
# Clone this repository
$ git clone https://github.com/eitchtee/WYGIWYH.git .
$ cp docker-compose.prod.yml docker-compose.yml
$ cp .env.example .env
# Now edit both files as you see fit
# Run the app
$ docker compose up -d --build
# Create the first admin account
$ docker compose exec -it web python manage.py createsuperuser
```
WYGIWYH and WYGIWYH--Procrastinate should be available on the Unraid Store. You need both for all features.
# How it works

View File

@@ -77,6 +77,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
"apps.common.middleware.thread_local.ThreadLocalMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
@@ -126,12 +127,12 @@ WSGI_APPLICATION = "WYGIWYH.wsgi.application"
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("SQL_DATABASE"),
"USER": os.environ.get("SQL_USER", "user"),
"PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
"HOST": os.environ.get("SQL_HOST", "localhost"),
"PORT": "5432",
"PORT": os.environ.get("SQL_PORT", "5432"),
}
}
@@ -163,7 +164,7 @@ AUTH_USER_MODEL = "users.User"
LANGUAGE_CODE = "en"
LANGUAGES = (
("en", "English"),
# ("nl", "Nederlands"),
("nl", "Nederlands"),
("pt-br", "Português (Brasil)"),
)
@@ -363,7 +364,13 @@ PWA_APP_SPLASH_SCREEN = [
]
PWA_APP_DIR = "ltr"
PWA_APP_LANG = "en-US"
PWA_APP_SHORTCUTS = []
PWA_APP_SHORTCUTS = [
{
"name": "New Transaction",
"url": "/add/",
"description": "Add new transaction",
}
]
PWA_APP_SCREENSHOTS = [
{
"src": "/static/img/pwa/splash-750x1334.png",

View File

@@ -0,0 +1,31 @@
from apps.common.middleware.thread_local import get_current_user
from django.utils.formats import get_format as original_get_format
def get_format(format_type=None, lang=None, use_l10n=None):
user = get_current_user()
if user and user.is_authenticated and hasattr(user, "settings"):
user_settings = user.settings
if format_type == "THOUSAND_SEPARATOR":
number_format = getattr(user_settings, "number_format", None)
if number_format == "DC":
return "."
elif number_format == "CD":
return ","
elif format_type == "DECIMAL_SEPARATOR":
number_format = getattr(user_settings, "number_format", None)
if number_format == "DC":
return ","
elif number_format == "CD":
return "."
elif format_type == "SHORT_DATE_FORMAT":
date_format = getattr(user_settings, "date_format", None)
if date_format and date_format != "SHORT_DATE_FORMAT":
return date_format
elif format_type == "SHORT_DATETIME_FORMAT":
datetime_format = getattr(user_settings, "datetime_format", None)
if datetime_format and datetime_format != "SHORT_DATETIME_FORMAT":
return datetime_format
return original_get_format(format_type, lang, use_l10n)

View File

@@ -1,14 +1,17 @@
import zoneinfo
from django.utils import formats
from django.utils import timezone, translation
from django.utils.translation import activate
from django.utils.functional import lazy
from apps.common.functions.format import get_format as custom_get_format
from apps.users.models import UserSettings
class LocalizationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.patch_get_format()
def __call__(self, request):
tz = request.COOKIES.get("mytz")
@@ -33,9 +36,14 @@ class LocalizationMiddleware:
timezone.activate(zoneinfo.ZoneInfo("UTC"))
if user_language and user_language != "auto":
activate(user_language)
translation.activate(user_language)
else:
detected_language = translation.get_language_from_request(request)
activate(detected_language)
translation.activate(detected_language)
return self.get_response(request)
@staticmethod
def patch_get_format():
formats.get_format = custom_get_format
formats.get_format_lazy = lazy(custom_get_format, str, list, tuple)

View File

@@ -0,0 +1,73 @@
"""
threadlocals middleware
~~~~~~~~~~~~~~~~~~~~~~~
make the request object everywhere available (e.g. in model instance).
based on: http://code.djangoproject.com/wiki/CookBookThreadlocalsAndUser
Put this into your settings:
--------------------------------------------------------------------------
MIDDLEWARE_CLASSES = (
...
'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware',
...
)
--------------------------------------------------------------------------
Usage:
--------------------------------------------------------------------------
from django_tools.middlewares import ThreadLocal
# Get the current request object:
request = ThreadLocal.get_current_request()
# You can get the current user directly with:
user = ThreadLocal.get_current_user()
--------------------------------------------------------------------------
:copyleft: 2009-2017 by the django-tools team, see AUTHORS for more details.
:license: GNU GPL v3 or above, see LICENSE for more details.
"""
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object # fallback for Django < 1.10
_thread_locals = local()
def get_current_request():
"""returns the request object for this thread"""
return getattr(_thread_locals, "request", None)
def get_current_user():
"""returns the current user, if exist, otherwise returns None"""
request = get_current_request()
if request:
return getattr(request, "user", None)
class ThreadLocalMiddleware(MiddlewareMixin):
"""Simple middleware that adds the request object in thread local storage."""
def process_request(self, request):
_thread_locals.request = request
def process_response(self, request, response):
if hasattr(_thread_locals, "request"):
del _thread_locals.request
return response
def process_exception(self, request, exception):
if hasattr(_thread_locals, "request"):
del _thread_locals.request

View File

@@ -1,32 +0,0 @@
from django import template
from django.template.defaultfilters import date as date_filter
from django.utils import formats, timezone
register = template.Library()
@register.filter
def custom_date(value, user=None):
if not value:
return ""
# Determine if the value is a datetime or just a date
is_datetime = hasattr(value, "hour")
# Convert to current timezone if it's a datetime
if is_datetime and timezone.is_aware(value):
value = timezone.localtime(value)
if user and user.is_authenticated:
user_settings = user.settings
if is_datetime:
format_setting = user_settings.datetime_format
else:
format_setting = user_settings.date_format
return formats.date_format(value, format_setting, use_l10n=True)
return date_filter(
value, "SHORT_DATE_FORMAT" if not is_datetime else "SHORT_DATETIME_FORMAT"
)

View File

@@ -1,6 +1,6 @@
from django import template
from django.utils.formats import get_format
from apps.common.functions.format import get_format
register = template.Library()

View File

@@ -13,4 +13,9 @@ urlpatterns = [
views.month_year_picker,
name="month_year_picker",
),
path(
"cache/invalidate/",
views.invalidate_cache,
name="invalidate_cache",
),
]

View File

@@ -35,7 +35,7 @@ def django_to_python_datetime(django_format):
def django_to_airdatepicker_datetime(django_format):
format_map = {
# Time
"h": "h", # Hour (12-hour)
"h": "hh", # Hour (12-hour)
"H": "H", # Hour (24-hour)
"i": "m", # Minutes
"A": "AA", # AM/PM uppercase
@@ -76,7 +76,7 @@ def django_to_airdatepicker_datetime(django_format):
def django_to_airdatepicker_datetime_separated(django_format):
format_map = {
# Time formats
"h": "hH", # Hour (12-hour)
"h": "hh", # Hour (12-hour)
"H": "HH", # Hour (24-hour)
"i": "mm", # Minutes
"A": "AA", # AM/PM uppercase

View File

@@ -1,17 +1,32 @@
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Count
from django.db.models.functions import ExtractYear, ExtractMonth
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from django.utils.translation import gettext_lazy as _
from cachalot.api import invalidate
from apps.common.decorators.htmx import only_htmx
from apps.transactions.models import Transaction
@only_htmx
@login_required
@require_http_methods(["GET"])
def toasts(request):
return render(request, "common/fragments/toasts.html")
@only_htmx
@login_required
@require_http_methods(["GET"])
def month_year_picker(request):
field = request.GET.get("field", "reference_date")
for_ = request.GET.get("for", None)
@@ -84,3 +99,19 @@ def month_year_picker(request):
"current_year": current_year,
},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def invalidate_cache(request):
invalidate()
messages.success(request, _("Cache cleared successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated",
},
)

View File

@@ -2,7 +2,6 @@ import datetime
from django.forms import widgets
from django.utils import formats, translation, dates
from django.utils.formats import get_format
from django.utils.translation import gettext_lazy as _
from apps.common.utils.django import (
@@ -10,6 +9,7 @@ from apps.common.utils.django import (
django_to_airdatepicker_datetime,
django_to_airdatepicker_datetime_separated,
)
from apps.common.functions.format import get_format
class AirDatePickerInput(widgets.DateInput):
@@ -19,12 +19,10 @@ class AirDatePickerInput(widgets.DateInput):
format=None,
clear_button=True,
auto_close=True,
user=None,
*args,
**kwargs,
):
attrs = attrs or {}
self.user = user
super().__init__(attrs=attrs, format=format, *args, **kwargs)
self.clear_button = clear_button
self.auto_close = auto_close
@@ -41,12 +39,6 @@ class AirDatePickerInput(widgets.DateInput):
if self.format:
return self.format
if self.user and hasattr(self.user, "settings"):
user_format = self.user.settings.date_format
if user_format == "SHORT_DATE_FORMAT":
return get_format("SHORT_DATE_FORMAT", use_l10n=True)
return user_format
return get_format("SHORT_DATE_FORMAT", use_l10n=True)
def build_attrs(self, base_attrs, extra_attrs=None):
@@ -97,12 +89,10 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
timepicker=True,
clear_button=True,
auto_close=True,
user=None,
*args,
**kwargs,
):
attrs = attrs or {}
self.user = user
super().__init__(attrs=attrs, format=format, *args, **kwargs)
self.timepicker = timepicker
self.clear_button = clear_button
@@ -120,12 +110,6 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
if self.format:
return self.format
if self.user and hasattr(self.user, "settings"):
user_format = self.user.settings.datetime_format
if user_format == "SHORT_DATETIME_FORMAT":
return get_format("SHORT_DATETIME_FORMAT", use_l10n=True)
return user_format
return get_format("SHORT_DATETIME_FORMAT", use_l10n=True)
def build_attrs(self, base_attrs, extra_attrs=None):
@@ -148,9 +132,14 @@ class AirDateTimePickerInput(widgets.DateTimeInput):
def format_value(self, value):
"""Format the value for display in the widget."""
if value:
if value and isinstance(value, (datetime.date, datetime.datetime)):
self.attrs["data-value"] = datetime.datetime.strftime(
value, "%Y-%m-%d %H:%M:00"
value, "%Y-%m-%dT%H:%M:00"
)
elif value and isinstance(value, str):
value = datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:00")
self.attrs["data-value"] = datetime.datetime.strftime(
value, "%Y-%m-%dT%H:%M:00"
)
if value is None:
@@ -195,6 +184,7 @@ class AirMonthYearPickerInput(AirDatePickerInput):
# Add data attributes for AirDatepicker configuration
attrs["data-now-button-txt"] = _("Today")
attrs["data-date-format"] = "MMMM yyyy"
return attrs

View File

@@ -1,7 +1,9 @@
from decimal import Decimal, InvalidOperation
from django import forms
from django.utils.formats import get_format, number_format
from django.utils.formats import number_format
from apps.common.functions.format import get_format
def convert_to_decimal(value: str):

View File

@@ -72,7 +72,7 @@ class ExchangeRateForm(forms.ModelForm):
model = ExchangeRate
fields = ["from_currency", "to_currency", "rate", "date"]
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -81,9 +81,7 @@ class ExchangeRateForm(forms.ModelForm):
self.helper.layout = Layout("date", "from_currency", "to_currency", "rate")
self.fields["rate"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDateTimePickerInput(
clear_button=False, user=user
)
self.fields["date"].widget = AirDateTimePickerInput(clear_button=False)
if self.instance and self.instance.pk:
self.helper.layout.append(

View File

@@ -72,7 +72,9 @@ class ExchangeRate(models.Model):
def clean(self):
super().clean()
if self.from_currency == self.to_currency:
raise ValidationError(
{"to_currency": _("From and To currencies cannot be the same.")}
)
# Check if the attributes exist before comparing them
if hasattr(self, "from_currency") and hasattr(self, "to_currency"):
if self.from_currency == self.to_currency:
raise ValidationError(
{"to_currency": _("From and To currencies cannot be the same.")}
)

View File

@@ -83,7 +83,7 @@ def exchange_rates_list_pair(request):
@require_http_methods(["GET", "POST"])
def exchange_rate_add(request):
if request.method == "POST":
form = ExchangeRateForm(request.POST, user=request.user)
form = ExchangeRateForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Exchange rate added successfully"))
@@ -95,7 +95,7 @@ def exchange_rate_add(request):
},
)
else:
form = ExchangeRateForm(user=request.user)
form = ExchangeRateForm()
return render(
request,
@@ -111,7 +111,7 @@ def exchange_rate_edit(request, pk):
exchange_rate = get_object_or_404(ExchangeRate, id=pk)
if request.method == "POST":
form = ExchangeRateForm(request.POST, instance=exchange_rate, user=request.user)
form = ExchangeRateForm(request.POST, instance=exchange_rate)
if form.is_valid():
form.save()
messages.success(request, _("Exchange rate updated successfully"))
@@ -123,7 +123,7 @@ def exchange_rate_edit(request, pk):
},
)
else:
form = ExchangeRateForm(instance=exchange_rate, user=request.user)
form = ExchangeRateForm(instance=exchange_rate)
return render(
request,

View File

@@ -65,7 +65,7 @@ class DCAEntryForm(forms.ModelForm):
"notes": forms.Textarea(attrs={"rows": 3}),
}
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
@@ -106,4 +106,4 @@ class DCAEntryForm(forms.ModelForm):
self.fields["amount_paid"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["amount_received"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
self.fields["date"].widget = AirDatePickerInput(clear_button=False)

View File

@@ -155,7 +155,7 @@ def strategy_detail(request, strategy_id):
def strategy_entry_add(request, strategy_id):
strategy = get_object_or_404(DCAStrategy, id=strategy_id)
if request.method == "POST":
form = DCAEntryForm(request.POST, user=request.user)
form = DCAEntryForm(request.POST)
if form.is_valid():
entry = form.save(commit=False)
entry.strategy = strategy
@@ -169,7 +169,7 @@ def strategy_entry_add(request, strategy_id):
},
)
else:
form = DCAEntryForm(user=request.user)
form = DCAEntryForm()
return render(
request,
@@ -184,7 +184,7 @@ def strategy_entry_edit(request, strategy_id, entry_id):
dca_entry = get_object_or_404(DCAEntry, id=entry_id, strategy__id=strategy_id)
if request.method == "POST":
form = DCAEntryForm(request.POST, instance=dca_entry, user=request.user)
form = DCAEntryForm(request.POST, instance=dca_entry)
if form.is_valid():
form.save()
messages.success(request, _("Entry updated successfully"))
@@ -196,7 +196,7 @@ def strategy_entry_edit(request, strategy_id, entry_id):
},
)
else:
form = DCAEntryForm(instance=dca_entry, user=request.user)
form = DCAEntryForm(instance=dca_entry)
return render(
request,

View File

@@ -41,7 +41,7 @@ def monthly_overview(request, month: int, year: int):
previous_month = 12 if month == 1 else month - 1
previous_year = year - 1 if previous_month == 12 and month == 1 else year
f = TransactionsFilter(request.GET, user=request.user)
f = TransactionsFilter(request.GET)
return render(
request,
@@ -64,7 +64,7 @@ def monthly_overview(request, month: int, year: int):
def transactions_list(request, month: int, year: int):
order = request.GET.get("order")
f = TransactionsFilter(request.GET, user=request.user)
f = TransactionsFilter(request.GET)
transactions_filtered = (
f.qs.filter()
.filter(

View File

@@ -52,19 +52,4 @@ urlpatterns = [
views.transaction_rule_action_delete,
name="transaction_rule_action_delete",
),
# path(
# "rules/<int:installment_plan_id>/transactions/",
# views.installment_plan_transactions,
# name="rule_view",
# ),
# path(
# "rules/<int:installment_plan_id>/edit/",
# views.installment_plan_edit,
# name="rule_edit",
# ),
# path(
# "rules/<int:installment_plan_id>/delete/",
# views.installment_plan_delete,
# name="rule_delete",
# ),
]

View File

@@ -133,7 +133,7 @@ class TransactionsFilter(django_filters.FilterSet):
"to_amount",
]
def __init__(self, data=None, user=None, *args, **kwargs):
def __init__(self, data=None, *args, **kwargs):
# if filterset is bound, use initial values as defaults
if data is not None:
# get a mutable copy of the QueryDict
@@ -182,5 +182,5 @@ class TransactionsFilter(django_filters.FilterSet):
self.form.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.form.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.form.fields["date_start"].widget = AirDatePickerInput(user=user)
self.form.fields["date_end"].widget = AirDatePickerInput(user=user)
self.form.fields["date_start"].widget = AirDatePickerInput()
self.form.fields["date_end"].widget = AirDatePickerInput()

View File

@@ -1,5 +1,5 @@
from crispy_bootstrap5.bootstrap5 import Switch
from crispy_forms.bootstrap import FormActions
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout,
@@ -86,7 +86,7 @@ class TransactionForm(forms.ModelForm):
"account": TomSelect(clear_button=False, group_by="group"),
}
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing a transaction display non-archived items and it's own item even if it's archived
@@ -115,7 +115,7 @@ class TransactionForm(forms.ModelForm):
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Switch("is_paid"),
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
Row(
Column("account", css_class="form-group col-md-6 mb-0"),
Column("entities", css_class="form-group col-md-6 mb-0"),
@@ -136,8 +136,48 @@ class TransactionForm(forms.ModelForm):
"notes",
)
self.helper_simple = FormHelper()
self.helper_simple.form_tag = False
self.helper_simple.form_method = "post"
self.helper_simple.layout = Layout(
Field(
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
"account",
Row(
Column(Field("date"), css_class="form-group col-md-6 mb-0"),
Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
"description",
Field("amount", inputmode="decimal"),
BS5Accordion(
AccordionGroup(
_("More"),
"entities",
Row(
Column("category", css_class="form-group col-md-6 mb-0"),
Column("tags", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
"notes",
active=False,
),
flush=False,
always_open=False,
css_class="mb-3",
),
FormActions(
NoClassSubmit(
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
),
),
)
self.fields["reference_date"].required = False
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
if self.instance and self.instance.pk:
decimal_places = self.instance.account.currency.decimal_places
@@ -183,6 +223,43 @@ class TransactionForm(forms.ModelForm):
return instance
class BulkEditTransactionForm(TransactionForm):
is_paid = forms.NullBooleanField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make all fields optional
for field_name, field in self.fields.items():
field.required = False
del self.helper.layout[-1] # Remove button
del self.helper.layout[0:2] # Remove type, is_paid field
self.helper.layout.insert(
0,
Field(
"type",
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
),
)
self.helper.layout.insert(
1,
Field(
"is_paid",
template="transactions/widgets/unselectable_paid_toggle_button.html",
),
)
self.helper.layout.append(
FormActions(
NoClassSubmit(
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
),
),
)
class TransferForm(forms.Form):
from_account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
@@ -256,7 +333,7 @@ class TransferForm(forms.Form):
label=_("Notes"),
)
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -325,7 +402,7 @@ class TransferForm(forms.Form):
self.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDatePickerInput(clear_button=False, user=user)
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
def clean(self):
cleaned_data = super().clean()
@@ -438,7 +515,7 @@ class InstallmentPlanForm(forms.ModelForm):
"notes": forms.Textarea(attrs={"rows": 3}),
}
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing display non-archived items and it's own item even if it's archived
@@ -495,9 +572,7 @@ class InstallmentPlanForm(forms.ModelForm):
)
self.fields["installment_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["start_date"].widget = AirDatePickerInput(
clear_button=False, user=user
)
self.fields["start_date"].widget = AirDatePickerInput(clear_button=False)
if self.instance and self.instance.pk:
self.helper.layout.append(
@@ -685,7 +760,7 @@ class RecurringTransactionForm(forms.ModelForm):
),
}
def __init__(self, *args, user=None, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing display non-archived items and it's own item even if it's archived
@@ -742,10 +817,8 @@ class RecurringTransactionForm(forms.ModelForm):
)
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["start_date"].widget = AirDatePickerInput(
clear_button=False, user=user
)
self.fields["end_date"].widget = AirDatePickerInput(user=user)
self.fields["start_date"].widget = AirDatePickerInput(clear_button=False)
self.fields["end_date"].widget = AirDatePickerInput()
if self.instance and self.instance.pk:
self.helper.layout.append(

View File

@@ -49,7 +49,7 @@ class SoftDeleteQuerySet(models.QuerySet):
class SoftDeleteManager(models.Manager):
def get_queryset(self):
qs = SoftDeleteQuerySet(self.model, using=self._db)
return qs if not settings.ENABLE_SOFT_DELETE else qs.filter(deleted=False)
return qs.filter(deleted=False)
class AllObjectsManager(models.Manager):
@@ -60,7 +60,7 @@ class AllObjectsManager(models.Manager):
class DeletedObjectsManager(models.Manager):
def get_queryset(self):
qs = SoftDeleteQuerySet(self.model, using=self._db)
return qs if not settings.ENABLE_SOFT_DELETE else qs.filter(deleted=True)
return qs.filter(deleted=True)
class TransactionCategory(models.Model):

View File

@@ -27,7 +27,7 @@ def generate_recurring_transactions(timestamp=None):
@app.periodic(cron="10 1 * * *")
@app.task
def cleanup_deleted_transactions():
def cleanup_deleted_transactions(timestamp=None):
with cachalot_disabled():
if settings.ENABLE_SOFT_DELETE and settings.KEEP_DELETED_TRANSACTIONS_FOR == 0:
return "KEEP_DELETED_TRANSACTIONS_FOR is 0, no cleanup performed."
@@ -44,7 +44,7 @@ def cleanup_deleted_transactions():
days=settings.KEEP_DELETED_TRANSACTIONS_FOR
)
invalidate("transactions.Transaction")
invalidate()
# Hard delete soft-deleted transactions older than the cutoff date
old_transactions = Transaction.deleted_objects.filter(deleted_at__lt=cutoff_date)

View File

@@ -12,7 +12,7 @@ urlpatterns = [
name="transactions_all_summary",
),
path(
"transactions/actions/pay",
"transactions/actions/pay/",
views.bulk_pay_transactions,
name="transactions_bulk_pay",
),
@@ -27,32 +27,47 @@ urlpatterns = [
name="transactions_bulk_delete",
),
path(
"transaction/<int:transaction_id>/pay",
"transactions/actions/duplicate/",
views.bulk_clone_transactions,
name="transactions_bulk_clone",
),
path(
"transaction/<int:transaction_id>/pay/",
views.transaction_pay,
name="transaction_pay",
),
path(
"transaction/<int:transaction_id>/delete",
"transaction/<int:transaction_id>/delete/",
views.transaction_delete,
name="transaction_delete",
),
path(
"transaction/<int:transaction_id>/edit",
"transaction/<int:transaction_id>/edit/",
views.transaction_edit,
name="transaction_edit",
),
path(
"transaction/<int:transaction_id>/clone",
"transactions/bulk-edit/",
views.transactions_bulk_edit,
name="transactions_bulk_edit",
),
path(
"transaction/<int:transaction_id>/clone/",
views.transaction_clone,
name="transaction_clone",
),
path(
"transaction/add",
"transaction/add/",
views.transaction_add,
name="transaction_add",
),
path(
"transactions/transfer",
"add/",
views.transaction_simple_add,
name="transaction_simple_add",
),
path(
"transactions/transfer/",
views.transactions_transfer,
name="transactions_transfer",
),

View File

@@ -1,5 +1,9 @@
from copy import deepcopy
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _, ngettext_lazy
from apps.common.decorators.htmx import only_htmx
from apps.transactions.models import Transaction
@@ -9,7 +13,19 @@ from apps.transactions.models import Transaction
@login_required
def bulk_pay_transactions(request):
selected_transactions = request.GET.getlist("transactions", [])
Transaction.objects.filter(id__in=selected_transactions).update(is_paid=True)
transactions = Transaction.objects.filter(id__in=selected_transactions)
count = transactions.count()
transactions.update(is_paid=True)
messages.success(
request,
ngettext_lazy(
"%(count)s transaction marked as paid",
"%(count)s transactions marked as paid",
count,
)
% {"count": count},
)
return HttpResponse(
status=204,
@@ -21,7 +37,19 @@ def bulk_pay_transactions(request):
@login_required
def bulk_unpay_transactions(request):
selected_transactions = request.GET.getlist("transactions", [])
Transaction.objects.filter(id__in=selected_transactions).update(is_paid=False)
transactions = Transaction.objects.filter(id__in=selected_transactions)
count = transactions.count()
transactions.update(is_paid=False)
messages.success(
request,
ngettext_lazy(
"%(count)s transaction marked as not paid",
"%(count)s transactions marked as not paid",
count,
)
% {"count": count},
)
return HttpResponse(
status=204,
@@ -33,7 +61,54 @@ def bulk_unpay_transactions(request):
@login_required
def bulk_delete_transactions(request):
selected_transactions = request.GET.getlist("transactions", [])
Transaction.objects.filter(id__in=selected_transactions).delete()
transactions = Transaction.objects.filter(id__in=selected_transactions)
count = transactions.count()
transactions.delete()
messages.success(
request,
ngettext_lazy(
"%(count)s transaction deleted successfully",
"%(count)s transactions deleted successfully",
count,
)
% {"count": count},
)
return HttpResponse(
status=204,
headers={"HX-Trigger": "updated"},
)
@only_htmx
@login_required
def bulk_clone_transactions(request):
selected_transactions = request.GET.getlist("transactions", [])
transactions = Transaction.objects.filter(id__in=selected_transactions)
count = transactions.count()
for transaction in transactions:
new_transaction = deepcopy(transaction)
new_transaction.pk = None
new_transaction.installment_plan = None
new_transaction.installment_id = None
new_transaction.recurring_transaction = None
new_transaction.internal_id = None
new_transaction.save()
new_transaction.tags.add(*transaction.tags.all())
new_transaction.entities.add(*transaction.entities.all())
messages.success(
request,
ngettext_lazy(
"%(count)s transaction duplicated successfully",
"%(count)s transactions duplicated successfully",
count,
)
% {"count": count},
)
return HttpResponse(
status=204,

View File

@@ -81,7 +81,7 @@ def installment_plan_transactions(request, installment_plan_id):
@require_http_methods(["GET", "POST"])
def installment_plan_add(request):
if request.method == "POST":
form = InstallmentPlanForm(request.POST, user=request.user)
form = InstallmentPlanForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Installment Plan added successfully"))
@@ -93,7 +93,7 @@ def installment_plan_add(request):
},
)
else:
form = InstallmentPlanForm(user=request.user)
form = InstallmentPlanForm()
return render(
request,
@@ -109,9 +109,7 @@ def installment_plan_edit(request, installment_plan_id):
installment_plan = get_object_or_404(InstallmentPlan, id=installment_plan_id)
if request.method == "POST":
form = InstallmentPlanForm(
request.POST, instance=installment_plan, user=request.user
)
form = InstallmentPlanForm(request.POST, instance=installment_plan)
if form.is_valid():
form.save()
messages.success(request, _("Installment Plan updated successfully"))
@@ -123,7 +121,7 @@ def installment_plan_edit(request, installment_plan_id):
},
)
else:
form = InstallmentPlanForm(instance=installment_plan, user=request.user)
form = InstallmentPlanForm(instance=installment_plan)
return render(
request,

View File

@@ -106,7 +106,7 @@ def recurring_transaction_transactions(request, recurring_transaction_id):
@require_http_methods(["GET", "POST"])
def recurring_transaction_add(request):
if request.method == "POST":
form = RecurringTransactionForm(request.POST, user=request.user)
form = RecurringTransactionForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Recurring Transaction added successfully"))
@@ -118,7 +118,7 @@ def recurring_transaction_add(request):
},
)
else:
form = RecurringTransactionForm(user=request.user)
form = RecurringTransactionForm()
return render(
request,
@@ -136,9 +136,7 @@ def recurring_transaction_edit(request, recurring_transaction_id):
)
if request.method == "POST":
form = RecurringTransactionForm(
request.POST, instance=recurring_transaction, user=request.user
)
form = RecurringTransactionForm(request.POST, instance=recurring_transaction)
if form.is_valid():
form.save()
messages.success(request, _("Recurring Transaction updated successfully"))
@@ -150,9 +148,7 @@ def recurring_transaction_edit(request, recurring_transaction_id):
},
)
else:
form = RecurringTransactionForm(
instance=recurring_transaction, user=request.user
)
form = RecurringTransactionForm(instance=recurring_transaction)
return render(
request,

View File

@@ -7,14 +7,18 @@ from django.core.paginator import Paginator
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, ngettext_lazy
from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.common.utils.dicts import remove_falsey_entries
from apps.rules.signals import transaction_created
from apps.rules.signals import transaction_created, transaction_updated
from apps.transactions.filters import TransactionsFilter
from apps.transactions.forms import TransactionForm, TransferForm
from apps.transactions.forms import (
TransactionForm,
TransferForm,
BulkEditTransactionForm,
)
from apps.transactions.models import Transaction
from apps.transactions.utils.calculations import (
calculate_currency_totals,
@@ -40,7 +44,7 @@ def transaction_add(request):
).date()
if request.method == "POST":
form = TransactionForm(request.POST, user=request.user)
form = TransactionForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Transaction added successfully"))
@@ -51,7 +55,6 @@ def transaction_add(request):
)
else:
form = TransactionForm(
user=request.user,
initial={
"date": expected_date,
"type": transaction_type,
@@ -65,6 +68,48 @@ def transaction_add(request):
)
@login_required
@require_http_methods(["GET", "POST"])
def transaction_simple_add(request):
month = int(request.GET.get("month", timezone.localdate(timezone.now()).month))
year = int(request.GET.get("year", timezone.localdate(timezone.now()).year))
transaction_type = Transaction.Type(request.GET.get("type", "IN"))
now = timezone.localdate(timezone.now())
expected_date = datetime.datetime(
day=now.day if month == now.month and year == now.year else 1,
month=month,
year=year,
).date()
if request.method == "POST":
form = TransactionForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Transaction added successfully"))
form = TransactionForm(
initial={
"date": expected_date,
"type": transaction_type,
},
)
else:
form = TransactionForm(
initial={
"date": expected_date,
"type": transaction_type,
},
)
return render(
request,
"transactions/pages/add.html",
{"form": form},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
@@ -72,7 +117,7 @@ def transaction_edit(request, transaction_id, **kwargs):
transaction = get_object_or_404(Transaction, id=transaction_id)
if request.method == "POST":
form = TransactionForm(request.POST, user=request.user, instance=transaction)
form = TransactionForm(request.POST, instance=transaction)
if form.is_valid():
form.save()
messages.success(request, _("Transaction updated successfully"))
@@ -82,7 +127,7 @@ def transaction_edit(request, transaction_id, **kwargs):
headers={"HX-Trigger": "updated, hide_offcanvas"},
)
else:
form = TransactionForm(instance=transaction, user=request.user)
form = TransactionForm(instance=transaction)
return render(
request,
@@ -91,6 +136,60 @@ def transaction_edit(request, transaction_id, **kwargs):
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def transactions_bulk_edit(request):
# Get selected transaction IDs from the URL parameter
transaction_ids = request.GET.getlist("transactions") or request.POST.getlist(
"transactions"
)
# Load the selected transactions
transactions = Transaction.objects.filter(id__in=transaction_ids)
count = transactions.count()
if request.method == "POST":
form = BulkEditTransactionForm(request.POST)
if form.is_valid():
# Apply changes from the form to all selected transactions
for transaction in transactions:
for field_name, value in form.cleaned_data.items():
if value or isinstance(
value, bool
): # Only update fields that have been filled in the form
if field_name == "tags":
transaction.tags.set(value)
elif field_name == "entities":
transaction.entities.set(value)
else:
setattr(transaction, field_name, value)
transaction.save()
transaction_updated.send(sender=transaction)
messages.success(
request,
ngettext_lazy(
"%(count)s transaction updated successfully",
"%(count)s transactions updated successfully",
count,
)
% {"count": count},
)
return HttpResponse(
status=204,
headers={"HX-Trigger": "updated, hide_offcanvas"},
)
else:
form = BulkEditTransactionForm(initial={"is_paid": None, "type": None})
context = {
"form": form,
"transactions": transactions,
}
return render(request, "transactions/fragments/bulk_edit.html", context)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
@@ -101,6 +200,7 @@ def transaction_clone(request, transaction_id, **kwargs):
new_transaction.installment_plan = None
new_transaction.installment_id = None
new_transaction.recurring_transaction = None
new_transaction.internal_id = None
new_transaction.save()
new_transaction.tags.add(*transaction.tags.all())
@@ -171,7 +271,7 @@ def transactions_transfer(request):
).date()
if request.method == "POST":
form = TransferForm(request.POST, user=request.user)
form = TransferForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, _("Transfer added successfully"))
@@ -185,7 +285,6 @@ def transactions_transfer(request):
"reference_date": expected_date,
"date": expected_date,
},
user=request.user,
)
return render(request, "transactions/fragments/transfer.html", {"form": form})
@@ -214,7 +313,7 @@ def transaction_pay(request, transaction_id):
@login_required
@require_http_methods(["GET"])
def transaction_all_index(request):
f = TransactionsFilter(request.GET, user=request.user)
f = TransactionsFilter(request.GET)
return render(request, "transactions/pages/transactions.html", {"filter": f})
@@ -236,7 +335,7 @@ def transaction_all_list(request):
transactions = default_order(transactions, order=order)
f = TransactionsFilter(request.GET, user=request.user, queryset=transactions)
f = TransactionsFilter(request.GET, queryset=transactions)
page_number = request.GET.get("page", 1)
paginator = Paginator(f.qs, 100)
@@ -266,7 +365,7 @@ def transaction_all_summary(request):
"installment_plan",
).all()
f = TransactionsFilter(request.GET, user=request.user, queryset=transactions)
f = TransactionsFilter(request.GET, queryset=transactions)
currency_data = calculate_currency_totals(f.qs.all(), ignore_empty=True)
currency_percentages = calculate_percentage_distribution(currency_data)

View File

@@ -81,6 +81,12 @@ class UserSettingsForm(forms.ModelForm):
("Y.m.d h:i A", "2025.01.20 03:30 PM"),
]
NUMBER_FORMAT_CHOICES = [
("AA", _("Default")),
("DC", "1.234,50"),
("CD", "1,234.50"),
]
date_format = forms.ChoiceField(
choices=DATE_FORMAT_CHOICES, initial="SHORT_DATE_FORMAT", label=_("Date Format")
)
@@ -90,6 +96,12 @@ class UserSettingsForm(forms.ModelForm):
label=_("Datetime Format"),
)
number_format = forms.ChoiceField(
choices=NUMBER_FORMAT_CHOICES,
initial="AA",
label=_("Number Format"),
)
class Meta:
model = UserSettings
fields = [
@@ -98,6 +110,7 @@ class UserSettingsForm(forms.ModelForm):
"start_page",
"date_format",
"datetime_format",
"number_format",
]
def __init__(self, *args, **kwargs):
@@ -111,6 +124,7 @@ class UserSettingsForm(forms.ModelForm):
"timezone",
"date_format",
"datetime_format",
"number_format",
"start_page",
FormActions(
NoClassSubmit(

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-01-25 18:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0015_alter_usersettings_language'),
]
operations = [
migrations.AlterField(
model_name='usersettings',
name='language',
field=models.CharField(choices=[('auto', 'Auto'), ('en', 'English'), ('nl', 'Nederlands'), ('pt-br', 'Português (Brasil)')], default='auto', max_length=10, verbose_name='Language'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.5 on 2025-01-27 12:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0016_alter_usersettings_language'),
]
operations = [
migrations.AddField(
model_name='usersettings',
name='number_format',
field=models.CharField(default='AA', max_length=2, verbose_name='Number Format'),
),
]

View File

@@ -1,8 +1,8 @@
import pytz
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.contrib.auth.models import AbstractUser, Group
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.users.managers import UserManager
@@ -44,6 +44,9 @@ class UserSettings(models.Model):
default="SHORT_DATETIME_FORMAT",
verbose_name=_("Datetime Format"),
)
number_format = models.CharField(
max_length=2, default="AA", verbose_name=_("Number Format")
)
language = models.CharField(
max_length=10,
@@ -66,3 +69,6 @@ class UserSettings(models.Model):
def __str__(self):
return f"{self.user.email}'s settings"
def clean(self):
super().clean()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,9 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-24 19:24+0000\n"
"PO-Revision-Date: 2025-01-24 16:25-0300\n"
"Last-Translator: \n"
"POT-Creation-Date: 2025-01-28 00:49+0000\n"
"PO-Revision-Date: 2025-01-27 21:49-0300\n"
"Last-Translator: Herculino Trotta\n"
"Language-Team: \n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
@@ -24,27 +24,28 @@ msgid "Group name"
msgstr "Nome do grupo"
#: apps/accounts/forms.py:40 apps/accounts/forms.py:96
#: apps/currencies/forms.py:52 apps/currencies/forms.py:92 apps/dca/forms.py:41
#: apps/currencies/forms.py:52 apps/currencies/forms.py:90 apps/dca/forms.py:41
#: apps/dca/forms.py:93 apps/import_app/forms.py:34 apps/rules/forms.py:45
#: apps/rules/forms.py:87 apps/transactions/forms.py:150
#: apps/transactions/forms.py:506 apps/transactions/forms.py:549
#: apps/transactions/forms.py:581 apps/transactions/forms.py:616
#: apps/transactions/forms.py:754
#: apps/rules/forms.py:87 apps/transactions/forms.py:190
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
#: apps/transactions/forms.py:624 apps/transactions/forms.py:656
#: apps/transactions/forms.py:691 apps/transactions/forms.py:827
msgid "Update"
msgstr "Atualizar"
#: apps/accounts/forms.py:48 apps/accounts/forms.py:104
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:60
#: apps/currencies/forms.py:100 apps/dca/forms.py:49 apps/dca/forms.py:102
#: apps/currencies/forms.py:98 apps/dca/forms.py:49 apps/dca/forms.py:102
#: apps/import_app/forms.py:42 apps/rules/forms.py:53 apps/rules/forms.py:95
#: apps/transactions/forms.py:159 apps/transactions/forms.py:514
#: apps/transactions/forms.py:557 apps/transactions/forms.py:589
#: apps/transactions/forms.py:624 apps/transactions/forms.py:762
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
#: apps/transactions/forms.py:664 apps/transactions/forms.py:699
#: apps/transactions/forms.py:835
#: templates/account_groups/fragments/list.html:9
#: templates/accounts/fragments/list.html:9
#: templates/categories/fragments/list.html:9
#: templates/currencies/fragments/list.html:9
#: templates/dca/fragments/strategy/details.html:38
#: templates/dca/fragments/strategy/details.html:37
#: templates/dca/fragments/strategy/list.html:9
#: templates/entities/fragments/list.html:9
#: templates/exchange_rates/fragments/list.html:10
@@ -66,17 +67,17 @@ msgid "New balance"
msgstr "Novo saldo"
#: apps/accounts/forms.py:119 apps/rules/models.py:27
#: apps/transactions/forms.py:39 apps/transactions/forms.py:214
#: apps/transactions/forms.py:221 apps/transactions/forms.py:401
#: apps/transactions/forms.py:648 apps/transactions/models.py:159
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
#: apps/transactions/models.py:311 apps/transactions/models.py:491
msgid "Category"
msgstr "Categoria"
#: apps/accounts/forms.py:126 apps/rules/models.py:28
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
#: apps/transactions/forms.py:230 apps/transactions/forms.py:238
#: apps/transactions/forms.py:394 apps/transactions/forms.py:641
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
#: apps/transactions/models.py:165 apps/transactions/models.py:313
#: apps/transactions/models.py:495 templates/includes/navbar.html:98
#: templates/tags/fragments/list.html:5 templates/tags/pages/index.html:4
@@ -148,8 +149,8 @@ msgstr ""
"Contas arquivadas não aparecem nem contam para o seu patrimônio líquido"
#: apps/accounts/models.py:59 apps/rules/models.py:19
#: apps/transactions/forms.py:59 apps/transactions/forms.py:386
#: apps/transactions/forms.py:633 apps/transactions/models.py:132
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
#: apps/transactions/models.py:271 apps/transactions/models.py:473
msgid "Account"
msgstr "Conta"
@@ -319,11 +320,15 @@ msgstr "Erro"
msgid "Info"
msgstr "Informação"
#: apps/common/widgets/datepicker.py:55 apps/common/widgets/datepicker.py:197
#: apps/common/views.py:110
msgid "Cache cleared successfully"
msgstr "Cache limpo com sucesso"
#: apps/common/widgets/datepicker.py:47 apps/common/widgets/datepicker.py:186
msgid "Today"
msgstr "Hoje"
#: apps/common/widgets/datepicker.py:139
#: apps/common/widgets/datepicker.py:123
msgid "Now"
msgstr "Agora"
@@ -333,7 +338,8 @@ msgstr "Remover"
#: apps/common/widgets/tom_select.py:14
#: templates/mini_tools/unit_price_calculator.html:174
#: templates/transactions/pages/transactions.html:18
#: templates/monthly_overview/pages/overview.html:132
#: templates/transactions/pages/transactions.html:17
msgid "Clear"
msgstr "Limpar"
@@ -350,10 +356,10 @@ msgid "Suffix"
msgstr "Sufixo"
#: apps/currencies/forms.py:68 apps/dca/models.py:156 apps/rules/models.py:22
#: apps/transactions/forms.py:63 apps/transactions/forms.py:242
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
#: apps/transactions/models.py:142
#: templates/dca/fragments/strategy/details.html:53
#: templates/exchange_rates/fragments/table.html:11
#: templates/dca/fragments/strategy/details.html:52
#: templates/exchange_rates/fragments/table.html:10
msgid "Date"
msgstr "Data"
@@ -403,7 +409,7 @@ msgstr "Data e Tempo"
msgid "Exchange Rates"
msgstr "Taxas de Câmbio"
#: apps/currencies/models.py:77
#: apps/currencies/models.py:79
msgid "From and To currencies cannot be the same."
msgstr "As moedas De e Para não podem ser as mesmas."
@@ -440,7 +446,7 @@ msgid "Payment Currency"
msgstr "Moeda de pagamento"
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/models.py:26
#: apps/transactions/forms.py:256 apps/transactions/models.py:155
#: apps/transactions/forms.py:333 apps/transactions/models.py:155
#: apps/transactions/models.py:320 apps/transactions/models.py:501
msgid "Notes"
msgstr "Notas"
@@ -457,11 +463,11 @@ msgstr "Estratégias CMP"
msgid "Strategy"
msgstr "Estratégia"
#: apps/dca/models.py:158 templates/dca/fragments/strategy/details.html:55
#: apps/dca/models.py:158 templates/dca/fragments/strategy/details.html:54
msgid "Amount Paid"
msgstr "Quantia paga"
#: apps/dca/models.py:161 templates/dca/fragments/strategy/details.html:54
#: apps/dca/models.py:161 templates/dca/fragments/strategy/details.html:53
msgid "Amount Received"
msgstr "Quantia recebida"
@@ -515,11 +521,6 @@ msgstr "Selecione um arquivo"
msgid "Import"
msgstr "Importar"
#: apps/import_app/models.py:12
#, python-brace-format
msgid "Version {number}"
msgstr "Versão {number}"
#: apps/import_app/models.py:15
msgid "YAML Configuration"
msgstr "Configuração YAML"
@@ -529,33 +530,38 @@ msgstr "Configuração YAML"
msgid "Version"
msgstr "Versão"
#: apps/import_app/models.py:35
#: apps/import_app/models.py:30
#, python-brace-format
msgid "Version {number}"
msgstr "Versão {number}"
#: apps/import_app/models.py:39
msgid "Invalid YAML Configuration: "
msgstr "Configuração YAML inválida: "
#: apps/import_app/models.py:41
#: apps/import_app/models.py:45
msgid "Queued"
msgstr "Na fila"
#: apps/import_app/models.py:42
#: apps/import_app/models.py:46
msgid "Processing"
msgstr "Processando"
#: apps/import_app/models.py:43
#: apps/import_app/models.py:47
msgid "Failed"
msgstr "Falhou"
#: apps/import_app/models.py:44
#: apps/import_app/models.py:48
#: templates/installment_plans/fragments/list.html:24
#: templates/recurring_transactions/fragments/list.html:27
msgid "Finished"
msgstr "Finalizado"
#: apps/import_app/models.py:50
#: apps/import_app/models.py:54
msgid "Status"
msgstr "Status"
#: apps/import_app/models.py:58
#: apps/import_app/models.py:62
msgid "File name"
msgstr "Nome do Arquivo"
@@ -604,7 +610,7 @@ msgid "A value for this field already exists in the rule."
msgstr "Já existe um valor para esse campo na regra."
#: apps/rules/models.py:10 apps/rules/models.py:25
#: apps/transactions/forms.py:248 apps/transactions/models.py:153
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:278 apps/transactions/models.py:487
msgid "Description"
msgstr "Descrição"
@@ -620,11 +626,13 @@ msgstr "Tipo"
#: apps/rules/models.py:21 apps/transactions/filters.py:23
#: apps/transactions/models.py:141
#: templates/transactions/widgets/paid_toggle_button.html:12
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:16
msgid "Paid"
msgstr "Pago"
#: apps/rules/models.py:23 apps/transactions/forms.py:66
#: apps/transactions/forms.py:245 apps/transactions/forms.py:415
#: apps/transactions/forms.py:322 apps/transactions/forms.py:492
#: apps/transactions/models.py:143 apps/transactions/models.py:294
#: apps/transactions/models.py:503
msgid "Reference Date"
@@ -636,8 +644,8 @@ msgid "Amount"
msgstr "Quantia"
#: apps/rules/models.py:29 apps/transactions/filters.py:81
#: apps/transactions/forms.py:55 apps/transactions/forms.py:409
#: apps/transactions/forms.py:656 apps/transactions/models.py:117
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
#: apps/transactions/models.py:170 apps/transactions/models.py:316
#: apps/transactions/models.py:498 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:100
@@ -685,6 +693,8 @@ msgid "Action deleted successfully"
msgstr "Ação apagada com sucesso"
#: apps/transactions/filters.py:24 templates/includes/navbar.html:45
#: templates/transactions/widgets/paid_toggle_button.html:8
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:12
msgid "Projected"
msgstr "Previsto"
@@ -721,23 +731,27 @@ msgstr "Quantia miníma"
msgid "Amount max"
msgstr "Quantia máxima"
#: apps/transactions/forms.py:189
#: apps/transactions/forms.py:158
msgid "More"
msgstr "Mais"
#: apps/transactions/forms.py:266
msgid "From Account"
msgstr "Conta de origem"
#: apps/transactions/forms.py:194
#: apps/transactions/forms.py:271
msgid "To Account"
msgstr "Conta de destino"
#: apps/transactions/forms.py:201
#: apps/transactions/forms.py:278
msgid "From Amount"
msgstr "Quantia de origem"
#: apps/transactions/forms.py:206
#: apps/transactions/forms.py:283
msgid "To Amount"
msgstr "Quantia de destino"
#: apps/transactions/forms.py:321
#: apps/transactions/forms.py:398
#: templates/calendar_view/pages/calendar.html:84
#: templates/monthly_overview/pages/overview.html:84
#: templates/yearly_overview/pages/overview_by_account.html:79
@@ -745,27 +759,27 @@ msgstr "Quantia de destino"
msgid "Transfer"
msgstr "Transferir"
#: apps/transactions/forms.py:336
#: apps/transactions/forms.py:413
msgid "From and To accounts must be different."
msgstr "As contas De e Para devem ser diferentes."
#: apps/transactions/forms.py:535
#: apps/transactions/forms.py:610
msgid "Tag name"
msgstr "Nome da Tag"
#: apps/transactions/forms.py:567
#: apps/transactions/forms.py:642
msgid "Entity name"
msgstr "Nome da entidade"
#: apps/transactions/forms.py:599
#: apps/transactions/forms.py:674
msgid "Category name"
msgstr "Nome da Categoria"
#: apps/transactions/forms.py:601
#: apps/transactions/forms.py:676
msgid "Muted categories won't count towards your monthly total"
msgstr "As categorias silenciadas não serão contabilizadas em seu total mensal"
#: apps/transactions/forms.py:773
#: apps/transactions/forms.py:846
msgid "End date should be after the start date"
msgstr "Data final deve ser após data inicial"
@@ -977,6 +991,34 @@ msgstr "%(value)s tem muitas casas decimais. O máximo é 30."
msgid "%(value)s is not a non-negative number"
msgstr "%(value)s não é um número positivo"
#: apps/transactions/views/actions.py:23
#, python-format
msgid "%(count)s transaction marked as paid"
msgid_plural "%(count)s transactions marked as paid"
msgstr[0] "%(count)s transação marcada como paga"
msgstr[1] "%(count)s transações marcadas como paga"
#: apps/transactions/views/actions.py:47
#, python-format
msgid "%(count)s transaction marked as not paid"
msgid_plural "%(count)s transactions marked as not paid"
msgstr[0] "%(count)s transação marcada como não paga"
msgstr[1] "%(count)s transações marcadas como não paga"
#: apps/transactions/views/actions.py:71
#, python-format
msgid "%(count)s transaction deleted successfully"
msgid_plural "%(count)s transactions deleted successfully"
msgstr[0] "%(count)s transação apagada com sucesso"
msgstr[1] "%(count)s transações apagadas com sucesso"
#: apps/transactions/views/actions.py:106
#, python-format
msgid "%(count)s transaction duplicated successfully"
msgid_plural "%(count)s transactions duplicated successfully"
msgstr[0] "%(count)s transação duplicada com sucesso"
msgstr[1] "%(count)s transações duplicadas com sucesso"
#: apps/transactions/views/categories.py:64
msgid "Category added successfully"
msgstr "Categoria adicionada com sucesso"
@@ -1005,15 +1047,15 @@ msgstr "Entidade apagada com sucesso"
msgid "Installment Plan added successfully"
msgstr "Parcelamento adicionado com sucesso"
#: apps/transactions/views/installment_plans.py:117
#: apps/transactions/views/installment_plans.py:115
msgid "Installment Plan updated successfully"
msgstr "Parcelamento atualizado com sucesso"
#: apps/transactions/views/installment_plans.py:142
#: apps/transactions/views/installment_plans.py:140
msgid "Installment Plan refreshed successfully"
msgstr "Parcelamento atualizado com sucesso"
#: apps/transactions/views/installment_plans.py:160
#: apps/transactions/views/installment_plans.py:158
msgid "Installment Plan deleted successfully"
msgstr "Parcelamento apagado com sucesso"
@@ -1021,23 +1063,23 @@ msgstr "Parcelamento apagado com sucesso"
msgid "Recurring Transaction added successfully"
msgstr "Transação Recorrente adicionada com sucesso"
#: apps/transactions/views/recurring_transactions.py:144
#: apps/transactions/views/recurring_transactions.py:142
msgid "Recurring Transaction updated successfully"
msgstr "Transação Recorrente atualizada com sucesso"
#: apps/transactions/views/recurring_transactions.py:190
#: apps/transactions/views/recurring_transactions.py:186
msgid "Recurring transaction unpaused successfully"
msgstr "Transação Recorrente despausada com sucesso"
#: apps/transactions/views/recurring_transactions.py:193
#: apps/transactions/views/recurring_transactions.py:189
msgid "Recurring transaction paused successfully"
msgstr "Transação Recorrente pausada com sucesso"
#: apps/transactions/views/recurring_transactions.py:219
#: apps/transactions/views/recurring_transactions.py:215
msgid "Recurring transaction finished successfully"
msgstr "Transação Recorrente finalizada com sucesso"
#: apps/transactions/views/recurring_transactions.py:239
#: apps/transactions/views/recurring_transactions.py:235
msgid "Recurring Transaction deleted successfully"
msgstr "Transação Recorrente apagada com sucesso"
@@ -1053,23 +1095,31 @@ msgstr "Tag atualizada com sucesso"
msgid "Tag deleted successfully"
msgstr "Tag apagada com sucesso"
#: apps/transactions/views/transactions.py:46
#: apps/transactions/views/transactions.py:50
#: apps/transactions/views/transactions.py:89
msgid "Transaction added successfully"
msgstr "Transação adicionada com sucesso"
#: apps/transactions/views/transactions.py:78
#: apps/transactions/views/transactions.py:123
msgid "Transaction updated successfully"
msgstr "Transação atualizada com sucesso"
#: apps/transactions/views/transactions.py:109
#: apps/transactions/views/transactions.py:173
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] "%(count)s transação atualizada com sucesso"
msgstr[1] "%(count)s transações atualizadas com sucesso"
#: apps/transactions/views/transactions.py:209
msgid "Transaction duplicated successfully"
msgstr "Transação duplicada com sucesso"
#: apps/transactions/views/transactions.py:151
#: apps/transactions/views/transactions.py:251
msgid "Transaction deleted successfully"
msgstr "Transação apagada com sucesso"
#: apps/transactions/views/transactions.py:177
#: apps/transactions/views/transactions.py:277
msgid "Transfer added successfully"
msgstr "Transferência adicionada com sucesso"
@@ -1109,21 +1159,25 @@ msgstr "E-mail ou senha inválidos"
msgid "This account is deactivated"
msgstr "Essa conta está desativada"
#: apps/users/forms.py:50 apps/users/forms.py:63
#: apps/users/forms.py:50 apps/users/forms.py:63 apps/users/forms.py:85
#: templates/monthly_overview/pages/overview.html:116
#: templates/transactions/pages/transactions.html:36
#: templates/transactions/pages/transactions.html:35
msgid "Default"
msgstr "Padrão"
#: apps/users/forms.py:85 apps/users/models.py:40
#: apps/users/forms.py:91 apps/users/models.py:40
msgid "Date Format"
msgstr "Formato de Data"
#: apps/users/forms.py:90 apps/users/models.py:45
#: apps/users/forms.py:96 apps/users/models.py:45
msgid "Datetime Format"
msgstr "Formato de Data e Hora"
#: apps/users/forms.py:117
#: apps/users/forms.py:102 apps/users/models.py:48
msgid "Number Format"
msgstr "Formato de Número"
#: apps/users/forms.py:131
msgid "Save"
msgstr "Salvar"
@@ -1147,19 +1201,19 @@ msgstr "Todas as transações"
msgid "Calendar"
msgstr "Calendário"
#: apps/users/models.py:50 apps/users/models.py:56
#: apps/users/models.py:53 apps/users/models.py:59
msgid "Auto"
msgstr "Automático"
#: apps/users/models.py:52
#: apps/users/models.py:55
msgid "Language"
msgstr "Linguagem"
#: apps/users/models.py:58
#: apps/users/models.py:61
msgid "Time Zone"
msgstr "Fuso horário"
#: apps/users/models.py:64
#: apps/users/models.py:67
msgid "Start page"
msgstr "Página inicial"
@@ -1195,9 +1249,9 @@ msgstr "Editar grupo de conta"
#: templates/accounts/fragments/list.html:37
#: templates/categories/fragments/table.html:24
#: templates/currencies/fragments/list.html:33
#: templates/dca/fragments/strategy/details.html:64
#: templates/dca/fragments/strategy/details.html:63
#: templates/entities/fragments/table.html:23
#: templates/exchange_rates/fragments/table.html:20
#: templates/exchange_rates/fragments/table.html:19
#: templates/import_app/fragments/profiles/list.html:44
#: templates/installment_plans/fragments/table.html:23
#: templates/recurring_transactions/fragments/table.html:25
@@ -1209,12 +1263,13 @@ msgstr "Ações"
#: templates/account_groups/fragments/list.html:36
#: templates/accounts/fragments/list.html:41
#: templates/categories/fragments/table.html:29
#: templates/cotton/transaction/item.html:110
#: templates/cotton/transaction/item.html:109
#: templates/cotton/ui/transactions_action_bar.html:47
#: templates/currencies/fragments/list.html:37
#: templates/dca/fragments/strategy/details.html:68
#: templates/dca/fragments/strategy/details.html:67
#: templates/dca/fragments/strategy/list.html:34
#: templates/entities/fragments/table.html:28
#: templates/exchange_rates/fragments/table.html:24
#: templates/exchange_rates/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:48
#: templates/installment_plans/fragments/table.html:27
#: templates/recurring_transactions/fragments/table.html:29
@@ -1227,13 +1282,13 @@ msgstr "Editar"
#: templates/account_groups/fragments/list.html:43
#: templates/accounts/fragments/list.html:48
#: templates/categories/fragments/table.html:36
#: templates/cotton/transaction/item.html:125
#: templates/cotton/ui/transactions_action_bar.html:50
#: templates/cotton/transaction/item.html:124
#: templates/cotton/ui/transactions_action_bar.html:84
#: templates/currencies/fragments/list.html:44
#: templates/dca/fragments/strategy/details.html:76
#: templates/dca/fragments/strategy/details.html:75
#: templates/dca/fragments/strategy/list.html:42
#: templates/entities/fragments/table.html:36
#: templates/exchange_rates/fragments/table.html:32
#: templates/exchange_rates/fragments/table.html:31
#: templates/import_app/fragments/profiles/list.html:69
#: templates/import_app/fragments/runs/list.html:102
#: templates/installment_plans/fragments/table.html:56
@@ -1248,13 +1303,13 @@ msgstr "Apagar"
#: templates/account_groups/fragments/list.html:47
#: templates/accounts/fragments/list.html:52
#: templates/categories/fragments/table.html:41
#: templates/cotton/transaction/item.html:129
#: templates/cotton/ui/transactions_action_bar.html:52
#: templates/cotton/transaction/item.html:128
#: templates/cotton/ui/transactions_action_bar.html:86
#: templates/currencies/fragments/list.html:48
#: templates/dca/fragments/strategy/details.html:81
#: templates/dca/fragments/strategy/details.html:80
#: templates/dca/fragments/strategy/list.html:46
#: templates/entities/fragments/table.html:40
#: templates/exchange_rates/fragments/table.html:37
#: templates/exchange_rates/fragments/table.html:36
#: templates/import_app/fragments/profiles/list.html:73
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:48
@@ -1272,13 +1327,13 @@ msgstr "Tem certeza?"
#: templates/account_groups/fragments/list.html:48
#: templates/accounts/fragments/list.html:53
#: templates/categories/fragments/table.html:42
#: templates/cotton/transaction/item.html:130
#: templates/cotton/ui/transactions_action_bar.html:53
#: templates/cotton/transaction/item.html:129
#: templates/cotton/ui/transactions_action_bar.html:87
#: templates/currencies/fragments/list.html:49
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/details.html:81
#: templates/dca/fragments/strategy/list.html:47
#: templates/entities/fragments/table.html:41
#: templates/exchange_rates/fragments/table.html:38
#: templates/exchange_rates/fragments/table.html:37
#: templates/import_app/fragments/profiles/list.html:74
#: templates/rules/fragments/list.html:49
#: templates/rules/fragments/transaction_rule/view.html:61
@@ -1289,12 +1344,12 @@ msgstr "Você não será capaz de reverter isso!"
#: templates/account_groups/fragments/list.html:49
#: templates/accounts/fragments/list.html:54
#: templates/categories/fragments/table.html:43
#: templates/cotton/transaction/item.html:131
#: templates/cotton/transaction/item.html:130
#: templates/currencies/fragments/list.html:50
#: templates/dca/fragments/strategy/details.html:83
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/list.html:48
#: templates/entities/fragments/table.html:42
#: templates/exchange_rates/fragments/table.html:39
#: templates/exchange_rates/fragments/table.html:38
#: templates/import_app/fragments/profiles/list.html:75
#: templates/import_app/fragments/runs/list.html:108
#: templates/installment_plans/fragments/table.html:62
@@ -1369,11 +1424,11 @@ msgstr "SÁB"
msgid "SUN"
msgstr "DOM"
#: templates/calendar_view/fragments/list_transactions.html:6
#: templates/calendar_view/fragments/list_transactions.html:5
msgid "Transactions on"
msgstr "Transações em"
#: templates/calendar_view/fragments/list_transactions.html:16
#: templates/calendar_view/fragments/list_transactions.html:15
msgid "No transactions on this date"
msgstr "Nenhuma transação nesta data"
@@ -1432,11 +1487,12 @@ msgstr "Fechar"
msgid "Search"
msgstr "Buscar"
#: templates/cotton/transaction/item.html:6
#: templates/cotton/transaction/item.html:5
msgid "Select"
msgstr "Selecionar"
#: templates/cotton/transaction/item.html:117
#: templates/cotton/transaction/item.html:116
#: templates/cotton/ui/transactions_action_bar.html:76
msgid "Duplicate"
msgstr "Duplicar"
@@ -1460,61 +1516,62 @@ msgstr "Despesas Previstas"
msgid "Current Expenses"
msgstr "Despesas Atuais"
#: templates/cotton/ui/transactions_action_bar.html:17
#: templates/cotton/ui/transactions_action_bar.html:29
msgid "Select All"
msgstr "Selecionar todos"
#: templates/cotton/ui/transactions_action_bar.html:23
#: templates/cotton/ui/transactions_action_bar.html:35
msgid "Unselect All"
msgstr "Desmarcar todos"
#: templates/cotton/ui/transactions_action_bar.html:34
msgid "Mark as paid"
msgstr "Marcar como pago"
#: templates/cotton/ui/transactions_action_bar.html:41
msgid "Mark as unpaid"
msgstr "Marcar como não pago"
#: templates/cotton/ui/transactions_action_bar.html:54
msgid "Yes, delete them!"
msgstr "Sim, apague!"
#: templates/cotton/ui/transactions_action_bar.html:101
#: templates/cotton/ui/transactions_action_bar.html:125
#: templates/cotton/ui/transactions_action_bar.html:145
#: templates/cotton/ui/transactions_action_bar.html:165
#: templates/cotton/ui/transactions_action_bar.html:185
#: templates/cotton/ui/transactions_action_bar.html:205
#: templates/cotton/ui/transactions_action_bar.html:225
msgid "copied!"
msgstr "copiado!"
#: templates/cotton/ui/transactions_action_bar.html:110
#: templates/cotton/ui/transactions_action_bar.html:52
#: templates/cotton/ui/transactions_action_bar.html:143
msgid "Toggle Dropdown"
msgstr "Alternar menu suspenso"
#: templates/cotton/ui/transactions_action_bar.html:118
#: templates/cotton/ui/transactions_action_bar.html:60
msgid "Mark as unpaid"
msgstr "Marcar como não pago"
#: templates/cotton/ui/transactions_action_bar.html:67
msgid "Mark as paid"
msgstr "Marcar como pago"
#: templates/cotton/ui/transactions_action_bar.html:88
msgid "Yes, delete them!"
msgstr "Sim, apague!"
#: templates/cotton/ui/transactions_action_bar.html:134
#: templates/cotton/ui/transactions_action_bar.html:158
#: templates/cotton/ui/transactions_action_bar.html:178
#: templates/cotton/ui/transactions_action_bar.html:198
#: templates/cotton/ui/transactions_action_bar.html:218
#: templates/cotton/ui/transactions_action_bar.html:238
#: templates/cotton/ui/transactions_action_bar.html:258
msgid "copied!"
msgstr "copiado!"
#: templates/cotton/ui/transactions_action_bar.html:151
msgid "Flat Total"
msgstr "Total Fixo"
#: templates/cotton/ui/transactions_action_bar.html:138
#: templates/cotton/ui/transactions_action_bar.html:171
msgid "Real Total"
msgstr "Total Real"
#: templates/cotton/ui/transactions_action_bar.html:158
#: templates/cotton/ui/transactions_action_bar.html:191
msgid "Mean"
msgstr "Média"
#: templates/cotton/ui/transactions_action_bar.html:178
#: templates/cotton/ui/transactions_action_bar.html:211
msgid "Max"
msgstr "Máximo"
#: templates/cotton/ui/transactions_action_bar.html:198
#: templates/cotton/ui/transactions_action_bar.html:231
msgid "Min"
msgstr "Minímo"
#: templates/cotton/ui/transactions_action_bar.html:218
#: templates/cotton/ui/transactions_action_bar.html:251
msgid "Count"
msgstr "Contagem"
@@ -1546,91 +1603,91 @@ msgstr "Editar entrada CMP"
msgid "Add DCA strategy"
msgstr "Adicionar estratégia CMP"
#: templates/dca/fragments/strategy/details.html:23
#: templates/dca/fragments/strategy/details.html:22
msgid "No exchange rate available"
msgstr "Nenhuma taxa de câmbio disponível"
#: templates/dca/fragments/strategy/details.html:34
#: templates/dca/fragments/strategy/details.html:33
msgid "Entries"
msgstr "Entradas"
#: templates/dca/fragments/strategy/details.html:56
#: templates/dca/fragments/strategy/details.html:55
msgid "Current Value"
msgstr "Valor atual"
#: templates/dca/fragments/strategy/details.html:57
#: templates/dca/fragments/strategy/details.html:56
msgid "P/L"
msgstr "P/L"
#: templates/dca/fragments/strategy/details.html:125
#: templates/dca/fragments/strategy/details.html:124
msgid "No entries for this DCA"
msgstr "Nenhuma entrada neste CMP"
#: templates/dca/fragments/strategy/details.html:126
#: templates/dca/fragments/strategy/details.html:125
#: templates/monthly_overview/fragments/list.html:41
#: templates/transactions/fragments/list_all.html:40
msgid "Try adding one"
msgstr "Tente adicionar uma"
#: templates/dca/fragments/strategy/details.html:136
#: templates/dca/fragments/strategy/details.html:135
msgid "Total Invested"
msgstr "Total investido"
#: templates/dca/fragments/strategy/details.html:150
#: templates/dca/fragments/strategy/details.html:149
msgid "Total Received"
msgstr "Total recebido"
#: templates/dca/fragments/strategy/details.html:164
#: templates/dca/fragments/strategy/details.html:163
msgid "Current Total Value"
msgstr "Valor total atual"
#: templates/dca/fragments/strategy/details.html:178
#: templates/dca/fragments/strategy/details.html:177
msgid "Average Entry Price"
msgstr "Preço médio de entrada"
#: templates/dca/fragments/strategy/details.html:192
#: templates/dca/fragments/strategy/details.html:191
msgid "Total P/L"
msgstr "P/L total"
#: templates/dca/fragments/strategy/details.html:208
#: templates/dca/fragments/strategy/details.html:207
#, python-format
msgid "Total %% P/L"
msgstr "P/L%% Total"
#: templates/dca/fragments/strategy/details.html:227
#: templates/dca/fragments/strategy/details.html:226
#, python-format
msgid "P/L %%"
msgstr "P/L %%"
#: templates/dca/fragments/strategy/details.html:289
#: templates/dca/fragments/strategy/details.html:288
msgid "Performance Over Time"
msgstr "Desempenho ao longo do tempo"
#: templates/dca/fragments/strategy/details.html:307
#: templates/dca/fragments/strategy/details.html:306
msgid "Entry Price"
msgstr "Preço de Entrada"
#: templates/dca/fragments/strategy/details.html:315
#: templates/dca/fragments/strategy/details.html:314
msgid "Current Price"
msgstr "Preço atual"
#: templates/dca/fragments/strategy/details.html:323
#: templates/dca/fragments/strategy/details.html:322
msgid "Amount Bought"
msgstr "Quantia comprada"
#: templates/dca/fragments/strategy/details.html:391
#: templates/dca/fragments/strategy/details.html:390
msgid "Entry Price vs Current Price"
msgstr "Preço de Entrada vs Preço Atual"
#: templates/dca/fragments/strategy/details.html:407
#: templates/dca/fragments/strategy/details.html:406
msgid "Days Between Investments"
msgstr "Dias entre investimentos"
#: templates/dca/fragments/strategy/details.html:454
#: templates/dca/fragments/strategy/details.html:453
msgid "Investment Frequency"
msgstr "Frequência de Investimento"
#: templates/dca/fragments/strategy/details.html:456
#: templates/dca/fragments/strategy/details.html:455
msgid "The straighter the blue line, the more consistent your DCA strategy is."
msgstr ""
"Quanto mais reta for a linha azul, mais consistente é sua estratégia de CMP."
@@ -1676,19 +1733,19 @@ msgstr "Editar taxa de câmbio"
msgid "All"
msgstr "Todas"
#: templates/exchange_rates/fragments/table.html:12
#: templates/exchange_rates/fragments/table.html:11
msgid "Pairing"
msgstr "Pares"
#: templates/exchange_rates/fragments/table.html:13
#: templates/exchange_rates/fragments/table.html:12
msgid "Rate"
msgstr "Taxa de Câmbio"
#: templates/exchange_rates/fragments/table.html:52
#: templates/exchange_rates/fragments/table.html:51
msgid "No exchange rates"
msgstr "Nenhuma taxa de câmbio"
#: templates/exchange_rates/fragments/table.html:59
#: templates/exchange_rates/fragments/table.html:58
#: templates/transactions/fragments/list_all.html:47
msgid "Page navigation"
msgstr "Navegação por página"
@@ -1845,7 +1902,11 @@ msgstr "Calculadora"
msgid "Settings"
msgstr "Configurações"
#: templates/includes/navbar/user_menu.html:37
#: templates/includes/navbar/user_menu.html:38
msgid "Clear cache"
msgstr "Limpar cache"
#: templates/includes/navbar/user_menu.html:42
msgid "Logout"
msgstr "Sair"
@@ -1981,17 +2042,17 @@ msgid "Filter transactions"
msgstr "Filtrar transações"
#: templates/monthly_overview/pages/overview.html:114
#: templates/transactions/pages/transactions.html:34
#: templates/transactions/pages/transactions.html:33
msgid "Order by"
msgstr "Ordernar por"
#: templates/monthly_overview/pages/overview.html:117
#: templates/transactions/pages/transactions.html:37
#: templates/transactions/pages/transactions.html:36
msgid "Oldest first"
msgstr "Mais antigas primeiro"
#: templates/monthly_overview/pages/overview.html:118
#: templates/transactions/pages/transactions.html:38
#: templates/transactions/pages/transactions.html:37
msgid "Newest first"
msgstr "Mais novas primeiro"
@@ -2151,6 +2212,7 @@ msgid "No tags"
msgstr "Nenhuma tag"
#: templates/transactions/fragments/add.html:5
#: templates/transactions/pages/add.html:5
msgid "New transaction"
msgstr "Nova transação"
@@ -2158,6 +2220,18 @@ msgstr "Nova transação"
msgid "Add Installment Plan"
msgstr "Adicionar parcelamento"
#: templates/transactions/fragments/bulk_edit.html:5
msgid "Bulk Editing"
msgstr "Edição em massa"
#: templates/transactions/fragments/bulk_edit.html:8
msgid "Editing"
msgstr "Editando"
#: templates/transactions/fragments/bulk_edit.html:8
msgid "transactions"
msgstr "transações"
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
@@ -2223,6 +2297,11 @@ msgstr "Nova transferência"
msgid "Filter"
msgstr "Filtro"
#: templates/transactions/widgets/unselectable_income_expense_toggle_buttons.html:14
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:8
msgid "Unchanged"
msgstr "Inalterado"
#: templates/users/generic/hide_amounts.html:2
msgid "Hide amounts"
msgstr "Esconder valores"
@@ -2249,6 +2328,11 @@ msgstr "Visão Anual"
msgid "Year"
msgstr "Ano"
#, fuzzy
#~| msgid "Transaction updated successfully"
#~ msgid "{count} transactions updated successfully"
#~ msgstr "Transação atualizada com sucesso"
#, fuzzy
#~| msgid "Important dates"
#~ msgid "Import Runs"

View File

@@ -39,23 +39,23 @@
{% for transaction in date.transactions %}
{% if transaction.is_paid %}
{% if transaction.type == "IN" and not transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-solid fa-circle-check tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "IN" and transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-solid fa-circle-check tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-solid fa-circle-check tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% elif transaction.type == "EX" and transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-solid fa-circle-check tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% endif %}
{% else %}
{% if transaction.type == "IN" and not transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-regular fa-circle tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "IN" and transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-regular fa-circle tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% elif transaction.type == "EX" and transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{{ transaction.description }}"></i>
<i class="fa-regular fa-circle tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% endif %}
{% endif %}
{% endfor %}

View File

@@ -1,19 +1,18 @@
{% extends 'extends/offcanvas.html' %}
{% load date %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Transactions on' %} {{ date|custom_date:request.user }}{% endblock %}
{% block title %}{% translate 'Transactions on' %} {{ date|date:"SHORT_DATE_FORMAT" }}{% endblock %}
{% block body %}
<div hx-get="{% url 'calendar_transactions_list' day=date.day month=date.month year=date.year %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
{% for transaction in transactions %}
<c-transaction.item
:transaction="transaction"
:disable-selection="True"></c-transaction.item>
{% empty %}
<c-msg.empty
title="{% translate 'No transactions on this date' %}"></c-msg.empty>
{% endfor %}
<div hx-get="{% url 'calendar_transactions_list' day=date.day month=date.month year=date.year %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading" id="transactions-list">
{% for transaction in transactions %}
<c-transaction.item :transaction="transaction"></c-transaction.item>
{% empty %}
<c-msg.empty
title="{% translate 'No transactions on this date' %}"></c-msg.empty>
{% endfor %}
{# Floating bar #}
<c-ui.transactions-action-bar></c-ui.transactions-action-bar>
</div>
{% endblock %}

View File

@@ -1,4 +1,3 @@
{% load date %}
{% load i18n %}
<div class="transaction d-flex my-1 {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
{% if not disable_selection %}
@@ -27,7 +26,7 @@
{# Date#}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.date|custom_date:request.user }} • {{ transaction.reference_date|date:"b/Y" }}</div>
<div class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
</div>
{# Description#}
<div class="mb-2 mb-lg-1 text-white tw-text-base">

View File

@@ -2,46 +2,80 @@
<div class="tw-sticky tw-bottom-4 tw-left-0 tw-right-0 tw-z-50 tw-hidden mx-auto tw-w-fit" id="actions-bar"
_="on change from #transactions-list or htmx:afterSettle from window
if no <input[type='checkbox']:checked/> in #transactions-list
add .tw-hidden to #actions-bar
if #actions-bar
add .slide-in-bottom-reverse then settle
then add .tw-hidden to #actions-bar
then remove .slide-in-bottom-reverse
end
else
remove .tw-hidden from #actions-bar
then trigger selected_transactions_updated
if #actions-bar
remove .tw-hidden from #actions-bar
then trigger selected_transactions_updated
end
end
end">
<div class="card slide-in-left">
<div class="card-body p-2">
<div class="card slide-in-bottom">
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3">
{% spaceless %}
<div class="btn-group" role="group">
<button class="btn btn-secondary btn-sm"
data-bs-toggle="tooltip"
data-bs-title="{% translate 'Select All' %}"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
<i class="fa-regular fa-square-check tw-text-green-400"></i>
</button>
<button class="btn btn-secondary btn-sm"
data-bs-toggle="tooltip"
data-bs-title="{% translate 'Unselect All' %}"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to false then call me.blur() then trigger change">
<i class="fa-regular fa-square tw-text-red-400"></i>
<div class="dropdown">
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fa-regular fa-square-check fa-fw"></i>
</button>
<ul class="dropdown-menu">
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
<i class="fa-regular fa-square-check tw-text-green-400 me-3"></i>{% translate 'Select All' %}
</div>
</li>
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to false then call me.blur() then trigger change">
<i class="fa-regular fa-square tw-text-red-400 me-3"></i>{% translate 'Unselect All' %}
</div>
</li>
</ul>
</div>
<div class="vr mx-3 tw-align-middle"></div>
<div class="btn-group me-3" role="group">
<div class="vr tw-align-middle"></div>
<div class="btn-group">
<button class="btn btn-secondary btn-sm"
hx-get="{% url 'transactions_bulk_pay' %}"
hx-get="{% url 'transactions_bulk_edit' %}"
hx-target="#generic-offcanvas"
hx-include=".transaction"
data-bs-toggle="tooltip"
data-bs-title="{% translate 'Mark as paid' %}">
<i class="fa-regular fa-circle-check tw-text-green-400"></i>
data-bs-title="{% translate 'Edit' %}">
<i class="fa-solid fa-pencil"></i>
</button>
<button class="btn btn-secondary btn-sm"
hx-get="{% url 'transactions_bulk_unpay' %}"
hx-include=".transaction"
data-bs-toggle="tooltip"
data-bs-title="{% translate 'Mark as unpaid' %}">
<i class="fa-regular fa-circle tw-text-red-400"></i>
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
</button>
<ul class="dropdown-menu">
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
hx-get="{% url 'transactions_bulk_unpay' %}"
hx-include=".transaction">
<i class="fa-regular fa-circle tw-text-red-400 fa-fw me-3"></i>{% translate 'Mark as unpaid' %}
</div>
</li>
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
hx-get="{% url 'transactions_bulk_pay' %}"
hx-include=".transaction">
<i class="fa-regular fa-circle-check tw-text-green-400 fa-fw me-3"></i>{% translate 'Mark as paid' %}
</div>
</li>
</ul>
</div>
<button class="btn btn-secondary btn-sm"
hx-get="{% url 'transactions_bulk_clone' %}"
hx-include=".transaction"
data-bs-toggle="tooltip"
data-bs-title="{% translate 'Duplicate' %}">
<i class="fa-solid fa-clone fa-fw"></i>
</button>
<button class="btn btn-secondary btn-sm"
hx-get="{% url 'transactions_bulk_delete' %}"
hx-include=".transaction"
@@ -55,9 +89,9 @@
_="install prompt_swal">
<i class="fa-solid fa-trash text-danger"></i>
</button>
<div class="vr mx-3 tw-align-middle"></div>
<div class="vr tw-align-middle"></div>
<div class="btn-group"
_="on selected_transactions_updated from #actions-bar
_="on selected_transactions_updated from #actions-bar
set realTotal to math.bignumber(0)
set flatTotal to math.bignumber(0)
set transactions to <.transaction:has(input[name='transactions']:checked)/>
@@ -93,8 +127,7 @@
put Math.min.apply(Math, realAmountValues).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-min's innerText
put mean.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-mean's innerText
put flatAmountValues.length.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 40}) into #calc-menu-count's innerText
end"
>
end">
<button class="btn btn-secondary btn-sm" _="on click
set original_value to #real-total-front's innerText
writeText(original_value) on navigator.clipboard
@@ -102,8 +135,8 @@
wait 1s
put original_value into #real-total-front's innerText
end">
<i class="fa-solid fa-plus fa-fw me-2 text-primary"></i>
<span id="real-total-front">0</span>
<i class="fa-solid fa-plus fa-fw me-md-2 text-primary"></i>
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
</button>
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">

View File

@@ -1,4 +1,3 @@
{% load date %}
{% load currency_display %}
{% load i18n %}
<div class="container-fluid px-md-3 py-3 column-gap-5">
@@ -17,7 +16,7 @@
:prefix="strategy.payment_currency.prefix"
:suffix="strategy.payment_currency.suffix"
:decimal_places="strategy.payment_currency.decimal_places">
• {{ strategy.current_price.1|custom_date:request.user }}
• {{ strategy.current_price.1|date:"SHORT_DATETIME_FORMAT" }}
</c-amount.display>
{% else %}
<div class="tw-text-red-400">{% trans "No exchange rate available" %}</div>
@@ -84,7 +83,7 @@
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td>{{ entry.date|custom_date:request.user }}</td>
<td>{{ entry.date|date:"SHORT_DATE_FORMAT" }}</td>
<td>
<c-amount.display
:amount="entry.amount_received"
@@ -222,7 +221,7 @@
new Chart(perfomancectx, {
type: 'line',
data: {
labels: [{% for entry in entries_data %}'{{ entry.entry.date|custom_date:request.user }}'{% if not forloop.last %}, {% endif %}{% endfor %}],
labels: [{% for entry in entries_data %}'{{ entry.entry.date|date:"SHORT_DATE_FORMAT" }}'{% if not forloop.last %}, {% endif %}{% endfor %}],
datasets: [{
label: '{% trans "P/L %" %}',
data: [{% for entry in entries_data %}{{ entry.profit_loss_percentage|floatformat:"-40u" }}{% if not forloop.last %}, {% endif %}{% endfor %}],

View File

@@ -1,4 +1,3 @@
{% load date %}
{% load currency_display %}
{% load i18n %}
<div class="card-body show-loading" hx-get="{% url 'exchange_rates_list_pair' %}" hx-trigger="updated from:window" hx-swap="outerHTML" hx-vals='{"page": "{{ page_obj.number }}", "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'>
@@ -40,7 +39,7 @@
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
</td>
<td class="col-3">{{ exchange_rate.date|custom_date:request.user }}</td>
<td class="col-3">{{ exchange_rate.date|date:"SHORT_DATETIME_FORMAT" }}</td>
<td class="col-3"><span class="badge rounded-pill text-bg-secondary">{{ exchange_rate.from_currency.code }}</span> x <span class="badge rounded-pill text-bg-secondary">{{ exchange_rate.to_currency.code }}</span></td>
<td class="col-3">1 {{ exchange_rate.from_currency.code }} ≅ {% currency_display amount=exchange_rate.rate prefix=exchange_rate.to_currency.prefix suffix=exchange_rate.to_currency.suffix decimal_places=exchange_rate.to_currency.decimal_places%}</td>
</tr>

View File

@@ -33,6 +33,11 @@
</li>
{% endspaceless %}
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" hx-get="{% url 'invalidate_cache' %}" role="button">
<i class="fa-solid fa-broom me-2 fa-fw"></i>{% translate 'Clear cache' %}
</a>
</li>
<li><a class="dropdown-item" href="{% url 'logout' %}"><i class="fa-solid fa-door-open me-2 fa-fw"></i
>{% translate 'Logout' %}</a></li>
</ul>

View File

@@ -9,4 +9,10 @@
end
end
end
on reset
for elm in <select/> in event.target
call elm.tomselect.clear()
end
end
</script>

View File

@@ -5,11 +5,11 @@
{% block title %}{% translate 'Installments' %}{% endblock %}
{% block body %}
<div hx-get="{% url 'installment_plan_transactions' installment_plan_id=installment_plan.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
{% for transaction in transactions %}
<c-transaction.item
:transaction="transaction"
:disable-selection="True"></c-transaction.item>
{% endfor %}
<div hx-get="{% url 'installment_plan_transactions' installment_plan_id=installment_plan.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading" id="transactions-list">
{% for transaction in transactions %}
<c-transaction.item :transaction="transaction"></c-transaction.item>
{% endfor %}
{# Floating bar #}
<c-ui.transactions-action-bar></c-ui.transactions-action-bar>
</div>
{% endblock %}

View File

@@ -23,8 +23,6 @@
<div id="content">
{% block content %}{% endblock %}
</div>
{% include 'includes/toasts.html' %}
{% include 'includes/scripts.html' %}
{% block extra_js %}{% endblock %}

View File

@@ -129,6 +129,7 @@
id="filter">
{% crispy filter.form %}
</form>
<button class="btn btn-outline-danger btn-sm" _="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
</div>
</div>
{# Transactions list#}

View File

@@ -5,11 +5,11 @@
{% block title %}{% translate 'Transactions' %}{% endblock %}
{% block body %}
<div hx-get="{% url 'recurring_transaction_transactions' recurring_transaction_id=recurring_transaction.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
{% for transaction in transactions %}
<c-transaction.item
:transaction="transaction"
:disable-selection="True"></c-transaction.item>
{% endfor %}
<div hx-get="{% url 'recurring_transaction_transactions' recurring_transaction_id=recurring_transaction.id %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading" id="transactions-list">
{% for transaction in transactions %}
<c-transaction.item :transaction="transaction"></c-transaction.item>
{% endfor %}
{# Floating bar #}
<c-ui.transactions-action-bar></c-ui.transactions-action-bar>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Bulk Editing' %}{% endblock %}
{% block body %}
<p>{% trans 'Editing' %} {{ transactions|length }} {% trans 'transactions' %}</p>
<div class="editing-transactions">
{% for transaction in transactions %}
<input type="hidden" name="transactions" value="{{ transaction.id }}"/>
{% endfor %}
</div>
<form hx-post="{% url 'transactions_bulk_edit' %}" hx-target="#generic-offcanvas" hx-include=".editing-transactions" novalidate>
{% crispy form %}
</form>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'layouts/base.html' %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block title %}{% translate 'New transaction' %}{% endblock %}
{% block content %}
<div class="container py-3 column-gap-5"
_="install init_tom_select
install init_datepicker">
<form hx-post="{% url 'transaction_simple_add' %}" hx-swap="outerHTML" hx-target="body" novalidate>
{% crispy form form.helper_simple %}
</form>
</div>
{% endblock %}

View File

@@ -14,8 +14,7 @@
<div class="d-flex mb-3 align-self-center">
<div class="me-auto"><h4><i class="fa-solid fa-filter me-2"></i>{% translate 'Filter' %}</h4></div>
<div class="align-self-center">
<a href="{% url 'transactions_all_index' %}" type="button" class="btn btn-outline-danger btn-sm"
hx-target="body" hx-boost="true">{% translate 'Clear' %}</a>
<button class="btn btn-outline-danger btn-sm" _="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
</div>
</div>
<hr>

View File

@@ -9,7 +9,7 @@
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
value="{{ choice.0 }}"
{% if choice.0 == field.value %}checked{% endif %}>
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %}"
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %} w-100"
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
{{ choice.1 }}
</label>

View File

@@ -0,0 +1,18 @@
{% load i18n %}
{% load crispy_forms_field %}
<div class="form-group mb-3">
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_false"
value="false" {% if not field.value %}checked{% endif %}>
<label class="btn btn-outline-primary w-50" for="{{ field.id_for_label }}_false">{% trans 'Projected' %}</label>
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_true"
value="true" {% if field.value %}checked{% endif %}>
<label class="btn btn-outline-success w-50" for="{{ field.id_for_label }}_true">{% trans 'Paid' %}</label>
</div>
{% if field.help_text %}
<div class="help-text">{{ field.help_text|safe }}</div>
{% endif %}
</div>

View File

@@ -15,7 +15,7 @@
id="{{ field.html_name }}_{{ forloop.counter }}"
value="{{ choice.0 }}"
{% if choice.0 in field.value %}checked{% endif %}>
<label class="btn btn-outline-dark"
<label class="btn btn-outline-dark w-100"
for="{{ field.html_name }}_{{ forloop.counter }}">
{{ choice.1 }}
</label>

View File

@@ -0,0 +1,40 @@
{% load i18n %}
{% load crispy_forms_field %}
<div class="form-group mb-3">
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
<input type="radio"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_none_tr"
value=""
{% if field.value is None %}checked{% endif %}>
<label class="btn btn-outline-secondary {% if field.errors %}is-invalid{% endif %} w-100"
for="{{ field.html_name }}_none_tr">
{% trans 'Unchanged' %}
</label>
{% for choice in field.field.choices %}
<input type="radio"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
value="{{ choice.0 }}"
{% if choice.0 == field.value %}checked{% endif %}>
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %} w-100"
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
{{ choice.1 }}
</label>
{% endfor %}
</div>
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>

View File

@@ -0,0 +1,22 @@
{% load i18n %}
{% load crispy_forms_field %}
<div class="form-group mb-3">
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_null"
value="" {% if field.value is None %}checked{% endif %}>
<label class="btn btn-outline-secondary w-100" for="{{ field.id_for_label }}_null">{% trans 'Unchanged' %}</label>
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_false"
value="false" {% if field.value is False %}checked{% endif %}">
<label class="btn btn-outline-primary w-100" for="{{ field.id_for_label }}_false">{% trans 'Projected' %}</label>
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_true"
value="true" {% if field.value is True %}checked{% endif %}>
<label class="btn btn-outline-success w-100" for="{{ field.id_for_label }}_true">{% trans 'Paid' %}</label>
</div>
{% if field.help_text %}
<div class="help-text">{{ field.help_text|safe }}</div>
{% endif %}
</div>

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-buster AS python-build-stage
FROM python:3.11-slim-bookworm AS python-build-stage
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential \
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
COPY ../requirements.txt .
RUN pip wheel --wheel-dir /usr/src/app/wheels -r requirements.txt
FROM python:3.11-slim-buster AS python-run-stage
FROM python:3.11-slim-bookworm AS python-run-stage
WORKDIR /usr/src/app

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-buster AS python-build-stage
FROM python:3.11-slim-bookworm AS python-build-stage
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential \
@@ -17,7 +17,7 @@ RUN --mount=type=cache,target=/root/.npm \
npm install --verbose && \
npm run build
FROM python:3.11-slim-buster AS python-run-stage
FROM python:3.11-slim-bookworm AS python-run-stage
COPY --from=webpack_build /usr/src/frontend/build /usr/src/frontend/build
WORKDIR /usr/src/app

View File

@@ -1,11 +1,13 @@
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 {createPopper} from '@popperjs/core';
const locales = {
'pt': ptBr,
'en': en
'en': en,
'nl': nl
};
function isMobileDevice() {
@@ -161,8 +163,8 @@ window.MonthYearPicker = function createDynamicDatePicker(element) {
let opts = {...baseOpts, ...positionConfig};
if (element.dataset.value) {
opts["selectedDates"] = [element.dataset.value];
opts["startDate"] = [element.dataset.value];
opts["selectedDates"] = [new Date(element.dataset.value + "T00:00:00")];
opts["startDate"] = [new Date(element.dataset.value + "T00:00:00")];
}
return new AirDatepicker(element, opts);
};

View File

@@ -205,3 +205,35 @@
.flashing {
animation: flash 1s infinite;
}
.slide-in-bottom {
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
.slide-in-bottom-reverse {
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) reverse both;
}
/* ----------------------------------------------
* Generated by Animista on 2025-1-25 12:30:4
* Licensed under FreeBSD License.
* See http://animista.net/license for more info.
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/**
* ----------------------------------------
* animation slide-in-bottom
* ----------------------------------------
*/
@keyframes slide-in-bottom {
0% {
transform: translateY(1000px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}