mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-05-17 13:17:05 +02:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f80f74a01a | |||
| 368342853f | |||
| 9ef8fdec49 | |||
| f29a8d8bc0 | |||
| 2cdcc4ee26 | |||
| f90a31f2b9 | |||
| dd1f6a6ef2 | |||
| c09ad0e49d | |||
| 9250131396 | |||
| 5f503149ce | |||
| d45b4f2942 | |||
| 4a8493c7d9 | |||
| c39c3ccacb | |||
| 4bb8ef6582 | |||
| d711ccca69 | |||
| 76d59f1038 | |||
| 5b6c123fa1 | |||
| 782ab11ae4 | |||
| 8db885f47d | |||
| 01bd8710d8 | |||
| 569d08711c | |||
| a285f055e4 | |||
| 6aae9b1207 | |||
| 9d2206f8a4 | |||
| d7e3c50c2c | |||
| 789fd4eb80 | |||
| 586b3a5d44 | |||
| 9248e8bd77 | |||
| c44247f6a5 | |||
| 8ba89434f8 | |||
| f2f41981a3 | |||
| 1153fd6b0a | |||
| 76822224a0 | |||
| 31b2b98eb9 | |||
| d7a4e79321 | |||
| 985f07e792 | |||
| 5465bb1eeb | |||
| 451a85a998 | |||
| 54c74e7c07 | |||
| d6e9e123b7 | |||
| 80c9c43a02 | |||
| 3e34f088fc | |||
| 5b9e5c6003 | |||
| c266b8809f | |||
| 8cda4116bc | |||
| c2510b2261 | |||
| dcdaf756f9 | |||
| 50ca08165a | |||
| f85618fa01 | |||
| 635f87a8ad | |||
| 1a073ba53d | |||
| 5412e5b12c | |||
| 2103ba1b38 | |||
| 04fb15224c | |||
| 2fc526beac | |||
| cc3ca4f4a3 | |||
| 8d3844c431 | |||
| 5e7e918085 | |||
| c3f02320b5 | |||
| da8bbbfb0b | |||
| e3f74538d2 | |||
| d8234950c6 | |||
| 58f19ce1ca | |||
| ef5f3580a0 | |||
| efe0f99cb4 | |||
| dccb5079ad | |||
| 6c90150661 | |||
| c33d6fab69 | |||
| c0c57a6d77 | |||
| f19d58a2bd | |||
| dfe99093e9 | |||
| d737e573cc | |||
| 805d3f419e | |||
| 9777aac746 | |||
| 61b782104d | |||
| 79dec2b515 | |||
| db23e162c4 | |||
| d81d89d9f6 | |||
| 6826cfe79a | |||
| 0832ec75ca | |||
| 3090f632de | |||
| 6b4fbee7a6 | |||
| e7fe6622cd | |||
| 3017593ed5 | |||
| ceb8e9ea97 | |||
| 9b5b7683dd | |||
| 514600e34a | |||
| 07dd805b07 | |||
| 905e9b4c54 | |||
| 60d367dec5 | |||
| 6e0842a697 | |||
| 858934b7c5 | |||
| 47d9e4078c | |||
| fa6f3e87c0 | |||
| 5f101af879 | |||
| b27633a28e | |||
| 7716eee0b3 | |||
| 37c447ae0a | |||
| e544d7068b | |||
| 8d93da99c1 | |||
| cc87477a2e | |||
| e86e0b8c08 | |||
| eb0c872c50 | |||
| b4578df242 | |||
| 756de12835 | |||
| d573d02657 | |||
| 250b352217 | |||
| b4e9446cf6 | |||
| 90944f0179 | |||
| 008d34b1d0 | |||
| 46dfc7dad4 | |||
| 22900b5d9e | |||
| 0c48e9fe3d | |||
| b2e100d1b0 | |||
| e49b38a442 | |||
| 1f2902eea9 | |||
| 7d60db8716 | |||
| 873b0baed7 | |||
| 2313c97761 | |||
| 9cd7337153 | |||
| d3b354e2b8 | |||
| e137666e99 | |||
| 4291a5b97d | |||
| c8d316857f | |||
| 3395a96949 | |||
| 8ab9624619 | |||
| f9056c3a45 | |||
| a9df684ee2 | |||
| e4d07c94d4 | |||
| 5d5d172b3b | |||
| 99f746b6be | |||
| a461a33dc2 | |||
| 1213ffebeb | |||
| c5a352cf4d | |||
| cfcca54aa6 | |||
| 234f8cd669 | |||
| 43184140f0 | |||
| acc325c150 | |||
| 46eb471a34 | |||
| 6dc14c73d6 | |||
| f942924e7c | |||
| aa6019e0a9 | |||
| 9dfbd346bc | |||
| 73b1d36dfd | |||
| 3662fb030a | |||
| a423ee1032 | |||
| 72eb59d24f | |||
| 1a0247e028 | |||
| 281a0fccda | |||
| 59ce50299a | |||
| be89509beb | |||
| 80cded234d | |||
| 030bb63586 | |||
| 66e8fc5884 | |||
| 363047337d |
@@ -31,3 +31,10 @@ ENABLE_SOFT_DELETE=false
|
|||||||
KEEP_DELETED_TRANSACTIONS_FOR=365
|
KEEP_DELETED_TRANSACTIONS_FOR=365
|
||||||
|
|
||||||
TASK_WORKERS=1 # This only work if you're using the single container option. Increase to have more open queues via procrastinate, you probably don't need to increase this.
|
TASK_WORKERS=1 # This only work if you're using the single container option. Increase to have more open queues via procrastinate, you probably don't need to increase this.
|
||||||
|
|
||||||
|
# OIDC Configuration. Uncomment the lines below if you want to add OIDC login to your instance
|
||||||
|
#OIDC_CLIENT_NAME=""
|
||||||
|
#OIDC_CLIENT_ID=""
|
||||||
|
#OIDC_CLIENT_SECRET=""
|
||||||
|
#OIDC_SERVER_URL=""
|
||||||
|
#OIDC_ALLOW_SIGNUP=true
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ Managing money can feel unnecessarily complex, but it doesn’t have to be. WYGI
|
|||||||
|
|
||||||
By sticking to this straightforward approach, you avoid dipping into your savings while still keeping tabs on where your money goes.
|
By sticking to this straightforward approach, you avoid dipping into your savings while still keeping tabs on where your money goes.
|
||||||
|
|
||||||
While this philosophy is simple, finding tools to make it work wasn’t. I initially used a spreadsheet, which served me well for years—until it became unwieldy as I started managing multiple currencies, accounts, and investments. I tried various financial management apps, but none met my key requirements:
|
While this philosophy is simple, finding tools to make it work wasn’t. I initially used a spreadsheet, which served me well for years, until it became unwieldy as I started managing multiple currencies, accounts, and investments. I tried various financial management apps, but none met my key requirements:
|
||||||
|
|
||||||
1. **Multi-currency support** to track income and expenses in different currencies.
|
1. **Multi-currency support** to track income and expenses in different currencies.
|
||||||
2. **Not a budgeting app** — as I dislike budgeting constraints.
|
2. **Not a budgeting app** as I dislike budgeting constraints.
|
||||||
3. **Web app usability** (ideally with mobile support, though optional).
|
3. **Web app usability** (ideally with mobile support, though optional).
|
||||||
4. **Automation-ready API** to integrate with other tools and services.
|
4. **Automation-ready API** to integrate with other tools and services.
|
||||||
5. **Custom transaction rules** for credit card billing cycles or similar quirks.
|
5. **Custom transaction rules** for credit card billing cycles or similar quirks.
|
||||||
|
|
||||||
Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH** — an opinionated yet powerful tool that I believe will resonate with like-minded users.
|
Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH**, an opinionated yet powerful tool that I believe will resonate with like-minded users.
|
||||||
|
|
||||||
# Key Features
|
# Key Features
|
||||||
|
|
||||||
@@ -144,6 +144,31 @@ To create the first user, open the container's console using Unraid's UI, by cli
|
|||||||
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
|
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
|
||||||
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
|
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
|
||||||
|
|
||||||
|
## OIDC Configuration
|
||||||
|
|
||||||
|
WYGIWYH supports login via OpenID Connect (OIDC) through `django-allauth`. This allows users to authenticate using an external OIDC provider.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Currently only OpenID Connect is supported as a provider, open an issue if you need something else.
|
||||||
|
|
||||||
|
To configure OIDC, you need to set the following environment variables:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `OIDC_CLIENT_NAME` | The name of the provider. will be displayed in the login page. Defaults to `OpenID Connect` |
|
||||||
|
| `OIDC_CLIENT_ID` | The Client ID provided by your OIDC provider. |
|
||||||
|
| `OIDC_CLIENT_SECRET` | The Client Secret provided by your OIDC provider. |
|
||||||
|
| `OIDC_SERVER_URL` | The base URL of your OIDC provider's discovery document or authorization server (e.g., `https://your-provider.com/auth/realms/your-realm`). `django-allauth` will use this to discover the necessary endpoints (authorization, token, userinfo, etc.). |
|
||||||
|
| `OIDC_ALLOW_SIGNUP` | Allow the automatic creation of inexistent accounts on a successfull authentication. Defaults to `true`. |
|
||||||
|
|
||||||
|
**Callback URL (Redirect URI):**
|
||||||
|
|
||||||
|
When configuring your OIDC provider, you will need to provide a callback URL (also known as a Redirect URI). For WYGIWYH, the default callback URL is:
|
||||||
|
|
||||||
|
`https://your.wygiwyh.domain/auth/oidc/<OIDC_CLIENT_NAME>/login/callback/`
|
||||||
|
|
||||||
|
Replace `https://your.wygiwyh.domain` with the actual URL where your WYGIWYH instance is accessible. And `<OIDC_CLIENT_NAME>` with the slugfied value set in OIDC_CLIENT_NAME or the default `openid-connect` if you haven't set this variable.
|
||||||
|
|
||||||
# How it works
|
# How it works
|
||||||
|
|
||||||
Check out our [Wiki](https://github.com/eitchtee/WYGIWYH/wiki) for more information.
|
Check out our [Wiki](https://github.com/eitchtee/WYGIWYH/wiki) for more information.
|
||||||
|
|||||||
+48
-1
@@ -14,6 +14,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
SITE_TITLE = "WYGIWYH"
|
SITE_TITLE = "WYGIWYH"
|
||||||
TITLE_SEPARATOR = "::"
|
TITLE_SEPARATOR = "::"
|
||||||
@@ -42,6 +43,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
|
"django.contrib.sites",
|
||||||
"whitenoise.runserver_nostatic",
|
"whitenoise.runserver_nostatic",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"webpack_boilerplate",
|
"webpack_boilerplate",
|
||||||
@@ -61,7 +63,6 @@ INSTALLED_APPS = [
|
|||||||
"apps.transactions.apps.TransactionsConfig",
|
"apps.transactions.apps.TransactionsConfig",
|
||||||
"apps.currencies.apps.CurrenciesConfig",
|
"apps.currencies.apps.CurrenciesConfig",
|
||||||
"apps.accounts.apps.AccountsConfig",
|
"apps.accounts.apps.AccountsConfig",
|
||||||
"apps.common.apps.CommonConfig",
|
|
||||||
"apps.net_worth.apps.NetWorthConfig",
|
"apps.net_worth.apps.NetWorthConfig",
|
||||||
"apps.import_app.apps.ImportConfig",
|
"apps.import_app.apps.ImportConfig",
|
||||||
"apps.export_app.apps.ExportConfig",
|
"apps.export_app.apps.ExportConfig",
|
||||||
@@ -74,8 +75,15 @@ INSTALLED_APPS = [
|
|||||||
"apps.calendar_view.apps.CalendarViewConfig",
|
"apps.calendar_view.apps.CalendarViewConfig",
|
||||||
"apps.dca.apps.DcaConfig",
|
"apps.dca.apps.DcaConfig",
|
||||||
"pwa",
|
"pwa",
|
||||||
|
"allauth",
|
||||||
|
"allauth.account",
|
||||||
|
"allauth.socialaccount",
|
||||||
|
"allauth.socialaccount.providers.openid_connect",
|
||||||
|
"apps.common.apps.CommonConfig",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||||
"apps.common.middleware.thread_local.ThreadLocalMiddleware",
|
"apps.common.middleware.thread_local.ThreadLocalMiddleware",
|
||||||
@@ -91,6 +99,7 @@ MIDDLEWARE = [
|
|||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"hijack.middleware.HijackUserMiddleware",
|
"hijack.middleware.HijackUserMiddleware",
|
||||||
|
"allauth.account.middleware.AccountMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "WYGIWYH.urls"
|
ROOT_URLCONF = "WYGIWYH.urls"
|
||||||
@@ -307,6 +316,42 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|||||||
|
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
LOGIN_URL = "/login/"
|
LOGIN_URL = "/login/"
|
||||||
|
LOGOUT_REDIRECT_URL = "/login/"
|
||||||
|
|
||||||
|
# Allauth settings
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"django.contrib.auth.backends.ModelBackend", # Keep default
|
||||||
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
|
]
|
||||||
|
|
||||||
|
SOCIALACCOUNT_PROVIDERS = {"openid_connect": {"APPS": []}}
|
||||||
|
|
||||||
|
if (
|
||||||
|
os.getenv("OIDC_CLIENT_ID")
|
||||||
|
and os.getenv("OIDC_CLIENT_SECRET")
|
||||||
|
and os.getenv("OIDC_SERVER_URL")
|
||||||
|
):
|
||||||
|
SOCIALACCOUNT_PROVIDERS["openid_connect"]["APPS"].append(
|
||||||
|
{
|
||||||
|
"provider_id": slugify(os.getenv("OIDC_CLIENT_NAME", "OpenID Connect")),
|
||||||
|
"name": os.getenv("OIDC_CLIENT_NAME", "OpenID Connect"),
|
||||||
|
"client_id": os.getenv("OIDC_CLIENT_ID"),
|
||||||
|
"secret": os.getenv("OIDC_CLIENT_SECRET"),
|
||||||
|
"settings": {
|
||||||
|
"server_url": os.getenv("OIDC_SERVER_URL"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ACCOUNT_LOGIN_METHODS = {"email"}
|
||||||
|
ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
|
||||||
|
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||||
|
ACCOUNT_EMAIL_VERIFICATION = "none"
|
||||||
|
SOCIALACCOUNT_LOGIN_ON_GET = True
|
||||||
|
SOCIALACCOUNT_ONLY = True
|
||||||
|
SOCIALACCOUNT_AUTO_SIGNUP = os.getenv("OIDC_ALLOW_SIGNUP", "true").lower() == "true"
|
||||||
|
ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter"
|
||||||
|
SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter"
|
||||||
|
|
||||||
# CRISPY FORMS
|
# CRISPY FORMS
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ["bootstrap5", "crispy_forms/pure_text"]
|
CRISPY_ALLOWED_TEMPLATE_PACKS = ["bootstrap5", "crispy_forms/pure_text"]
|
||||||
@@ -442,6 +487,8 @@ else:
|
|||||||
|
|
||||||
CACHALOT_UNCACHABLE_TABLES = ("django_migrations", "procrastinate_jobs")
|
CACHALOT_UNCACHABLE_TABLES = ("django_migrations", "procrastinate_jobs")
|
||||||
|
|
||||||
|
# Procrastinate
|
||||||
|
PROCRASTINATE_ON_APP_READY = "apps.common.procrastinate.on_app_ready"
|
||||||
|
|
||||||
# PWA
|
# PWA
|
||||||
PWA_APP_NAME = SITE_TITLE
|
PWA_APP_NAME = SITE_TITLE
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ from drf_spectacular.views import (
|
|||||||
SpectacularAPIView,
|
SpectacularAPIView,
|
||||||
SpectacularSwaggerView,
|
SpectacularSwaggerView,
|
||||||
)
|
)
|
||||||
|
from allauth.socialaccount.providers.openid_connect.views import login, callback
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
@@ -36,6 +38,13 @@ urlpatterns = [
|
|||||||
SpectacularSwaggerView.as_view(url_name="schema"),
|
SpectacularSwaggerView.as_view(url_name="schema"),
|
||||||
name="swagger-ui",
|
name="swagger-ui",
|
||||||
),
|
),
|
||||||
|
path("auth/", include("allauth.urls")), # allauth urls
|
||||||
|
# path("auth/oidc/<str:provider_id>/login/", login, name="openid_connect_login"),
|
||||||
|
# path(
|
||||||
|
# "auth/oidc/<str:provider_id>/login/callback/",
|
||||||
|
# callback,
|
||||||
|
# name="openid_connect_callback",
|
||||||
|
# ),
|
||||||
path("", include("apps.transactions.urls")),
|
path("", include("apps.transactions.urls")),
|
||||||
path("", include("apps.common.urls")),
|
path("", include("apps.common.urls")),
|
||||||
path("", include("apps.users.urls")),
|
path("", include("apps.users.urls")),
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0014_alter_account_options_alter_accountgroup_options'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountgroup',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountgroup',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountgroup',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -41,7 +41,10 @@ class TransactionCategoryField(serializers.Field):
|
|||||||
def get_schema():
|
def get_schema():
|
||||||
return {
|
return {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string", "description": "TransactionTag ID or name"},
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "TransactionCategory ID or name",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models import Q
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class AccountSerializer(serializers.ModelSerializer):
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
currency = CurrencySerializer(read_only=True)
|
currency = CurrencySerializer(read_only=True)
|
||||||
currency_id = serializers.PrimaryKeyRelatedField(
|
currency_id = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=Currency.objects.all(), source="currency", write_only=True
|
queryset=Currency.objects.all(), source="currency", write_only=True
|
||||||
@@ -50,6 +52,13 @@ class AccountSerializer(serializers.ModelSerializer):
|
|||||||
"is_asset",
|
"is_asset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request and request.user.is_authenticated:
|
||||||
|
# Reload the queryset to get an updated version with the requesting user
|
||||||
|
self.fields["group_id"].queryset = AccountGroup.objects.all()
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
return Account.objects.create(**validated_data)
|
return Account.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description=_("Make public"))
|
||||||
|
def make_public(modeladmin, request, queryset):
|
||||||
|
queryset.update(visibility="public")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description=_("Make private"))
|
||||||
|
def make_private(modeladmin, request, queryset):
|
||||||
|
queryset.update(visibility="private")
|
||||||
|
|
||||||
|
|
||||||
class SharedObjectModelAdmin(admin.ModelAdmin):
|
class SharedObjectModelAdmin(admin.ModelAdmin):
|
||||||
|
actions = [make_public, make_private]
|
||||||
|
|
||||||
|
list_display = ("__str__", "visibility", "owner", "get_shared_with")
|
||||||
|
|
||||||
|
@admin.display(description=_("Shared with users"))
|
||||||
|
def get_shared_with(self, obj):
|
||||||
|
return ", ".join([p.email for p in obj.shared_with.all()])
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
# Use the all_objects manager to show all transactions, including deleted ones
|
# Use the all_objects manager to show all transactions, including deleted ones
|
||||||
return self.model.all_objects.all()
|
return self.model.all_objects.all()
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
class CommonConfig(AppConfig):
|
class CommonConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "apps.common"
|
name = "apps.common"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
from allauth.socialaccount.models import (
|
||||||
|
SocialAccount,
|
||||||
|
SocialApp,
|
||||||
|
SocialToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.unregister(Site)
|
||||||
|
admin.site.unregister(SocialAccount)
|
||||||
|
admin.site.unregister(SocialApp)
|
||||||
|
admin.site.unregister(SocialToken)
|
||||||
|
|
||||||
|
# Delete the cache for update checks to prevent false-positives when the app is restarted
|
||||||
|
# this will be recreated by the check_for_updates task
|
||||||
|
cache.delete("update_check")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from crispy_forms.bootstrap import FormActions
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Field, Submit, Div, HTML
|
from crispy_forms.layout import Layout, Field, Submit, Div, HTML
|
||||||
|
|
||||||
@@ -81,6 +82,23 @@ class SharedObjectForm(forms.Form):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
owner = cleaned_data.get("owner")
|
||||||
|
shared_with_users = cleaned_data.get("shared_with_users", [])
|
||||||
|
|
||||||
|
# Raise validation error if owner is in shared_with_users
|
||||||
|
if owner and owner in shared_with_users:
|
||||||
|
self.add_error(
|
||||||
|
"shared_with_users",
|
||||||
|
ValidationError(
|
||||||
|
_("You cannot share this item with its owner."),
|
||||||
|
code="invalid_share",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
instance = self.instance
|
instance = self.instance
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.utils.formats import get_format as original_get_format
|
|||||||
def get_format(format_type=None, lang=None, use_l10n=None):
|
def get_format(format_type=None, lang=None, use_l10n=None):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
|
||||||
if user and user.is_authenticated and hasattr(user, "settings"):
|
if user and user.is_authenticated and hasattr(user, "settings") and use_l10n:
|
||||||
user_settings = user.settings
|
user_settings = user.settings
|
||||||
if format_type == "THOUSAND_SEPARATOR":
|
if format_type == "THOUSAND_SEPARATOR":
|
||||||
number_format = getattr(user_settings, "number_format", None)
|
number_format = getattr(user_settings, "number_format", None)
|
||||||
|
|||||||
@@ -36,12 +36,19 @@ class SharedObject(models.Model):
|
|||||||
related_name="%(class)s_owned",
|
related_name="%(class)s_owned",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
verbose_name=_("Owner"),
|
||||||
)
|
)
|
||||||
visibility = models.CharField(
|
visibility = models.CharField(
|
||||||
max_length=10, choices=Visibility.choices, default=Visibility.private
|
max_length=10,
|
||||||
|
choices=Visibility.choices,
|
||||||
|
default=Visibility.private,
|
||||||
|
verbose_name=_("Visibility"),
|
||||||
)
|
)
|
||||||
shared_with = models.ManyToManyField(
|
shared_with = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL, related_name="%(class)s_shared", blank=True
|
settings.AUTH_USER_MODEL,
|
||||||
|
related_name="%(class)s_shared",
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Shared with users"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use as abstract base class
|
# Use as abstract base class
|
||||||
@@ -65,6 +72,18 @@ class SharedObject(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OwnedObjectManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Return only objects the user can access"""
|
||||||
|
user = get_current_user()
|
||||||
|
base_qs = super().get_queryset()
|
||||||
|
|
||||||
|
if user and user.is_authenticated:
|
||||||
|
return base_qs.filter(Q(owner=user) | Q(owner=None)).distinct()
|
||||||
|
|
||||||
|
return base_qs
|
||||||
|
|
||||||
|
|
||||||
class OwnedObject(models.Model):
|
class OwnedObject(models.Model):
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import procrastinate
|
||||||
|
|
||||||
|
|
||||||
|
def on_app_ready(app: procrastinate.App):
|
||||||
|
"""This function is ran upon procrastinate initialization."""
|
||||||
|
...
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from packaging.version import parse as parse_version, InvalidVersion
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from django.db import DEFAULT_DB_ALIAS
|
from django.db import DEFAULT_DB_ALIAS
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
from procrastinate import builtin_tasks
|
from procrastinate import builtin_tasks
|
||||||
from procrastinate.contrib.django import app
|
from procrastinate.contrib.django import app
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -79,3 +83,46 @@ def reset_demo_data(timestamp=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error during daily demo data reset: {e}")
|
logger.exception(f"Error during daily demo data reset: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@app.periodic(cron="0 */12 * * *") # Every 12 hours
|
||||||
|
@app.task(
|
||||||
|
name="check_for_updates",
|
||||||
|
)
|
||||||
|
def check_for_updates(timestamp=None):
|
||||||
|
url = "https://api.github.com/repos/eitchtee/WYGIWYH/releases/latest"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=60)
|
||||||
|
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
latest_version = data.get("tag_name")
|
||||||
|
|
||||||
|
if latest_version:
|
||||||
|
try:
|
||||||
|
current_v = parse_version(settings.APP_VERSION)
|
||||||
|
except InvalidVersion:
|
||||||
|
current_v = parse_version("0.0.0")
|
||||||
|
try:
|
||||||
|
latest_v = parse_version(latest_version)
|
||||||
|
except InvalidVersion:
|
||||||
|
latest_v = parse_version("0.0.0")
|
||||||
|
|
||||||
|
update_info = {
|
||||||
|
"update_available": False,
|
||||||
|
"current_version": str(current_v),
|
||||||
|
"latest_version": str(latest_v),
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest_v > current_v:
|
||||||
|
update_info["update_available"] = True
|
||||||
|
|
||||||
|
# Cache the entire dictionary
|
||||||
|
cache.set("update_check", update_info, 60 * 60 * 25)
|
||||||
|
logger.info(f"Update check complete. Result: {update_info}")
|
||||||
|
else:
|
||||||
|
logger.warning("Could not find 'tag_name' in GitHub API response.")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Failed to fetch updates from GitHub: {e}")
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# core/templatetags/update_tags.py
|
||||||
|
from django import template
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def get_update_check():
|
||||||
|
"""
|
||||||
|
Retrieves the update status dictionary from the cache.
|
||||||
|
Returns a default dictionary if nothing is found.
|
||||||
|
"""
|
||||||
|
return cache.get("update_check") or {
|
||||||
|
"update_available": False,
|
||||||
|
"latest_version": "N/A",
|
||||||
|
}
|
||||||
@@ -37,7 +37,9 @@ class AirDatePickerInput(widgets.DateInput):
|
|||||||
def _get_current_language():
|
def _get_current_language():
|
||||||
"""Get current language code in format compatible with AirDatepicker"""
|
"""Get current language code in format compatible with AirDatepicker"""
|
||||||
lang_code = translation.get_language()
|
lang_code = translation.get_language()
|
||||||
# AirDatepicker uses simple language codes
|
# AirDatepicker uses simple language codes, except for pt-br
|
||||||
|
if lang_code.lower() == "pt-br":
|
||||||
|
return "pt-BR"
|
||||||
return lang_code.split("-")[0]
|
return lang_code.split("-")[0]
|
||||||
|
|
||||||
def _get_format(self):
|
def _get_format(self):
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class ArbitraryDecimalDisplayNumberInput(forms.TextInput):
|
|||||||
"x-data": "",
|
"x-data": "",
|
||||||
"x-mask:dynamic": f"$money($input, '{get_format('DECIMAL_SEPARATOR')}', "
|
"x-mask:dynamic": f"$money($input, '{get_format('DECIMAL_SEPARATOR')}', "
|
||||||
f"'{get_format('THOUSAND_SEPARATOR')}', '30')",
|
f"'{get_format('THOUSAND_SEPARATOR')}', '30')",
|
||||||
|
"x-on:keyup": "$el.dispatchEvent(new Event('input'))",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dca', '0003_dcastrategy_owner_dcastrategy_shared_with_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dcastrategy',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dcastrategy',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dcastrategy',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -91,6 +91,8 @@ def get_transactions(request, include_unpaid=True, include_silent=False):
|
|||||||
transactions = transactions.filter(is_paid=True)
|
transactions = transactions.filter(is_paid=True)
|
||||||
|
|
||||||
if not include_silent:
|
if not include_silent:
|
||||||
transactions = transactions.exclude(Q(category__mute=True) & ~Q(category=None))
|
transactions = transactions.exclude(
|
||||||
|
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
|
||||||
|
)
|
||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ def emergency_fund(request):
|
|||||||
reference_date__gte=start_date,
|
reference_date__gte=start_date,
|
||||||
reference_date__lte=end_date,
|
reference_date__lte=end_date,
|
||||||
category__mute=False,
|
category__mute=False,
|
||||||
|
mute=False,
|
||||||
)
|
)
|
||||||
.values("reference_date", "account__currency")
|
.values("reference_date", "account__currency")
|
||||||
.annotate(monthly_total=Sum("amount"))
|
.annotate(monthly_total=Sum("amount"))
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def monthly_summary(request, month: int, year: int):
|
|||||||
# Base queryset with all required filters
|
# Base queryset with all required filters
|
||||||
base_queryset = Transaction.objects.filter(
|
base_queryset = Transaction.objects.filter(
|
||||||
reference_date__year=year, reference_date__month=month, account__is_asset=False
|
reference_date__year=year, reference_date__month=month, account__is_asset=False
|
||||||
).exclude(Q(category__mute=True) & ~Q(category=None))
|
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||||
|
|
||||||
data = calculate_currency_totals(base_queryset, ignore_empty=True)
|
data = calculate_currency_totals(base_queryset, ignore_empty=True)
|
||||||
percentages = calculate_percentage_distribution(data)
|
percentages = calculate_percentage_distribution(data)
|
||||||
@@ -143,7 +143,7 @@ def monthly_account_summary(request, month: int, year: int):
|
|||||||
base_queryset = Transaction.objects.filter(
|
base_queryset = Transaction.objects.filter(
|
||||||
reference_date__year=year,
|
reference_date__year=year,
|
||||||
reference_date__month=month,
|
reference_date__month=month,
|
||||||
).exclude(Q(category__mute=True) & ~Q(category=None))
|
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||||
|
|
||||||
account_data = calculate_account_totals(transactions_queryset=base_queryset.all())
|
account_data = calculate_account_totals(transactions_queryset=base_queryset.all())
|
||||||
account_percentages = calculate_percentage_distribution(account_data)
|
account_percentages = calculate_percentage_distribution(account_data)
|
||||||
@@ -168,7 +168,7 @@ def monthly_currency_summary(request, month: int, year: int):
|
|||||||
base_queryset = Transaction.objects.filter(
|
base_queryset = Transaction.objects.filter(
|
||||||
reference_date__year=year,
|
reference_date__year=year,
|
||||||
reference_date__month=month,
|
reference_date__month=month,
|
||||||
).exclude(Q(category__mute=True) & ~Q(category=None))
|
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||||
|
|
||||||
currency_data = calculate_currency_totals(base_queryset.all(), ignore_empty=True)
|
currency_data = calculate_currency_totals(base_queryset.all(), ignore_empty=True)
|
||||||
currency_percentages = calculate_percentage_distribution(currency_data)
|
currency_percentages = calculate_percentage_distribution(currency_data)
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("net-worth/current/", views.net_worth_current, name="net_worth_current"),
|
path("net-worth/current/", views.net_worth_current, name="net_worth_current"),
|
||||||
path("net-worth/projected/", views.net_worth_projected, name="net_worth_projected"),
|
path("net-worth/projected/", views.net_worth_projected, name="net_worth_projected"),
|
||||||
|
path("net-worth/", views.net_worth, name="net_worth"),
|
||||||
]
|
]
|
||||||
|
|||||||
+34
-103
@@ -2,7 +2,7 @@ import json
|
|||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, redirect
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.net_worth.utils.calculate_net_worth import (
|
from apps.net_worth.utils.calculate_net_worth import (
|
||||||
@@ -18,7 +18,15 @@ from apps.transactions.utils.calculations import (
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def net_worth_current(request):
|
def net_worth(request):
|
||||||
|
if "view_type" in request.GET:
|
||||||
|
print(request.GET["view_type"])
|
||||||
|
view_type = request.GET["view_type"]
|
||||||
|
request.session["networth_view_type"] = view_type
|
||||||
|
else:
|
||||||
|
view_type = request.session.get("networth_view_type", "current")
|
||||||
|
|
||||||
|
if view_type == "current":
|
||||||
transactions_currency_queryset = Transaction.objects.filter(
|
transactions_currency_queryset = Transaction.objects.filter(
|
||||||
is_paid=True, account__is_archived=False
|
is_paid=True, account__is_archived=False
|
||||||
).order_by(
|
).order_by(
|
||||||
@@ -30,9 +38,21 @@ def net_worth_current(request):
|
|||||||
"account__group__name",
|
"account__group__name",
|
||||||
"account__name",
|
"account__name",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
transactions_currency_queryset = Transaction.objects.filter(
|
||||||
|
account__is_archived=False
|
||||||
|
).order_by(
|
||||||
|
"account__currency__name",
|
||||||
|
)
|
||||||
|
transactions_account_queryset = Transaction.objects.filter(
|
||||||
|
account__is_archived=False
|
||||||
|
).order_by(
|
||||||
|
"account__group__name",
|
||||||
|
"account__name",
|
||||||
|
)
|
||||||
|
|
||||||
currency_net_worth = calculate_currency_totals(
|
currency_net_worth = calculate_currency_totals(
|
||||||
transactions_queryset=transactions_currency_queryset
|
transactions_queryset=transactions_currency_queryset, deep_search=True
|
||||||
)
|
)
|
||||||
account_net_worth = calculate_account_totals(
|
account_net_worth = calculate_account_totals(
|
||||||
transactions_queryset=transactions_account_queryset
|
transactions_queryset=transactions_account_queryset
|
||||||
@@ -116,111 +136,22 @@ def net_worth_current(request):
|
|||||||
"currencies": currencies,
|
"currencies": currencies,
|
||||||
"chart_data_accounts_json": chart_data_accounts_json,
|
"chart_data_accounts_json": chart_data_accounts_json,
|
||||||
"accounts": accounts,
|
"accounts": accounts,
|
||||||
"type": "current",
|
"type": view_type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def net_worth_current(request):
|
||||||
|
request.session["networth_view_type"] = "current"
|
||||||
|
|
||||||
|
return redirect("net_worth")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def net_worth_projected(request):
|
def net_worth_projected(request):
|
||||||
transactions_currency_queryset = Transaction.objects.filter(
|
request.session["networth_view_type"] = "projected"
|
||||||
account__is_archived=False
|
|
||||||
).order_by(
|
|
||||||
"account__currency__name",
|
|
||||||
)
|
|
||||||
transactions_account_queryset = Transaction.objects.filter(
|
|
||||||
account__is_archived=False
|
|
||||||
).order_by(
|
|
||||||
"account__group__name",
|
|
||||||
"account__name",
|
|
||||||
)
|
|
||||||
|
|
||||||
currency_net_worth = calculate_currency_totals(
|
return redirect("net_worth")
|
||||||
transactions_queryset=transactions_currency_queryset
|
|
||||||
)
|
|
||||||
account_net_worth = calculate_account_totals(
|
|
||||||
transactions_queryset=transactions_account_queryset
|
|
||||||
)
|
|
||||||
|
|
||||||
historical_currency_net_worth = calculate_historical_currency_net_worth(
|
|
||||||
queryset=transactions_currency_queryset
|
|
||||||
)
|
|
||||||
|
|
||||||
labels = (
|
|
||||||
list(historical_currency_net_worth.keys())
|
|
||||||
if historical_currency_net_worth
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
currencies = (
|
|
||||||
list(historical_currency_net_worth[labels[0]].keys())
|
|
||||||
if historical_currency_net_worth
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
datasets = []
|
|
||||||
for i, currency in enumerate(currencies):
|
|
||||||
data = [
|
|
||||||
float(month_data[currency])
|
|
||||||
for month_data in historical_currency_net_worth.values()
|
|
||||||
]
|
|
||||||
datasets.append(
|
|
||||||
{
|
|
||||||
"label": currency,
|
|
||||||
"data": data,
|
|
||||||
"yAxisID": f"y{i}",
|
|
||||||
"fill": False,
|
|
||||||
"tension": 0.1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
chart_data_currency = {"labels": labels, "datasets": datasets}
|
|
||||||
|
|
||||||
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
|
|
||||||
|
|
||||||
historical_account_balance = calculate_historical_account_balance(
|
|
||||||
queryset=transactions_account_queryset
|
|
||||||
)
|
|
||||||
|
|
||||||
labels = (
|
|
||||||
list(historical_account_balance.keys()) if historical_account_balance else []
|
|
||||||
)
|
|
||||||
accounts = (
|
|
||||||
list(historical_account_balance[labels[0]].keys())
|
|
||||||
if historical_account_balance
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
datasets = []
|
|
||||||
for i, account in enumerate(accounts):
|
|
||||||
data = [
|
|
||||||
float(month_data[account])
|
|
||||||
for month_data in historical_account_balance.values()
|
|
||||||
]
|
|
||||||
datasets.append(
|
|
||||||
{
|
|
||||||
"label": account,
|
|
||||||
"data": data,
|
|
||||||
"fill": False,
|
|
||||||
"tension": 0.1,
|
|
||||||
"yAxisID": f"y-axis-{i}", # Assign each dataset to its own Y-axis
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
chart_data_accounts = {"labels": labels, "datasets": datasets}
|
|
||||||
|
|
||||||
chart_data_accounts_json = json.dumps(chart_data_accounts, cls=DjangoJSONEncoder)
|
|
||||||
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"net_worth/net_worth.html",
|
|
||||||
{
|
|
||||||
"currency_net_worth": currency_net_worth,
|
|
||||||
"account_net_worth": account_net_worth,
|
|
||||||
"chart_data_currency_json": chart_data_currency_json,
|
|
||||||
"currencies": currencies,
|
|
||||||
"chart_data_accounts_json": chart_data_accounts_json,
|
|
||||||
"accounts": accounts,
|
|
||||||
"type": "projected",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rules', '0013_transactionrule_on_delete'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionrule',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionrule',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionrule',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -7,6 +7,7 @@ from apps.transactions.models import (
|
|||||||
InstallmentPlan,
|
InstallmentPlan,
|
||||||
RecurringTransaction,
|
RecurringTransaction,
|
||||||
TransactionEntity,
|
TransactionEntity,
|
||||||
|
QuickTransaction,
|
||||||
)
|
)
|
||||||
from apps.common.admin import SharedObjectModelAdmin
|
from apps.common.admin import SharedObjectModelAdmin
|
||||||
|
|
||||||
@@ -49,19 +50,22 @@ class TransactionInline(admin.TabularInline):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(InstallmentPlan)
|
@admin.register(InstallmentPlan)
|
||||||
class InstallmentPlanAdmin(SharedObjectModelAdmin):
|
class InstallmentPlanAdmin(admin.ModelAdmin):
|
||||||
inlines = [
|
inlines = [
|
||||||
TransactionInline,
|
TransactionInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RecurringTransaction)
|
@admin.register(RecurringTransaction)
|
||||||
class RecurringTransactionAdmin(SharedObjectModelAdmin):
|
class RecurringTransactionAdmin(admin.ModelAdmin):
|
||||||
inlines = [
|
inlines = [
|
||||||
TransactionInline,
|
TransactionInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(QuickTransaction)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TransactionCategory)
|
@admin.register(TransactionCategory)
|
||||||
class TransactionCategoryModelAdmin(SharedObjectModelAdmin):
|
class TransactionCategoryModelAdmin(SharedObjectModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from crispy_forms.layout import (
|
|||||||
Column,
|
Column,
|
||||||
Field,
|
Field,
|
||||||
Div,
|
Div,
|
||||||
|
HTML,
|
||||||
)
|
)
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -29,8 +30,8 @@ from apps.transactions.models import (
|
|||||||
InstallmentPlan,
|
InstallmentPlan,
|
||||||
RecurringTransaction,
|
RecurringTransaction,
|
||||||
TransactionEntity,
|
TransactionEntity,
|
||||||
|
QuickTransaction,
|
||||||
)
|
)
|
||||||
from apps.common.middleware.thread_local import get_current_user
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionForm(forms.ModelForm):
|
class TransactionForm(forms.ModelForm):
|
||||||
@@ -247,6 +248,145 @@ class TransactionForm(forms.ModelForm):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class QuickTransactionForm(forms.ModelForm):
|
||||||
|
category = DynamicModelChoiceField(
|
||||||
|
create_field="name",
|
||||||
|
model=TransactionCategory,
|
||||||
|
required=False,
|
||||||
|
label=_("Category"),
|
||||||
|
queryset=TransactionCategory.objects.filter(active=True),
|
||||||
|
)
|
||||||
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
model=TransactionTag,
|
||||||
|
to_field_name="name",
|
||||||
|
create_field="name",
|
||||||
|
required=False,
|
||||||
|
label=_("Tags"),
|
||||||
|
queryset=TransactionTag.objects.filter(active=True),
|
||||||
|
)
|
||||||
|
entities = DynamicModelMultipleChoiceField(
|
||||||
|
model=TransactionEntity,
|
||||||
|
to_field_name="name",
|
||||||
|
create_field="name",
|
||||||
|
required=False,
|
||||||
|
label=_("Entities"),
|
||||||
|
)
|
||||||
|
account = forms.ModelChoiceField(
|
||||||
|
queryset=Account.objects.filter(is_archived=False),
|
||||||
|
label=_("Account"),
|
||||||
|
widget=TomSelect(clear_button=False, group_by="group"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = QuickTransaction
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"account",
|
||||||
|
"type",
|
||||||
|
"is_paid",
|
||||||
|
"amount",
|
||||||
|
"description",
|
||||||
|
"notes",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"entities",
|
||||||
|
"mute",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||||
|
"account": TomSelect(clear_button=False, group_by="group"),
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
"mute": _("Muted transactions won't be displayed on monthly summaries")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if self.instance.id:
|
||||||
|
self.fields["account"].queryset = Account.objects.filter(
|
||||||
|
Q(is_archived=False) | Q(transactions=self.instance.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||||
|
Q(active=True) | Q(transaction=self.instance.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["tags"].queryset = TransactionTag.objects.filter(
|
||||||
|
Q(active=True) | Q(transaction=self.instance.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||||
|
Q(active=True) | Q(transactions=self.instance.id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields["account"].queryset = Account.objects.filter(
|
||||||
|
is_archived=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||||
|
self.fields["entities"].queryset = TransactionEntity.objects.all()
|
||||||
|
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_tag = False
|
||||||
|
self.helper.form_method = "post"
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Field(
|
||||||
|
"type",
|
||||||
|
template="transactions/widgets/income_expense_toggle_buttons.html",
|
||||||
|
),
|
||||||
|
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
|
||||||
|
"name",
|
||||||
|
HTML("<hr />"),
|
||||||
|
Row(
|
||||||
|
Column("account", css_class="form-group col-md-6 mb-0"),
|
||||||
|
Column("entities", css_class="form-group col-md-6 mb-0"),
|
||||||
|
css_class="form-row",
|
||||||
|
),
|
||||||
|
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"),
|
||||||
|
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",
|
||||||
|
Switch("mute"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
decimal_places = self.instance.account.currency.decimal_places
|
||||||
|
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput(
|
||||||
|
decimal_places=decimal_places
|
||||||
|
)
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||||
|
self.helper.layout.append(
|
||||||
|
Div(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Add"), css_class="btn btn-outline-primary"
|
||||||
|
),
|
||||||
|
css_class="d-grid gap-2",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BulkEditTransactionForm(TransactionForm):
|
class BulkEditTransactionForm(TransactionForm):
|
||||||
is_paid = forms.NullBooleanField(required=False)
|
is_paid = forms.NullBooleanField(required=False)
|
||||||
|
|
||||||
@@ -359,6 +499,13 @@ class TransferForm(forms.Form):
|
|||||||
label=_("Notes"),
|
label=_("Notes"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mute = forms.BooleanField(
|
||||||
|
label=_("Mute"),
|
||||||
|
initial=True,
|
||||||
|
required=False,
|
||||||
|
help_text=_("Muted transactions won't be displayed on monthly summaries"),
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -377,6 +524,7 @@ class TransferForm(forms.Form):
|
|||||||
),
|
),
|
||||||
Field("description"),
|
Field("description"),
|
||||||
Field("notes"),
|
Field("notes"),
|
||||||
|
Switch("mute"),
|
||||||
Row(
|
Row(
|
||||||
Column(
|
Column(
|
||||||
Row(
|
Row(
|
||||||
@@ -459,6 +607,8 @@ class TransferForm(forms.Form):
|
|||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
mute = self.cleaned_data["mute"]
|
||||||
|
|
||||||
from_account = self.cleaned_data["from_account"]
|
from_account = self.cleaned_data["from_account"]
|
||||||
to_account = self.cleaned_data["to_account"]
|
to_account = self.cleaned_data["to_account"]
|
||||||
from_amount = self.cleaned_data["from_amount"]
|
from_amount = self.cleaned_data["from_amount"]
|
||||||
@@ -481,6 +631,7 @@ class TransferForm(forms.Form):
|
|||||||
description=description,
|
description=description,
|
||||||
category=from_category,
|
category=from_category,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
|
mute=mute,
|
||||||
)
|
)
|
||||||
from_transaction.tags.set(self.cleaned_data.get("from_tags", []))
|
from_transaction.tags.set(self.cleaned_data.get("from_tags", []))
|
||||||
|
|
||||||
@@ -495,6 +646,7 @@ class TransferForm(forms.Form):
|
|||||||
description=description,
|
description=description,
|
||||||
category=to_category,
|
category=to_category,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
|
mute=mute,
|
||||||
)
|
)
|
||||||
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
|
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
|
||||||
|
|
||||||
@@ -733,7 +885,7 @@ class TransactionCategoryForm(forms.ModelForm):
|
|||||||
fields = ["name", "mute", "active"]
|
fields = ["name", "mute", "active"]
|
||||||
labels = {"name": _("Category name")}
|
labels = {"name": _("Category name")}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
"mute": _("Muted categories won't count towards your monthly total")
|
"mute": _("Muted categories won't be displayed on monthly summaries")
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-06-20 03:57
|
||||||
|
|
||||||
|
import apps.transactions.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0014_alter_account_options_alter_accountgroup_options'),
|
||||||
|
('transactions', '0042_alter_transactioncategory_options_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuickTransaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Name')),
|
||||||
|
('type', models.CharField(choices=[('IN', 'Income'), ('EX', 'Expense')], default='EX', max_length=2, verbose_name='Type')),
|
||||||
|
('is_paid', models.BooleanField(default=True, verbose_name='Paid')),
|
||||||
|
('amount', models.DecimalField(decimal_places=30, max_digits=42, validators=[apps.transactions.validators.validate_non_negative, apps.transactions.validators.validate_decimal_places], verbose_name='Amount')),
|
||||||
|
('description', models.CharField(blank=True, max_length=500, verbose_name='Description')),
|
||||||
|
('notes', models.TextField(blank=True, verbose_name='Notes')),
|
||||||
|
('internal_note', models.TextField(blank=True, verbose_name='Internal Note')),
|
||||||
|
('internal_id', models.TextField(blank=True, null=True, unique=True, verbose_name='Internal ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quick_transactions', to='accounts.account', verbose_name='Account')),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='transactions.transactioncategory', verbose_name='Category')),
|
||||||
|
('entities', models.ManyToManyField(blank=True, related_name='quick_transactions', to='transactions.transactionentity', verbose_name='Entities')),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('tags', models.ManyToManyField(blank=True, to='transactions.transactiontag', verbose_name='Tags')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Quick Transaction',
|
||||||
|
'verbose_name_plural': 'Quick Transactions',
|
||||||
|
'db_table': 'quick_transactions',
|
||||||
|
'default_manager_name': 'objects',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-06-20 04:02
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('transactions', '0043_quicktransaction'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='quicktransaction',
|
||||||
|
unique_together={('name', 'owner')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-07-19 18:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('transactions', '0044_alter_quicktransaction_unique_together'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='mute',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Mute'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.11 on 2025-07-19 18:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('transactions', '0045_transaction_mute'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='quicktransaction',
|
||||||
|
name='mute',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Mute'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('transactions', '0046_quicktransaction_mute'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactioncategory',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactioncategory',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactioncategory',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionentity',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionentity',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionentity',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactiontag',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactiontag',
|
||||||
|
name='shared_with',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactiontag',
|
||||||
|
name='visibility',
|
||||||
|
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -16,7 +16,12 @@ from apps.common.templatetags.decimal import localize_number, drop_trailing_zero
|
|||||||
from apps.currencies.utils.convert import convert
|
from apps.currencies.utils.convert import convert
|
||||||
from apps.transactions.validators import validate_decimal_places, validate_non_negative
|
from apps.transactions.validators import validate_decimal_places, validate_non_negative
|
||||||
from apps.common.middleware.thread_local import get_current_user
|
from apps.common.middleware.thread_local import get_current_user
|
||||||
from apps.common.models import SharedObject, SharedObjectManager, OwnedObject
|
from apps.common.models import (
|
||||||
|
SharedObject,
|
||||||
|
SharedObjectManager,
|
||||||
|
OwnedObject,
|
||||||
|
OwnedObjectManager,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
@@ -118,13 +123,20 @@ class SoftDeleteManager(models.Manager):
|
|||||||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
if user and not user.is_anonymous:
|
if user and not user.is_anonymous:
|
||||||
return qs.filter(
|
account_ids = (
|
||||||
|
qs.filter(
|
||||||
Q(account__visibility="public")
|
Q(account__visibility="public")
|
||||||
| Q(account__owner=user)
|
| Q(account__owner=user)
|
||||||
| Q(account__shared_with=user)
|
| Q(account__shared_with=user)
|
||||||
| Q(account__visibility="private", account__owner=None),
|
| Q(account__visibility="private", account__owner=None),
|
||||||
deleted=False,
|
deleted=False,
|
||||||
).distinct()
|
)
|
||||||
|
.values_list("account__id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
return qs.filter(account_id__in=account_ids, deleted=False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return qs.filter(
|
return qs.filter(
|
||||||
deleted=False,
|
deleted=False,
|
||||||
@@ -287,6 +299,7 @@ class Transaction(OwnedObject):
|
|||||||
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
|
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
|
||||||
date = models.DateField(verbose_name=_("Date"))
|
date = models.DateField(verbose_name=_("Date"))
|
||||||
reference_date = MonthYearModelField(verbose_name=_("Reference Date"))
|
reference_date = MonthYearModelField(verbose_name=_("Reference Date"))
|
||||||
|
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
|
||||||
|
|
||||||
amount = models.DecimalField(
|
amount = models.DecimalField(
|
||||||
max_digits=42,
|
max_digits=42,
|
||||||
@@ -879,3 +892,90 @@ class RecurringTransaction(models.Model):
|
|||||||
"""
|
"""
|
||||||
today = timezone.localdate(timezone.now())
|
today = timezone.localdate(timezone.now())
|
||||||
self.transactions.filter(is_paid=False, date__gt=today).delete()
|
self.transactions.filter(is_paid=False, date__gt=today).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class QuickTransaction(OwnedObject):
|
||||||
|
class Type(models.TextChoices):
|
||||||
|
INCOME = "IN", _("Income")
|
||||||
|
EXPENSE = "EX", _("Expense")
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
account = models.ForeignKey(
|
||||||
|
"accounts.Account",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Account"),
|
||||||
|
related_name="quick_transactions",
|
||||||
|
)
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=Type,
|
||||||
|
default=Type.EXPENSE,
|
||||||
|
verbose_name=_("Type"),
|
||||||
|
)
|
||||||
|
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
|
||||||
|
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
|
||||||
|
|
||||||
|
amount = models.DecimalField(
|
||||||
|
max_digits=42,
|
||||||
|
decimal_places=30,
|
||||||
|
verbose_name=_("Amount"),
|
||||||
|
validators=[validate_non_negative, validate_decimal_places],
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=500, verbose_name=_("Description"), blank=True
|
||||||
|
)
|
||||||
|
notes = models.TextField(blank=True, verbose_name=_("Notes"))
|
||||||
|
category = models.ForeignKey(
|
||||||
|
TransactionCategory,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("Category"),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
tags = models.ManyToManyField(
|
||||||
|
TransactionTag,
|
||||||
|
verbose_name=_("Tags"),
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
entities = models.ManyToManyField(
|
||||||
|
TransactionEntity,
|
||||||
|
verbose_name=_("Entities"),
|
||||||
|
blank=True,
|
||||||
|
related_name="quick_transactions",
|
||||||
|
)
|
||||||
|
|
||||||
|
internal_note = models.TextField(blank=True, verbose_name=_("Internal Note"))
|
||||||
|
internal_id = models.TextField(
|
||||||
|
blank=True, null=True, unique=True, verbose_name=_("Internal ID")
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = OwnedObjectManager()
|
||||||
|
all_objects = models.Manager() # Unfiltered manager
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Quick Transaction")
|
||||||
|
verbose_name_plural = _("Quick Transactions")
|
||||||
|
unique_together = ("name", "owner")
|
||||||
|
db_table = "quick_transactions"
|
||||||
|
default_manager_name = "objects"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.amount = truncate_decimal(
|
||||||
|
value=self.amount, decimal_places=self.account.currency.decimal_places
|
||||||
|
)
|
||||||
|
|
||||||
|
self.full_clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ urlpatterns = [
|
|||||||
views.transaction_pay,
|
views.transaction_pay,
|
||||||
name="transaction_pay",
|
name="transaction_pay",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"transaction/<int:transaction_id>/mute/",
|
||||||
|
views.transaction_mute,
|
||||||
|
name="transaction_mute",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/<int:transaction_id>/delete/",
|
"transaction/<int:transaction_id>/delete/",
|
||||||
views.transaction_delete,
|
views.transaction_delete,
|
||||||
@@ -307,4 +312,44 @@ urlpatterns = [
|
|||||||
views.recurring_transaction_finish,
|
views.recurring_transaction_finish,
|
||||||
name="recurring_transaction_finish",
|
name="recurring_transaction_finish",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/",
|
||||||
|
views.quick_transactions_index,
|
||||||
|
name="quick_transactions_index",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/list/",
|
||||||
|
views.quick_transactions_list,
|
||||||
|
name="quick_transactions_list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/add/",
|
||||||
|
views.quick_transaction_add,
|
||||||
|
name="quick_transaction_add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/<int:quick_transaction_id>/edit/",
|
||||||
|
views.quick_transaction_edit,
|
||||||
|
name="quick_transaction_edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/<int:quick_transaction_id>/delete/",
|
||||||
|
views.quick_transaction_delete,
|
||||||
|
name="quick_transaction_delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/create-menu/",
|
||||||
|
views.quick_transactions_create_menu,
|
||||||
|
name="quick_transactions_create_menu",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quick-transactions/<int:quick_transaction_id>/create/",
|
||||||
|
views.quick_transaction_add_as_transaction,
|
||||||
|
name="quick_transaction_add_as_transaction",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transactions/<int:transaction_id>/add-as-quick-transaction/",
|
||||||
|
views.quick_transaction_add_as_quick_transaction,
|
||||||
|
name="quick_transaction_add_as_quick_transaction",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ from apps.currencies.utils.convert import convert
|
|||||||
from apps.currencies.models import Currency
|
from apps.currencies.models import Currency
|
||||||
|
|
||||||
|
|
||||||
def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
def calculate_currency_totals(
|
||||||
|
transactions_queryset, ignore_empty=False, deep_search=False
|
||||||
|
):
|
||||||
# Prepare the aggregation expressions
|
# Prepare the aggregation expressions
|
||||||
currency_totals = (
|
currency_totals_from_transactions = (
|
||||||
transactions_queryset.values(
|
transactions_queryset.values(
|
||||||
"account__currency",
|
"account__currency",
|
||||||
"account__currency__code",
|
"account__currency__code",
|
||||||
@@ -19,7 +21,14 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
"account__currency__decimal_places",
|
"account__currency__decimal_places",
|
||||||
"account__currency__prefix",
|
"account__currency__prefix",
|
||||||
"account__currency__suffix",
|
"account__currency__suffix",
|
||||||
"account__currency__exchange_currency",
|
"account__currency__exchange_currency", # ID of the exchange currency for the account's currency
|
||||||
|
# Fields for the exchange currency itself (if account.currency.exchange_currency is set)
|
||||||
|
# These might be null if not set, so handle appropriately.
|
||||||
|
"account__currency__exchange_currency__code",
|
||||||
|
"account__currency__exchange_currency__name",
|
||||||
|
"account__currency__exchange_currency__decimal_places",
|
||||||
|
"account__currency__exchange_currency__prefix",
|
||||||
|
"account__currency__exchange_currency__suffix",
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
expense_current=Coalesce(
|
expense_current=Coalesce(
|
||||||
@@ -72,15 +81,21 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
.order_by()
|
.order_by()
|
||||||
)
|
)
|
||||||
|
|
||||||
# First pass: Process basic totals and store all currency data
|
|
||||||
result = {}
|
result = {}
|
||||||
currencies_using_exchange = (
|
# currencies_using_exchange maps:
|
||||||
{}
|
# exchange_currency_id -> list of [
|
||||||
) # Track which currencies use which exchange currencies
|
# { "currency_id": original_currency_id, (the currency that was exchanged FROM)
|
||||||
|
# "exchanged": { field: amount_in_exchange_currency, ... } (the values of original_currency_id converted TO exchange_currency_id)
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
currencies_using_exchange = {}
|
||||||
|
|
||||||
for total in currency_totals:
|
# --- First Pass: Process transactions from the queryset ---
|
||||||
# Skip empty currencies if ignore_empty is True
|
for total in currency_totals_from_transactions:
|
||||||
if ignore_empty and all(
|
if (
|
||||||
|
ignore_empty
|
||||||
|
and not deep_search
|
||||||
|
and all(
|
||||||
total[field] == Decimal("0")
|
total[field] == Decimal("0")
|
||||||
for field in [
|
for field in [
|
||||||
"expense_current",
|
"expense_current",
|
||||||
@@ -88,20 +103,33 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
"income_current",
|
"income_current",
|
||||||
"income_projected",
|
"income_projected",
|
||||||
]
|
]
|
||||||
|
)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Calculate derived totals
|
currency_id = total["account__currency"]
|
||||||
|
try:
|
||||||
|
from_currency_obj = Currency.objects.get(id=currency_id)
|
||||||
|
except Currency.DoesNotExist:
|
||||||
|
# This should ideally not happen if database is consistent
|
||||||
|
continue
|
||||||
|
|
||||||
|
exchange_currency_for_this_total_id = total[
|
||||||
|
"account__currency__exchange_currency"
|
||||||
|
]
|
||||||
|
exchange_currency_obj_for_this_total = None
|
||||||
|
if exchange_currency_for_this_total_id:
|
||||||
|
try:
|
||||||
|
# Use pre-fetched values if available, otherwise query
|
||||||
|
exchange_currency_obj_for_this_total = Currency.objects.get(
|
||||||
|
id=exchange_currency_for_this_total_id
|
||||||
|
)
|
||||||
|
except Currency.DoesNotExist:
|
||||||
|
pass # Exchange currency might not exist or be set
|
||||||
|
|
||||||
total_current = total["income_current"] - total["expense_current"]
|
total_current = total["income_current"] - total["expense_current"]
|
||||||
total_projected = total["income_projected"] - total["expense_projected"]
|
total_projected = total["income_projected"] - total["expense_projected"]
|
||||||
total_final = total_current + total_projected
|
total_final = total_current + total_projected
|
||||||
currency_id = total["account__currency"]
|
|
||||||
from_currency = Currency.objects.get(id=currency_id)
|
|
||||||
exchange_currency = (
|
|
||||||
Currency.objects.get(id=total["account__currency__exchange_currency"])
|
|
||||||
if total["account__currency__exchange_currency"]
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
currency_data = {
|
currency_data = {
|
||||||
"currency": {
|
"currency": {
|
||||||
@@ -120,9 +148,16 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
"total_final": total_final,
|
"total_final": total_final,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add exchanged values if exchange_currency exists
|
if exchange_currency_obj_for_this_total:
|
||||||
if exchange_currency:
|
exchanged_details = {
|
||||||
exchanged = {}
|
"currency": {
|
||||||
|
"code": exchange_currency_obj_for_this_total.code,
|
||||||
|
"name": exchange_currency_obj_for_this_total.name,
|
||||||
|
"decimal_places": exchange_currency_obj_for_this_total.decimal_places,
|
||||||
|
"prefix": exchange_currency_obj_for_this_total.prefix,
|
||||||
|
"suffix": exchange_currency_obj_for_this_total.suffix,
|
||||||
|
}
|
||||||
|
}
|
||||||
for field in [
|
for field in [
|
||||||
"expense_current",
|
"expense_current",
|
||||||
"expense_projected",
|
"expense_projected",
|
||||||
@@ -132,50 +167,78 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
"total_projected",
|
"total_projected",
|
||||||
"total_final",
|
"total_final",
|
||||||
]:
|
]:
|
||||||
amount, prefix, suffix, decimal_places = convert(
|
amount_to_convert = currency_data[field]
|
||||||
amount=currency_data[field],
|
converted_val, _, _, _ = convert(
|
||||||
from_currency=from_currency,
|
amount=amount_to_convert,
|
||||||
to_currency=exchange_currency,
|
from_currency=from_currency_obj,
|
||||||
|
to_currency=exchange_currency_obj_for_this_total,
|
||||||
|
)
|
||||||
|
exchanged_details[field] = (
|
||||||
|
converted_val if converted_val is not None else Decimal("0")
|
||||||
)
|
)
|
||||||
if amount is not None:
|
|
||||||
exchanged[field] = amount
|
|
||||||
if "currency" not in exchanged:
|
|
||||||
exchanged["currency"] = {
|
|
||||||
"prefix": prefix,
|
|
||||||
"suffix": suffix,
|
|
||||||
"decimal_places": decimal_places,
|
|
||||||
"code": exchange_currency.code,
|
|
||||||
"name": exchange_currency.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
if exchanged:
|
currency_data["exchanged"] = exchanged_details
|
||||||
currency_data["exchanged"] = exchanged
|
|
||||||
# Track which currencies are using which exchange currencies
|
if exchange_currency_obj_for_this_total.id not in currencies_using_exchange:
|
||||||
if exchange_currency.id not in currencies_using_exchange:
|
currencies_using_exchange[exchange_currency_obj_for_this_total.id] = []
|
||||||
currencies_using_exchange[exchange_currency.id] = []
|
currencies_using_exchange[exchange_currency_obj_for_this_total.id].append(
|
||||||
currencies_using_exchange[exchange_currency.id].append(
|
{"currency_id": currency_id, "exchanged": exchanged_details}
|
||||||
{"currency_id": currency_id, "exchanged": exchanged}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
result[currency_id] = currency_data
|
result[currency_id] = currency_data
|
||||||
|
|
||||||
# Second pass: Add consolidated totals for currencies that are used as exchange currencies
|
# --- Deep Search: Add transaction-less currencies that are exchange targets ---
|
||||||
for currency_id, currency_data in result.items():
|
if deep_search:
|
||||||
if currency_id in currencies_using_exchange:
|
# Iteratively add exchange targets that might not have had direct transactions
|
||||||
consolidated = {
|
# Start with known exchange targets from the first pass
|
||||||
"currency": currency_data["currency"].copy(),
|
queue = list(currencies_using_exchange.keys())
|
||||||
"expense_current": currency_data["expense_current"],
|
processed_for_deep_add = set(
|
||||||
"expense_projected": currency_data["expense_projected"],
|
result.keys()
|
||||||
"income_current": currency_data["income_current"],
|
) # Track currencies already in result or added by this deep search step
|
||||||
"income_projected": currency_data["income_projected"],
|
|
||||||
"total_current": currency_data["total_current"],
|
while queue:
|
||||||
"total_projected": currency_data["total_projected"],
|
target_id = queue.pop(0)
|
||||||
"total_final": currency_data["total_final"],
|
if target_id in processed_for_deep_add:
|
||||||
|
continue
|
||||||
|
processed_for_deep_add.add(target_id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
target_id not in result
|
||||||
|
): # If this exchange target had no direct transactions
|
||||||
|
try:
|
||||||
|
db_currency = Currency.objects.get(id=target_id)
|
||||||
|
except Currency.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Initialize data for this transaction-less exchange target currency
|
||||||
|
currency_data_for_db_currency = {
|
||||||
|
"currency": {
|
||||||
|
"code": db_currency.code,
|
||||||
|
"name": db_currency.name,
|
||||||
|
"decimal_places": db_currency.decimal_places,
|
||||||
|
"prefix": db_currency.prefix,
|
||||||
|
"suffix": db_currency.suffix,
|
||||||
|
},
|
||||||
|
"expense_current": Decimal("0"),
|
||||||
|
"expense_projected": Decimal("0"),
|
||||||
|
"income_current": Decimal("0"),
|
||||||
|
"income_projected": Decimal("0"),
|
||||||
|
"total_current": Decimal("0"),
|
||||||
|
"total_projected": Decimal("0"),
|
||||||
|
"total_final": Decimal("0"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add exchanged values from all currencies using this as exchange currency
|
# If this newly added transaction-less currency ALSO has an exchange_currency set for itself
|
||||||
for using_currency in currencies_using_exchange[currency_id]:
|
if db_currency.exchange_currency:
|
||||||
exchanged = using_currency["exchanged"]
|
exchanged_details_for_db_currency = {
|
||||||
|
"currency": {
|
||||||
|
"code": db_currency.exchange_currency.code,
|
||||||
|
"name": db_currency.exchange_currency.name,
|
||||||
|
"decimal_places": db_currency.exchange_currency.decimal_places,
|
||||||
|
"prefix": db_currency.exchange_currency.prefix,
|
||||||
|
"suffix": db_currency.exchange_currency.suffix,
|
||||||
|
}
|
||||||
|
}
|
||||||
for field in [
|
for field in [
|
||||||
"expense_current",
|
"expense_current",
|
||||||
"expense_projected",
|
"expense_projected",
|
||||||
@@ -185,10 +248,89 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
"total_projected",
|
"total_projected",
|
||||||
"total_final",
|
"total_final",
|
||||||
]:
|
]:
|
||||||
if field in exchanged:
|
converted_val, _, _, _ = convert(
|
||||||
consolidated[field] += exchanged[field]
|
Decimal("0"), db_currency, db_currency.exchange_currency
|
||||||
|
)
|
||||||
|
exchanged_details_for_db_currency[field] = (
|
||||||
|
converted_val if converted_val is not None else Decimal("0")
|
||||||
|
)
|
||||||
|
|
||||||
result[currency_id]["consolidated"] = consolidated
|
currency_data_for_db_currency["exchanged"] = (
|
||||||
|
exchanged_details_for_db_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure its own exchange_currency is registered in currencies_using_exchange
|
||||||
|
# and add it to the queue if it hasn't been processed yet for deep add.
|
||||||
|
target_id_for_this_db_curr = db_currency.exchange_currency.id
|
||||||
|
if target_id_for_this_db_curr not in currencies_using_exchange:
|
||||||
|
currencies_using_exchange[target_id_for_this_db_curr] = []
|
||||||
|
|
||||||
|
# Avoid adding duplicate entries
|
||||||
|
already_present_in_cue = any(
|
||||||
|
entry["currency_id"] == db_currency.id
|
||||||
|
for entry in currencies_using_exchange[
|
||||||
|
target_id_for_this_db_curr
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if not already_present_in_cue:
|
||||||
|
currencies_using_exchange[target_id_for_this_db_curr].append(
|
||||||
|
{
|
||||||
|
"currency_id": db_currency.id,
|
||||||
|
"exchanged": exchanged_details_for_db_currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_id_for_this_db_curr not in processed_for_deep_add:
|
||||||
|
queue.append(target_id_for_this_db_curr)
|
||||||
|
|
||||||
|
result[db_currency.id] = currency_data_for_db_currency
|
||||||
|
|
||||||
|
# --- Second Pass: Calculate consolidated totals for all currencies in result ---
|
||||||
|
for currency_id_consolidated, data_consolidated_currency in result.items():
|
||||||
|
consolidated_data = {
|
||||||
|
"currency": data_consolidated_currency["currency"].copy(),
|
||||||
|
"expense_current": data_consolidated_currency["expense_current"],
|
||||||
|
"expense_projected": data_consolidated_currency["expense_projected"],
|
||||||
|
"income_current": data_consolidated_currency["income_current"],
|
||||||
|
"income_projected": data_consolidated_currency["income_projected"],
|
||||||
|
"total_current": data_consolidated_currency["total_current"],
|
||||||
|
"total_projected": data_consolidated_currency["total_projected"],
|
||||||
|
"total_final": data_consolidated_currency["total_final"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if currency_id_consolidated in currencies_using_exchange:
|
||||||
|
for original_currency_info in currencies_using_exchange[
|
||||||
|
currency_id_consolidated
|
||||||
|
]:
|
||||||
|
exchanged_values_from_original = original_currency_info["exchanged"]
|
||||||
|
for field in [
|
||||||
|
"expense_current",
|
||||||
|
"expense_projected",
|
||||||
|
"income_current",
|
||||||
|
"income_projected",
|
||||||
|
"total_current",
|
||||||
|
"total_projected",
|
||||||
|
"total_final",
|
||||||
|
]:
|
||||||
|
if field in exchanged_values_from_original:
|
||||||
|
consolidated_data[field] += exchanged_values_from_original[
|
||||||
|
field
|
||||||
|
]
|
||||||
|
|
||||||
|
result[currency_id_consolidated]["consolidated"] = consolidated_data
|
||||||
|
|
||||||
|
# Sort currencies by their final_total or consolidated final_total, descending
|
||||||
|
result = {
|
||||||
|
k: v
|
||||||
|
for k, v in sorted(
|
||||||
|
result.items(),
|
||||||
|
reverse=True,
|
||||||
|
key=lambda item: max(
|
||||||
|
item[1].get("total_final", Decimal("0")),
|
||||||
|
item[1].get("consolidated", {}).get("total_final", Decimal("0")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ from .categories import *
|
|||||||
from .actions import *
|
from .actions import *
|
||||||
from .installment_plans import *
|
from .installment_plans import *
|
||||||
from .recurring_transactions import *
|
from .recurring_transactions import *
|
||||||
|
from .quick_transactions import *
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def bulk_unpay_transactions(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def bulk_delete_transactions(request):
|
def bulk_delete_transactions(request):
|
||||||
selected_transactions = request.GET.getlist("transactions", [])
|
selected_transactions = request.GET.getlist("transactions", [])
|
||||||
transactions = Transaction.all_objects.filter(id__in=selected_transactions)
|
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||||
count = transactions.count()
|
count = transactions.count()
|
||||||
transactions.delete()
|
transactions.delete()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.forms import model_to_dict
|
||||||
|
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.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.transactions.forms import QuickTransactionForm
|
||||||
|
from apps.transactions.models import QuickTransaction, transaction_created
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transactions_index(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/pages/index.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transactions_list(request):
|
||||||
|
quick_transactions = QuickTransaction.objects.all().order_by("name")
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/list.html",
|
||||||
|
context={"quick_transactions": quick_transactions},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def quick_transaction_add(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = QuickTransactionForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Item added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = QuickTransactionForm()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/add.html",
|
||||||
|
{"form": form},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def quick_transaction_edit(request, quick_transaction_id):
|
||||||
|
quick_transaction = get_object_or_404(QuickTransaction, id=quick_transaction_id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = QuickTransactionForm(request.POST, instance=quick_transaction)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Item updated successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = QuickTransactionForm(instance=quick_transaction)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/edit.html",
|
||||||
|
{"form": form, "quick_transaction": quick_transaction},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["DELETE"])
|
||||||
|
def quick_transaction_delete(request, quick_transaction_id):
|
||||||
|
quick_transaction = get_object_or_404(QuickTransaction, id=quick_transaction_id)
|
||||||
|
|
||||||
|
quick_transaction.delete()
|
||||||
|
|
||||||
|
messages.success(request, _("Item deleted successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transactions_create_menu(request):
|
||||||
|
quick_transactions = QuickTransaction.objects.all().order_by("name")
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"quick_transactions/fragments/create_menu.html",
|
||||||
|
context={"quick_transactions": quick_transactions},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transaction_add_as_transaction(request, quick_transaction_id):
|
||||||
|
quick_transaction: QuickTransaction = get_object_or_404(
|
||||||
|
QuickTransaction, id=quick_transaction_id
|
||||||
|
)
|
||||||
|
today = timezone.localdate(timezone.now())
|
||||||
|
|
||||||
|
quick_transaction_data = model_to_dict(
|
||||||
|
quick_transaction,
|
||||||
|
exclude=[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"owner",
|
||||||
|
"account",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"entities",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
new_transaction = Transaction(**quick_transaction_data)
|
||||||
|
new_transaction.account = quick_transaction.account
|
||||||
|
new_transaction.category = quick_transaction.category
|
||||||
|
|
||||||
|
new_transaction.date = today
|
||||||
|
new_transaction.reference_date = today.replace(day=1)
|
||||||
|
new_transaction.save()
|
||||||
|
new_transaction.tags.set(quick_transaction.tags.all())
|
||||||
|
new_transaction.entities.set(quick_transaction.entities.all())
|
||||||
|
|
||||||
|
transaction_created.send(sender=new_transaction)
|
||||||
|
|
||||||
|
messages.success(request, _("Transaction added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def quick_transaction_add_as_quick_transaction(request, transaction_id):
|
||||||
|
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
transaction.description
|
||||||
|
and QuickTransaction.objects.filter(
|
||||||
|
name__startswith=transaction.description
|
||||||
|
).exists()
|
||||||
|
) or QuickTransaction.objects.filter(
|
||||||
|
name__startswith=_("Quick Transaction")
|
||||||
|
).exists():
|
||||||
|
if transaction.description:
|
||||||
|
count = QuickTransaction.objects.filter(
|
||||||
|
name__startswith=transaction.description
|
||||||
|
).count()
|
||||||
|
qt_name = transaction.description + f" ({count + 1})"
|
||||||
|
else:
|
||||||
|
count = QuickTransaction.objects.filter(
|
||||||
|
name__startswith=_("Quick Transaction")
|
||||||
|
).count()
|
||||||
|
qt_name = _("Quick Transaction") + f" ({count + 1})"
|
||||||
|
else:
|
||||||
|
qt_name = transaction.description or _("Quick Transaction")
|
||||||
|
|
||||||
|
transaction_data = model_to_dict(
|
||||||
|
transaction,
|
||||||
|
exclude=[
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"owner",
|
||||||
|
"account",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"entities",
|
||||||
|
"date",
|
||||||
|
"reference_date",
|
||||||
|
"installment_plan",
|
||||||
|
"installment_id",
|
||||||
|
"recurring_transaction",
|
||||||
|
"deleted",
|
||||||
|
"deleted_at",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
new_quick_transaction = QuickTransaction(**transaction_data)
|
||||||
|
new_quick_transaction.account = transaction.account
|
||||||
|
new_quick_transaction.category = transaction.category
|
||||||
|
|
||||||
|
new_quick_transaction.name = qt_name
|
||||||
|
|
||||||
|
new_quick_transaction.save()
|
||||||
|
new_quick_transaction.tags.set(transaction.tags.all())
|
||||||
|
new_quick_transaction.entities.set(transaction.entities.all())
|
||||||
|
|
||||||
|
messages.success(request, _("Item added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "toasts",
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@ from copy import deepcopy
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Q
|
from django.db.models import Q, When, Case, Value, IntegerField
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -388,6 +388,26 @@ def transaction_pay(request, transaction_id):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def transaction_mute(request, transaction_id):
|
||||||
|
transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||||
|
|
||||||
|
new_mute = False if transaction.mute else True
|
||||||
|
transaction.mute = new_mute
|
||||||
|
transaction.save()
|
||||||
|
transaction_updated.send(sender=transaction)
|
||||||
|
|
||||||
|
response = render(
|
||||||
|
request,
|
||||||
|
"transactions/fragments/item.html",
|
||||||
|
context={"transaction": transaction, **request.GET},
|
||||||
|
)
|
||||||
|
response.headers["HX-Trigger"] = "selective_update"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def transaction_all_index(request):
|
def transaction_all_index(request):
|
||||||
@@ -586,11 +606,26 @@ def get_recent_transactions(request, filter_type=None):
|
|||||||
# Get search term from query params
|
# Get search term from query params
|
||||||
search_term = request.GET.get("q", "").strip()
|
search_term = request.GET.get("q", "").strip()
|
||||||
|
|
||||||
|
today = timezone.localdate(timezone.now())
|
||||||
|
yesterday = today - timezone.timedelta(days=1)
|
||||||
|
tomorrow = today + timezone.timedelta(days=1)
|
||||||
|
|
||||||
# Base queryset with selected fields
|
# Base queryset with selected fields
|
||||||
queryset = (
|
queryset = (
|
||||||
Transaction.objects.filter(deleted=False)
|
Transaction.objects.filter(deleted=False)
|
||||||
|
.annotate(
|
||||||
|
date_order=Case(
|
||||||
|
When(date=today, then=Value(0)),
|
||||||
|
When(date=tomorrow, then=Value(1)),
|
||||||
|
When(date=yesterday, then=Value(2)),
|
||||||
|
When(date__gt=tomorrow, then=Value(3)),
|
||||||
|
When(date__lt=yesterday, then=Value(4)),
|
||||||
|
default=Value(5),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
.select_related("account", "category")
|
.select_related("account", "category")
|
||||||
.order_by("-created_at")
|
.order_by("date_order", "date", "id")
|
||||||
)
|
)
|
||||||
|
|
||||||
if filter_type:
|
if filter_type:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from crispy_forms.bootstrap import (
|
|||||||
FormActions,
|
FormActions,
|
||||||
)
|
)
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
|
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div, HTML
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.forms import (
|
from django.contrib.auth.forms import (
|
||||||
@@ -115,6 +115,7 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
"date_format",
|
"date_format",
|
||||||
"datetime_format",
|
"datetime_format",
|
||||||
"number_format",
|
"number_format",
|
||||||
|
"volume",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -126,10 +127,14 @@ class UserSettingsForm(forms.ModelForm):
|
|||||||
self.helper.layout = Layout(
|
self.helper.layout = Layout(
|
||||||
"language",
|
"language",
|
||||||
"timezone",
|
"timezone",
|
||||||
|
HTML("<hr />"),
|
||||||
"date_format",
|
"date_format",
|
||||||
"datetime_format",
|
"datetime_format",
|
||||||
"number_format",
|
"number_format",
|
||||||
|
HTML("<hr />"),
|
||||||
"start_page",
|
"start_page",
|
||||||
|
HTML("<hr />"),
|
||||||
|
"volume",
|
||||||
FormActions(
|
FormActions(
|
||||||
NoClassSubmit(
|
NoClassSubmit(
|
||||||
"submit", _("Save"), css_class="btn btn-outline-primary w-100"
|
"submit", _("Save"), css_class="btn btn-outline-primary w-100"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,479 @@
|
|||||||
|
# Generated by Django 5.1.1 on 2025-06-29 00:48
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("users", "0021_alter_usersettings_timezone"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="usersettings",
|
||||||
|
name="volume",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
default=10,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(10),
|
||||||
|
],
|
||||||
|
verbose_name="Volume",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="usersettings",
|
||||||
|
name="timezone",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("auto", "Auto"),
|
||||||
|
("Africa/Abidjan", "Africa/Abidjan"),
|
||||||
|
("Africa/Accra", "Africa/Accra"),
|
||||||
|
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
||||||
|
("Africa/Algiers", "Africa/Algiers"),
|
||||||
|
("Africa/Asmara", "Africa/Asmara"),
|
||||||
|
("Africa/Bamako", "Africa/Bamako"),
|
||||||
|
("Africa/Bangui", "Africa/Bangui"),
|
||||||
|
("Africa/Banjul", "Africa/Banjul"),
|
||||||
|
("Africa/Bissau", "Africa/Bissau"),
|
||||||
|
("Africa/Blantyre", "Africa/Blantyre"),
|
||||||
|
("Africa/Brazzaville", "Africa/Brazzaville"),
|
||||||
|
("Africa/Bujumbura", "Africa/Bujumbura"),
|
||||||
|
("Africa/Cairo", "Africa/Cairo"),
|
||||||
|
("Africa/Casablanca", "Africa/Casablanca"),
|
||||||
|
("Africa/Ceuta", "Africa/Ceuta"),
|
||||||
|
("Africa/Conakry", "Africa/Conakry"),
|
||||||
|
("Africa/Dakar", "Africa/Dakar"),
|
||||||
|
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
||||||
|
("Africa/Djibouti", "Africa/Djibouti"),
|
||||||
|
("Africa/Douala", "Africa/Douala"),
|
||||||
|
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
||||||
|
("Africa/Freetown", "Africa/Freetown"),
|
||||||
|
("Africa/Gaborone", "Africa/Gaborone"),
|
||||||
|
("Africa/Harare", "Africa/Harare"),
|
||||||
|
("Africa/Johannesburg", "Africa/Johannesburg"),
|
||||||
|
("Africa/Juba", "Africa/Juba"),
|
||||||
|
("Africa/Kampala", "Africa/Kampala"),
|
||||||
|
("Africa/Khartoum", "Africa/Khartoum"),
|
||||||
|
("Africa/Kigali", "Africa/Kigali"),
|
||||||
|
("Africa/Kinshasa", "Africa/Kinshasa"),
|
||||||
|
("Africa/Lagos", "Africa/Lagos"),
|
||||||
|
("Africa/Libreville", "Africa/Libreville"),
|
||||||
|
("Africa/Lome", "Africa/Lome"),
|
||||||
|
("Africa/Luanda", "Africa/Luanda"),
|
||||||
|
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
||||||
|
("Africa/Lusaka", "Africa/Lusaka"),
|
||||||
|
("Africa/Malabo", "Africa/Malabo"),
|
||||||
|
("Africa/Maputo", "Africa/Maputo"),
|
||||||
|
("Africa/Maseru", "Africa/Maseru"),
|
||||||
|
("Africa/Mbabane", "Africa/Mbabane"),
|
||||||
|
("Africa/Mogadishu", "Africa/Mogadishu"),
|
||||||
|
("Africa/Monrovia", "Africa/Monrovia"),
|
||||||
|
("Africa/Nairobi", "Africa/Nairobi"),
|
||||||
|
("Africa/Ndjamena", "Africa/Ndjamena"),
|
||||||
|
("Africa/Niamey", "Africa/Niamey"),
|
||||||
|
("Africa/Nouakchott", "Africa/Nouakchott"),
|
||||||
|
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
||||||
|
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
||||||
|
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
||||||
|
("Africa/Tripoli", "Africa/Tripoli"),
|
||||||
|
("Africa/Tunis", "Africa/Tunis"),
|
||||||
|
("Africa/Windhoek", "Africa/Windhoek"),
|
||||||
|
("America/Adak", "America/Adak"),
|
||||||
|
("America/Anchorage", "America/Anchorage"),
|
||||||
|
("America/Anguilla", "America/Anguilla"),
|
||||||
|
("America/Antigua", "America/Antigua"),
|
||||||
|
("America/Araguaina", "America/Araguaina"),
|
||||||
|
(
|
||||||
|
"America/Argentina/Buenos_Aires",
|
||||||
|
"America/Argentina/Buenos_Aires",
|
||||||
|
),
|
||||||
|
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
|
||||||
|
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
||||||
|
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||||
|
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
||||||
|
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
||||||
|
(
|
||||||
|
"America/Argentina/Rio_Gallegos",
|
||||||
|
"America/Argentina/Rio_Gallegos",
|
||||||
|
),
|
||||||
|
("America/Argentina/Salta", "America/Argentina/Salta"),
|
||||||
|
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
||||||
|
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
||||||
|
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
||||||
|
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
||||||
|
("America/Aruba", "America/Aruba"),
|
||||||
|
("America/Asuncion", "America/Asuncion"),
|
||||||
|
("America/Atikokan", "America/Atikokan"),
|
||||||
|
("America/Bahia", "America/Bahia"),
|
||||||
|
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
||||||
|
("America/Barbados", "America/Barbados"),
|
||||||
|
("America/Belem", "America/Belem"),
|
||||||
|
("America/Belize", "America/Belize"),
|
||||||
|
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
||||||
|
("America/Boa_Vista", "America/Boa_Vista"),
|
||||||
|
("America/Bogota", "America/Bogota"),
|
||||||
|
("America/Boise", "America/Boise"),
|
||||||
|
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
||||||
|
("America/Campo_Grande", "America/Campo_Grande"),
|
||||||
|
("America/Cancun", "America/Cancun"),
|
||||||
|
("America/Caracas", "America/Caracas"),
|
||||||
|
("America/Cayenne", "America/Cayenne"),
|
||||||
|
("America/Cayman", "America/Cayman"),
|
||||||
|
("America/Chicago", "America/Chicago"),
|
||||||
|
("America/Chihuahua", "America/Chihuahua"),
|
||||||
|
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||||
|
("America/Costa_Rica", "America/Costa_Rica"),
|
||||||
|
("America/Creston", "America/Creston"),
|
||||||
|
("America/Cuiaba", "America/Cuiaba"),
|
||||||
|
("America/Curacao", "America/Curacao"),
|
||||||
|
("America/Danmarkshavn", "America/Danmarkshavn"),
|
||||||
|
("America/Dawson", "America/Dawson"),
|
||||||
|
("America/Dawson_Creek", "America/Dawson_Creek"),
|
||||||
|
("America/Denver", "America/Denver"),
|
||||||
|
("America/Detroit", "America/Detroit"),
|
||||||
|
("America/Dominica", "America/Dominica"),
|
||||||
|
("America/Edmonton", "America/Edmonton"),
|
||||||
|
("America/Eirunepe", "America/Eirunepe"),
|
||||||
|
("America/El_Salvador", "America/El_Salvador"),
|
||||||
|
("America/Fort_Nelson", "America/Fort_Nelson"),
|
||||||
|
("America/Fortaleza", "America/Fortaleza"),
|
||||||
|
("America/Glace_Bay", "America/Glace_Bay"),
|
||||||
|
("America/Goose_Bay", "America/Goose_Bay"),
|
||||||
|
("America/Grand_Turk", "America/Grand_Turk"),
|
||||||
|
("America/Grenada", "America/Grenada"),
|
||||||
|
("America/Guadeloupe", "America/Guadeloupe"),
|
||||||
|
("America/Guatemala", "America/Guatemala"),
|
||||||
|
("America/Guayaquil", "America/Guayaquil"),
|
||||||
|
("America/Guyana", "America/Guyana"),
|
||||||
|
("America/Halifax", "America/Halifax"),
|
||||||
|
("America/Havana", "America/Havana"),
|
||||||
|
("America/Hermosillo", "America/Hermosillo"),
|
||||||
|
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
||||||
|
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||||
|
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||||
|
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
||||||
|
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
||||||
|
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
||||||
|
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
||||||
|
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
||||||
|
("America/Inuvik", "America/Inuvik"),
|
||||||
|
("America/Iqaluit", "America/Iqaluit"),
|
||||||
|
("America/Jamaica", "America/Jamaica"),
|
||||||
|
("America/Juneau", "America/Juneau"),
|
||||||
|
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
|
||||||
|
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
|
||||||
|
("America/Kralendijk", "America/Kralendijk"),
|
||||||
|
("America/La_Paz", "America/La_Paz"),
|
||||||
|
("America/Lima", "America/Lima"),
|
||||||
|
("America/Los_Angeles", "America/Los_Angeles"),
|
||||||
|
("America/Lower_Princes", "America/Lower_Princes"),
|
||||||
|
("America/Maceio", "America/Maceio"),
|
||||||
|
("America/Managua", "America/Managua"),
|
||||||
|
("America/Manaus", "America/Manaus"),
|
||||||
|
("America/Marigot", "America/Marigot"),
|
||||||
|
("America/Martinique", "America/Martinique"),
|
||||||
|
("America/Matamoros", "America/Matamoros"),
|
||||||
|
("America/Mazatlan", "America/Mazatlan"),
|
||||||
|
("America/Menominee", "America/Menominee"),
|
||||||
|
("America/Merida", "America/Merida"),
|
||||||
|
("America/Metlakatla", "America/Metlakatla"),
|
||||||
|
("America/Mexico_City", "America/Mexico_City"),
|
||||||
|
("America/Miquelon", "America/Miquelon"),
|
||||||
|
("America/Moncton", "America/Moncton"),
|
||||||
|
("America/Monterrey", "America/Monterrey"),
|
||||||
|
("America/Montevideo", "America/Montevideo"),
|
||||||
|
("America/Montserrat", "America/Montserrat"),
|
||||||
|
("America/Nassau", "America/Nassau"),
|
||||||
|
("America/New_York", "America/New_York"),
|
||||||
|
("America/Nome", "America/Nome"),
|
||||||
|
("America/Noronha", "America/Noronha"),
|
||||||
|
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
||||||
|
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
||||||
|
(
|
||||||
|
"America/North_Dakota/New_Salem",
|
||||||
|
"America/North_Dakota/New_Salem",
|
||||||
|
),
|
||||||
|
("America/Nuuk", "America/Nuuk"),
|
||||||
|
("America/Ojinaga", "America/Ojinaga"),
|
||||||
|
("America/Panama", "America/Panama"),
|
||||||
|
("America/Paramaribo", "America/Paramaribo"),
|
||||||
|
("America/Phoenix", "America/Phoenix"),
|
||||||
|
("America/Port-au-Prince", "America/Port-au-Prince"),
|
||||||
|
("America/Port_of_Spain", "America/Port_of_Spain"),
|
||||||
|
("America/Porto_Velho", "America/Porto_Velho"),
|
||||||
|
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||||
|
("America/Punta_Arenas", "America/Punta_Arenas"),
|
||||||
|
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
||||||
|
("America/Recife", "America/Recife"),
|
||||||
|
("America/Regina", "America/Regina"),
|
||||||
|
("America/Resolute", "America/Resolute"),
|
||||||
|
("America/Rio_Branco", "America/Rio_Branco"),
|
||||||
|
("America/Santarem", "America/Santarem"),
|
||||||
|
("America/Santiago", "America/Santiago"),
|
||||||
|
("America/Santo_Domingo", "America/Santo_Domingo"),
|
||||||
|
("America/Sao_Paulo", "America/Sao_Paulo"),
|
||||||
|
("America/Scoresbysund", "America/Scoresbysund"),
|
||||||
|
("America/Sitka", "America/Sitka"),
|
||||||
|
("America/St_Barthelemy", "America/St_Barthelemy"),
|
||||||
|
("America/St_Johns", "America/St_Johns"),
|
||||||
|
("America/St_Kitts", "America/St_Kitts"),
|
||||||
|
("America/St_Lucia", "America/St_Lucia"),
|
||||||
|
("America/St_Thomas", "America/St_Thomas"),
|
||||||
|
("America/St_Vincent", "America/St_Vincent"),
|
||||||
|
("America/Swift_Current", "America/Swift_Current"),
|
||||||
|
("America/Tegucigalpa", "America/Tegucigalpa"),
|
||||||
|
("America/Thule", "America/Thule"),
|
||||||
|
("America/Tijuana", "America/Tijuana"),
|
||||||
|
("America/Toronto", "America/Toronto"),
|
||||||
|
("America/Tortola", "America/Tortola"),
|
||||||
|
("America/Vancouver", "America/Vancouver"),
|
||||||
|
("America/Whitehorse", "America/Whitehorse"),
|
||||||
|
("America/Winnipeg", "America/Winnipeg"),
|
||||||
|
("America/Yakutat", "America/Yakutat"),
|
||||||
|
("Antarctica/Casey", "Antarctica/Casey"),
|
||||||
|
("Antarctica/Davis", "Antarctica/Davis"),
|
||||||
|
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
||||||
|
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
||||||
|
("Antarctica/Mawson", "Antarctica/Mawson"),
|
||||||
|
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
||||||
|
("Antarctica/Palmer", "Antarctica/Palmer"),
|
||||||
|
("Antarctica/Rothera", "Antarctica/Rothera"),
|
||||||
|
("Antarctica/Syowa", "Antarctica/Syowa"),
|
||||||
|
("Antarctica/Troll", "Antarctica/Troll"),
|
||||||
|
("Antarctica/Vostok", "Antarctica/Vostok"),
|
||||||
|
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
||||||
|
("Asia/Aden", "Asia/Aden"),
|
||||||
|
("Asia/Almaty", "Asia/Almaty"),
|
||||||
|
("Asia/Amman", "Asia/Amman"),
|
||||||
|
("Asia/Anadyr", "Asia/Anadyr"),
|
||||||
|
("Asia/Aqtau", "Asia/Aqtau"),
|
||||||
|
("Asia/Aqtobe", "Asia/Aqtobe"),
|
||||||
|
("Asia/Ashgabat", "Asia/Ashgabat"),
|
||||||
|
("Asia/Atyrau", "Asia/Atyrau"),
|
||||||
|
("Asia/Baghdad", "Asia/Baghdad"),
|
||||||
|
("Asia/Bahrain", "Asia/Bahrain"),
|
||||||
|
("Asia/Baku", "Asia/Baku"),
|
||||||
|
("Asia/Bangkok", "Asia/Bangkok"),
|
||||||
|
("Asia/Barnaul", "Asia/Barnaul"),
|
||||||
|
("Asia/Beirut", "Asia/Beirut"),
|
||||||
|
("Asia/Bishkek", "Asia/Bishkek"),
|
||||||
|
("Asia/Brunei", "Asia/Brunei"),
|
||||||
|
("Asia/Chita", "Asia/Chita"),
|
||||||
|
("Asia/Colombo", "Asia/Colombo"),
|
||||||
|
("Asia/Damascus", "Asia/Damascus"),
|
||||||
|
("Asia/Dhaka", "Asia/Dhaka"),
|
||||||
|
("Asia/Dili", "Asia/Dili"),
|
||||||
|
("Asia/Dubai", "Asia/Dubai"),
|
||||||
|
("Asia/Dushanbe", "Asia/Dushanbe"),
|
||||||
|
("Asia/Famagusta", "Asia/Famagusta"),
|
||||||
|
("Asia/Gaza", "Asia/Gaza"),
|
||||||
|
("Asia/Hebron", "Asia/Hebron"),
|
||||||
|
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
||||||
|
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
||||||
|
("Asia/Hovd", "Asia/Hovd"),
|
||||||
|
("Asia/Irkutsk", "Asia/Irkutsk"),
|
||||||
|
("Asia/Jakarta", "Asia/Jakarta"),
|
||||||
|
("Asia/Jayapura", "Asia/Jayapura"),
|
||||||
|
("Asia/Jerusalem", "Asia/Jerusalem"),
|
||||||
|
("Asia/Kabul", "Asia/Kabul"),
|
||||||
|
("Asia/Kamchatka", "Asia/Kamchatka"),
|
||||||
|
("Asia/Karachi", "Asia/Karachi"),
|
||||||
|
("Asia/Kathmandu", "Asia/Kathmandu"),
|
||||||
|
("Asia/Khandyga", "Asia/Khandyga"),
|
||||||
|
("Asia/Kolkata", "Asia/Kolkata"),
|
||||||
|
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||||
|
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
||||||
|
("Asia/Kuching", "Asia/Kuching"),
|
||||||
|
("Asia/Kuwait", "Asia/Kuwait"),
|
||||||
|
("Asia/Macau", "Asia/Macau"),
|
||||||
|
("Asia/Magadan", "Asia/Magadan"),
|
||||||
|
("Asia/Makassar", "Asia/Makassar"),
|
||||||
|
("Asia/Manila", "Asia/Manila"),
|
||||||
|
("Asia/Muscat", "Asia/Muscat"),
|
||||||
|
("Asia/Nicosia", "Asia/Nicosia"),
|
||||||
|
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
||||||
|
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
||||||
|
("Asia/Omsk", "Asia/Omsk"),
|
||||||
|
("Asia/Oral", "Asia/Oral"),
|
||||||
|
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
||||||
|
("Asia/Pontianak", "Asia/Pontianak"),
|
||||||
|
("Asia/Pyongyang", "Asia/Pyongyang"),
|
||||||
|
("Asia/Qatar", "Asia/Qatar"),
|
||||||
|
("Asia/Qostanay", "Asia/Qostanay"),
|
||||||
|
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
||||||
|
("Asia/Riyadh", "Asia/Riyadh"),
|
||||||
|
("Asia/Sakhalin", "Asia/Sakhalin"),
|
||||||
|
("Asia/Samarkand", "Asia/Samarkand"),
|
||||||
|
("Asia/Seoul", "Asia/Seoul"),
|
||||||
|
("Asia/Shanghai", "Asia/Shanghai"),
|
||||||
|
("Asia/Singapore", "Asia/Singapore"),
|
||||||
|
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
||||||
|
("Asia/Taipei", "Asia/Taipei"),
|
||||||
|
("Asia/Tashkent", "Asia/Tashkent"),
|
||||||
|
("Asia/Tbilisi", "Asia/Tbilisi"),
|
||||||
|
("Asia/Tehran", "Asia/Tehran"),
|
||||||
|
("Asia/Thimphu", "Asia/Thimphu"),
|
||||||
|
("Asia/Tokyo", "Asia/Tokyo"),
|
||||||
|
("Asia/Tomsk", "Asia/Tomsk"),
|
||||||
|
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||||
|
("Asia/Urumqi", "Asia/Urumqi"),
|
||||||
|
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
||||||
|
("Asia/Vientiane", "Asia/Vientiane"),
|
||||||
|
("Asia/Vladivostok", "Asia/Vladivostok"),
|
||||||
|
("Asia/Yakutsk", "Asia/Yakutsk"),
|
||||||
|
("Asia/Yangon", "Asia/Yangon"),
|
||||||
|
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
||||||
|
("Asia/Yerevan", "Asia/Yerevan"),
|
||||||
|
("Atlantic/Azores", "Atlantic/Azores"),
|
||||||
|
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
||||||
|
("Atlantic/Canary", "Atlantic/Canary"),
|
||||||
|
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
||||||
|
("Atlantic/Faroe", "Atlantic/Faroe"),
|
||||||
|
("Atlantic/Madeira", "Atlantic/Madeira"),
|
||||||
|
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
||||||
|
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
||||||
|
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
||||||
|
("Atlantic/Stanley", "Atlantic/Stanley"),
|
||||||
|
("Australia/Adelaide", "Australia/Adelaide"),
|
||||||
|
("Australia/Brisbane", "Australia/Brisbane"),
|
||||||
|
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
||||||
|
("Australia/Darwin", "Australia/Darwin"),
|
||||||
|
("Australia/Eucla", "Australia/Eucla"),
|
||||||
|
("Australia/Hobart", "Australia/Hobart"),
|
||||||
|
("Australia/Lindeman", "Australia/Lindeman"),
|
||||||
|
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
||||||
|
("Australia/Melbourne", "Australia/Melbourne"),
|
||||||
|
("Australia/Perth", "Australia/Perth"),
|
||||||
|
("Australia/Sydney", "Australia/Sydney"),
|
||||||
|
("Canada/Atlantic", "Canada/Atlantic"),
|
||||||
|
("Canada/Central", "Canada/Central"),
|
||||||
|
("Canada/Eastern", "Canada/Eastern"),
|
||||||
|
("Canada/Mountain", "Canada/Mountain"),
|
||||||
|
("Canada/Newfoundland", "Canada/Newfoundland"),
|
||||||
|
("Canada/Pacific", "Canada/Pacific"),
|
||||||
|
("Europe/Amsterdam", "Europe/Amsterdam"),
|
||||||
|
("Europe/Andorra", "Europe/Andorra"),
|
||||||
|
("Europe/Astrakhan", "Europe/Astrakhan"),
|
||||||
|
("Europe/Athens", "Europe/Athens"),
|
||||||
|
("Europe/Belgrade", "Europe/Belgrade"),
|
||||||
|
("Europe/Berlin", "Europe/Berlin"),
|
||||||
|
("Europe/Bratislava", "Europe/Bratislava"),
|
||||||
|
("Europe/Brussels", "Europe/Brussels"),
|
||||||
|
("Europe/Bucharest", "Europe/Bucharest"),
|
||||||
|
("Europe/Budapest", "Europe/Budapest"),
|
||||||
|
("Europe/Busingen", "Europe/Busingen"),
|
||||||
|
("Europe/Chisinau", "Europe/Chisinau"),
|
||||||
|
("Europe/Copenhagen", "Europe/Copenhagen"),
|
||||||
|
("Europe/Dublin", "Europe/Dublin"),
|
||||||
|
("Europe/Gibraltar", "Europe/Gibraltar"),
|
||||||
|
("Europe/Guernsey", "Europe/Guernsey"),
|
||||||
|
("Europe/Helsinki", "Europe/Helsinki"),
|
||||||
|
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
||||||
|
("Europe/Istanbul", "Europe/Istanbul"),
|
||||||
|
("Europe/Jersey", "Europe/Jersey"),
|
||||||
|
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
||||||
|
("Europe/Kirov", "Europe/Kirov"),
|
||||||
|
("Europe/Kyiv", "Europe/Kyiv"),
|
||||||
|
("Europe/Lisbon", "Europe/Lisbon"),
|
||||||
|
("Europe/Ljubljana", "Europe/Ljubljana"),
|
||||||
|
("Europe/London", "Europe/London"),
|
||||||
|
("Europe/Luxembourg", "Europe/Luxembourg"),
|
||||||
|
("Europe/Madrid", "Europe/Madrid"),
|
||||||
|
("Europe/Malta", "Europe/Malta"),
|
||||||
|
("Europe/Mariehamn", "Europe/Mariehamn"),
|
||||||
|
("Europe/Minsk", "Europe/Minsk"),
|
||||||
|
("Europe/Monaco", "Europe/Monaco"),
|
||||||
|
("Europe/Moscow", "Europe/Moscow"),
|
||||||
|
("Europe/Oslo", "Europe/Oslo"),
|
||||||
|
("Europe/Paris", "Europe/Paris"),
|
||||||
|
("Europe/Podgorica", "Europe/Podgorica"),
|
||||||
|
("Europe/Prague", "Europe/Prague"),
|
||||||
|
("Europe/Riga", "Europe/Riga"),
|
||||||
|
("Europe/Rome", "Europe/Rome"),
|
||||||
|
("Europe/Samara", "Europe/Samara"),
|
||||||
|
("Europe/San_Marino", "Europe/San_Marino"),
|
||||||
|
("Europe/Sarajevo", "Europe/Sarajevo"),
|
||||||
|
("Europe/Saratov", "Europe/Saratov"),
|
||||||
|
("Europe/Simferopol", "Europe/Simferopol"),
|
||||||
|
("Europe/Skopje", "Europe/Skopje"),
|
||||||
|
("Europe/Sofia", "Europe/Sofia"),
|
||||||
|
("Europe/Stockholm", "Europe/Stockholm"),
|
||||||
|
("Europe/Tallinn", "Europe/Tallinn"),
|
||||||
|
("Europe/Tirane", "Europe/Tirane"),
|
||||||
|
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
||||||
|
("Europe/Vaduz", "Europe/Vaduz"),
|
||||||
|
("Europe/Vatican", "Europe/Vatican"),
|
||||||
|
("Europe/Vienna", "Europe/Vienna"),
|
||||||
|
("Europe/Vilnius", "Europe/Vilnius"),
|
||||||
|
("Europe/Volgograd", "Europe/Volgograd"),
|
||||||
|
("Europe/Warsaw", "Europe/Warsaw"),
|
||||||
|
("Europe/Zagreb", "Europe/Zagreb"),
|
||||||
|
("Europe/Zurich", "Europe/Zurich"),
|
||||||
|
("GMT", "GMT"),
|
||||||
|
("Indian/Antananarivo", "Indian/Antananarivo"),
|
||||||
|
("Indian/Chagos", "Indian/Chagos"),
|
||||||
|
("Indian/Christmas", "Indian/Christmas"),
|
||||||
|
("Indian/Cocos", "Indian/Cocos"),
|
||||||
|
("Indian/Comoro", "Indian/Comoro"),
|
||||||
|
("Indian/Kerguelen", "Indian/Kerguelen"),
|
||||||
|
("Indian/Mahe", "Indian/Mahe"),
|
||||||
|
("Indian/Maldives", "Indian/Maldives"),
|
||||||
|
("Indian/Mauritius", "Indian/Mauritius"),
|
||||||
|
("Indian/Mayotte", "Indian/Mayotte"),
|
||||||
|
("Indian/Reunion", "Indian/Reunion"),
|
||||||
|
("Pacific/Apia", "Pacific/Apia"),
|
||||||
|
("Pacific/Auckland", "Pacific/Auckland"),
|
||||||
|
("Pacific/Bougainville", "Pacific/Bougainville"),
|
||||||
|
("Pacific/Chatham", "Pacific/Chatham"),
|
||||||
|
("Pacific/Chuuk", "Pacific/Chuuk"),
|
||||||
|
("Pacific/Easter", "Pacific/Easter"),
|
||||||
|
("Pacific/Efate", "Pacific/Efate"),
|
||||||
|
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
||||||
|
("Pacific/Fiji", "Pacific/Fiji"),
|
||||||
|
("Pacific/Funafuti", "Pacific/Funafuti"),
|
||||||
|
("Pacific/Galapagos", "Pacific/Galapagos"),
|
||||||
|
("Pacific/Gambier", "Pacific/Gambier"),
|
||||||
|
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
||||||
|
("Pacific/Guam", "Pacific/Guam"),
|
||||||
|
("Pacific/Honolulu", "Pacific/Honolulu"),
|
||||||
|
("Pacific/Kanton", "Pacific/Kanton"),
|
||||||
|
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
||||||
|
("Pacific/Kosrae", "Pacific/Kosrae"),
|
||||||
|
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
||||||
|
("Pacific/Majuro", "Pacific/Majuro"),
|
||||||
|
("Pacific/Marquesas", "Pacific/Marquesas"),
|
||||||
|
("Pacific/Midway", "Pacific/Midway"),
|
||||||
|
("Pacific/Nauru", "Pacific/Nauru"),
|
||||||
|
("Pacific/Niue", "Pacific/Niue"),
|
||||||
|
("Pacific/Norfolk", "Pacific/Norfolk"),
|
||||||
|
("Pacific/Noumea", "Pacific/Noumea"),
|
||||||
|
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
||||||
|
("Pacific/Palau", "Pacific/Palau"),
|
||||||
|
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
||||||
|
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
||||||
|
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
||||||
|
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
||||||
|
("Pacific/Saipan", "Pacific/Saipan"),
|
||||||
|
("Pacific/Tahiti", "Pacific/Tahiti"),
|
||||||
|
("Pacific/Tarawa", "Pacific/Tarawa"),
|
||||||
|
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
||||||
|
("Pacific/Wake", "Pacific/Wake"),
|
||||||
|
("Pacific/Wallis", "Pacific/Wallis"),
|
||||||
|
("US/Alaska", "US/Alaska"),
|
||||||
|
("US/Arizona", "US/Arizona"),
|
||||||
|
("US/Central", "US/Central"),
|
||||||
|
("US/Eastern", "US/Eastern"),
|
||||||
|
("US/Hawaii", "US/Hawaii"),
|
||||||
|
("US/Mountain", "US/Mountain"),
|
||||||
|
("US/Pacific", "US/Pacific"),
|
||||||
|
("UTC", "UTC"),
|
||||||
|
],
|
||||||
|
default="auto",
|
||||||
|
max_length=50,
|
||||||
|
verbose_name="Time Zone",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
+444
-1
@@ -2,11 +2,449 @@ import pytz
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import AbstractUser, Group
|
from django.contrib.auth.models import AbstractUser, Group
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from apps.users.managers import UserManager
|
from apps.users.managers import UserManager
|
||||||
|
|
||||||
|
timezones = [
|
||||||
|
("auto", _("Auto")),
|
||||||
|
("Africa/Abidjan", "Africa/Abidjan"),
|
||||||
|
("Africa/Accra", "Africa/Accra"),
|
||||||
|
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
||||||
|
("Africa/Algiers", "Africa/Algiers"),
|
||||||
|
("Africa/Asmara", "Africa/Asmara"),
|
||||||
|
("Africa/Bamako", "Africa/Bamako"),
|
||||||
|
("Africa/Bangui", "Africa/Bangui"),
|
||||||
|
("Africa/Banjul", "Africa/Banjul"),
|
||||||
|
("Africa/Bissau", "Africa/Bissau"),
|
||||||
|
("Africa/Blantyre", "Africa/Blantyre"),
|
||||||
|
("Africa/Brazzaville", "Africa/Brazzaville"),
|
||||||
|
("Africa/Bujumbura", "Africa/Bujumbura"),
|
||||||
|
("Africa/Cairo", "Africa/Cairo"),
|
||||||
|
("Africa/Casablanca", "Africa/Casablanca"),
|
||||||
|
("Africa/Ceuta", "Africa/Ceuta"),
|
||||||
|
("Africa/Conakry", "Africa/Conakry"),
|
||||||
|
("Africa/Dakar", "Africa/Dakar"),
|
||||||
|
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
||||||
|
("Africa/Djibouti", "Africa/Djibouti"),
|
||||||
|
("Africa/Douala", "Africa/Douala"),
|
||||||
|
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
||||||
|
("Africa/Freetown", "Africa/Freetown"),
|
||||||
|
("Africa/Gaborone", "Africa/Gaborone"),
|
||||||
|
("Africa/Harare", "Africa/Harare"),
|
||||||
|
("Africa/Johannesburg", "Africa/Johannesburg"),
|
||||||
|
("Africa/Juba", "Africa/Juba"),
|
||||||
|
("Africa/Kampala", "Africa/Kampala"),
|
||||||
|
("Africa/Khartoum", "Africa/Khartoum"),
|
||||||
|
("Africa/Kigali", "Africa/Kigali"),
|
||||||
|
("Africa/Kinshasa", "Africa/Kinshasa"),
|
||||||
|
("Africa/Lagos", "Africa/Lagos"),
|
||||||
|
("Africa/Libreville", "Africa/Libreville"),
|
||||||
|
("Africa/Lome", "Africa/Lome"),
|
||||||
|
("Africa/Luanda", "Africa/Luanda"),
|
||||||
|
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
||||||
|
("Africa/Lusaka", "Africa/Lusaka"),
|
||||||
|
("Africa/Malabo", "Africa/Malabo"),
|
||||||
|
("Africa/Maputo", "Africa/Maputo"),
|
||||||
|
("Africa/Maseru", "Africa/Maseru"),
|
||||||
|
("Africa/Mbabane", "Africa/Mbabane"),
|
||||||
|
("Africa/Mogadishu", "Africa/Mogadishu"),
|
||||||
|
("Africa/Monrovia", "Africa/Monrovia"),
|
||||||
|
("Africa/Nairobi", "Africa/Nairobi"),
|
||||||
|
("Africa/Ndjamena", "Africa/Ndjamena"),
|
||||||
|
("Africa/Niamey", "Africa/Niamey"),
|
||||||
|
("Africa/Nouakchott", "Africa/Nouakchott"),
|
||||||
|
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
||||||
|
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
||||||
|
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
||||||
|
("Africa/Tripoli", "Africa/Tripoli"),
|
||||||
|
("Africa/Tunis", "Africa/Tunis"),
|
||||||
|
("Africa/Windhoek", "Africa/Windhoek"),
|
||||||
|
("America/Adak", "America/Adak"),
|
||||||
|
("America/Anchorage", "America/Anchorage"),
|
||||||
|
("America/Anguilla", "America/Anguilla"),
|
||||||
|
("America/Antigua", "America/Antigua"),
|
||||||
|
("America/Araguaina", "America/Araguaina"),
|
||||||
|
("America/Argentina/Buenos_Aires", "America/Argentina/Buenos_Aires"),
|
||||||
|
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
|
||||||
|
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
||||||
|
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||||
|
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
||||||
|
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
||||||
|
("America/Argentina/Rio_Gallegos", "America/Argentina/Rio_Gallegos"),
|
||||||
|
("America/Argentina/Salta", "America/Argentina/Salta"),
|
||||||
|
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
||||||
|
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
||||||
|
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
||||||
|
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
||||||
|
("America/Aruba", "America/Aruba"),
|
||||||
|
("America/Asuncion", "America/Asuncion"),
|
||||||
|
("America/Atikokan", "America/Atikokan"),
|
||||||
|
("America/Bahia", "America/Bahia"),
|
||||||
|
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
||||||
|
("America/Barbados", "America/Barbados"),
|
||||||
|
("America/Belem", "America/Belem"),
|
||||||
|
("America/Belize", "America/Belize"),
|
||||||
|
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
||||||
|
("America/Boa_Vista", "America/Boa_Vista"),
|
||||||
|
("America/Bogota", "America/Bogota"),
|
||||||
|
("America/Boise", "America/Boise"),
|
||||||
|
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
||||||
|
("America/Campo_Grande", "America/Campo_Grande"),
|
||||||
|
("America/Cancun", "America/Cancun"),
|
||||||
|
("America/Caracas", "America/Caracas"),
|
||||||
|
("America/Cayenne", "America/Cayenne"),
|
||||||
|
("America/Cayman", "America/Cayman"),
|
||||||
|
("America/Chicago", "America/Chicago"),
|
||||||
|
("America/Chihuahua", "America/Chihuahua"),
|
||||||
|
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||||
|
("America/Costa_Rica", "America/Costa_Rica"),
|
||||||
|
("America/Coyhaique", "America/Coyhaique"),
|
||||||
|
("America/Creston", "America/Creston"),
|
||||||
|
("America/Cuiaba", "America/Cuiaba"),
|
||||||
|
("America/Curacao", "America/Curacao"),
|
||||||
|
("America/Danmarkshavn", "America/Danmarkshavn"),
|
||||||
|
("America/Dawson", "America/Dawson"),
|
||||||
|
("America/Dawson_Creek", "America/Dawson_Creek"),
|
||||||
|
("America/Denver", "America/Denver"),
|
||||||
|
("America/Detroit", "America/Detroit"),
|
||||||
|
("America/Dominica", "America/Dominica"),
|
||||||
|
("America/Edmonton", "America/Edmonton"),
|
||||||
|
("America/Eirunepe", "America/Eirunepe"),
|
||||||
|
("America/El_Salvador", "America/El_Salvador"),
|
||||||
|
("America/Fort_Nelson", "America/Fort_Nelson"),
|
||||||
|
("America/Fortaleza", "America/Fortaleza"),
|
||||||
|
("America/Glace_Bay", "America/Glace_Bay"),
|
||||||
|
("America/Goose_Bay", "America/Goose_Bay"),
|
||||||
|
("America/Grand_Turk", "America/Grand_Turk"),
|
||||||
|
("America/Grenada", "America/Grenada"),
|
||||||
|
("America/Guadeloupe", "America/Guadeloupe"),
|
||||||
|
("America/Guatemala", "America/Guatemala"),
|
||||||
|
("America/Guayaquil", "America/Guayaquil"),
|
||||||
|
("America/Guyana", "America/Guyana"),
|
||||||
|
("America/Halifax", "America/Halifax"),
|
||||||
|
("America/Havana", "America/Havana"),
|
||||||
|
("America/Hermosillo", "America/Hermosillo"),
|
||||||
|
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
||||||
|
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||||
|
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||||
|
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
||||||
|
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
||||||
|
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
||||||
|
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
||||||
|
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
||||||
|
("America/Inuvik", "America/Inuvik"),
|
||||||
|
("America/Iqaluit", "America/Iqaluit"),
|
||||||
|
("America/Jamaica", "America/Jamaica"),
|
||||||
|
("America/Juneau", "America/Juneau"),
|
||||||
|
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
|
||||||
|
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
|
||||||
|
("America/Kralendijk", "America/Kralendijk"),
|
||||||
|
("America/La_Paz", "America/La_Paz"),
|
||||||
|
("America/Lima", "America/Lima"),
|
||||||
|
("America/Los_Angeles", "America/Los_Angeles"),
|
||||||
|
("America/Lower_Princes", "America/Lower_Princes"),
|
||||||
|
("America/Maceio", "America/Maceio"),
|
||||||
|
("America/Managua", "America/Managua"),
|
||||||
|
("America/Manaus", "America/Manaus"),
|
||||||
|
("America/Marigot", "America/Marigot"),
|
||||||
|
("America/Martinique", "America/Martinique"),
|
||||||
|
("America/Matamoros", "America/Matamoros"),
|
||||||
|
("America/Mazatlan", "America/Mazatlan"),
|
||||||
|
("America/Menominee", "America/Menominee"),
|
||||||
|
("America/Merida", "America/Merida"),
|
||||||
|
("America/Metlakatla", "America/Metlakatla"),
|
||||||
|
("America/Mexico_City", "America/Mexico_City"),
|
||||||
|
("America/Miquelon", "America/Miquelon"),
|
||||||
|
("America/Moncton", "America/Moncton"),
|
||||||
|
("America/Monterrey", "America/Monterrey"),
|
||||||
|
("America/Montevideo", "America/Montevideo"),
|
||||||
|
("America/Montserrat", "America/Montserrat"),
|
||||||
|
("America/Nassau", "America/Nassau"),
|
||||||
|
("America/New_York", "America/New_York"),
|
||||||
|
("America/Nome", "America/Nome"),
|
||||||
|
("America/Noronha", "America/Noronha"),
|
||||||
|
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
||||||
|
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
||||||
|
("America/North_Dakota/New_Salem", "America/North_Dakota/New_Salem"),
|
||||||
|
("America/Nuuk", "America/Nuuk"),
|
||||||
|
("America/Ojinaga", "America/Ojinaga"),
|
||||||
|
("America/Panama", "America/Panama"),
|
||||||
|
("America/Paramaribo", "America/Paramaribo"),
|
||||||
|
("America/Phoenix", "America/Phoenix"),
|
||||||
|
("America/Port-au-Prince", "America/Port-au-Prince"),
|
||||||
|
("America/Port_of_Spain", "America/Port_of_Spain"),
|
||||||
|
("America/Porto_Velho", "America/Porto_Velho"),
|
||||||
|
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||||
|
("America/Punta_Arenas", "America/Punta_Arenas"),
|
||||||
|
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
||||||
|
("America/Recife", "America/Recife"),
|
||||||
|
("America/Regina", "America/Regina"),
|
||||||
|
("America/Resolute", "America/Resolute"),
|
||||||
|
("America/Rio_Branco", "America/Rio_Branco"),
|
||||||
|
("America/Santarem", "America/Santarem"),
|
||||||
|
("America/Santiago", "America/Santiago"),
|
||||||
|
("America/Santo_Domingo", "America/Santo_Domingo"),
|
||||||
|
("America/Sao_Paulo", "America/Sao_Paulo"),
|
||||||
|
("America/Scoresbysund", "America/Scoresbysund"),
|
||||||
|
("America/Sitka", "America/Sitka"),
|
||||||
|
("America/St_Barthelemy", "America/St_Barthelemy"),
|
||||||
|
("America/St_Johns", "America/St_Johns"),
|
||||||
|
("America/St_Kitts", "America/St_Kitts"),
|
||||||
|
("America/St_Lucia", "America/St_Lucia"),
|
||||||
|
("America/St_Thomas", "America/St_Thomas"),
|
||||||
|
("America/St_Vincent", "America/St_Vincent"),
|
||||||
|
("America/Swift_Current", "America/Swift_Current"),
|
||||||
|
("America/Tegucigalpa", "America/Tegucigalpa"),
|
||||||
|
("America/Thule", "America/Thule"),
|
||||||
|
("America/Tijuana", "America/Tijuana"),
|
||||||
|
("America/Toronto", "America/Toronto"),
|
||||||
|
("America/Tortola", "America/Tortola"),
|
||||||
|
("America/Vancouver", "America/Vancouver"),
|
||||||
|
("America/Whitehorse", "America/Whitehorse"),
|
||||||
|
("America/Winnipeg", "America/Winnipeg"),
|
||||||
|
("America/Yakutat", "America/Yakutat"),
|
||||||
|
("Antarctica/Casey", "Antarctica/Casey"),
|
||||||
|
("Antarctica/Davis", "Antarctica/Davis"),
|
||||||
|
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
||||||
|
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
||||||
|
("Antarctica/Mawson", "Antarctica/Mawson"),
|
||||||
|
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
||||||
|
("Antarctica/Palmer", "Antarctica/Palmer"),
|
||||||
|
("Antarctica/Rothera", "Antarctica/Rothera"),
|
||||||
|
("Antarctica/Syowa", "Antarctica/Syowa"),
|
||||||
|
("Antarctica/Troll", "Antarctica/Troll"),
|
||||||
|
("Antarctica/Vostok", "Antarctica/Vostok"),
|
||||||
|
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
||||||
|
("Asia/Aden", "Asia/Aden"),
|
||||||
|
("Asia/Almaty", "Asia/Almaty"),
|
||||||
|
("Asia/Amman", "Asia/Amman"),
|
||||||
|
("Asia/Anadyr", "Asia/Anadyr"),
|
||||||
|
("Asia/Aqtau", "Asia/Aqtau"),
|
||||||
|
("Asia/Aqtobe", "Asia/Aqtobe"),
|
||||||
|
("Asia/Ashgabat", "Asia/Ashgabat"),
|
||||||
|
("Asia/Atyrau", "Asia/Atyrau"),
|
||||||
|
("Asia/Baghdad", "Asia/Baghdad"),
|
||||||
|
("Asia/Bahrain", "Asia/Bahrain"),
|
||||||
|
("Asia/Baku", "Asia/Baku"),
|
||||||
|
("Asia/Bangkok", "Asia/Bangkok"),
|
||||||
|
("Asia/Barnaul", "Asia/Barnaul"),
|
||||||
|
("Asia/Beirut", "Asia/Beirut"),
|
||||||
|
("Asia/Bishkek", "Asia/Bishkek"),
|
||||||
|
("Asia/Brunei", "Asia/Brunei"),
|
||||||
|
("Asia/Chita", "Asia/Chita"),
|
||||||
|
("Asia/Colombo", "Asia/Colombo"),
|
||||||
|
("Asia/Damascus", "Asia/Damascus"),
|
||||||
|
("Asia/Dhaka", "Asia/Dhaka"),
|
||||||
|
("Asia/Dili", "Asia/Dili"),
|
||||||
|
("Asia/Dubai", "Asia/Dubai"),
|
||||||
|
("Asia/Dushanbe", "Asia/Dushanbe"),
|
||||||
|
("Asia/Famagusta", "Asia/Famagusta"),
|
||||||
|
("Asia/Gaza", "Asia/Gaza"),
|
||||||
|
("Asia/Hebron", "Asia/Hebron"),
|
||||||
|
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
||||||
|
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
||||||
|
("Asia/Hovd", "Asia/Hovd"),
|
||||||
|
("Asia/Irkutsk", "Asia/Irkutsk"),
|
||||||
|
("Asia/Jakarta", "Asia/Jakarta"),
|
||||||
|
("Asia/Jayapura", "Asia/Jayapura"),
|
||||||
|
("Asia/Jerusalem", "Asia/Jerusalem"),
|
||||||
|
("Asia/Kabul", "Asia/Kabul"),
|
||||||
|
("Asia/Kamchatka", "Asia/Kamchatka"),
|
||||||
|
("Asia/Karachi", "Asia/Karachi"),
|
||||||
|
("Asia/Kathmandu", "Asia/Kathmandu"),
|
||||||
|
("Asia/Khandyga", "Asia/Khandyga"),
|
||||||
|
("Asia/Kolkata", "Asia/Kolkata"),
|
||||||
|
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||||
|
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
||||||
|
("Asia/Kuching", "Asia/Kuching"),
|
||||||
|
("Asia/Kuwait", "Asia/Kuwait"),
|
||||||
|
("Asia/Macau", "Asia/Macau"),
|
||||||
|
("Asia/Magadan", "Asia/Magadan"),
|
||||||
|
("Asia/Makassar", "Asia/Makassar"),
|
||||||
|
("Asia/Manila", "Asia/Manila"),
|
||||||
|
("Asia/Muscat", "Asia/Muscat"),
|
||||||
|
("Asia/Nicosia", "Asia/Nicosia"),
|
||||||
|
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
||||||
|
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
||||||
|
("Asia/Omsk", "Asia/Omsk"),
|
||||||
|
("Asia/Oral", "Asia/Oral"),
|
||||||
|
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
||||||
|
("Asia/Pontianak", "Asia/Pontianak"),
|
||||||
|
("Asia/Pyongyang", "Asia/Pyongyang"),
|
||||||
|
("Asia/Qatar", "Asia/Qatar"),
|
||||||
|
("Asia/Qostanay", "Asia/Qostanay"),
|
||||||
|
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
||||||
|
("Asia/Riyadh", "Asia/Riyadh"),
|
||||||
|
("Asia/Sakhalin", "Asia/Sakhalin"),
|
||||||
|
("Asia/Samarkand", "Asia/Samarkand"),
|
||||||
|
("Asia/Seoul", "Asia/Seoul"),
|
||||||
|
("Asia/Shanghai", "Asia/Shanghai"),
|
||||||
|
("Asia/Singapore", "Asia/Singapore"),
|
||||||
|
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
||||||
|
("Asia/Taipei", "Asia/Taipei"),
|
||||||
|
("Asia/Tashkent", "Asia/Tashkent"),
|
||||||
|
("Asia/Tbilisi", "Asia/Tbilisi"),
|
||||||
|
("Asia/Tehran", "Asia/Tehran"),
|
||||||
|
("Asia/Thimphu", "Asia/Thimphu"),
|
||||||
|
("Asia/Tokyo", "Asia/Tokyo"),
|
||||||
|
("Asia/Tomsk", "Asia/Tomsk"),
|
||||||
|
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||||
|
("Asia/Urumqi", "Asia/Urumqi"),
|
||||||
|
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
||||||
|
("Asia/Vientiane", "Asia/Vientiane"),
|
||||||
|
("Asia/Vladivostok", "Asia/Vladivostok"),
|
||||||
|
("Asia/Yakutsk", "Asia/Yakutsk"),
|
||||||
|
("Asia/Yangon", "Asia/Yangon"),
|
||||||
|
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
||||||
|
("Asia/Yerevan", "Asia/Yerevan"),
|
||||||
|
("Atlantic/Azores", "Atlantic/Azores"),
|
||||||
|
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
||||||
|
("Atlantic/Canary", "Atlantic/Canary"),
|
||||||
|
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
||||||
|
("Atlantic/Faroe", "Atlantic/Faroe"),
|
||||||
|
("Atlantic/Madeira", "Atlantic/Madeira"),
|
||||||
|
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
||||||
|
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
||||||
|
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
||||||
|
("Atlantic/Stanley", "Atlantic/Stanley"),
|
||||||
|
("Australia/Adelaide", "Australia/Adelaide"),
|
||||||
|
("Australia/Brisbane", "Australia/Brisbane"),
|
||||||
|
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
||||||
|
("Australia/Darwin", "Australia/Darwin"),
|
||||||
|
("Australia/Eucla", "Australia/Eucla"),
|
||||||
|
("Australia/Hobart", "Australia/Hobart"),
|
||||||
|
("Australia/Lindeman", "Australia/Lindeman"),
|
||||||
|
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
||||||
|
("Australia/Melbourne", "Australia/Melbourne"),
|
||||||
|
("Australia/Perth", "Australia/Perth"),
|
||||||
|
("Australia/Sydney", "Australia/Sydney"),
|
||||||
|
("Canada/Atlantic", "Canada/Atlantic"),
|
||||||
|
("Canada/Central", "Canada/Central"),
|
||||||
|
("Canada/Eastern", "Canada/Eastern"),
|
||||||
|
("Canada/Mountain", "Canada/Mountain"),
|
||||||
|
("Canada/Newfoundland", "Canada/Newfoundland"),
|
||||||
|
("Canada/Pacific", "Canada/Pacific"),
|
||||||
|
("Europe/Amsterdam", "Europe/Amsterdam"),
|
||||||
|
("Europe/Andorra", "Europe/Andorra"),
|
||||||
|
("Europe/Astrakhan", "Europe/Astrakhan"),
|
||||||
|
("Europe/Athens", "Europe/Athens"),
|
||||||
|
("Europe/Belgrade", "Europe/Belgrade"),
|
||||||
|
("Europe/Berlin", "Europe/Berlin"),
|
||||||
|
("Europe/Bratislava", "Europe/Bratislava"),
|
||||||
|
("Europe/Brussels", "Europe/Brussels"),
|
||||||
|
("Europe/Bucharest", "Europe/Bucharest"),
|
||||||
|
("Europe/Budapest", "Europe/Budapest"),
|
||||||
|
("Europe/Busingen", "Europe/Busingen"),
|
||||||
|
("Europe/Chisinau", "Europe/Chisinau"),
|
||||||
|
("Europe/Copenhagen", "Europe/Copenhagen"),
|
||||||
|
("Europe/Dublin", "Europe/Dublin"),
|
||||||
|
("Europe/Gibraltar", "Europe/Gibraltar"),
|
||||||
|
("Europe/Guernsey", "Europe/Guernsey"),
|
||||||
|
("Europe/Helsinki", "Europe/Helsinki"),
|
||||||
|
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
||||||
|
("Europe/Istanbul", "Europe/Istanbul"),
|
||||||
|
("Europe/Jersey", "Europe/Jersey"),
|
||||||
|
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
||||||
|
("Europe/Kirov", "Europe/Kirov"),
|
||||||
|
("Europe/Kyiv", "Europe/Kyiv"),
|
||||||
|
("Europe/Lisbon", "Europe/Lisbon"),
|
||||||
|
("Europe/Ljubljana", "Europe/Ljubljana"),
|
||||||
|
("Europe/London", "Europe/London"),
|
||||||
|
("Europe/Luxembourg", "Europe/Luxembourg"),
|
||||||
|
("Europe/Madrid", "Europe/Madrid"),
|
||||||
|
("Europe/Malta", "Europe/Malta"),
|
||||||
|
("Europe/Mariehamn", "Europe/Mariehamn"),
|
||||||
|
("Europe/Minsk", "Europe/Minsk"),
|
||||||
|
("Europe/Monaco", "Europe/Monaco"),
|
||||||
|
("Europe/Moscow", "Europe/Moscow"),
|
||||||
|
("Europe/Oslo", "Europe/Oslo"),
|
||||||
|
("Europe/Paris", "Europe/Paris"),
|
||||||
|
("Europe/Podgorica", "Europe/Podgorica"),
|
||||||
|
("Europe/Prague", "Europe/Prague"),
|
||||||
|
("Europe/Riga", "Europe/Riga"),
|
||||||
|
("Europe/Rome", "Europe/Rome"),
|
||||||
|
("Europe/Samara", "Europe/Samara"),
|
||||||
|
("Europe/San_Marino", "Europe/San_Marino"),
|
||||||
|
("Europe/Sarajevo", "Europe/Sarajevo"),
|
||||||
|
("Europe/Saratov", "Europe/Saratov"),
|
||||||
|
("Europe/Simferopol", "Europe/Simferopol"),
|
||||||
|
("Europe/Skopje", "Europe/Skopje"),
|
||||||
|
("Europe/Sofia", "Europe/Sofia"),
|
||||||
|
("Europe/Stockholm", "Europe/Stockholm"),
|
||||||
|
("Europe/Tallinn", "Europe/Tallinn"),
|
||||||
|
("Europe/Tirane", "Europe/Tirane"),
|
||||||
|
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
||||||
|
("Europe/Vaduz", "Europe/Vaduz"),
|
||||||
|
("Europe/Vatican", "Europe/Vatican"),
|
||||||
|
("Europe/Vienna", "Europe/Vienna"),
|
||||||
|
("Europe/Vilnius", "Europe/Vilnius"),
|
||||||
|
("Europe/Volgograd", "Europe/Volgograd"),
|
||||||
|
("Europe/Warsaw", "Europe/Warsaw"),
|
||||||
|
("Europe/Zagreb", "Europe/Zagreb"),
|
||||||
|
("Europe/Zurich", "Europe/Zurich"),
|
||||||
|
("GMT", "GMT"),
|
||||||
|
("Indian/Antananarivo", "Indian/Antananarivo"),
|
||||||
|
("Indian/Chagos", "Indian/Chagos"),
|
||||||
|
("Indian/Christmas", "Indian/Christmas"),
|
||||||
|
("Indian/Cocos", "Indian/Cocos"),
|
||||||
|
("Indian/Comoro", "Indian/Comoro"),
|
||||||
|
("Indian/Kerguelen", "Indian/Kerguelen"),
|
||||||
|
("Indian/Mahe", "Indian/Mahe"),
|
||||||
|
("Indian/Maldives", "Indian/Maldives"),
|
||||||
|
("Indian/Mauritius", "Indian/Mauritius"),
|
||||||
|
("Indian/Mayotte", "Indian/Mayotte"),
|
||||||
|
("Indian/Reunion", "Indian/Reunion"),
|
||||||
|
("Pacific/Apia", "Pacific/Apia"),
|
||||||
|
("Pacific/Auckland", "Pacific/Auckland"),
|
||||||
|
("Pacific/Bougainville", "Pacific/Bougainville"),
|
||||||
|
("Pacific/Chatham", "Pacific/Chatham"),
|
||||||
|
("Pacific/Chuuk", "Pacific/Chuuk"),
|
||||||
|
("Pacific/Easter", "Pacific/Easter"),
|
||||||
|
("Pacific/Efate", "Pacific/Efate"),
|
||||||
|
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
||||||
|
("Pacific/Fiji", "Pacific/Fiji"),
|
||||||
|
("Pacific/Funafuti", "Pacific/Funafuti"),
|
||||||
|
("Pacific/Galapagos", "Pacific/Galapagos"),
|
||||||
|
("Pacific/Gambier", "Pacific/Gambier"),
|
||||||
|
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
||||||
|
("P2025-06-29T01:43:14.671389745Z acific/Guam", "Pacific/Guam"),
|
||||||
|
("Pacific/Honolulu", "Pacific/Honolulu"),
|
||||||
|
("Pacific/Kanton", "Pacific/Kanton"),
|
||||||
|
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
||||||
|
("Pacific/Kosrae", "Pacific/Kosrae"),
|
||||||
|
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
||||||
|
("Pacific/Majuro", "Pacific/Majuro"),
|
||||||
|
("Pacific/Marquesas", "Pacific/Marquesas"),
|
||||||
|
("Pacific/Midway", "Pacific/Midway"),
|
||||||
|
("Pacific/Nauru", "Pacific/Nauru"),
|
||||||
|
("Pacific/Niue", "Pacific/Niue"),
|
||||||
|
("Pacific/Norfolk", "Pacific/Norfolk"),
|
||||||
|
("Pacific/Noumea", "Pacific/Noumea"),
|
||||||
|
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
||||||
|
("Pacific/Palau", "Pacific/Palau"),
|
||||||
|
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
||||||
|
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
||||||
|
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
||||||
|
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
||||||
|
("Pacific/Saipan", "Pacific/Saipan"),
|
||||||
|
("Pacific/Tahiti", "Pacific/Tahiti"),
|
||||||
|
("Pacific/Tarawa", "Pacific/Tarawa"),
|
||||||
|
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
||||||
|
("Pacific/Wake", "Pacific/Wake"),
|
||||||
|
("Pacific/Wallis", "Pacific/Wallis"),
|
||||||
|
("US/Alaska", "US/Alaska"),
|
||||||
|
("US/Arizona", "US/Arizona"),
|
||||||
|
("US/Central", "US/Central"),
|
||||||
|
("US/Eastern", "US/Eastern"),
|
||||||
|
("US/Hawaii", "US/Hawaii"),
|
||||||
|
("US/Mountain", "US/Mountain"),
|
||||||
|
("US/Pacific", "US/Pacific"),
|
||||||
|
("UTC", "UTC"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
username = None
|
username = None
|
||||||
@@ -36,6 +474,11 @@ class UserSettings(models.Model):
|
|||||||
)
|
)
|
||||||
hide_amounts = models.BooleanField(default=False)
|
hide_amounts = models.BooleanField(default=False)
|
||||||
mute_sounds = models.BooleanField(default=False)
|
mute_sounds = models.BooleanField(default=False)
|
||||||
|
volume = models.PositiveIntegerField(
|
||||||
|
default=10,
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(10)],
|
||||||
|
verbose_name=_("Volume"),
|
||||||
|
)
|
||||||
|
|
||||||
date_format = models.CharField(
|
date_format = models.CharField(
|
||||||
max_length=100, default="SHORT_DATE_FORMAT", verbose_name=_("Date Format")
|
max_length=100, default="SHORT_DATE_FORMAT", verbose_name=_("Date Format")
|
||||||
@@ -57,7 +500,7 @@ class UserSettings(models.Model):
|
|||||||
)
|
)
|
||||||
timezone = models.CharField(
|
timezone = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=[("auto", _("Auto"))] + [(tz, tz) for tz in pytz.common_timezones],
|
choices=timezones,
|
||||||
default="auto",
|
default="auto",
|
||||||
verbose_name=_("Time Zone"),
|
verbose_name=_("Time Zone"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("yearly/", views.index, name="yearly_index"),
|
||||||
path("yearly/currency/", views.index_by_currency, name="yearly_index_currency"),
|
path("yearly/currency/", views.index_by_currency, name="yearly_index_currency"),
|
||||||
path("yearly/account/", views.index_by_account, name="yearly_index_account"),
|
path("yearly/account/", views.index_by_account, name="yearly_index_account"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -16,6 +16,22 @@ from apps.transactions.utils.calculations import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def index(request):
|
||||||
|
if "view_type" in request.GET:
|
||||||
|
view_type = request.GET["view_type"]
|
||||||
|
request.session["yearly_view_type"] = view_type
|
||||||
|
else:
|
||||||
|
view_type = request.session.get("yearly_view_type", "currency")
|
||||||
|
|
||||||
|
now = timezone.localdate(timezone.now())
|
||||||
|
|
||||||
|
if view_type == "currency":
|
||||||
|
return redirect(to="yearly_overview_currency", year=now.year)
|
||||||
|
else:
|
||||||
|
return redirect(to="yearly_overview_account", year=now.year)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index_by_currency(request):
|
def index_by_currency(request):
|
||||||
now = timezone.localdate(timezone.now())
|
now = timezone.localdate(timezone.now())
|
||||||
@@ -32,6 +48,8 @@ def index_by_account(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index_yearly_overview_by_currency(request, year: int):
|
def index_yearly_overview_by_currency(request, year: int):
|
||||||
|
request.session["yearly_view_type"] = "currency"
|
||||||
|
|
||||||
next_year = year + 1
|
next_year = year + 1
|
||||||
previous_year = year - 1
|
previous_year = year - 1
|
||||||
|
|
||||||
@@ -49,6 +67,7 @@ def index_yearly_overview_by_currency(request, year: int):
|
|||||||
"previous_year": previous_year,
|
"previous_year": previous_year,
|
||||||
"months": month_options,
|
"months": month_options,
|
||||||
"currencies": currency_options,
|
"currencies": currency_options,
|
||||||
|
"type": "currency",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,7 +94,7 @@ def yearly_overview_by_currency(request, year: int):
|
|||||||
|
|
||||||
transactions = (
|
transactions = (
|
||||||
Transaction.objects.filter(**filter_params)
|
Transaction.objects.filter(**filter_params)
|
||||||
.exclude(Q(category__mute=True) & ~Q(category=None))
|
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||||
.order_by("account__currency__name")
|
.order_by("account__currency__name")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,6 +114,7 @@ def yearly_overview_by_currency(request, year: int):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index_yearly_overview_by_account(request, year: int):
|
def index_yearly_overview_by_account(request, year: int):
|
||||||
|
request.session["yearly_view_type"] = "account"
|
||||||
next_year = year + 1
|
next_year = year + 1
|
||||||
previous_year = year - 1
|
previous_year = year - 1
|
||||||
|
|
||||||
@@ -115,6 +135,7 @@ def index_yearly_overview_by_account(request, year: int):
|
|||||||
"previous_year": previous_year,
|
"previous_year": previous_year,
|
||||||
"months": month_options,
|
"months": month_options,
|
||||||
"accounts": account_options,
|
"accounts": account_options,
|
||||||
|
"type": "account",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,7 +162,7 @@ def yearly_overview_by_account(request, year: int):
|
|||||||
|
|
||||||
transactions = (
|
transactions = (
|
||||||
Transaction.objects.filter(**filter_params)
|
Transaction.objects.filter(**filter_params)
|
||||||
.exclude(Q(category__mute=True) & ~Q(category=None))
|
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||||
.order_by(
|
.order_by(
|
||||||
"account__group__name",
|
"account__group__name",
|
||||||
"account__name",
|
"account__name",
|
||||||
|
|||||||
+492
-372
File diff suppressed because it is too large
Load Diff
+442
-322
File diff suppressed because it is too large
Load Diff
+602
-574
File diff suppressed because it is too large
Load Diff
+662
-673
File diff suppressed because it is too large
Load Diff
+455
-335
File diff suppressed because it is too large
Load Diff
+470
-326
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+442
-322
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Account Groups' %}<span>
|
<div>{% translate 'Account Groups' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Accounts' %}<span>
|
<div>{% translate 'Accounts' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="tw-hidden lg:tw-grid lg:tw-grid-cols-7 tw-gap-4 lg:tw-gap-0">
|
<div class="tw:hidden tw:lg:grid tw:lg:grid-cols-7 tw:gap-4 tw:lg:gap-0">
|
||||||
<div class="border-start border-top border-bottom p-2 text-center">
|
<div class="border-start border-top border-bottom p-2 text-center">
|
||||||
{% translate 'MON' %}
|
{% translate 'MON' %}
|
||||||
</div>
|
</div>
|
||||||
@@ -25,44 +25,44 @@
|
|||||||
{% translate 'SUN' %}
|
{% translate 'SUN' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-grid tw-grid-cols-1 tw-grid-rows-1 lg:tw-grid-cols-7 lg:tw-grid-rows-6 tw-gap-4 lg:tw-gap-0">
|
<div class="tw:grid tw:grid-cols-1 tw:grid-rows-1 tw:lg:grid-cols-7 tw:lg:grid-rows-6 tw:gap-4 tw:lg:gap-0">
|
||||||
{% for date in dates %}
|
{% for date in dates %}
|
||||||
{% if date %}
|
{% if date %}
|
||||||
<div class="card h-100 hover:tw-bg-zinc-900 rounded-0{% if not date.transactions %} !tw-hidden lg:!tw-flex{% endif %}{% if today == date.date %} tw-border-yellow-300 border-primary{% endif %} " role="button"
|
<div class="card h-100 tw:hover:bg-zinc-900! rounded-0{% if not date.transactions %} tw:hidden! tw:lg:flex!{% endif %}{% if today == date.date %} tw:border-yellow-300 border-primary{% endif %} " role="button"
|
||||||
hx-get="{% url 'calendar_transactions_list' day=date.date.day month=date.date.month year=date.date.year %}"
|
hx-get="{% url 'calendar_transactions_list' day=date.date.day month=date.date.month year=date.date.year %}"
|
||||||
hx-target="#persistent-generic-offcanvas-left">
|
hx-target="#persistent-generic-offcanvas-left">
|
||||||
<div class="card-header border-0 bg-transparent text-end tw-flex justify-content-between p-2 w-100">
|
<div class="card-header border-0 bg-transparent text-end tw:flex justify-content-between p-2 w-100">
|
||||||
<div class="lg:tw-hidden text-start w-100">{{ date.date|date:"l"|lower }}</div>
|
<div class="tw:lg:hidden text-start w-100">{{ date.date|date:"l"|lower }}</div>
|
||||||
<div class="text-end w-100">{{ date.day }}</div>
|
<div class="text-end w-100">{{ date.day }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
{% for transaction in date.transactions %}
|
{% for transaction in date.transactions %}
|
||||||
{% if transaction.is_paid %}
|
{% if transaction.is_paid %}
|
||||||
{% if transaction.type == "IN" and not transaction.account.is_asset %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></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 %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></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 %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></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 %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></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 %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if transaction.type == "IN" and not transaction.account.is_asset %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></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 %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></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 %}
|
{% 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="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></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 %}
|
{% elif transaction.type == "EX" and transaction.account.is_asset %}
|
||||||
<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>
|
<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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="!tw-hidden lg:!tw-block card h-100 rounded-0"></div>
|
<div class="tw:hidden! tw:lg:block! card h-100 rounded-0"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
|
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
|
||||||
{# Date picker#}
|
{# Date picker#}
|
||||||
<div class="col-12 col-xl-4 flex-row align-items-center d-flex">
|
<div class="col-12 col-xl-4 flex-row align-items-center d-flex">
|
||||||
<div class="tw-text-base h-100 align-items-center d-flex">
|
<div class="tw:text-base h-100 align-items-center d-flex">
|
||||||
<a role="button"
|
<a role="button"
|
||||||
class="pe-4 py-2"
|
class="pe-4 py-2"
|
||||||
hx-boost="true"
|
hx-boost="true"
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
href="{% url 'calendar' month=previous_month year=previous_year %}"><i
|
href="{% url 'calendar' month=previous_month year=previous_year %}"><i
|
||||||
class="fa-solid fa-chevron-left"></i></a>
|
class="fa-solid fa-chevron-left"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full text-center"
|
||||||
hx-get="{% url 'month_year_picker' %}"
|
hx-get="{% url 'month_year_picker' %}"
|
||||||
hx-target="#generic-offcanvas-left"
|
hx-target="#generic-offcanvas-left"
|
||||||
hx-trigger="click, date_picker from:window"
|
hx-trigger="click, date_picker from:window"
|
||||||
hx-vals='{"month": {{ month }}, "year": {{ year }}, "for": "calendar", "field": "date"}' role="button">
|
hx-vals='{"month": {{ month }}, "year": {{ year }}, "for": "calendar", "field": "date"}' role="button">
|
||||||
{{ month|month_name }} {{ year }}
|
{{ month|month_name }} {{ year }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
|
<div class="tw:text-base mx-2 h-100 align-items-center d-flex">
|
||||||
<a role="button"
|
<a role="button"
|
||||||
class="ps-3 py-2"
|
class="ps-3 py-2"
|
||||||
hx-boost="true"
|
hx-boost="true"
|
||||||
@@ -42,16 +42,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{# Action buttons#}
|
{# Action buttons#}
|
||||||
<div class="col-12 col-xl-8">
|
<div class="col-12 col-xl-8">
|
||||||
<c-ui.quick-transactions-buttons
|
{# <c-ui.quick-transactions-buttons#}
|
||||||
:year="year"
|
{# :year="year"#}
|
||||||
:month="month"
|
{# :month="month"#}
|
||||||
></c-ui.quick-transactions-buttons>
|
{# ></c-ui.quick-transactions-buttons>#}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="show-loading" hx-get="{% url 'calendar_list' month=month year=year %}" hx-trigger="load, updated from:window, selective_update from:window"></div>
|
<div class="show-loading" hx-get="{% url 'calendar_list' month=month year=year %}"
|
||||||
|
hx-trigger="load, updated from:window, selective_update from:window, every 10m"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Categories' %}<span>
|
<div>{% translate 'Categories' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
tabindex="0">
|
tabindex="0">
|
||||||
<ul class="list-group list-group-flush" id="month-year-list">
|
<ul class="list-group list-group-flush" id="month-year-list">
|
||||||
{% for month_data in x.list %}
|
{% for month_data in x.list %}
|
||||||
<li class="list-group-item hover:tw-bg-zinc-900
|
<li class="list-group-item tw:hover:bg-zinc-900
|
||||||
{% if month_data.month == current_month and month_data.year == current_year %} disabled bg-primary{% endif %}"
|
{% if month_data.month == current_month and month_data.year == current_year %} disabled bg-primary{% endif %}"
|
||||||
{% if month_data.month == current_month and month_data.year == current_year %}aria-disabled="true"{% endif %}>
|
{% if month_data.month == current_month and month_data.year == current_year %}aria-disabled="true"{% endif %}>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% if not divless %}
|
{% if not divless %}
|
||||||
<div class="{% if text_end %}text-end{% elif text_start %}text-start{% endif %}">
|
<div class="{% if text_end %}text-end{% elif text_start %}text-start{% endif %}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="amount{% if color == 'grey' or color == "gray" %} tw-text-gray-500{% elif color == 'green' %} tw-text-green-400{% elif color == 'red' %} tw-text-red-400{% endif %} {{ custom_class }}"
|
<span class="amount{% if color == 'grey' or color == "gray" %} tw:text-gray-500{% elif color == 'green' %} tw:text-green-400{% elif color == 'red' %} tw:text-red-400{% endif %} {{ custom_class }}"
|
||||||
data-original-value="{% currency_display amount=amount prefix=prefix suffix=suffix decimal_places=decimal_places %}"
|
data-original-value="{% currency_display amount=amount prefix=prefix suffix=suffix decimal_places=decimal_places %}"
|
||||||
data-amount="{{ amount|floatformat:"-40u" }}">
|
data-amount="{{ amount|floatformat:"-40u" }}">
|
||||||
</span><span>{{ slot }}</span>
|
</span><span>{{ slot }}</span>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<div class="tw:min-h-16">
|
||||||
|
<div
|
||||||
|
id="fab-wrapper"
|
||||||
|
class="tw:fixed tw:bottom-5 tw:right-5 tw:ml-auto tw:w-max tw:flex tw:flex-col tw:items-end mt-5 tw:z-20">
|
||||||
|
<div
|
||||||
|
id="menu"
|
||||||
|
class="tw:flex tw:flex-col tw:items-end tw:space-y-6 tw:transition-all tw:duration-300 tw:ease-in-out tw:opacity-0 tw:invisible tw:hidden tw:mb-2">
|
||||||
|
|
||||||
|
{{ slot }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary rounded-circle p-0 tw:w-12 tw:h-12 tw:flex tw:items-center tw:justify-center tw:shadow-lg tw:hover:shadow-xl tw:focus:shadow-xl tw:transition-all tw:duration-300 tw:ease-in-out"
|
||||||
|
_="
|
||||||
|
on click or focusout
|
||||||
|
if #menu.classList.contains('tw:invisible') and event.type === 'click'
|
||||||
|
add .{'tw:rotate-45'} to #fab-icon
|
||||||
|
remove .{'tw:invisible'} from #menu
|
||||||
|
remove .{'tw:hidden'} from #menu
|
||||||
|
remove .{'tw:opacity-0'} from #menu
|
||||||
|
else
|
||||||
|
wait 0.2s
|
||||||
|
remove .{'tw:rotate-45'} from #fab-icon
|
||||||
|
add .{'tw:invisible'} to #menu
|
||||||
|
add .{'tw:hidden'} to #menu
|
||||||
|
add .{'tw:opacity-0'} to #menu
|
||||||
|
end
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i id="fab-icon" class="fa-solid fa-plus tw:text-3xl tw:transition-transform tw:duration-300 tw:ease-in-out"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<div class="tw:relative fab-item">
|
||||||
|
<button class="btn btn-sm btn-{{ color }}"
|
||||||
|
hx-get="{{ url }}"
|
||||||
|
hx-trigger="{{ hx_trigger }}"
|
||||||
|
hx-target="{{ hx_target }}"
|
||||||
|
hx-vals='{{ hx_vals }}'>
|
||||||
|
<i class="{{ icon }} me-2"></i>
|
||||||
|
{{ title }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<li class="tw:lg:hidden tw:lg:group-hover:block">
|
||||||
|
<div class="d-flex align-items-center" data-bs-toggle="collapse" href="#{{ title|slugify }}" role="button"
|
||||||
|
aria-expanded="false" aria-controls="{{ title|slugify }}">
|
||||||
|
<span
|
||||||
|
class="text-muted small fw-bold text-uppercase tw:lg:hidden tw:lg:group-hover:inline me-2">{{ title }}</span>
|
||||||
|
<hr class="flex-grow-1"/>
|
||||||
|
<i class="fas fa-chevron-down text-muted tw:lg:before:hidden tw:lg:group-hover:before:inline tw:ml-2 tw:lg:ml-0 tw:lg:group-hover:ml-2"></i>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<div class="collapse tw:lg:hidden tw:lg:group-hover:block" id="{{ title|slugify }}">
|
||||||
|
{{ slot }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<li>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="text-muted small fw-bold text-uppercase tw:lg:hidden tw:lg:group-hover:inline me-2">{{ title }}</span>
|
||||||
|
<hr class="flex-grow-1"/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{% load active_link %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url url %}"
|
||||||
|
class="tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views=active css_class="sidebar-active" %}"
|
||||||
|
{% if tooltip %}
|
||||||
|
data-bs-placement="right"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{{ tooltip }}"
|
||||||
|
{% endif %}>
|
||||||
|
<i class="{{ icon }} fa-fw"></i>
|
||||||
|
<span
|
||||||
|
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">{{ title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{% load active_link %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url }}"
|
||||||
|
hx-boost="false"
|
||||||
|
class="tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views=active css_class="sidebar-active" %}"
|
||||||
|
{% if tooltip %}
|
||||||
|
data-bs-placement="right"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{{ tooltip }}"
|
||||||
|
{% endif %}>
|
||||||
|
|
||||||
|
<i class="{{ icon }} fa-fw"></i>
|
||||||
|
<span
|
||||||
|
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">{{ title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="row {% if not remove_padding %}p-5{% endif %}">
|
<div class="row {% if not remove_padding %}p-5{% endif %}">
|
||||||
<div class="col {% if not remove_padding %}p-5{% endif %}">
|
<div class="col {% if not remove_padding %}p-5{% endif %}">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<i class="{% if icon %}{{ icon }}{% else %}fa-solid fa-circle-xmark{% endif %} tw-text-6xl"></i>
|
<i class="{% if icon %}{{ icon }}{% else %}fa-solid fa-circle-xmark{% endif %} tw:text-6xl"></i>
|
||||||
<p class="lead mt-4 mb-0">{{ title }}</p>
|
<p class="lead mt-4 mb-0">{{ title }}</p>
|
||||||
<p class="tw-text-gray-500">{{ subtitle }}</p>
|
<p class="tw:text-gray-500">{{ subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
|
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} tw:group/transaction tw:relative tw:hover:z-10">
|
||||||
<div class="d-flex my-1">
|
<div class="d-flex my-1">
|
||||||
{% if not disable_selection %}
|
{% if not disable_selection %}
|
||||||
<label class="px-3 d-flex align-items-center justify-content-center">
|
<label class="px-3 d-flex align-items-center justify-content-center">
|
||||||
@@ -8,16 +8,15 @@
|
|||||||
id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>
|
id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="tw-border-s-6 tw-border-e-0 tw-border-t-0 tw-border-b-0 border-bottom
|
<div class="tw:border-s-4 tw:border-e-0 tw:border-t-0 tw:border-b-0 border-bottom
|
||||||
hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{% else %}tw-border-solid{% endif %}
|
tw:hover:bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw:border-dashed{% else %}tw:border-solid{% endif %}
|
||||||
{% if transaction.type == "EX" %}tw-border-red-500{% else %}tw-border-green-500{% endif %} tw-relative
|
{% if transaction.type == "EX" %}tw:border-red-500{% else %}tw:border-green-500{% endif %} tw:relative
|
||||||
w-100 transaction-item"
|
w-100 transaction-item">
|
||||||
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
|
<div class="row font-monospace tw:text-sm align-items-center">
|
||||||
on mouseout add .tw-invisible to the first .transaction-actions in me end">
|
<div
|
||||||
<div class="row font-monospace tw-text-sm align-items-center">
|
class="col-lg-auto col-12 d-flex align-items-center tw:text-2xl tw:lg:text-xl text-lg-center text-center p-0 ps-1">
|
||||||
<div class="col-lg-auto col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center text-center p-0 ps-1">
|
|
||||||
{% if not transaction.deleted %}
|
{% if not transaction.deleted %}
|
||||||
<a class="text-decoration-none p-3 tw-text-gray-500"
|
<a class="text-decoration-none p-3 tw:text-gray-500!"
|
||||||
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
|
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
|
||||||
role="button"
|
role="button"
|
||||||
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
|
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
|
||||||
@@ -27,41 +26,41 @@
|
|||||||
class="fa-regular fa-circle"></i>{% endif %}
|
class="fa-regular fa-circle"></i>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-decoration-none p-3 tw-text-gray-500"
|
<div class="text-decoration-none p-3 tw:text-gray-500!"
|
||||||
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}">
|
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}">
|
||||||
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
|
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
|
||||||
class="fa-regular fa-circle"></i>{% endif %}
|
class="fa-regular fa-circle"></i>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg col-12">
|
<div class="col-lg col-12 {% if transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||||
{# Date#}
|
{# Date#}
|
||||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
<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-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
|
||||||
<div
|
<div
|
||||||
class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
|
class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
|
||||||
</div>
|
</div>
|
||||||
{# Description#}
|
{# Description#}
|
||||||
<div class="mb-2 mb-lg-1 text-white tw-text-base">
|
<div class="mb-2 mb-lg-1 text-body tw:text-base">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<span class="{% if transaction.description %}me-2{% endif %}">{{ transaction.description }}</span>
|
<span class="{% if transaction.description %}me-2{% endif %}">{{ transaction.description }}</span>
|
||||||
{% if transaction.installment_plan and transaction.installment_id %}
|
{% if transaction.installment_plan and transaction.installment_id %}
|
||||||
<span
|
<span
|
||||||
class="badge text-bg-secondary">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
|
class="badge text-bg-secondary mx-1">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if transaction.recurring_transaction %}
|
{% if transaction.recurring_transaction %}
|
||||||
<span class="text-primary tw-text-xs"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
|
<span class="text-primary tw:text-xs mx-1"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if transaction.dca_expense_entries.all or transaction.dca_income_entries.all %}
|
{% if transaction.dca_expense_entries.all or transaction.dca_income_entries.all %}
|
||||||
<span class="badge text-bg-secondary">{% trans 'DCA' %}</span>
|
<span class="badge text-bg-secondary mx-1">{% trans 'DCA' %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-gray-400 tw-text-sm">
|
<div class="tw:text-gray-400 tw:text-sm">
|
||||||
{# Entities #}
|
{# Entities #}
|
||||||
{% with transaction.entities.all as entities %}
|
{% with transaction.entities.all as entities %}
|
||||||
{% if entities %}
|
{% if entities %}
|
||||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||||
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
|
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
|
||||||
<div class="col ps-0">{{ entities|join:", " }}</div>
|
<div class="col ps-0">{{ entities|join:", " }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,14 +68,14 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# Notes#}
|
{# Notes#}
|
||||||
{% if transaction.notes %}
|
{% if transaction.notes %}
|
||||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||||
<div class="col-auto pe-1"><i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i></div>
|
<div class="col-auto pe-1"><i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i></div>
|
||||||
<div class="col ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
|
<div class="col ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Category#}
|
{# Category#}
|
||||||
{% if transaction.category %}
|
{% if transaction.category %}
|
||||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||||
<div class="col-auto pe-1"><i class="fa-solid fa-icons fa-fw me-1 fa-xs"></i></div>
|
<div class="col-auto pe-1"><i class="fa-solid fa-icons fa-fw me-1 fa-xs"></i></div>
|
||||||
<div class="col ps-0">{{ transaction.category.name }}</div>
|
<div class="col ps-0">{{ transaction.category.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +83,7 @@
|
|||||||
{# Tags#}
|
{# Tags#}
|
||||||
{% with transaction.tags.all as tags %}
|
{% with transaction.tags.all as tags %}
|
||||||
{% if tags %}
|
{% if tags %}
|
||||||
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
|
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||||
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
|
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
|
||||||
<div class="col ps-0">{{ tags|join:", " }}</div>
|
<div class="col ps-0">{{ tags|join:", " }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-auto col-12 text-lg-end align-self-end">
|
<div class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||||
<div class="main-amount mb-2 mb-lg-0">
|
<div class="main-amount mb-2 mb-lg-0">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="transaction.amount"
|
:amount="transaction.amount"
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{# Item actions#}
|
{# Item actions#}
|
||||||
<div
|
<div
|
||||||
class="transaction-actions !tw-absolute tw-left-1/2 tw-top-0 tw--translate-x-1/2 tw--translate-y-1/2 tw-invisible d-flex flex-row card">
|
class="transaction-actions tw:absolute! tw:left-1/2 tw:top-0 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:invisible tw:group-hover/transaction:visible d-flex flex-row card">
|
||||||
<div class="card-body p-1 shadow-lg">
|
<div class="card-body p-1 shadow-lg">
|
||||||
{% if not transaction.deleted %}
|
{% if not transaction.deleted %}
|
||||||
<a class="btn btn-secondary btn-sm transaction-action"
|
<a class="btn btn-secondary btn-sm transaction-action"
|
||||||
@@ -131,14 +130,6 @@
|
|||||||
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
|
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
|
||||||
hx-target="#generic-offcanvas" hx-swap="innerHTML">
|
hx-target="#generic-offcanvas" hx-swap="innerHTML">
|
||||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||||
<a class="btn btn-secondary btn-sm transaction-action"
|
|
||||||
role="button"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-title="{% translate "Duplicate" %}"
|
|
||||||
hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"
|
|
||||||
_="on click if event.ctrlKey set @hx-get to `{% url 'transaction_clone' transaction_id=transaction.id %}?edit=true` then call htmx.process(me) end then trigger ready"
|
|
||||||
hx-trigger="ready">
|
|
||||||
<i class="fa-solid fa-clone fa-fw"></i></a>
|
|
||||||
<a class="btn btn-secondary btn-sm transaction-action"
|
<a class="btn btn-secondary btn-sm transaction-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
@@ -151,6 +142,29 @@
|
|||||||
data-confirm-text="{% translate "Yes, delete it!" %}"
|
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<button class="btn btn-secondary btn-sm transaction-action" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-ellipsis fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% if transaction.category.mute %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||||
|
<i class="fa-solid fa-eye fa-fw me-2"></i>
|
||||||
|
<div>
|
||||||
|
{% translate 'Show on summaries' %}
|
||||||
|
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% elif transaction.mute %}
|
||||||
|
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye-slash fa-fw me-2"></i>{% translate 'Hide from summaries' %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a class="dropdown-item" href="#" hx-get="{% url 'quick_transaction_add_as_quick_transaction' transaction_id=transaction.id %}"><i class="fa-solid fa-person-running fa-fw me-2"></i>{% translate 'Add as quick transaction' %}</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"><i class="fa-solid fa-clone fa-fw me-2"></i>{% translate 'Duplicate' %}</a></li>
|
||||||
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="btn btn-secondary btn-sm transaction-action"
|
<a class="btn btn-secondary btn-sm transaction-action"
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="card mb-2 transaction-item">
|
<div class="card mb-2 transaction-item">
|
||||||
<div class="card-body p-2 tw-flex tw-items-center tw-gap-3" data-bs-toggle="collapse" data-bs-target="#{{ transaction.id }}" role="button" aria-expanded="false" aria-controls="{{ transaction.id }}">
|
<div class="card-body p-2 tw:flex tw:items-center tw:gap-3" data-bs-toggle="collapse" data-bs-target="#{{ transaction.id }}" role="button" aria-expanded="false" aria-controls="{{ transaction.id }}">
|
||||||
<!-- Main visible content -->
|
<!-- Main visible content -->
|
||||||
<div class="tw-flex flex-lg-row flex-column lg:tw-items-center tw-w-full tw-gap-3">
|
<div class="tw:flex flex-lg-row flex-column tw:lg:items-center tw:w-full tw:gap-3">
|
||||||
<!-- Type indicator -->
|
<!-- Type indicator -->
|
||||||
<div class="tw-w-8">
|
<div class="tw:w-8">
|
||||||
{% if transaction.type == 'IN' %}
|
{% if transaction.type == 'IN' %}
|
||||||
<span class="badge bg-success">↑</span>
|
<span class="badge bg-success">↑</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment status -->
|
<!-- Payment status -->
|
||||||
<div class="tw-w-8">
|
<div class="tw:w-8">
|
||||||
{% if transaction.is_paid %}
|
{% if transaction.is_paid %}
|
||||||
<span class="badge bg-success">✓</span>
|
<span class="badge bg-success">✓</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div class="tw-flex-grow">
|
<div class="tw:flex-grow">
|
||||||
<span class="tw-font-medium">{{ transaction.description }}</span>
|
<span class="tw:font-medium">{{ transaction.description }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Amount -->
|
<!-- Amount -->
|
||||||
<div class="tw-text-right tw-whitespace-nowrap">
|
<div class="tw:text-right tw:whitespace-nowrap">
|
||||||
<span class="{% if transaction.type == 'IN' %}tw-text-green-400{% else %}tw-text-red-400{% endif %}">
|
<span class="{% if transaction.type == 'IN' %}tw:text-green-400{% else %}tw:text-red-400{% endif %}">
|
||||||
{{ transaction.amount }}
|
{{ transaction.amount }}
|
||||||
</span>
|
</span>
|
||||||
{% if transaction.exchanged_amount %}
|
{% if transaction.exchanged_amount %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="col card shadow">
|
<div class="col card shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if account.account.group %}
|
{% if account.account.group %}
|
||||||
<div class="tw-text-sm mb-2">
|
<div class="tw:text-sm mb-2">
|
||||||
<span class="badge text-bg-primary ">{{ account.account.group }}</span>
|
<span class="badge text-bg-primary ">{{ account.account.group }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected income' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
{% if account.income_projected != 0 %}
|
{% if account.income_projected != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-green-400">
|
<div class="text-end font-monospace tw:text-green-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.income_projected"
|
:amount="account.income_projected"
|
||||||
:prefix="account.currency.prefix"
|
:prefix="account.currency.prefix"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged and account.exchanged.income_projected %}
|
{% if account.exchanged and account.exchanged.income_projected %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.income_projected"
|
:amount="account.exchanged.income_projected"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
@@ -38,12 +38,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected expenses' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected expenses' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div>
|
<div>
|
||||||
{% if account.expense_projected != 0 %}
|
{% if account.expense_projected != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-red-400">
|
<div class="text-end font-monospace tw:text-red-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.expense_projected"
|
:amount="account.expense_projected"
|
||||||
:prefix="account.currency.prefix"
|
:prefix="account.currency.prefix"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged and account.exchanged.expense_projected %}
|
{% if account.exchanged and account.exchanged.expense_projected %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.expense_projected"
|
:amount="account.exchanged.expense_projected"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected total' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div
|
<div
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged.total_projected and account.exchanged.total_projected %}
|
{% if account.exchanged.total_projected and account.exchanged.total_projected %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.total_projected"
|
:amount="account.exchanged.total_projected"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
@@ -91,11 +91,11 @@
|
|||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current income' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
{% if account.income_current != 0 %}
|
{% if account.income_current != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-green-400">
|
<div class="text-end font-monospace tw:text-green-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.income_current"
|
:amount="account.income_current"
|
||||||
:prefix="account.currency.prefix"
|
:prefix="account.currency.prefix"
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged and account.exchanged.income_current %}
|
{% if account.exchanged and account.exchanged.income_current %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.income_current"
|
:amount="account.exchanged.income_current"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
@@ -117,11 +117,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current expenses' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
{% if account.expense_current != 0 %}
|
{% if account.expense_current != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-red-400">
|
<div class="text-end font-monospace tw:text-red-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.expense_current"
|
:amount="account.expense_current"
|
||||||
:prefix="account.currency.prefix"
|
:prefix="account.currency.prefix"
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged and account.exchanged.expense_current %}
|
{% if account.exchanged and account.exchanged.expense_current %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.expense_current"
|
:amount="account.exchanged.expense_current"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current total' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged and account.exchanged.total_current %}
|
{% if account.exchanged and account.exchanged.total_current %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.total_current"
|
:amount="account.exchanged.total_current"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'final total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'final total' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if account.exchanged and account.exchanged.total_final %}
|
{% if account.exchanged and account.exchanged.total_final %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="account.exchanged.total_final"
|
:amount="account.exchanged.total_final"
|
||||||
:prefix="account.exchanged.currency.prefix"
|
:prefix="account.exchanged.currency.prefix"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="card tw-relative h-100 shadow">
|
<div class="card tw:relative h-100 shadow">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ slot }}
|
{{ slot }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected income' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
{% if currency.income_projected != 0 %}
|
{% if currency.income_projected != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-green-400">
|
<div class="text-end font-monospace tw:text-green-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.income_projected"
|
:amount="currency.income_projected"
|
||||||
:prefix="currency.currency.prefix"
|
:prefix="currency.currency.prefix"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged and currency.exchanged.income_projected %}
|
{% if currency.exchanged and currency.exchanged.income_projected %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.income_projected"
|
:amount="currency.exchanged.income_projected"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected expenses' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected expenses' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div>
|
<div>
|
||||||
{% if currency.expense_projected != 0 %}
|
{% if currency.expense_projected != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-red-400">
|
<div class="text-end font-monospace tw:text-red-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.expense_projected"
|
:amount="currency.expense_projected"
|
||||||
:prefix="currency.currency.prefix"
|
:prefix="currency.currency.prefix"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged and currency.exchanged.expense_projected %}
|
{% if currency.exchanged and currency.exchanged.expense_projected %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.expense_projected"
|
:amount="currency.exchanged.expense_projected"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected total' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged.total_projected and currency.exchanged.total_projected %}
|
{% if currency.exchanged.total_projected and currency.exchanged.total_projected %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.total_projected"
|
:amount="currency.exchanged.total_projected"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
@@ -85,11 +85,11 @@
|
|||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current income' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
{% if currency.income_current != 0 %}
|
{% if currency.income_current != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-green-400">
|
<div class="text-end font-monospace tw:text-green-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.income_current"
|
:amount="currency.income_current"
|
||||||
:prefix="currency.currency.prefix"
|
:prefix="currency.currency.prefix"
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged and currency.exchanged.income_current %}
|
{% if currency.exchanged and currency.exchanged.income_current %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.income_current"
|
:amount="currency.exchanged.income_current"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
@@ -111,11 +111,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current expenses' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
{% if currency.expense_current != 0 %}
|
{% if currency.expense_current != 0 %}
|
||||||
<div class="text-end font-monospace tw-text-red-400">
|
<div class="text-end font-monospace tw:text-red-400">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.expense_current"
|
:amount="currency.expense_current"
|
||||||
:prefix="currency.currency.prefix"
|
:prefix="currency.currency.prefix"
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged and currency.exchanged.expense_current %}
|
{% if currency.exchanged and currency.exchanged.expense_current %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.expense_current"
|
:amount="currency.exchanged.expense_current"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current total' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged and currency.exchanged.total_current %}
|
{% if currency.exchanged and currency.exchanged.total_current %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.total_current"
|
:amount="currency.exchanged.total_current"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
<div class="d-flex justify-content-between align-items-baseline mt-2">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'final total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'final total' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dotted-line flex-grow-1"></div>
|
<div class="dotted-line flex-grow-1"></div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if currency.exchanged and currency.exchanged.total_final %}
|
{% if currency.exchanged and currency.exchanged.total_final %}
|
||||||
<div class="text-end font-monospace tw-text-gray-500">
|
<div class="text-end font-monospace tw:text-gray-500">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="currency.exchanged.total_final"
|
:amount="currency.exchanged.total_final"
|
||||||
:prefix="currency.exchanged.currency.prefix"
|
:prefix="currency.exchanged.currency.prefix"
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<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"
|
<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
|
_="on change from #transactions-list or htmx:afterSettle from window
|
||||||
if #actions-bar then
|
if #actions-bar then
|
||||||
if no <input[type='checkbox']:checked/> in #transactions-list
|
if no <input[type='checkbox']:checked/> in #transactions-list
|
||||||
if #actions-bar
|
if #actions-bar
|
||||||
add .slide-in-bottom-reverse then settle
|
add .slide-in-bottom-reverse then settle
|
||||||
then add .tw-hidden to #actions-bar
|
then add .tw:hidden to #actions-bar
|
||||||
then remove .slide-in-bottom-reverse
|
then remove .slide-in-bottom-reverse
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if #actions-bar
|
if #actions-bar
|
||||||
remove .tw-hidden from #actions-bar
|
remove .tw:hidden from #actions-bar
|
||||||
then trigger selected_transactions_updated
|
then trigger selected_transactions_updated
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -26,20 +26,20 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<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">
|
_="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' %}
|
<i class="fa-regular fa-square-check tw:text-green-400 me-3"></i>{% translate 'Select All' %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<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">
|
_="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' %}
|
<i class="fa-regular fa-square tw:text-red-400 me-3"></i>{% translate 'Unselect All' %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="vr tw-align-middle"></div>
|
<div class="vr tw:align-middle"></div>
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm"
|
||||||
hx-get="{% url 'transactions_bulk_undelete' %}"
|
hx-get="{% url 'transactions_bulk_undelete' %}"
|
||||||
hx-include=".transaction"
|
hx-include=".transaction"
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
_="install prompt_swal">
|
_="install prompt_swal">
|
||||||
<i class="fa-solid fa-trash text-danger"></i>
|
<i class="fa-solid fa-trash text-danger"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="vr tw-align-middle"></div>
|
<div class="vr tw:align-middle"></div>
|
||||||
<div class="btn-group"
|
<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 realTotal to math.bignumber(0)
|
||||||
@@ -118,10 +118,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Flat Total" %}
|
{% trans "Flat Total" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-flat-total"
|
id="calc-menu-flat-total"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -138,10 +138,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Real Total" %}
|
{% trans "Real Total" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-real-total"
|
id="calc-menu-real-total"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -158,10 +158,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Mean" %}
|
{% trans "Mean" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-mean"
|
id="calc-menu-mean"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -178,10 +178,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Max" %}
|
{% trans "Max" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-max"
|
id="calc-menu-max"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -198,10 +198,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Min" %}
|
{% trans "Min" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-min"
|
id="calc-menu-min"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -218,10 +218,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Count" %}
|
{% trans "Count" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-count"
|
id="calc-menu-count"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<span class="tw-text-xs text-white-50 mx-1"
|
<span class="tw:text-xs text-white-50 mx-1"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{{ content }}">
|
data-bs-title="{{ content }}">
|
||||||
<i class="{% if not icon %}fa-solid fa-circle-question{% else %}{{ icon }}{% endif %} fa-fw"></i>
|
<i class="{% if not icon %}fa-solid fa-circle-question{% else %}{{ icon }}{% endif %} fa-fw"></i>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="card tw-relative h-100 shadow">
|
<div class="card tw:relative h-100 shadow">
|
||||||
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-{{ color }}-300 tw-text-{{ color }}-800 text-center align-items-center d-flex justify-content-center rounded-2">
|
<div class="tw:absolute tw:h-8 tw:w-8 tw:right-2 tw:top-2 tw:bg-{{ color }}-300 tw:text-{{ color }}-800 text-center align-items-center d-flex justify-content-center rounded-2">
|
||||||
{% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %}
|
{% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="tw-text-{{ color }}-400 fw-bold tw-mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5>
|
<h5 class="tw:text-{{ color }}-400 fw-bold tw:mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5>
|
||||||
{{ slot }}
|
{{ slot }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="progress-stacked">
|
<div class="progress-stacked">
|
||||||
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Projected Income' %} ({{ percentage.percentages.income_projected|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.income_projected|floatformat:"2u" }}%">
|
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Projected Income' %} ({{ percentage.percentages.income_projected|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.income_projected|floatformat:"2u" }}%">
|
||||||
<div class="progress-bar progress-bar-striped !tw-bg-green-300"
|
<div class="progress-bar progress-bar-striped tw:bg-green-300!"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
title="{% trans 'Projected Income' %} ({{ percentage.percentages.income_projected|floatformat:2 }}%)">
|
title="{% trans 'Projected Income' %} ({{ percentage.percentages.income_projected|floatformat:2 }}%)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Current Income' %} ({{ percentage.percentages.income_current|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.income_current|floatformat:"2u" }}%">
|
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Current Income' %} ({{ percentage.percentages.income_current|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.income_current|floatformat:"2u" }}%">
|
||||||
<div class="progress-bar !tw-bg-green-400"
|
<div class="progress-bar tw:bg-green-400!"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
title="{% trans 'Current Income' %} ({{ p.percentages.income_current|floatformat:2 }}%)">
|
title="{% trans 'Current Income' %} ({{ p.percentages.income_current|floatformat:2 }}%)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Projected Expenses' %} ({{ percentage.percentages.expense_projected|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.expense_projected|floatformat:"2u" }}%">
|
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Projected Expenses' %} ({{ percentage.percentages.expense_projected|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.expense_projected|floatformat:"2u" }}%">
|
||||||
<div class="progress-bar progress-bar-striped !tw-bg-red-300"
|
<div class="progress-bar progress-bar-striped tw:bg-red-300!"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
title="{% trans 'Projected Expenses' %} ({{ percentage.percentages.expense_projected|floatformat:2 }}%)">
|
title="{% trans 'Projected Expenses' %} ({{ percentage.percentages.expense_projected|floatformat:2 }}%)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Current Expenses' %} ({{ percentage.percentages.expense_current|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.expense_current|floatformat:"2u" }}%">
|
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Current Expenses' %} ({{ percentage.percentages.expense_current|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.expense_current|floatformat:"2u" }}%">
|
||||||
<div class="progress-bar !tw-bg-red-400"
|
<div class="progress-bar tw:bg-red-400!"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
title="{% trans 'Current Expenses' %} ({{ percentage.percentages.expense_current|floatformat:2 }}%)">
|
title="{% trans 'Current Expenses' %} ({{ percentage.percentages.expense_current|floatformat:2 }}%)">
|
||||||
|
|||||||
@@ -1,45 +1,48 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<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"
|
<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
|
_="on change from #transactions-list or htmx:afterSettle from window
|
||||||
if #actions-bar then
|
if #actions-bar then
|
||||||
if no <input[type='checkbox']:checked/> in #transactions-list
|
if no <input[type='checkbox']:checked/> in #transactions-list
|
||||||
if #actions-bar
|
if #actions-bar
|
||||||
add .slide-in-bottom-reverse then settle
|
add .slide-in-bottom-reverse then settle
|
||||||
then add .tw-hidden to #actions-bar
|
then add .tw:hidden to #actions-bar
|
||||||
then remove .slide-in-bottom-reverse
|
then remove .slide-in-bottom-reverse
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if #actions-bar
|
if #actions-bar
|
||||||
remove .tw-hidden from #actions-bar
|
set #selected-count's innerHTML to length of <input[type='checkbox']:checked/> in #transactions-list
|
||||||
|
then remove .tw:hidden from #actions-bar
|
||||||
then trigger selected_transactions_updated
|
then trigger selected_transactions_updated
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end">
|
end">
|
||||||
<div class="card slide-in-bottom">
|
<div class="card slide-in-bottom tw:max-w-[90vw]">
|
||||||
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3">
|
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3 tw:overflow-x-auto">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
<div class="tw:font-bold tw:text-md ms-2" id="selected-count">0</div>
|
||||||
|
<div class="vr tw:align-middle"></div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false" data-bs-popper-config='{"strategy":"fixed"}'>
|
||||||
<i class="fa-regular fa-square-check fa-fw"></i>
|
<i class="fa-regular fa-square-check fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
_="on click set <#transactions-list .transaction:not([style*='display: none']) input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
|
_="on click set <#transactions-list .transaction:not([style*='display: none']) 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' %}
|
<i class="fa-regular fa-square-check tw:text-green-400 me-3"></i>{% translate 'Select All' %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<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">
|
_="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' %}
|
<i class="fa-regular fa-square tw:text-red-400 me-3"></i>{% translate 'Unselect All' %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="vr tw-align-middle"></div>
|
<div class="vr tw:align-middle"></div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm"
|
||||||
hx-get="{% url 'transactions_bulk_edit' %}"
|
hx-get="{% url 'transactions_bulk_edit' %}"
|
||||||
@@ -50,23 +53,24 @@
|
|||||||
<i class="fa-solid fa-pencil"></i>
|
<i class="fa-solid fa-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
<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">
|
data-bs-toggle="dropdown" data-bs-popper-config='{"strategy":"fixed"}' aria-expanded="false"
|
||||||
|
data-bs-auto-close="outside">
|
||||||
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
hx-get="{% url 'transactions_bulk_unpay' %}"
|
hx-get="{% url 'transactions_bulk_unpay' %}"
|
||||||
hx-include=".transaction">
|
hx-include=".transaction">
|
||||||
<i class="fa-regular fa-circle tw-text-red-400 fa-fw me-3"></i>{% translate 'Mark as unpaid' %}
|
<i class="fa-regular fa-circle tw:text-red-400 fa-fw me-3"></i>{% translate 'Mark as unpaid' %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
hx-get="{% url 'transactions_bulk_pay' %}"
|
hx-get="{% url 'transactions_bulk_pay' %}"
|
||||||
hx-include=".transaction">
|
hx-include=".transaction">
|
||||||
<i class="fa-regular fa-circle-check tw-text-green-400 fa-fw me-3"></i>{% translate 'Mark as paid' %}
|
<i class="fa-regular fa-circle-check tw:text-green-400 fa-fw me-3"></i>{% translate 'Mark as paid' %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -91,7 +95,7 @@
|
|||||||
_="install prompt_swal">
|
_="install prompt_swal">
|
||||||
<i class="fa-solid fa-trash text-danger"></i>
|
<i class="fa-solid fa-trash text-danger"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="vr tw-align-middle"></div>
|
<div class="vr tw:align-middle"></div>
|
||||||
<div class="btn-group"
|
<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 realTotal to math.bignumber(0)
|
||||||
@@ -141,7 +145,8 @@
|
|||||||
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
|
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
<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">
|
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside"
|
||||||
|
data-bs-popper-config='{"strategy":"fixed"}'>
|
||||||
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -149,10 +154,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Flat Total" %}
|
{% trans "Flat Total" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-flat-total"
|
id="calc-menu-flat-total"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -169,10 +174,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Real Total" %}
|
{% trans "Real Total" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-real-total"
|
id="calc-menu-real-total"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -189,10 +194,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Mean" %}
|
{% trans "Mean" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-mean"
|
id="calc-menu-mean"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -209,10 +214,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Max" %}
|
{% trans "Max" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-max"
|
id="calc-menu-max"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -229,10 +234,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Min" %}
|
{% trans "Min" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-min"
|
id="calc-menu-min"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
@@ -249,10 +254,10 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="dropdown-item-text p-0">
|
<div class="dropdown-item-text p-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
|
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
|
||||||
{% trans "Count" %}
|
{% trans "Count" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item px-3 tw-cursor-pointer"
|
<div class="dropdown-item px-3 tw:cursor-pointer"
|
||||||
id="calc-menu-count"
|
id="calc-menu-count"
|
||||||
_="on click
|
_="on click
|
||||||
set original_value to my innerText
|
set original_value to my innerText
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<c-components.fab>
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="success"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, add_income from:window"
|
||||||
|
hx_vals='{"year": {{ year }}, {% if month %}"month": {{ month }},{% endif %} "type": "IN"}'
|
||||||
|
url="{% url 'transaction_add' %}"
|
||||||
|
icon="fa-solid fa-arrow-right-to-bracket"
|
||||||
|
title="{% translate "Income" %}"></c-components.fab_menu_button>
|
||||||
|
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="danger"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, add_income from:window"
|
||||||
|
hx_vals='{"year": {{ year }}, {% if month %}"month": {{ month }},{% endif %} "type": "EX"}'
|
||||||
|
url="{% url 'transaction_add' %}"
|
||||||
|
icon="fa-solid fa-arrow-right-from-bracket"
|
||||||
|
title="{% translate "Expense" %}"></c-components.fab_menu_button>
|
||||||
|
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="warning"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, installment from:window"
|
||||||
|
url="{% url 'installment_plan_add' %}"
|
||||||
|
icon="fa-solid fa-divide"
|
||||||
|
title="{% translate "Installment" %}"></c-components.fab_menu_button>
|
||||||
|
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="warning"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, recurring from:window"
|
||||||
|
url="{% url 'recurring_transaction_add' %}"
|
||||||
|
icon="fa-solid fa-repeat"
|
||||||
|
title="{% translate "Recurring" %}"></c-components.fab_menu_button>
|
||||||
|
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="info"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, transfer from:window"
|
||||||
|
hx_vals='{"year": {{ year }} {% if month %}, "month": {{ month }}{% endif %}}'
|
||||||
|
url="{% url 'transactions_transfer' %}"
|
||||||
|
icon="fa-solid fa-money-bill-transfer"
|
||||||
|
title="{% translate "Transfer" %}"></c-components.fab_menu_button>
|
||||||
|
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="info"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, balance from:window"
|
||||||
|
url="{% url 'account_reconciliation' %}"
|
||||||
|
icon="fa-solid fa-scale-balanced"
|
||||||
|
title="{% translate "Balance" %}"></c-components.fab_menu_button>
|
||||||
|
<c-components.fab_menu_button
|
||||||
|
color="secondary"
|
||||||
|
hx_target="#generic-offcanvas"
|
||||||
|
hx_trigger="click, quick_transaction from:window"
|
||||||
|
url="{% url 'quick_transactions_create_menu' %}"
|
||||||
|
icon="fa-solid fa-person-running"
|
||||||
|
title="{% translate "Quick Transaction" %}"></c-components.fab_menu_button>
|
||||||
|
</c-components.fab>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Currencies' %}<span>
|
<div>{% translate 'Currencies' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container-fluid px-md-3 py-3 column-gap-5">
|
<div class="container-fluid px-md-3 py-3 column-gap-5">
|
||||||
<div class="d-lg-flex justify-content-between mb-3 w-100">
|
<div class="d-lg-flex justify-content-between mb-3 w-100">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace d-flex align-items-center">
|
<div class="tw:text-3xl fw-bold font-monospace d-flex align-items-center">
|
||||||
{{ strategy.name }}
|
{{ strategy.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm text-lg-end mt-2 mt-lg-0">
|
<div class="tw:text-sm text-lg-end mt-2 mt-lg-0">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<span class="badge rounded-pill text-bg-secondary">{{ strategy.payment_currency.name }}</span> x <span class="badge rounded-pill text-bg-secondary">{{ strategy.target_currency.name }}</span>
|
<span class="badge rounded-pill text-bg-secondary">{{ strategy.payment_currency.name }}</span> x <span class="badge rounded-pill text-bg-secondary">{{ strategy.target_currency.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
• {{ strategy.current_price.1|date:"SHORT_DATETIME_FORMAT" }}
|
• {{ strategy.current_price.1|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
</c-amount.display>
|
</c-amount.display>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tw-text-red-400">{% trans "No exchange rate available" %}</div>
|
<div class="tw:text-red-400">{% trans "No exchange rate available" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div class="card-title tw-text-xl">{% trans "Entries" %}<span>
|
<div class="card-title tw:text-xl">{% trans "Entries" %}<span>
|
||||||
<a class="text-decoration-none p-1 category-action"
|
<a class="text-decoration-none p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{% trans "Total P/L" %}</h5>
|
<h5 class="card-title">{% trans "Total P/L" %}</h5>
|
||||||
<div
|
<div
|
||||||
class="card-text {% if strategy.total_profit_loss >= 0 %}tw-text-green-400{% else %}tw-text-red-400{% endif %}">
|
class="card-text {% if strategy.total_profit_loss >= 0 %}tw:text-green-400{% else %}tw:text-red-400{% endif %}">
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="strategy.total_profit_loss"
|
:amount="strategy.total_profit_loss"
|
||||||
:prefix="strategy.payment_currency.prefix"
|
:prefix="strategy.payment_currency.prefix"
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{% trans "Total % P/L" %}</h5>
|
<h5 class="card-title">{% trans "Total % P/L" %}</h5>
|
||||||
<div
|
<div
|
||||||
class="card-text {% if strategy.total_profit_loss >= 0 %}tw-text-green-400{% else %}tw-text-red-400{% endif %}">
|
class="card-text {% if strategy.total_profit_loss >= 0 %}tw:text-green-400{% else %}tw:text-red-400{% endif %}">
|
||||||
{{ strategy.total_profit_loss_percentage|floatformat:2 }}%
|
{{ strategy.total_profit_loss_percentage|floatformat:2 }}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -451,7 +451,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{% trans "Investment Frequency" %}</h5>
|
<h5 class="card-title">{% trans "Investment Frequency" %}</h5>
|
||||||
<p class="card-text tw-text-gray-400">
|
<p class="card-text tw:text-gray-400">
|
||||||
{% trans "The straighter the blue line, the more consistent your DCA strategy is." %}
|
{% trans "The straighter the blue line, the more consistent your DCA strategy is." %}
|
||||||
</p>
|
</p>
|
||||||
<canvas id="frequencyChart"></canvas>
|
<canvas id="frequencyChart"></canvas>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Dollar Cost Average Strategies' %}<span>
|
<div>{% translate 'Dollar Cost Average Strategies' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
<a href="{% url 'dca_strategy_detail_index' strategy_id=strategy.id %}" hx-boost="true"
|
<a href="{% url 'dca_strategy_detail_index' strategy_id=strategy.id %}" hx-boost="true"
|
||||||
class="text-decoration-none card-body">
|
class="text-decoration-none card-body">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="card-title tw-text-xl">{{ strategy.name }}</div>
|
<div class="card-title tw:text-xl">{{ strategy.name }}</div>
|
||||||
<div class="card-text tw-text-gray-400">{{ strategy.notes }}</div>
|
<div class="card-text tw:text-gray-400">{{ strategy.notes }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-footer text-end">
|
<div class="card-footer text-end">
|
||||||
<a class="text-decoration-none tw-text-gray-400 p-1"
|
<a class="text-decoration-none tw:text-gray-400 p-1"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Edit" %}"
|
data-bs-title="{% translate "Edit" %}"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Entities' %}<span>
|
<div>{% translate 'Entities' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{% load currency_display %}
|
{% load currency_display %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Exchange Rates' %}<span>
|
<div>{% translate 'Exchange Rates' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<nav aria-label="{% translate 'Page navigation' %}">
|
<nav aria-label="{% translate 'Page navigation' %}">
|
||||||
<ul class="pagination justify-content-center mt-5">
|
<ul class="pagination justify-content-center mt-5">
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link tw-cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
|
<a class="page-link tw:cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
|
||||||
hx-get="{% if page_obj.has_previous %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
hx-get="{% if page_obj.has_previous %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
||||||
hx-vals='{"page": 1, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": 1, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-include="#filter, #order"
|
hx-include="#filter, #order"
|
||||||
@@ -79,13 +79,13 @@
|
|||||||
{% if page_number <= page_obj.number|add:3 and page_number >= page_obj.number|add:-3 %}
|
{% if page_number <= page_obj.number|add:3 and page_number >= page_obj.number|add:-3 %}
|
||||||
{% if page_obj.number == page_number %}
|
{% if page_obj.number == page_number %}
|
||||||
<li class="page-item active">
|
<li class="page-item active">
|
||||||
<a class="page-link tw-cursor-pointer">
|
<a class="page-link tw:cursor-pointer">
|
||||||
{{ page_number }}
|
{{ page_number }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link tw-cursor-pointer"
|
<a class="page-link tw:cursor-pointer"
|
||||||
hx-get="{% url 'exchange_rates_list_pair' %}"
|
hx-get="{% url 'exchange_rates_list_pair' %}"
|
||||||
hx-vals='{"page": {{ page_number }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": {{ page_number }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-target="#exchange-rates-table"
|
hx-target="#exchange-rates-table"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link tw-cursor-pointer"
|
<a class="page-link tw:cursor-pointer"
|
||||||
hx-get="{% url 'exchange_rates_list_pair' %}" hx-target="#exchange-rates-table"
|
hx-get="{% url 'exchange_rates_list_pair' %}" hx-target="#exchange-rates-table"
|
||||||
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-include="#filter, #order"
|
hx-include="#filter, #order"
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw-cursor-pointer"
|
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw:cursor-pointer"
|
||||||
hx-get="{% if page_obj.has_next %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
hx-get="{% if page_obj.has_next %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
||||||
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-include="#filter, #order"
|
hx-include="#filter, #order"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{% load currency_display %}
|
{% load currency_display %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Automatic Exchange Rates' %}<span>
|
<div>{% translate 'Automatic Exchange Rates' %}<span>
|
||||||
<a class="text-decoration-none tw-text-2xl p-1 category-action"
|
<a class="text-decoration-none tw:text-2xl p-1 category-action"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}"
|
data-bs-title="{% translate "Add" %}"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<nav aria-label="{% translate 'Page navigation' %}">
|
<nav aria-label="{% translate 'Page navigation' %}">
|
||||||
<ul class="pagination justify-content-center mt-5">
|
<ul class="pagination justify-content-center mt-5">
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link tw-cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
|
<a class="page-link tw:cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
|
||||||
hx-get="{% if page_obj.has_previous %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
hx-get="{% if page_obj.has_previous %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
||||||
hx-vals='{"page": 1, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": 1, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-include="#filter, #order"
|
hx-include="#filter, #order"
|
||||||
@@ -79,13 +79,13 @@
|
|||||||
{% if page_number <= page_obj.number|add:3 and page_number >= page_obj.number|add:-3 %}
|
{% if page_number <= page_obj.number|add:3 and page_number >= page_obj.number|add:-3 %}
|
||||||
{% if page_obj.number == page_number %}
|
{% if page_obj.number == page_number %}
|
||||||
<li class="page-item active">
|
<li class="page-item active">
|
||||||
<a class="page-link tw-cursor-pointer">
|
<a class="page-link tw:cursor-pointer">
|
||||||
{{ page_number }}
|
{{ page_number }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link tw-cursor-pointer"
|
<a class="page-link tw:cursor-pointer"
|
||||||
hx-get="{% url 'exchange_rates_list_pair' %}"
|
hx-get="{% url 'exchange_rates_list_pair' %}"
|
||||||
hx-vals='{"page": {{ page_number }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": {{ page_number }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-target="#exchange-rates-table"
|
hx-target="#exchange-rates-table"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link tw-cursor-pointer"
|
<a class="page-link tw:cursor-pointer"
|
||||||
hx-get="{% url 'exchange_rates_list_pair' %}" hx-target="#exchange-rates-table"
|
hx-get="{% url 'exchange_rates_list_pair' %}" hx-target="#exchange-rates-table"
|
||||||
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-include="#filter, #order"
|
hx-include="#filter, #order"
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw-cursor-pointer"
|
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw:cursor-pointer"
|
||||||
hx-get="{% if page_obj.has_next %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
hx-get="{% if page_obj.has_next %}{% url 'exchange_rates_list_pair' %}{% endif %}"
|
||||||
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
|
||||||
hx-include="#filter, #order"
|
hx-include="#filter, #order"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div class="alert alert-info" role="alert" id="msg" hx-preserve="true">
|
<div class="alert alert-info" role="alert" id="msg" hx-preserve="true">
|
||||||
<h6 class="alert-heading tw-italic tw-font-bold">{% trans 'A message from the author' %}</h6>
|
<h6 class="alert-heading tw:italic tw:font-bold">{% trans 'A message from the author' %}</h6>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="mb-0">{{ message|linebreaksbr }}</p>
|
<p class="mb-0">{{ message|linebreaksbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5">
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<div>{% translate 'Import Profiles' %}<span>
|
<div>{% translate 'Import Profiles' %}<span>
|
||||||
<span class="dropdown" data-bs-toggle="tooltip"
|
<span class="dropdown" data-bs-toggle="tooltip"
|
||||||
data-bs-title="{% translate "Add" %}">
|
data-bs-title="{% translate "Add" %}">
|
||||||
<a class="text-decoration-none tw-text-2xl p-1" role="button"
|
<a class="text-decoration-none tw:text-2xl p-1" role="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
data-bs-title="{% translate "Add" %}" aria-expanded="false">
|
data-bs-title="{% translate "Add" %}" aria-expanded="false">
|
||||||
<i class="fa-solid fa-circle-plus fa-fw"></i>
|
<i class="fa-solid fa-circle-plus fa-fw"></i>
|
||||||
|
|||||||
@@ -15,20 +15,20 @@
|
|||||||
{% for run in runs %}
|
{% for run in runs %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header tw-text-sm {% if run.status == run.Status.QUEUED %}tw-text-white{% elif run.status == run.Status.PROCESSING %}text-warning{% elif run.status == run.Status.FINISHED %}text-success{% else %}text-danger{% endif %}">
|
<div class="card-header tw:text-sm {% if run.status == run.Status.QUEUED %}text-body{% elif run.status == run.Status.PROCESSING %}text-warning{% elif run.status == run.Status.FINISHED %}text-success{% else %}text-danger{% endif %}">
|
||||||
<span><i class="fa-solid {% if run.status == run.Status.QUEUED %}fa-hourglass-half{% elif run.status == run.Status.PROCESSING %}fa-spinner{% elif run.status == run.Status.FINISHED %}fa-check{% else %}fa-xmark{% endif %} fa-fw me-2"></i>{{ run.get_status_display }}</span>
|
<span><i class="fa-solid {% if run.status == run.Status.QUEUED %}fa-hourglass-half{% elif run.status == run.Status.PROCESSING %}fa-spinner{% elif run.status == run.Status.FINISHED %}fa-check{% else %}fa-xmark{% endif %} fa-fw me-2"></i>{{ run.get_status_display }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><i class="fa-solid fa-hashtag me-1 tw-text-xs tw-text-gray-400"></i>{{ run.id }}<span class="tw-text-xs tw-text-gray-400 ms-1">({{ run.file_name }})</span></h5>
|
<h5 class="card-title"><i class="fa-solid fa-hashtag me-1 tw:text-xs tw:text-gray-400"></i>{{ run.id }}<span class="tw:text-xs tw:text-gray-400 ms-1">({{ run.file_name }})</span></h5>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 w-100 g-4">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 w-100 g-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
<div class="text-body-secondary tw:text-xs tw:font-medium">
|
||||||
{% trans 'Total Items' %}
|
{% trans 'Total Items' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm">
|
<div class="tw:text-sm">
|
||||||
{{ run.total_rows }}
|
{{ run.total_rows }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
<div class="text-body-secondary tw:text-xs tw:font-medium">
|
||||||
{% trans 'Processed Items' %}
|
{% trans 'Processed Items' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm">
|
<div class="tw:text-sm">
|
||||||
{{ run.processed_rows }}
|
{{ run.processed_rows }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,10 +51,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
<div class="text-body-secondary tw:text-xs tw:font-medium">
|
||||||
{% trans 'Skipped Items' %}
|
{% trans 'Skipped Items' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm">
|
<div class="tw:text-sm">
|
||||||
{{ run.skipped_rows }}
|
{{ run.skipped_rows }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,10 +64,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
<div class="text-body-secondary tw:text-xs tw:font-medium">
|
||||||
{% trans 'Failed Items' %}
|
{% trans 'Failed Items' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm">
|
<div class="tw:text-sm">
|
||||||
{{ run.failed_rows }}
|
{{ run.failed_rows }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
<div class="text-body-secondary tw:text-xs tw:font-medium">
|
||||||
{% trans 'Successful Items' %}
|
{% trans 'Successful Items' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm">
|
<div class="tw:text-sm">
|
||||||
{{ run.successful_rows }}
|
{{ run.successful_rows }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block title %}{% translate 'Logs for' %} #{{ run.id }}{% endblock %}
|
{% block title %}{% translate 'Logs for' %} #{{ run.id }}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="card tw-max-h-full tw-overflow-auto">
|
<div class="card tw:max-h-full tw:overflow-auto">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{{ run.logs|linebreaks }}
|
{{ run.logs|linebreaks }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{% load cache_access %}
|
||||||
|
{% load settings %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load active_link %}
|
||||||
|
<nav class="navbar border-bottom bg-body-tertiary d-flex d-lg-none" hx-boost="true">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand fw-bold text-primary font-base" href="{% url 'index' %}">
|
||||||
|
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="40" width="40" title="WYGIWYH"/>
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebar"
|
||||||
|
aria-controls="sidebar" aria-label={% translate "Toggle navigation" %}>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% load cache_access %}
|
||||||
{% load settings %}
|
{% load settings %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarContent">
|
<div class="collapse navbar-collapse" id="navbarContent">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 nav-underline" hx-push-url="true">
|
<ul class="navbar-nav me-auto mb-3 mb-lg-0 nav-underline" hx-push-url="true">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% active_link views='monthly_overview||yearly_overview_currency||yearly_overview_account||calendar' %}"
|
<a class="nav-link dropdown-toggle {% active_link views='monthly_overview||yearly_overview_currency||yearly_overview_account||calendar' %}"
|
||||||
href="#"
|
href="#"
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
<a class="nav-link {% active_link views='insights_index' %}" href="{% url 'insights_index' %}">{% trans 'Insights' %}</a>
|
<a class="nav-link {% active_link views='insights_index' %}" href="{% url 'insights_index' %}">{% trans 'Insights' %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% active_link views='installment_plans_index||recurring_trasanctions_index||transactions_all_index||transactions_trash_index' %}"
|
<a class="nav-link dropdown-toggle {% active_link views='installment_plans_index||quick_transactions_index||recurring_trasanctions_index||transactions_all_index||transactions_trash_index' %}"
|
||||||
href="#" role="button"
|
href="#" role="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
@@ -68,6 +69,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
|
<li><a class="dropdown-item {% active_link views='quick_transactions_index' %}"
|
||||||
|
href="{% url 'quick_transactions_index' %}">{% translate 'Quick Transactions' %}</a></li>
|
||||||
<li><a class="dropdown-item {% active_link views='installment_plans_index' %}"
|
<li><a class="dropdown-item {% active_link views='installment_plans_index' %}"
|
||||||
href="{% url 'installment_plans_index' %}">{% translate 'Installment Plans' %}</a></li>
|
href="{% url 'installment_plans_index' %}">{% translate 'Installment Plans' %}</a></li>
|
||||||
<li><a class="dropdown-item {% active_link views='recurring_trasanctions_index' %}"
|
<li><a class="dropdown-item {% active_link views='recurring_trasanctions_index' %}"
|
||||||
@@ -159,16 +162,22 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav mt-3 mb-2 mb-lg-0 mt-lg-0">
|
<ul class="navbar-nav mb-2 mb-lg-0 gap-3">
|
||||||
<li class="nav-item text-center w-100">
|
{% get_update_check as update_check %}
|
||||||
<a class="nav-item tw-text-2xl tw-cursor-pointer me-lg-4"
|
{% if update_check.update_available %}
|
||||||
|
<li class="nav-item my-auto">
|
||||||
|
<a class="badge text-bg-secondary text-decoration-none tw:cursor-pointer" href="https://github.com/eitchtee/WYGIWYH/releases/latest" target="_blank"><i class="fa-solid fa-circle-info fa-fw me-2"></i>v.{{ update_check.latest_version }} {% translate 'is available' %}!</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<div class="nav-link tw:lg:text-2xl! tw:cursor-pointer"
|
||||||
data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="{% trans "Calculator" %}"
|
data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="{% trans "Calculator" %}"
|
||||||
_="on click trigger show on #calculator">
|
_="on click trigger show on #calculator">
|
||||||
<i class="fa-solid fa-calculator"></i>
|
<i class="fa-solid fa-calculator"></i>
|
||||||
</a>
|
<span class="d-lg-none d-inline">{% trans "Calculator" %}</span>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="text-center w-100">{% include 'includes/navbar/user_menu.html' %}</li>
|
<li class="w-100">{% include 'includes/navbar/user_menu.html' %}</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
{% load settings %}
|
{% load settings %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="tw-text-2xl" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<div class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fa-solid fa-user"></i>
|
<i class="fa-solid fa-cog"></i>
|
||||||
</a>
|
</div>
|
||||||
<ul class="dropdown-menu dropdown-menu-start dropdown-menu-lg-end">
|
<ul class="dropdown-menu dropdown-menu-start dropdown-menu-lg-end">
|
||||||
<li class="dropdown-item-text">{{ user.email }}</li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item"
|
<li><a class="dropdown-item"
|
||||||
hx-get="{% url 'user_settings' %}"
|
hx-get="{% url 'user_settings' %}"
|
||||||
hx-target="#generic-offcanvas"
|
hx-target="#generic-offcanvas"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div id="persistent-generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
|
<div id="persistent-generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl tw:z-1100!"
|
||||||
data-bs-backdrop="static"
|
data-bs-backdrop="static"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
on force_hide_offcanvas call bootstrap.Offcanvas.getOrCreateInstance(me).hide() end
|
on force_hide_offcanvas call bootstrap.Offcanvas.getOrCreateInstance(me).hide() end
|
||||||
on hidden.bs.offcanvas set my innerHTML to '' end">
|
on hidden.bs.offcanvas set my innerHTML to '' end">
|
||||||
</div>
|
</div>
|
||||||
<div id="persistent-generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
|
<div id="persistent-generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl tw:z-1100!"
|
||||||
data-bs-backdrop="static"
|
data-bs-backdrop="static"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
|
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl tw:z-1100!"
|
||||||
data-bs-backdrop="static"
|
data-bs-backdrop="static"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
on htmx:beforeOnLoad[detail.boosted] call bootstrap.Offcanvas.getOrCreateInstance(me).hide()
|
on htmx:beforeOnLoad[detail.boosted] call bootstrap.Offcanvas.getOrCreateInstance(me).hide()
|
||||||
on hidden.bs.offcanvas set my innerHTML to '' end">
|
on hidden.bs.offcanvas set my innerHTML to '' end">
|
||||||
</div>
|
</div>
|
||||||
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
|
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl tw:z-1100!"
|
||||||
data-bs-backdrop="static"
|
data-bs-backdrop="static"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{# We use this to preload dynamically generated tailwind classes so the compiler can build them ahead of time #}
|
{# We use this to preload dynamically generated tailwind classes so the compiler can build them ahead of time #}
|
||||||
|
|
||||||
<div class="tw-text-blue-800"></div>
|
<div class="tw:text-blue-800"></div>
|
||||||
<div class="tw-text-yellow-800"></div>
|
<div class="tw:text-yellow-800"></div>
|
||||||
<div class="tw-text-red-800"></div>
|
<div class="tw:text-red-800"></div>
|
||||||
<div class="tw-text-green-800"></div>
|
<div class="tw:text-green-800"></div>
|
||||||
<div class="tw-text-blue-400"></div>
|
<div class="tw:text-blue-400"></div>
|
||||||
<div class="tw-text-yellow-400"></div>
|
<div class="tw:text-yellow-400"></div>
|
||||||
<div class="tw-text-red-400"></div>
|
<div class="tw:text-red-400"></div>
|
||||||
<div class="tw-text-green-400"></div>
|
<div class="tw:text-green-400"></div>
|
||||||
<div class="tw-bg-blue-300"></div>
|
<div class="tw:bg-blue-300"></div>
|
||||||
<div class="tw-bg-yellow-300"></div>
|
<div class="tw:bg-yellow-300"></div>
|
||||||
<div class="tw-bg-red-300"></div>
|
<div class="tw:bg-red-300"></div>
|
||||||
<div class="tw-bg-green-300"></div>
|
<div class="tw:bg-green-300"></div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ behavior htmx_error_handler
|
|||||||
title: '{% trans "Access Denied" %}',
|
title: '{% trans "Access Denied" %}',
|
||||||
text: '{% trans "You do not have permission to perform this action or access this resource." %}',
|
text: '{% trans "You do not have permission to perform this action or access this resource." %}',
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
|
timer: 60000,
|
||||||
customClass: {
|
customClass: {
|
||||||
confirmButton: 'btn btn-warning' -- Optional: different button style
|
confirmButton: 'btn btn-warning' -- Optional: different button style
|
||||||
},
|
},
|
||||||
@@ -18,6 +19,7 @@ behavior htmx_error_handler
|
|||||||
title: '{% trans "Something went wrong loading your data" %}',
|
title: '{% trans "Something went wrong loading your data" %}',
|
||||||
text: '{% trans "Try reloading the page or check the console for more information." %}',
|
text: '{% trans "Try reloading the page or check the console for more information." %}',
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
|
timer: 60000,
|
||||||
customClass: {
|
customClass: {
|
||||||
confirmButton: 'btn btn-primary'
|
confirmButton: 'btn btn-primary'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<script type="text/hyperscript">
|
<script type="text/hyperscript">
|
||||||
on paid if body do not include #settings-mute-sound
|
on paid if body do not include #settings-mute-sound
|
||||||
js
|
js
|
||||||
|
volume = JSON.parse(document.getElementById('volume').textContent) / 10
|
||||||
paidSound.pause()
|
paidSound.pause()
|
||||||
paidSound.currentTime = 0
|
paidSound.currentTime = 0
|
||||||
|
paidSound.volume = volume
|
||||||
paidSound.play()
|
paidSound.play()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
on unpaid if body do not include #settings-mute-sound
|
on unpaid if body do not include #settings-mute-sound
|
||||||
js
|
js
|
||||||
|
volume = JSON.parse(document.getElementById('volume').textContent) / 10
|
||||||
unpaidSound.pause()
|
unpaidSound.pause()
|
||||||
unpaidSound.currentTime = 0
|
unpaidSound.currentTime = 0
|
||||||
|
unpaidSound.volume = volume
|
||||||
unpaidSound.play()
|
unpaidSound.play()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
{% load active_link %}
|
||||||
|
{% load cache_access %}
|
||||||
|
{% load settings %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tw:group tw:lg:w-16 tw:lg:hover:w-112 tw:transition-all tw:duration-100 tw:fixed tw:top-0 tw:start-0 tw:h-full tw:z-1020">
|
||||||
|
<nav
|
||||||
|
id="sidebar"
|
||||||
|
hx-boost="true"
|
||||||
|
data-bs-scroll="true"
|
||||||
|
class="offcanvas-lg offcanvas-start d-lg-flex flex-column position-fixed top-0 start-0 h-100 bg-body-tertiary shadow-sm tw:z-1020 tw:lg:w-16 tw:lg:group-hover:w-104 tw:transition-all tw:duration-100 tw:overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="tw:hidden tw:lg:group-hover:block tw:absolute tw:top-0 tw:left-104 tw:w-8 tw:h-full tw:bg-transparent tw:pointer-events-auto tw:z-10"></div>
|
||||||
|
|
||||||
|
<a href="{% url 'index' %}" class="d-none d-lg-flex tw:justify-start p-3 text-decoration-none">
|
||||||
|
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
|
||||||
|
<span class="fs-4 fw-bold ms-3 tw:lg:invisible tw:lg:group-hover:visible">WYGIWYH</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<a href="{% url 'index' %}" class="offcanvas-title d-flex tw:justify-start text-decoration-none">
|
||||||
|
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
|
||||||
|
<span class="fs-4 fw-bold ms-3 tw:lg:invisible tw:lg:group-hover:visible">WYGIWYH</span>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn-close" data-bs-target="#sidebar" data-bs-dismiss="offcanvas"
|
||||||
|
aria-label={% translate 'Close' %}></button>
|
||||||
|
</div>
|
||||||
|
<hr class="m-0">
|
||||||
|
|
||||||
|
<ul class="list-unstyled p-3 d-flex flex-column gap-0 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:overflow-y-auto tw:overflow-x-hidden tw:text-nowrap tw:lg:group-hover:animate-[disable-pointer-events]"
|
||||||
|
style="animation-duration: 100ms">
|
||||||
|
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Monthly' %}"
|
||||||
|
url='monthly_index'
|
||||||
|
active="monthly_overview"
|
||||||
|
icon="fa-solid fa-list">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Yearly' %}"
|
||||||
|
url='yearly_index'
|
||||||
|
active="yearly_overview_currency||yearly_overview_account"
|
||||||
|
icon="fa-solid fa-calendar-plus">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Calendar' %}"
|
||||||
|
url='calendar_index'
|
||||||
|
active="calendar"
|
||||||
|
icon="fa-solid fa-calendar-days">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Insights' %}"
|
||||||
|
url='insights_index'
|
||||||
|
active="insights_index"
|
||||||
|
icon="fa-solid fa-chart-simple">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Net Worth' %}"
|
||||||
|
url='net_worth'
|
||||||
|
active="net_worth"
|
||||||
|
icon="fa-solid fa-money-bill">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Transactions' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'All' %}"
|
||||||
|
url='transactions_all_index'
|
||||||
|
active="transactions_all_index"
|
||||||
|
icon="fa-solid fa-right-left">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
{% settings "ENABLE_SOFT_DELETE" as enable_soft_delete %}
|
||||||
|
{% if enable_soft_delete %}
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Trash Can' %}"
|
||||||
|
url='transactions_trash_index'
|
||||||
|
active="transactions_trash_index"
|
||||||
|
icon="fa-solid fa-trash">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
{% endif %}
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Quick Transactions' %}"
|
||||||
|
url='quick_transactions_index'
|
||||||
|
active="quick_transactions_index"
|
||||||
|
icon="fa-solid fa-person-running">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Installment Plans' %}"
|
||||||
|
url='installment_plans_index'
|
||||||
|
active="installment_plans_index"
|
||||||
|
icon="fa-solid fa-divide">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Recurring Transactions' %}"
|
||||||
|
url='recurring_trasanctions_index'
|
||||||
|
active="recurring_trasanctions_index"
|
||||||
|
icon="fa-solid fa-repeat">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Tools' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Dollar Cost Average Tracker' %}"
|
||||||
|
url='dca_strategy_index'
|
||||||
|
active="dca_strategy_index||dca_strategy_detail_index"
|
||||||
|
icon="fa-solid fa-magnifying-glass-chart">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Unit Price Calculator' %}"
|
||||||
|
url='unit_price_calculator'
|
||||||
|
active="unit_price_calculator"
|
||||||
|
icon="fa-solid fa-tags">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Currency Converter' %}"
|
||||||
|
url='currency_converter'
|
||||||
|
active="currency_converter"
|
||||||
|
icon="fa-solid fa-money-bill-transfer">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapsible-panel"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="collapsible-panel"
|
||||||
|
class="tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views='tags_index||entities_index||categories_index||accounts_index||account_groups_index||currencies_index||exchange_rates_index||rules_index||import_profiles_index||automatic_exchange_rates_index||export_index||users_index' css_class="sidebar-active" %}">
|
||||||
|
<i class="fa-solid fa-toolbox fa-fw"></i>
|
||||||
|
<span
|
||||||
|
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">
|
||||||
|
{% translate 'Management' %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto p-2 w-100">
|
||||||
|
<div id="collapsible-panel"
|
||||||
|
class="p-0 collapse tw:absolute tw:bottom-0 tw:left-0 tw:w-full tw:z-30 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:max-h-screen">
|
||||||
|
<div class="tw:h-screen tw:backdrop-blur-3xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="tw:flex tw:justify-between tw:items-center tw:p-4 tw:border-b tw:border-gray-600 tw:lg:hidden tw:lg:group-hover:flex">
|
||||||
|
<h5 class="tw:text-lg tw:font-semibold tw:text-gray-800 tw:lg:invisible tw:lg:group-hover:visible">
|
||||||
|
{% trans 'Management' %}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<button type="button" class="btn-close tw:lg:hidden tw:lg:group-hover:inline" aria-label="Close"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapsible-panel"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-controls="collapsible-panel">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-unstyled p-3 d-flex flex-column gap-1 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:overflow-y-auto tw:overflow-x-hidden tw:text-nowrap tw:lg:group-hover:animate-[disable-pointer-events]"
|
||||||
|
style="animation-duration: 100ms">
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Transactions' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Categories' %}"
|
||||||
|
url='categories_index'
|
||||||
|
active="categories_index"
|
||||||
|
icon="fa-solid fa-icons">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Tags' %}"
|
||||||
|
url='tags_index'
|
||||||
|
active="tags_index"
|
||||||
|
icon="fa-solid fa-hashtag">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Entities' %}"
|
||||||
|
url='entities_index'
|
||||||
|
active="entities_index"
|
||||||
|
icon="fa-solid fa-user-group">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Accounts' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Accounts' %}"
|
||||||
|
url='accounts_index'
|
||||||
|
active="accounts_index"
|
||||||
|
icon="fa-solid fa-wallet">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Account Groups' %}"
|
||||||
|
url='account_groups_index'
|
||||||
|
active="account_groups_index"
|
||||||
|
icon="fa-solid fa-wallet">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Currencies' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Currencies' %}"
|
||||||
|
url='currencies_index'
|
||||||
|
active="currencies_index"
|
||||||
|
icon="fa-solid fa-coins">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Exchange Rates' %}"
|
||||||
|
url='exchange_rates_index'
|
||||||
|
active="exchange_rates_index"
|
||||||
|
icon="fa-solid fa-right-left">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Automation' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Rules' %}"
|
||||||
|
url='rules_index'
|
||||||
|
active="rules_index"
|
||||||
|
icon="fa-solid fa-pen-ruler">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Import' %}"
|
||||||
|
url='import_profiles_index'
|
||||||
|
active="import_profiles_index"
|
||||||
|
icon="fa-solid fa-file-import">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Export and Restore' %}"
|
||||||
|
url='export_index'
|
||||||
|
active="export_index"
|
||||||
|
icon="fa-solid fa-file-export">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
{% endif %}
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Automatic Exchange Rates' %}"
|
||||||
|
url='automatic_exchange_rates_index'
|
||||||
|
active="automatic_exchange_rates_index"
|
||||||
|
icon="fa-solid fa-right-left">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<c-components.sidebar-menu-header title="{% translate 'Admin' %}"></c-components.sidebar-menu-header>
|
||||||
|
<c-components.sidebar-menu-item
|
||||||
|
title="{% translate 'Users' %}"
|
||||||
|
url='users_index'
|
||||||
|
active="users_index"
|
||||||
|
icon="fa-solid fa-users">
|
||||||
|
</c-components.sidebar-menu-item>
|
||||||
|
<c-components.sidebar-menu-url-item
|
||||||
|
title="{% translate 'Django Admin' %}"
|
||||||
|
tooltip="{% translate "Only use this if you know what you're doing" %}"
|
||||||
|
url='/admin/'
|
||||||
|
icon="fa-solid fa-screwdriver-wrench">
|
||||||
|
</c-components.sidebar-menu-url-item>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% get_update_check as update_check %}
|
||||||
|
{% if update_check.update_available %}
|
||||||
|
<div class="my-3">
|
||||||
|
<a class="px-3 badge text-bg-primary text-decoration-none tw:cursor-pointer w-100 tw:text-xs!"
|
||||||
|
href="https://github.com/eitchtee/WYGIWYH/releases/latest" target="_blank"><i
|
||||||
|
class="fa-solid fa-circle-exclamation fa-fw me-2"></i><span
|
||||||
|
class="tw:lg:invisible tw:lg:group-hover:visible">v.{{ update_check.latest_version }} {% translate 'is available' %}!</span></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% trans "Calculator" %}"
|
||||||
|
_="on click trigger show on #calculator">
|
||||||
|
<i class="fa-solid fa-calculator fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr class="my-1">
|
||||||
|
<div
|
||||||
|
class="ps-4 pe-2 py-2 d-flex align-items-center text-body-secondary text-decoration-none justify-content-between tw:text-wrap tw:lg:text-nowrap">
|
||||||
|
<div>
|
||||||
|
<i class="fa-solid fa-circle-user"></i>
|
||||||
|
<strong class="ms-2 tw:lg:invisible tw:lg:group-hover:visible">{{ user.email }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="tw:lg:invisible tw:lg:group-hover:visible">
|
||||||
|
{% include 'includes/navbar/user_menu.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="tw:hidden tw:lg:group-hover:block tw:absolute tw:top-0 tw:left-104 tw:w-16 tw:h-full tw:bg-transparent"></div>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load webpack_loader %}
|
{% load webpack_loader %}
|
||||||
|
|
||||||
{% stylesheet_pack 'style' %}
|
{% stylesheet_pack 'style' %}
|
||||||
{#{% stylesheet_pack 'select' %}#}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user