mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-14 12:59:52 +02:00
Compare commits
164 Commits
0.12.4
...
feat/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c7e32d1576 | ||
|
|
157e59a1d1 | ||
|
|
d9c505ac79 | ||
|
|
7274a13f3c | ||
|
|
5d64665ddd | ||
|
|
e0d92d15c8 | ||
|
|
48dd658627 | ||
|
|
80dbbd02f0 | ||
|
|
4b7ca61c29 | ||
|
|
b2f04ae1f9 | ||
|
|
f34d4b5e28 | ||
|
|
d2ebfbd615 | ||
|
|
812abbe488 | ||
|
|
9602a4affc | ||
|
|
bf548c0747 | ||
|
|
55ad2be08b | ||
|
|
2cd58c2464 | ||
|
|
4675ba9d56 | ||
|
|
a25c992d5c | ||
|
|
2eadfe99a5 | ||
|
|
11086a726f | ||
|
|
cd99b40b0a | ||
|
|
63aa51dc0d | ||
|
|
4708c5bc7e | ||
|
|
5a8462c050 | ||
|
|
6cac02e01f | ||
|
|
8d12ceeebb | ||
|
|
4681d3ca1d | ||
|
|
60ded03ea9 | ||
|
|
b20d137dc3 | ||
|
|
29ca6eed6c | ||
|
|
fa85303f36 | ||
|
|
a5f4f43678 | ||
|
|
d807bd5da3 | ||
|
|
85314fb749 | ||
|
|
c4d5e93a41 | ||
|
|
86f0c4365e | ||
|
|
202592b940 | ||
|
|
aea149bd13 | ||
|
|
411365f101 | ||
|
|
2008476021 | ||
|
|
53afe5b8eb | ||
|
|
6193c7a048 | ||
|
|
41f81d90d7 | ||
|
|
bf623cf16b | ||
|
|
ec213330cd | ||
|
|
7aedf524c6 | ||
|
|
04602b1964 | ||
|
|
15cfc4f300 | ||
|
|
3463c7c62c | ||
|
|
7b76c10093 | ||
|
|
7ad26a2e7b | ||
|
|
7706ca2d5f | ||
|
|
56198e93ce | ||
|
|
a74323f739 | ||
|
|
e4efde177b | ||
|
|
5871a03ee2 | ||
|
|
67af4430e1 | ||
|
|
696dcdf951 | ||
|
|
e35bad0e08 | ||
|
|
904f7cac22 | ||
|
|
ccd73963ca | ||
|
|
b5469b0413 | ||
|
|
dae848d951 | ||
|
|
45a33ad0c0 | ||
|
|
89e50b17bd | ||
|
|
ac54ba3da1 | ||
|
|
2da610f15e | ||
|
|
4ab6c4c6b6 | ||
|
|
68dbedd938 | ||
|
|
2800c53346 | ||
|
|
132547a074 | ||
|
|
61ed87dc45 | ||
|
|
96c1227c4f | ||
|
|
33f1ac1785 | ||
|
|
e9e94a8343 | ||
|
|
ba24a53853 | ||
|
|
4955fbde33 | ||
|
|
d04067a91d | ||
|
|
01333a439b | ||
|
|
d26907ea94 | ||
|
|
c98d9d3ce9 | ||
|
|
bfa4d3dea3 | ||
|
|
90323049eb | ||
|
|
b62122ed23 | ||
|
|
f74946cba7 |
@@ -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
|
||||||
|
|||||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: eitchtee
|
||||||
|
custom: ["https://www.paypal.com/donate/?hosted_button_id=FFWM4W9NQDMM6"]
|
||||||
36
README.md
36
README.md
@@ -51,6 +51,17 @@ Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH**
|
|||||||
* **Built-in Dollar-Cost Average (DCA) tracker**: Essential for tracking recurring investments, especially for crypto and stocks.
|
* **Built-in Dollar-Cost Average (DCA) tracker**: Essential for tracking recurring investments, especially for crypto and stocks.
|
||||||
* **API support for automation**: Seamlessly integrate with existing services to synchronize transactions.
|
* **API support for automation**: Seamlessly integrate with existing services to synchronize transactions.
|
||||||
|
|
||||||
|
# Demo
|
||||||
|
|
||||||
|
You can try WYGIWYH on [wygiwyh-demo.herculino.com](https://wygiwyh-demo.herculino.com/) with the credentials below:
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> E-mail: `demo@demo.com`
|
||||||
|
>
|
||||||
|
> Password: `wygiwyhdemo`
|
||||||
|
|
||||||
|
Keep in mind that **any data you add will be wiped in 24 hours or less**. And that **most automation features like the API, Rules, Automatic Exchange Rates and Import/Export are disabled**.
|
||||||
|
|
||||||
# How To Use
|
# How To Use
|
||||||
|
|
||||||
To run this application, you'll need [Docker](https://docs.docker.com/engine/install/) with [docker-compose](https://docs.docker.com/compose/install/).
|
To run this application, you'll need [Docker](https://docs.docker.com/engine/install/) with [docker-compose](https://docs.docker.com/compose/install/).
|
||||||
@@ -133,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.
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -163,10 +172,105 @@ AUTH_USER_MODEL = "users.User"
|
|||||||
|
|
||||||
LANGUAGE_CODE = "en"
|
LANGUAGE_CODE = "en"
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
|
("af", "Afrikaans"),
|
||||||
|
("ar", "العربية"),
|
||||||
|
("ar-dz", "العربية (الجزائر)"), # Algerian Arabic often uses the base name + region
|
||||||
|
("ast", "Asturianu"),
|
||||||
|
("az", "Azərbaycan"),
|
||||||
|
("bg", "Български"),
|
||||||
|
("be", "Беларуская"),
|
||||||
|
("bn", "বাংলা"),
|
||||||
|
("br", "Brezhoneg"),
|
||||||
|
("bs", "Bosanski"),
|
||||||
|
("ca", "Català"),
|
||||||
|
("ckb", "کوردیی ناوەندی"), # Central Kurdish (Sorani)
|
||||||
|
("cs", "Čeština"),
|
||||||
|
("cy", "Cymraeg"),
|
||||||
|
("da", "Dansk"),
|
||||||
("de", "Deutsch"),
|
("de", "Deutsch"),
|
||||||
|
("dsb", "Dolnoserbšćina"),
|
||||||
|
("el", "Ελληνικά"),
|
||||||
("en", "English"),
|
("en", "English"),
|
||||||
|
("en-au", "English (Australia)"),
|
||||||
|
("en-gb", "English (UK)"),
|
||||||
|
("eo", "Esperanto"),
|
||||||
|
("es", "Español"),
|
||||||
|
("es-ar", "Español (Argentina)"),
|
||||||
|
("es-co", "Español (Colombia)"),
|
||||||
|
("es-mx", "Español (México)"),
|
||||||
|
("es-ni", "Español (Nicaragua)"),
|
||||||
|
("es-ve", "Español (Venezuela)"),
|
||||||
|
("et", "Eesti"),
|
||||||
|
("eu", "Euskara"),
|
||||||
|
("fa", "فارسی"),
|
||||||
|
("fi", "Suomi"),
|
||||||
|
("fr", "Français"),
|
||||||
|
("fy", "Frysk"),
|
||||||
|
("ga", "Gaeilge"),
|
||||||
|
("gd", "Gàidhlig"),
|
||||||
|
("gl", "Galego"),
|
||||||
|
("he", "עברית"),
|
||||||
|
("hi", "हिन्दी"),
|
||||||
|
("hr", "Hrvatski"),
|
||||||
|
("hsb", "Hornjoserbšćina"),
|
||||||
|
("hu", "Magyar"),
|
||||||
|
("hy", "Հայերեն"),
|
||||||
|
("ia", "Interlingua"),
|
||||||
|
("id", "Bahasa Indonesia"),
|
||||||
|
("ig", "Igbo"),
|
||||||
|
("io", "Ido"),
|
||||||
|
("is", "Íslenska"),
|
||||||
|
("it", "Italiano"),
|
||||||
|
("ja", "日本語"),
|
||||||
|
("ka", "ქართული"),
|
||||||
|
("kab", "Taqbaylit"),
|
||||||
|
("kk", "Қазақша"),
|
||||||
|
("km", "ខ្មែរ"),
|
||||||
|
("kn", "ಕನ್ನಡ"),
|
||||||
|
("ko", "한국어"),
|
||||||
|
("ky", "Кыргызча"),
|
||||||
|
("lb", "Lëtzebuergesch"),
|
||||||
|
("lt", "Lietuvių"),
|
||||||
|
("lv", "Latviešu"),
|
||||||
|
("mk", "Македонски"),
|
||||||
|
("ml", "മലയാളം"),
|
||||||
|
("mn", "Монгол"),
|
||||||
|
("mr", "मराठी"),
|
||||||
|
("ms", "Bahasa Melayu"),
|
||||||
|
("my", "မြန်မာဘာသာ"),
|
||||||
|
("nb", "Norsk (Bokmål)"),
|
||||||
|
("ne", "नेपाली"),
|
||||||
("nl", "Nederlands"),
|
("nl", "Nederlands"),
|
||||||
|
("nn", "Norsk (Nynorsk)"),
|
||||||
|
("os", "Ирон"), # Ossetic
|
||||||
|
("pa", "ਪੰਜਾਬੀ"),
|
||||||
|
("pl", "Polski"),
|
||||||
|
("pt", "Português"),
|
||||||
("pt-br", "Português (Brasil)"),
|
("pt-br", "Português (Brasil)"),
|
||||||
|
("ro", "Română"),
|
||||||
|
("ru", "Русский"),
|
||||||
|
("sk", "Slovenčina"),
|
||||||
|
("sl", "Slovenščina"),
|
||||||
|
("sq", "Shqip"),
|
||||||
|
("sr", "Српски"),
|
||||||
|
("sr-latn", "Srpski (Latinica)"),
|
||||||
|
("sv", "Svenska"),
|
||||||
|
("sw", "Kiswahili"),
|
||||||
|
("ta", "தமிழ்"),
|
||||||
|
("te", "తెలుగు"),
|
||||||
|
("tg", "Тоҷикӣ"),
|
||||||
|
("th", "ไทย"),
|
||||||
|
("tk", "Türkmençe"),
|
||||||
|
("tr", "Türkçe"),
|
||||||
|
("tt", "Татарча"),
|
||||||
|
("udm", "Удмурт"),
|
||||||
|
("ug", "ئۇيغۇرچە"),
|
||||||
|
("uk", "Українська"),
|
||||||
|
("ur", "اردو"),
|
||||||
|
("uz", "Oʻzbekcha"),
|
||||||
|
("vi", "Tiếng Việt"),
|
||||||
|
("zh-hans", "简体中文"),
|
||||||
|
("zh-hant", "繁體中文"),
|
||||||
)
|
)
|
||||||
|
|
||||||
TIME_ZONE = os.getenv("TZ", "UTC")
|
TIME_ZONE = os.getenv("TZ", "UTC")
|
||||||
@@ -212,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"]
|
||||||
@@ -397,4 +537,4 @@ PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
|
|||||||
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
||||||
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
||||||
APP_VERSION = os.getenv("APP_VERSION", "unknown")
|
APP_VERSION = os.getenv("APP_VERSION", "unknown")
|
||||||
DEMO = os.getenv("DEMO_MODE", "false").lower() == "true"
|
DEMO = os.getenv("DEMO", "false").lower() == "true"
|
||||||
|
|||||||
@@ -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")),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"account-groups/<int:pk>/share/",
|
"account-groups/<int:pk>/share/",
|
||||||
views.account_share,
|
views.account_group_share,
|
||||||
name="account_group_share_settings",
|
name="account_group_share_settings",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ def account_group_take_ownership(request, pk):
|
|||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def account_share(request, pk):
|
def account_group_share(request, pk):
|
||||||
obj = get_object_or_404(AccountGroup, id=pk)
|
obj = get_object_or_404(AccountGroup, id=pk)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
if obj.owner and obj.owner != request.user:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,17 @@ from django.apps import AppConfig
|
|||||||
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)
|
||||||
|
|||||||
78
app/apps/common/decorators/user.py
Normal file
78
app/apps/common/decorators/user.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse, NoReverseMatch
|
||||||
|
|
||||||
|
|
||||||
|
def is_superuser(view):
|
||||||
|
@wraps(view)
|
||||||
|
def _view(request, *args, **kwargs):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
return view(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return _view
|
||||||
|
|
||||||
|
|
||||||
|
def htmx_login_required(function=None, login_url=None):
|
||||||
|
"""
|
||||||
|
Decorator that checks if the user is logged in.
|
||||||
|
|
||||||
|
Allows overriding the default login URL.
|
||||||
|
|
||||||
|
If the user is not logged in:
|
||||||
|
- If "hx-request" is present in the request header, it returns a 200 response
|
||||||
|
with a "HX-Redirect" header containing the determined login URL (including the "next" parameter).
|
||||||
|
- If "hx-request" is not present, it redirects to the determined login page normally.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function: The view function to decorate.
|
||||||
|
login_url: Optional. The URL or URL name to redirect to for login.
|
||||||
|
Defaults to settings.LOGIN_URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(view_func):
|
||||||
|
# Simplified @wraps usage - it handles necessary attribute assignments by default
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapped_view(request, *args, **kwargs):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
# Determine the login URL
|
||||||
|
resolved_login_url = login_url
|
||||||
|
if not resolved_login_url:
|
||||||
|
resolved_login_url = settings.LOGIN_URL
|
||||||
|
|
||||||
|
# Try to reverse the URL name if it's not a path
|
||||||
|
try:
|
||||||
|
# Check if it looks like a URL path already
|
||||||
|
if "/" not in resolved_login_url and "." not in resolved_login_url:
|
||||||
|
login_url_path = reverse(resolved_login_url)
|
||||||
|
else:
|
||||||
|
login_url_path = resolved_login_url
|
||||||
|
except NoReverseMatch:
|
||||||
|
# If reverse fails, assume it's already a URL path
|
||||||
|
login_url_path = resolved_login_url
|
||||||
|
|
||||||
|
# Construct the full redirect path with 'next' parameter
|
||||||
|
# Ensure request.path is URL-encoded if needed, though Django usually handles this
|
||||||
|
redirect_path = f"{login_url_path}?next={request.get_full_path()}" # Use get_full_path() to include query params
|
||||||
|
|
||||||
|
if request.headers.get("hx-request"):
|
||||||
|
# For HTMX requests, return a 200 with the HX-Redirect header.
|
||||||
|
response = HttpResponse()
|
||||||
|
response["HX-Redirect"] = login_url_path
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
# For regular requests, redirect to the login page.
|
||||||
|
return redirect(redirect_path)
|
||||||
|
|
||||||
|
return wrapped_view
|
||||||
|
|
||||||
|
if function:
|
||||||
|
return decorator(function)
|
||||||
|
return decorator
|
||||||
@@ -20,7 +20,15 @@ class MonthYearModelField(models.DateField):
|
|||||||
# Set the day to 1
|
# Set the day to 1
|
||||||
return date.replace(day=1).date()
|
return date.replace(day=1).date()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError(_("Invalid date format. Use YYYY-MM."))
|
try:
|
||||||
|
# Also accept YYYY-MM-DD format (for loaddata)
|
||||||
|
return (
|
||||||
|
datetime.datetime.strptime(value, "%Y-%m-%d").replace(day=1).date()
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Invalid date format. Use YYYY-MM or YYYY-MM-DD.")
|
||||||
|
)
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
kwargs["widget"] = MonthYearWidget
|
kwargs["widget"] = MonthYearWidget
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -65,6 +65,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,
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ async def remove_expired_sessions(timestamp=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.periodic(cron="0 6 * * *")
|
@app.periodic(cron="0 8 * * *")
|
||||||
@app.task(name="reset_demo_data")
|
@app.task(name="reset_demo_data")
|
||||||
def reset_demo_data():
|
def reset_demo_data(timestamp=None):
|
||||||
"""
|
"""
|
||||||
Wipes the database and loads fresh demo data if DEMO mode is active.
|
Wipes the database and loads fresh demo data if DEMO mode is active.
|
||||||
Runs daily at 6:00 AM.
|
Runs daily at 8:00 AM.
|
||||||
"""
|
"""
|
||||||
if not settings.DEMO:
|
if not settings.DEMO:
|
||||||
return # Exit if not in demo mode
|
return # Exit if not in demo mode
|
||||||
@@ -59,13 +59,14 @@ def reset_demo_data():
|
|||||||
try:
|
try:
|
||||||
# 1. Flush the database (wipe all data)
|
# 1. Flush the database (wipe all data)
|
||||||
logger.info("Flushing the database...")
|
logger.info("Flushing the database...")
|
||||||
# Using --noinput prevents prompts. Specify database if not default.
|
|
||||||
management.call_command(
|
management.call_command(
|
||||||
"flush", "--noinput", database=DEFAULT_DB_ALIAS, verbosity=1
|
"flush", "--noinput", database=DEFAULT_DB_ALIAS, verbosity=1
|
||||||
)
|
)
|
||||||
logger.info("Database flushed successfully.")
|
logger.info("Database flushed successfully.")
|
||||||
|
|
||||||
# 2. Load data from the fixture
|
# 2. Load data from the fixture
|
||||||
|
# TO-DO: Roll dates over based on today's date
|
||||||
fixture_name = "fixtures/demo_data.json"
|
fixture_name = "fixtures/demo_data.json"
|
||||||
logger.info(f"Loading data from fixture: {fixture_name}...")
|
logger.info(f"Loading data from fixture: {fixture_name}...")
|
||||||
management.call_command(
|
management.call_command(
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ from cachalot.api import invalidate
|
|||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
from apps.transactions.models import Transaction
|
from apps.transactions.models import Transaction
|
||||||
|
from apps.common.decorators.user import htmx_login_required
|
||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@htmx_login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def toasts(request):
|
def toasts(request):
|
||||||
return render(request, "common/fragments/toasts.html")
|
return render(request, "common/fragments/toasts.html")
|
||||||
|
|||||||
@@ -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'))",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from apps.currencies.utils.convert import convert
|
|||||||
|
|
||||||
|
|
||||||
def get_categories_totals(transactions_queryset, ignore_empty=False):
|
def get_categories_totals(transactions_queryset, ignore_empty=False):
|
||||||
# Get metrics for each category and currency in a single query
|
# First get the category totals as before
|
||||||
category_currency_metrics = (
|
category_currency_metrics = (
|
||||||
transactions_queryset.values(
|
transactions_queryset.values(
|
||||||
"category",
|
"category",
|
||||||
@@ -74,9 +74,65 @@ def get_categories_totals(transactions_queryset, ignore_empty=False):
|
|||||||
.order_by("category__name")
|
.order_by("category__name")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get tag totals within each category with currency details
|
||||||
|
tag_metrics = transactions_queryset.values(
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"tags__name",
|
||||||
|
"account__currency",
|
||||||
|
"account__currency__code",
|
||||||
|
"account__currency__name",
|
||||||
|
"account__currency__decimal_places",
|
||||||
|
"account__currency__prefix",
|
||||||
|
"account__currency__suffix",
|
||||||
|
"account__currency__exchange_currency",
|
||||||
|
).annotate(
|
||||||
|
expense_current=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(type=Transaction.Type.EXPENSE, is_paid=True, then="amount"),
|
||||||
|
default=Value(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
),
|
||||||
|
expense_projected=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(type=Transaction.Type.EXPENSE, is_paid=False, then="amount"),
|
||||||
|
default=Value(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
),
|
||||||
|
income_current=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(type=Transaction.Type.INCOME, is_paid=True, then="amount"),
|
||||||
|
default=Value(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
),
|
||||||
|
income_projected=Coalesce(
|
||||||
|
Sum(
|
||||||
|
Case(
|
||||||
|
When(type=Transaction.Type.INCOME, is_paid=False, then="amount"),
|
||||||
|
default=Value(0),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Process the results to structure by category
|
# Process the results to structure by category
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
|
# Process category totals first
|
||||||
for metric in category_currency_metrics:
|
for metric in category_currency_metrics:
|
||||||
# Skip empty categories if ignore_empty is True
|
# Skip empty categories if ignore_empty is True
|
||||||
if ignore_empty and all(
|
if ignore_empty and all(
|
||||||
@@ -101,7 +157,11 @@ def get_categories_totals(transactions_queryset, ignore_empty=False):
|
|||||||
currency_id = metric["account__currency"]
|
currency_id = metric["account__currency"]
|
||||||
|
|
||||||
if category_id not in result:
|
if category_id not in result:
|
||||||
result[category_id] = {"name": metric["category__name"], "currencies": {}}
|
result[category_id] = {
|
||||||
|
"name": metric["category__name"],
|
||||||
|
"currencies": {},
|
||||||
|
"tags": {}, # Add tags container
|
||||||
|
}
|
||||||
|
|
||||||
# Add currency data
|
# Add currency data
|
||||||
currency_data = {
|
currency_data = {
|
||||||
@@ -162,4 +222,101 @@ def get_categories_totals(transactions_queryset, ignore_empty=False):
|
|||||||
|
|
||||||
result[category_id]["currencies"][currency_id] = currency_data
|
result[category_id]["currencies"][currency_id] = currency_data
|
||||||
|
|
||||||
|
# Process tag totals and add them to the result, including untagged
|
||||||
|
for tag_metric in tag_metrics:
|
||||||
|
category_id = tag_metric["category"]
|
||||||
|
tag_id = tag_metric["tags"] # Will be None for untagged transactions
|
||||||
|
|
||||||
|
if category_id in result:
|
||||||
|
# Initialize the tag container if not exists
|
||||||
|
if "tags" not in result[category_id]:
|
||||||
|
result[category_id]["tags"] = {}
|
||||||
|
|
||||||
|
# Determine if this is a tagged or untagged transaction
|
||||||
|
tag_key = tag_id if tag_id is not None else "untagged"
|
||||||
|
tag_name = tag_metric["tags__name"] if tag_id is not None else None
|
||||||
|
|
||||||
|
if tag_key not in result[category_id]["tags"]:
|
||||||
|
result[category_id]["tags"][tag_key] = {
|
||||||
|
"name": tag_name,
|
||||||
|
"currencies": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
currency_id = tag_metric["account__currency"]
|
||||||
|
|
||||||
|
# Calculate tag totals
|
||||||
|
tag_total_current = (
|
||||||
|
tag_metric["income_current"] - tag_metric["expense_current"]
|
||||||
|
)
|
||||||
|
tag_total_projected = (
|
||||||
|
tag_metric["income_projected"] - tag_metric["expense_projected"]
|
||||||
|
)
|
||||||
|
tag_total_income = (
|
||||||
|
tag_metric["income_current"] + tag_metric["income_projected"]
|
||||||
|
)
|
||||||
|
tag_total_expense = (
|
||||||
|
tag_metric["expense_current"] + tag_metric["expense_projected"]
|
||||||
|
)
|
||||||
|
tag_total_final = tag_total_current + tag_total_projected
|
||||||
|
|
||||||
|
tag_currency_data = {
|
||||||
|
"currency": {
|
||||||
|
"code": tag_metric["account__currency__code"],
|
||||||
|
"name": tag_metric["account__currency__name"],
|
||||||
|
"decimal_places": tag_metric["account__currency__decimal_places"],
|
||||||
|
"prefix": tag_metric["account__currency__prefix"],
|
||||||
|
"suffix": tag_metric["account__currency__suffix"],
|
||||||
|
},
|
||||||
|
"expense_current": tag_metric["expense_current"],
|
||||||
|
"expense_projected": tag_metric["expense_projected"],
|
||||||
|
"total_expense": tag_total_expense,
|
||||||
|
"income_current": tag_metric["income_current"],
|
||||||
|
"income_projected": tag_metric["income_projected"],
|
||||||
|
"total_income": tag_total_income,
|
||||||
|
"total_current": tag_total_current,
|
||||||
|
"total_projected": tag_total_projected,
|
||||||
|
"total_final": tag_total_final,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add exchange currency support for tags
|
||||||
|
if tag_metric["account__currency__exchange_currency"]:
|
||||||
|
from_currency = Currency.objects.get(id=currency_id)
|
||||||
|
exchange_currency = Currency.objects.get(
|
||||||
|
id=tag_metric["account__currency__exchange_currency"]
|
||||||
|
)
|
||||||
|
|
||||||
|
exchanged = {}
|
||||||
|
for field in [
|
||||||
|
"expense_current",
|
||||||
|
"expense_projected",
|
||||||
|
"income_current",
|
||||||
|
"income_projected",
|
||||||
|
"total_income",
|
||||||
|
"total_expense",
|
||||||
|
"total_current",
|
||||||
|
"total_projected",
|
||||||
|
"total_final",
|
||||||
|
]:
|
||||||
|
amount, prefix, suffix, decimal_places = convert(
|
||||||
|
amount=tag_currency_data[field],
|
||||||
|
from_currency=from_currency,
|
||||||
|
to_currency=exchange_currency,
|
||||||
|
)
|
||||||
|
if amount is not None:
|
||||||
|
exchanged[field] = amount
|
||||||
|
if "currency" not in exchanged:
|
||||||
|
exchanged["currency"] = {
|
||||||
|
"prefix": prefix,
|
||||||
|
"suffix": suffix,
|
||||||
|
"decimal_places": decimal_places,
|
||||||
|
"code": exchange_currency.code,
|
||||||
|
"name": exchange_currency.name,
|
||||||
|
}
|
||||||
|
if exchanged:
|
||||||
|
tag_currency_data["exchanged"] = exchanged
|
||||||
|
|
||||||
|
result[category_id]["tags"][tag_key]["currencies"][
|
||||||
|
currency_id
|
||||||
|
] = tag_currency_data
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import decimal
|
|
||||||
import json
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Sum, Avg, F
|
from django.db.models import Sum
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
@@ -22,13 +20,13 @@ from apps.insights.utils.category_explorer import (
|
|||||||
get_category_sums_by_account,
|
get_category_sums_by_account,
|
||||||
get_category_sums_by_currency,
|
get_category_sums_by_currency,
|
||||||
)
|
)
|
||||||
|
from apps.insights.utils.category_overview import get_categories_totals
|
||||||
from apps.insights.utils.sankey import (
|
from apps.insights.utils.sankey import (
|
||||||
generate_sankey_data_by_account,
|
generate_sankey_data_by_account,
|
||||||
generate_sankey_data_by_currency,
|
generate_sankey_data_by_currency,
|
||||||
)
|
)
|
||||||
from apps.insights.utils.transactions import get_transactions
|
from apps.insights.utils.transactions import get_transactions
|
||||||
from apps.transactions.models import TransactionCategory, Transaction
|
from apps.transactions.models import TransactionCategory, Transaction
|
||||||
from apps.insights.utils.category_overview import get_categories_totals
|
|
||||||
from apps.transactions.utils.calculations import calculate_currency_totals
|
from apps.transactions.utils.calculations import calculate_currency_totals
|
||||||
|
|
||||||
|
|
||||||
@@ -170,6 +168,24 @@ def category_sum_by_currency(request):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def category_overview(request):
|
def category_overview(request):
|
||||||
|
if "view_type" in request.GET:
|
||||||
|
view_type = request.GET["view_type"]
|
||||||
|
request.session["insights_category_explorer_view_type"] = view_type
|
||||||
|
else:
|
||||||
|
view_type = request.session.get("insights_category_explorer_view_type", "table")
|
||||||
|
|
||||||
|
if "show_tags" in request.GET:
|
||||||
|
show_tags = request.GET["show_tags"] == "on"
|
||||||
|
request.session["insights_category_explorer_show_tags"] = show_tags
|
||||||
|
else:
|
||||||
|
show_tags = request.session.get("insights_category_explorer_show_tags", True)
|
||||||
|
|
||||||
|
if "showing" in request.GET:
|
||||||
|
showing = request.GET["showing"]
|
||||||
|
request.session["insights_category_explorer_showing"] = showing
|
||||||
|
else:
|
||||||
|
showing = request.session.get("insights_category_explorer_showing", "final")
|
||||||
|
|
||||||
# Get filtered transactions
|
# Get filtered transactions
|
||||||
transactions = get_transactions(request, include_silent=True)
|
transactions = get_transactions(request, include_silent=True)
|
||||||
|
|
||||||
@@ -180,7 +196,12 @@ def category_overview(request):
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"insights/fragments/category_overview/index.html",
|
"insights/fragments/category_overview/index.html",
|
||||||
{"total_table": total_table},
|
{
|
||||||
|
"total_table": total_table,
|
||||||
|
"view_type": view_type,
|
||||||
|
"show_tags": show_tags,
|
||||||
|
"showing": showing,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def net_worth_current(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
@@ -137,7 +137,7 @@ def net_worth_projected(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from crispy_forms.layout import (
|
|||||||
Row,
|
Row,
|
||||||
Column,
|
Column,
|
||||||
Field,
|
Field,
|
||||||
|
Div,
|
||||||
|
HTML,
|
||||||
)
|
)
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -28,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):
|
||||||
@@ -206,10 +208,21 @@ class TransactionForm(forms.ModelForm):
|
|||||||
else:
|
else:
|
||||||
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||||
self.helper.layout.append(
|
self.helper.layout.append(
|
||||||
FormActions(
|
Div(
|
||||||
NoClassSubmit(
|
NoClassSubmit(
|
||||||
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
|
"submit", _("Add"), css_class="btn btn-outline-primary"
|
||||||
),
|
),
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit_and_similar",
|
||||||
|
_("Save and add similar"),
|
||||||
|
css_class="btn btn-outline-primary",
|
||||||
|
),
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit_and_another",
|
||||||
|
_("Save and add another"),
|
||||||
|
css_class="btn btn-outline-primary",
|
||||||
|
),
|
||||||
|
css_class="d-grid gap-2",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -235,6 +248,140 @@ 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",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||||
|
"account": TomSelect(clear_button=False, group_by="group"),
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|||||||
45
app/apps/transactions/migrations/0043_quicktransaction.py
Normal file
45
app/apps/transactions/migrations/0043_quicktransaction.py
Normal file
@@ -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')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 = (
|
||||||
Q(account__visibility="public")
|
qs.filter(
|
||||||
| Q(account__owner=user)
|
Q(account__visibility="public")
|
||||||
| Q(account__shared_with=user)
|
| Q(account__owner=user)
|
||||||
| Q(account__visibility="private", account__owner=None),
|
| Q(account__shared_with=user)
|
||||||
deleted=False,
|
| Q(account__visibility="private", account__owner=None),
|
||||||
).distinct()
|
deleted=False,
|
||||||
|
)
|
||||||
|
.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,
|
||||||
@@ -574,6 +586,7 @@ class InstallmentPlan(models.Model):
|
|||||||
installment_plan=self,
|
installment_plan=self,
|
||||||
installment_id=i,
|
installment_id=i,
|
||||||
notes=self.notes if self.add_notes_to_transaction else "",
|
notes=self.notes if self.add_notes_to_transaction else "",
|
||||||
|
owner=self.account.owner,
|
||||||
)
|
)
|
||||||
new_transaction.tags.set(self.tags.all())
|
new_transaction.tags.set(self.tags.all())
|
||||||
new_transaction.entities.set(self.entities.all())
|
new_transaction.entities.set(self.entities.all())
|
||||||
@@ -640,6 +653,7 @@ class InstallmentPlan(models.Model):
|
|||||||
installment_plan=self,
|
installment_plan=self,
|
||||||
installment_id=i,
|
installment_id=i,
|
||||||
notes=self.notes if self.add_notes_to_transaction else "",
|
notes=self.notes if self.add_notes_to_transaction else "",
|
||||||
|
owner=self.account.owner,
|
||||||
)
|
)
|
||||||
new_transaction.tags.set(self.tags.all())
|
new_transaction.tags.set(self.tags.all())
|
||||||
new_transaction.entities.set(self.entities.all())
|
new_transaction.entities.set(self.entities.all())
|
||||||
@@ -775,6 +789,7 @@ class RecurringTransaction(models.Model):
|
|||||||
is_paid=False,
|
is_paid=False,
|
||||||
recurring_transaction=self,
|
recurring_transaction=self,
|
||||||
notes=self.notes if self.add_notes_to_transaction else "",
|
notes=self.notes if self.add_notes_to_transaction else "",
|
||||||
|
owner=self.account.owner,
|
||||||
)
|
)
|
||||||
if self.tags.exists():
|
if self.tags.exists():
|
||||||
created_transaction.tags.set(self.tags.all())
|
created_transaction.tags.set(self.tags.all())
|
||||||
@@ -797,12 +812,16 @@ class RecurringTransaction(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def generate_upcoming_transactions(cls):
|
def generate_upcoming_transactions(cls):
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
recurring_transactions = cls.objects.filter(
|
recurring_transactions = cls.all_objects.filter(
|
||||||
Q(models.Q(end_date__isnull=True) | Q(end_date__gte=today))
|
Q(models.Q(end_date__isnull=True) | Q(end_date__gte=today))
|
||||||
& Q(is_paused=False)
|
& Q(is_paused=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
for recurring_transaction in recurring_transactions:
|
for recurring_transaction in recurring_transactions:
|
||||||
|
logger.info(
|
||||||
|
f"Processing recurring transaction: {recurring_transaction.description}..."
|
||||||
|
)
|
||||||
|
|
||||||
if recurring_transaction.last_generated_date:
|
if recurring_transaction.last_generated_date:
|
||||||
start_date = recurring_transaction.get_next_date(
|
start_date = recurring_transaction.get_next_date(
|
||||||
recurring_transaction.last_generated_date
|
recurring_transaction.last_generated_date
|
||||||
@@ -821,7 +840,10 @@ class RecurringTransaction(models.Model):
|
|||||||
today + (recurring_transaction.get_recurrence_delta() * 6),
|
today + (recurring_transaction.get_recurrence_delta() * 6),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"End date: {end_date}")
|
||||||
|
|
||||||
while current_date <= end_date:
|
while current_date <= end_date:
|
||||||
|
logger.info(f"Creating transaction for date: {current_date}")
|
||||||
recurring_transaction.create_transaction(current_date, reference_date)
|
recurring_transaction.create_transaction(current_date, reference_date)
|
||||||
current_date = recurring_transaction.get_next_date(current_date)
|
current_date = recurring_transaction.get_next_date(current_date)
|
||||||
reference_date = recurring_transaction.get_next_date(reference_date)
|
reference_date = recurring_transaction.get_next_date(reference_date)
|
||||||
@@ -869,3 +891,86 @@ 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"))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -307,4 +307,39 @@ 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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,36 +81,55 @@ 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 (
|
||||||
total[field] == Decimal("0")
|
ignore_empty
|
||||||
for field in [
|
and not deep_search
|
||||||
"expense_current",
|
and all(
|
||||||
"expense_projected",
|
total[field] == Decimal("0")
|
||||||
"income_current",
|
for field in [
|
||||||
"income_projected",
|
"expense_current",
|
||||||
]
|
"expense_projected",
|
||||||
|
"income_current",
|
||||||
|
"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,142 @@ 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"],
|
|
||||||
"total_projected": currency_data["total_projected"],
|
|
||||||
"total_final": currency_data["total_final"],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add exchanged values from all currencies using this as exchange currency
|
while queue:
|
||||||
for using_currency in currencies_using_exchange[currency_id]:
|
target_id = queue.pop(0)
|
||||||
exchanged = using_currency["exchanged"]
|
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"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# If this newly added transaction-less currency ALSO has an exchange_currency set for itself
|
||||||
|
if db_currency.exchange_currency:
|
||||||
|
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 [
|
||||||
|
"expense_current",
|
||||||
|
"expense_projected",
|
||||||
|
"income_current",
|
||||||
|
"income_projected",
|
||||||
|
"total_current",
|
||||||
|
"total_projected",
|
||||||
|
"total_final",
|
||||||
|
]:
|
||||||
|
converted_val, _, _, _ = convert(
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
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 [
|
for field in [
|
||||||
"expense_current",
|
"expense_current",
|
||||||
"expense_projected",
|
"expense_projected",
|
||||||
@@ -185,10 +312,25 @@ def calculate_currency_totals(transactions_queryset, ignore_empty=False):
|
|||||||
"total_projected",
|
"total_projected",
|
||||||
"total_final",
|
"total_final",
|
||||||
]:
|
]:
|
||||||
if field in exchanged:
|
if field in exchanged_values_from_original:
|
||||||
consolidated[field] += exchanged[field]
|
consolidated_data[field] += exchanged_values_from_original[
|
||||||
|
field
|
||||||
|
]
|
||||||
|
|
||||||
result[currency_id]["consolidated"] = consolidated
|
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 *
|
||||||
|
|||||||
154
app/apps/transactions/views/quick_transactions.py
Normal file
154
app/apps/transactions/views/quick_transactions.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -43,16 +43,71 @@ def transaction_add(request):
|
|||||||
year=year,
|
year=year,
|
||||||
).date()
|
).date()
|
||||||
|
|
||||||
|
update = False
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = TransactionForm(request.POST)
|
form = TransactionForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
saved_instance = form.save()
|
||||||
messages.success(request, _("Transaction added successfully"))
|
messages.success(request, _("Transaction added successfully"))
|
||||||
|
|
||||||
return HttpResponse(
|
if "submit" in request.POST:
|
||||||
status=204,
|
return HttpResponse(
|
||||||
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
status=204,
|
||||||
)
|
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||||
|
)
|
||||||
|
elif "submit_and_another" in request.POST:
|
||||||
|
form = TransactionForm(
|
||||||
|
initial={
|
||||||
|
"date": expected_date,
|
||||||
|
"type": transaction_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
update = True
|
||||||
|
elif "submit_and_similar" in request.POST:
|
||||||
|
initial_data = {}
|
||||||
|
|
||||||
|
# Define fields to copy from the SAVED instance
|
||||||
|
direct_fields_to_copy = [
|
||||||
|
"account", # ForeignKey -> will copy the ID
|
||||||
|
"type", # ChoiceField -> will copy the value
|
||||||
|
"is_paid", # BooleanField -> will copy True/False
|
||||||
|
"date", # DateField -> will copy the date object
|
||||||
|
"reference_date", # DateField -> will copy the date object
|
||||||
|
"amount", # DecimalField -> will copy the decimal
|
||||||
|
"description", # CharField -> will copy the string
|
||||||
|
"notes", # TextField -> will copy the string
|
||||||
|
"category", # ForeignKey -> will copy the ID
|
||||||
|
]
|
||||||
|
m2m_fields_to_copy = [
|
||||||
|
"tags", # ManyToManyField -> will copy list of IDs
|
||||||
|
"entities", # ManyToManyField -> will copy list of IDs
|
||||||
|
]
|
||||||
|
|
||||||
|
# Copy direct fields from the saved instance
|
||||||
|
for field_name in direct_fields_to_copy:
|
||||||
|
value = getattr(saved_instance, field_name, None)
|
||||||
|
if value is not None:
|
||||||
|
# Handle ForeignKey: use the pk
|
||||||
|
if hasattr(value, "pk"):
|
||||||
|
initial_data[field_name] = value.pk
|
||||||
|
# Handle Date/DateTime/Decimal/Boolean/etc.: use the Python object directly
|
||||||
|
else:
|
||||||
|
initial_data[field_name] = (
|
||||||
|
value # This correctly handles date objects!
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy M2M fields: provide a list of related object pks
|
||||||
|
for field_name in m2m_fields_to_copy:
|
||||||
|
m2m_manager = getattr(saved_instance, field_name)
|
||||||
|
initial_data[field_name] = list(
|
||||||
|
m2m_manager.values_list("name", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new form instance pre-filled with the correctly typed initial data
|
||||||
|
form = TransactionForm(initial=initial_data)
|
||||||
|
update = True # Signal HTMX to update the form area
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = TransactionForm(
|
form = TransactionForm(
|
||||||
initial={
|
initial={
|
||||||
@@ -61,11 +116,15 @@ def transaction_add(request):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(
|
response = render(
|
||||||
request,
|
request,
|
||||||
"transactions/fragments/add.html",
|
"transactions/fragments/add.html",
|
||||||
{"form": form},
|
{"form": form},
|
||||||
)
|
)
|
||||||
|
if update:
|
||||||
|
response["HX-Trigger"] = "updated"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ 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
|
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.forms import (
|
from django.contrib.auth.forms import (
|
||||||
UsernameField,
|
UsernameField,
|
||||||
AuthenticationForm,
|
AuthenticationForm,
|
||||||
|
UserCreationForm,
|
||||||
)
|
)
|
||||||
|
from django.db import transaction
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||||
from apps.users.models import UserSettings
|
from apps.users.models import UserSettings
|
||||||
|
from apps.common.middleware.thread_local import get_current_user
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(AuthenticationForm):
|
class LoginForm(AuthenticationForm):
|
||||||
@@ -111,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):
|
||||||
@@ -122,13 +127,283 @@ 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"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.fields["language"].help_text = _(
|
||||||
|
"This changes the language (if available) and how numbers and dates are displayed\n"
|
||||||
|
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
|
||||||
|
) % {
|
||||||
|
"translation_link": '<a href="https://translations.herculino.com" target="_blank">translations.herculino.com</a>'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdateForm(forms.ModelForm):
|
||||||
|
new_password1 = forms.CharField(
|
||||||
|
label=_("New Password"),
|
||||||
|
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
||||||
|
required=False,
|
||||||
|
help_text=_("Leave blank to keep the current password."),
|
||||||
|
)
|
||||||
|
new_password2 = forms.CharField(
|
||||||
|
label=_("Confirm New Password"),
|
||||||
|
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = get_user_model()
|
||||||
|
# Add the administrative fields
|
||||||
|
fields = ["first_name", "last_name", "email", "is_active", "is_superuser"]
|
||||||
|
# Help texts can be defined here or directly in the layout/field definition
|
||||||
|
help_texts = {
|
||||||
|
"is_active": _(
|
||||||
|
"Designates whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
|
),
|
||||||
|
"is_superuser": _(
|
||||||
|
"Designates that this user has all permissions without explicitly assigning them."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.instance = kwargs.get("instance") # Store instance for validation/checks
|
||||||
|
self.requesting_user = get_current_user()
|
||||||
|
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_tag = False
|
||||||
|
self.helper.form_method = "post"
|
||||||
|
|
||||||
|
# Define the layout using Crispy Forms, including the new fields
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Row(
|
||||||
|
Column("first_name", css_class="form-group col-md-6"),
|
||||||
|
Column("last_name", css_class="form-group col-md-6"),
|
||||||
|
css_class="row",
|
||||||
|
),
|
||||||
|
Field("email"),
|
||||||
|
# Group password fields (optional visual grouping)
|
||||||
|
Div(
|
||||||
|
Field("new_password1"),
|
||||||
|
Field("new_password2"),
|
||||||
|
css_class="border p-3 rounded mb-3",
|
||||||
|
),
|
||||||
|
# Group administrative status fields
|
||||||
|
Div(
|
||||||
|
Field("is_active"),
|
||||||
|
Field("is_superuser"),
|
||||||
|
css_class="border p-3 rounded mb-3 text-bg-secondary", # Example visual separation
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.requesting_user == self.instance
|
||||||
|
or not self.requesting_user.is_superuser
|
||||||
|
):
|
||||||
|
self.fields["is_superuser"].disabled = True
|
||||||
|
self.fields["is_active"].disabled = True
|
||||||
|
|
||||||
|
# Keep existing clean methods
|
||||||
|
def clean_email(self):
|
||||||
|
email = self.cleaned_data.get("email")
|
||||||
|
# Use case-insensitive comparison for email uniqueness check
|
||||||
|
if (
|
||||||
|
self.instance
|
||||||
|
and get_user_model()
|
||||||
|
.objects.filter(email__iexact=email)
|
||||||
|
.exclude(pk=self.instance.pk)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("This email address is already in use by another account.")
|
||||||
|
)
|
||||||
|
return email
|
||||||
|
|
||||||
|
def clean_new_password2(self):
|
||||||
|
new_password1 = self.cleaned_data.get("new_password1")
|
||||||
|
new_password2 = self.cleaned_data.get("new_password2")
|
||||||
|
if new_password1 and new_password1 != new_password2:
|
||||||
|
raise forms.ValidationError(_("The two password fields didn't match."))
|
||||||
|
if new_password1 and not new_password2:
|
||||||
|
raise forms.ValidationError(_("Please confirm your new password."))
|
||||||
|
if new_password2 and not new_password1:
|
||||||
|
raise forms.ValidationError(_("Please enter the new password first."))
|
||||||
|
return new_password2
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
is_active_val = cleaned_data.get("is_active")
|
||||||
|
is_superuser_val = cleaned_data.get("is_superuser")
|
||||||
|
|
||||||
|
# --- Crucial Security Check Example ---
|
||||||
|
# Prevent the requesting user from deactivating or removing superuser status
|
||||||
|
# from their *own* account via this form.
|
||||||
|
if (
|
||||||
|
self.requesting_user
|
||||||
|
and self.instance
|
||||||
|
and self.requesting_user.pk == self.instance.pk
|
||||||
|
):
|
||||||
|
# Check if 'is_active' field exists and user is trying to set it to False
|
||||||
|
if "is_active" in self.fields and is_active_val is False:
|
||||||
|
self.add_error(
|
||||||
|
"is_active",
|
||||||
|
_("You cannot deactivate your own account using this form."),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if 'is_superuser' field exists, the user *is* currently a superuser,
|
||||||
|
# and they are trying to set it to False
|
||||||
|
if (
|
||||||
|
"is_superuser" in self.fields
|
||||||
|
and self.instance.is_superuser
|
||||||
|
and is_superuser_val is False
|
||||||
|
):
|
||||||
|
if get_user_model().objects.filter(is_superuser=True).count() <= 1:
|
||||||
|
self.add_error(
|
||||||
|
"is_superuser",
|
||||||
|
_("Cannot remove status from the last superuser."),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.add_error(
|
||||||
|
"is_superuser",
|
||||||
|
_(
|
||||||
|
"You cannot remove your own superuser status using this form."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
# Save method remains the same, ModelForm handles boolean fields correctly
|
||||||
|
def save(self, commit=True):
|
||||||
|
user = super().save(commit=False)
|
||||||
|
new_password = self.cleaned_data.get("new_password1")
|
||||||
|
if new_password:
|
||||||
|
user.set_password(new_password)
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class UserAddForm(UserCreationForm):
|
||||||
|
"""
|
||||||
|
A form for administrators to create new users.
|
||||||
|
Includes fields for first name, last name, email, active status,
|
||||||
|
and superuser status. Uses email as the username field.
|
||||||
|
Inherits password handling from UserCreationForm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta(UserCreationForm.Meta):
|
||||||
|
model = get_user_model()
|
||||||
|
# Specify the fields to include. UserCreationForm automatically handles
|
||||||
|
# 'password1' and 'password2'. We replace 'username' with 'email'.
|
||||||
|
fields = ("email", "first_name", "last_name", "is_active", "is_superuser")
|
||||||
|
field_classes = {
|
||||||
|
"email": forms.EmailField
|
||||||
|
} # Ensure email field uses EmailField validation
|
||||||
|
help_texts = {
|
||||||
|
"is_active": _(
|
||||||
|
"Designates whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
|
),
|
||||||
|
"is_superuser": _(
|
||||||
|
"Designates that this user has all permissions without explicitly assigning them."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Set is_active to True by default for new users, can be overridden by admin
|
||||||
|
self.fields["is_active"].initial = True
|
||||||
|
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_tag = False
|
||||||
|
self.helper.form_method = "post"
|
||||||
|
|
||||||
|
# Define the layout, including password fields from UserCreationForm
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Field("email"),
|
||||||
|
Row(
|
||||||
|
Column("first_name", css_class="form-group col-md-6"),
|
||||||
|
Column("last_name", css_class="form-group col-md-6"),
|
||||||
|
css_class="row",
|
||||||
|
),
|
||||||
|
# UserCreationForm provides 'password1' and 'password2' fields
|
||||||
|
Div(
|
||||||
|
Field("password1", autocomplete="new-password"),
|
||||||
|
Field("password2", autocomplete="new-password"),
|
||||||
|
css_class="border p-3 rounded mb-3",
|
||||||
|
),
|
||||||
|
# Administrative status fields
|
||||||
|
Div(
|
||||||
|
Field("is_active"),
|
||||||
|
Field("is_superuser"),
|
||||||
|
css_class="border p-3 rounded mb-3 text-bg-secondary",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.helper.layout.append(
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
"""Ensure email uniqueness (case-insensitive)."""
|
||||||
|
email = self.cleaned_data.get("email")
|
||||||
|
if email and get_user_model().objects.filter(email__iexact=email).exists():
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("A user with this email address already exists.")
|
||||||
|
)
|
||||||
|
return email
|
||||||
|
|
||||||
|
@transaction.atomic # Ensure user creation is atomic
|
||||||
|
def save(self, commit=True):
|
||||||
|
"""
|
||||||
|
Save the user instance. UserCreationForm's save handles password hashing.
|
||||||
|
Our Meta class ensures other fields are included.
|
||||||
|
"""
|
||||||
|
user = super().save(commit=False)
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.8 on 2025-04-13 03:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0019_alter_usersettings_language'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='usersettings',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('auto', 'Auto'), ('af', 'Afrikaans'), ('ar', 'العربية'), ('ar-dz', 'العربية (الجزائر)'), ('ast', 'Asturianu'), ('az', 'Azərbaycan'), ('bg', 'Български'), ('be', 'Беларуская'), ('bn', 'বাংলা'), ('br', 'Brezhoneg'), ('bs', 'Bosanski'), ('ca', 'Català'), ('ckb', 'کوردیی ناوەندی'), ('cs', 'Čeština'), ('cy', 'Cymraeg'), ('da', 'Dansk'), ('de', 'Deutsch'), ('dsb', 'Dolnoserbšćina'), ('el', 'Ελληνικά'), ('en', 'English'), ('en-au', 'English (Australia)'), ('en-gb', 'English (UK)'), ('eo', 'Esperanto'), ('es', 'Español'), ('es-ar', 'Español (Argentina)'), ('es-co', 'Español (Colombia)'), ('es-mx', 'Español (México)'), ('es-ni', 'Español (Nicaragua)'), ('es-ve', 'Español (Venezuela)'), ('et', 'Eesti'), ('eu', 'Euskara'), ('fa', 'فارسی'), ('fi', 'Suomi'), ('fr', 'Français'), ('fy', 'Frysk'), ('ga', 'Gaeilge'), ('gd', 'Gàidhlig'), ('gl', 'Galego'), ('he', 'עברית'), ('hi', 'हिन्दी'), ('hr', 'Hrvatski'), ('hsb', 'Hornjoserbšćina'), ('hu', 'Magyar'), ('hy', 'Հայերեն'), ('ia', 'Interlingua'), ('id', 'Bahasa Indonesia'), ('ig', 'Igbo'), ('io', 'Ido'), ('is', 'Íslenska'), ('it', 'Italiano'), ('ja', '日本語'), ('ka', 'ქართული'), ('kab', 'Taqbaylit'), ('kk', 'Қазақша'), ('km', 'ខ្មែរ'), ('kn', 'ಕನ್ನಡ'), ('ko', '한국어'), ('ky', 'Кыргызча'), ('lb', 'Lëtzebuergesch'), ('lt', 'Lietuvių'), ('lv', 'Latviešu'), ('mk', 'Македонски'), ('ml', 'മലയാളം'), ('mn', 'Монгол'), ('mr', 'मराठी'), ('ms', 'Bahasa Melayu'), ('my', 'မြန်မာဘာသာ'), ('nb', 'Norsk (Bokmål)'), ('ne', 'नेपाली'), ('nl', 'Nederlands'), ('nn', 'Norsk (Nynorsk)'), ('os', 'Ирон'), ('pa', 'ਪੰਜਾਬੀ'), ('pl', 'Polski'), ('pt', 'Português'), ('pt-br', 'Português (Brasil)'), ('ro', 'Română'), ('ru', 'Русский'), ('sk', 'Slovenčina'), ('sl', 'Slovenščina'), ('sq', 'Shqip'), ('sr', 'Српски'), ('sr-latn', 'Srpski (Latinica)'), ('sv', 'Svenska'), ('sw', 'Kiswahili'), ('ta', 'தமிழ்'), ('te', 'తెలుగు'), ('tg', 'Тоҷикӣ'), ('th', 'ไทย'), ('tk', 'Türkmençe'), ('tr', 'Türkçe'), ('tt', 'Татарча'), ('udm', 'Удмурт'), ('ug', 'ئۇيغۇرچە'), ('uk', 'Українська'), ('ur', 'اردو'), ('uz', 'Oʻzbekcha'), ('vi', 'Tiếng Việt'), ('zh-hans', '简体中文'), ('zh-hant', '繁體中文')], default='auto', max_length=10, verbose_name='Language'),
|
||||||
|
),
|
||||||
|
]
|
||||||
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
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,4 +22,24 @@ urlpatterns = [
|
|||||||
views.update_settings,
|
views.update_settings,
|
||||||
name="user_settings",
|
name="user_settings",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"users/",
|
||||||
|
views.users_index,
|
||||||
|
name="users_index",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/list/",
|
||||||
|
views.users_list,
|
||||||
|
name="users_list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"user/add/",
|
||||||
|
views.user_add,
|
||||||
|
name="user_add",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"user/<int:pk>/edit/",
|
||||||
|
views.user_edit,
|
||||||
|
name="user_edit",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import logout
|
from django.contrib.auth import logout, get_user_model
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.views import (
|
from django.contrib.auth.views import (
|
||||||
LoginView,
|
LoginView,
|
||||||
)
|
)
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render, get_object_or_404
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.common.decorators.user import is_superuser, htmx_login_required
|
||||||
from apps.users.forms import (
|
from apps.users.forms import (
|
||||||
LoginForm,
|
LoginForm,
|
||||||
UserSettingsForm,
|
UserSettingsForm,
|
||||||
|
UserUpdateForm,
|
||||||
|
UserAddForm,
|
||||||
)
|
)
|
||||||
from apps.common.decorators.htmx import only_htmx
|
|
||||||
from apps.users.models import UserSettings
|
from apps.users.models import UserSettings
|
||||||
|
from apps.common.decorators.demo import disabled_on_demo
|
||||||
|
|
||||||
|
|
||||||
def logout_view(request):
|
def logout_view(request):
|
||||||
@@ -22,7 +28,7 @@ def logout_view(request):
|
|||||||
return redirect(reverse("login"))
|
return redirect(reverse("login"))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@htmx_login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
if request.user.settings.start_page == UserSettings.StartPage.MONTHLY:
|
if request.user.settings.start_page == UserSettings.StartPage.MONTHLY:
|
||||||
return redirect(reverse("monthly_index"))
|
return redirect(reverse("monthly_index"))
|
||||||
@@ -49,7 +55,7 @@ class UserLoginView(LoginView):
|
|||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@htmx_login_required
|
||||||
def toggle_amount_visibility(request):
|
def toggle_amount_visibility(request):
|
||||||
user_settings, created = UserSettings.objects.get_or_create(user=request.user)
|
user_settings, created = UserSettings.objects.get_or_create(user=request.user)
|
||||||
current_hide_amounts = user_settings.hide_amounts
|
current_hide_amounts = user_settings.hide_amounts
|
||||||
@@ -70,7 +76,7 @@ def toggle_amount_visibility(request):
|
|||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@htmx_login_required
|
||||||
def toggle_sound_playing(request):
|
def toggle_sound_playing(request):
|
||||||
user_settings, created = UserSettings.objects.get_or_create(user=request.user)
|
user_settings, created = UserSettings.objects.get_or_create(user=request.user)
|
||||||
current_mute_sounds = user_settings.mute_sounds
|
current_mute_sounds = user_settings.mute_sounds
|
||||||
@@ -91,7 +97,7 @@ def toggle_sound_playing(request):
|
|||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@htmx_login_required
|
||||||
def update_settings(request):
|
def update_settings(request):
|
||||||
user_settings = request.user.settings
|
user_settings = request.user.settings
|
||||||
|
|
||||||
@@ -108,3 +114,86 @@ def update_settings(request):
|
|||||||
form = UserSettingsForm(instance=user_settings)
|
form = UserSettingsForm(instance=user_settings)
|
||||||
|
|
||||||
return render(request, "users/fragments/user_settings.html", {"form": form})
|
return render(request, "users/fragments/user_settings.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@htmx_login_required
|
||||||
|
@is_superuser
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def users_index(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"users/pages/index.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@htmx_login_required
|
||||||
|
@is_superuser
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def users_list(request):
|
||||||
|
users = get_user_model().objects.all().order_by("id")
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"users/fragments/list.html",
|
||||||
|
{"users": users},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@htmx_login_required
|
||||||
|
@is_superuser
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def user_add(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = UserAddForm(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 = UserAddForm()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"users/fragments/add.html",
|
||||||
|
{"form": form},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@htmx_login_required
|
||||||
|
@disabled_on_demo
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def user_edit(request, pk):
|
||||||
|
user = get_object_or_404(get_user_model(), id=pk)
|
||||||
|
|
||||||
|
if not request.user.is_superuser and user != request.user:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = UserUpdateForm(request.POST, instance=user)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Item updated successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated, hide_offcanvas",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = UserUpdateForm(instance=user)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"users/fragments/edit.html",
|
||||||
|
{"form": form, "user": user},
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3777
app/locale/es/LC_MESSAGES/django.po
Normal file
3777
app/locale/es/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3541
app/locale/pt/LC_MESSAGES/django.po
Normal file
3541
app/locale/pt/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3238
app/locale/sv/LC_MESSAGES/django.po
Normal file
3238
app/locale/sv/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
3253
app/locale/uk/LC_MESSAGES/django.po
Normal file
3253
app/locale/uk/LC_MESSAGES/django.po
Normal file
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,45 +13,47 @@
|
|||||||
{% 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"
|
||||||
hx-trigger="click, previous_month from:window"
|
hx-trigger="click, previous_month from:window"
|
||||||
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 class="tw:text-3xl fw-bold font-monospace tw:w-full text-center"
|
||||||
|
hx-get="{% url 'month_year_picker' %}"
|
||||||
|
hx-target="#generic-offcanvas-left"
|
||||||
|
hx-trigger="click, date_picker from:window"
|
||||||
|
hx-vals='{"month": {{ month }}, "year": {{ year }}, "for": "calendar", "field": "date"}' role="button">
|
||||||
|
{{ month|month_name }} {{ year }}
|
||||||
|
</div>
|
||||||
|
<div class="tw:text-base mx-2 h-100 align-items-center d-flex">
|
||||||
|
<a role="button"
|
||||||
|
class="ps-3 py-2"
|
||||||
|
hx-boost="true"
|
||||||
|
hx-trigger="click, next_month from:window"
|
||||||
|
href="{% url 'calendar' month=next_month year=next_year %}">
|
||||||
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"
|
{# Action buttons#}
|
||||||
hx-get="{% url 'month_year_picker' %}"
|
<div class="col-12 col-xl-8">
|
||||||
hx-target="#generic-offcanvas-left"
|
{# <c-ui.quick-transactions-buttons#}
|
||||||
hx-trigger="click, date_picker from:window"
|
{# :year="year"#}
|
||||||
hx-vals='{"month": {{ month }}, "year": {{ year }}, "for": "calendar", "field": "date"}' role="button">
|
{# :month="month"#}
|
||||||
{{ month|month_name }} {{ year }}
|
{# ></c-ui.quick-transactions-buttons>#}
|
||||||
</div>
|
|
||||||
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
|
|
||||||
<a role="button"
|
|
||||||
class="ps-3 py-2"
|
|
||||||
hx-boost="true"
|
|
||||||
hx-trigger="click, next_month from:window"
|
|
||||||
href="{% url 'calendar' month=next_month year=next_year %}">
|
|
||||||
<i class="fa-solid fa-chevron-right"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{# Action buttons#}
|
<div class="row">
|
||||||
<div class="col-12 col-xl-8">
|
<div class="show-loading" hx-get="{% url 'calendar_list' month=month year=year %}"
|
||||||
<c-ui.quick-transactions-buttons
|
hx-trigger="load, updated from:window, selective_update from:window, every 10m"></div>
|
||||||
:year="year"
|
|
||||||
:month="month"
|
|
||||||
></c-ui.quick-transactions-buttons>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
{% 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,10 +3,10 @@
|
|||||||
{% 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>
|
||||||
{% if not divless %}
|
{% if not divless %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
33
app/templates/cotton/components/fab.html
Normal file
33
app/templates/cotton/components/fab.html
Normal file
@@ -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">
|
||||||
|
<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>
|
||||||
11
app/templates/cotton/components/fab_menu_button.html
Normal file
11
app/templates/cotton/components/fab_menu_button.html
Normal file
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -8,16 +8,17 @@
|
|||||||
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
|
_="on mouseover remove .{'tw:invisible'} from the first .transaction-actions in me end
|
||||||
on mouseout add .tw-invisible to the first .transaction-actions in me end">
|
on mouseout add .{'tw:invisible'} to the first .transaction-actions in me end">
|
||||||
<div class="row font-monospace tw-text-sm align-items-center">
|
<div class="row font-monospace tw:text-sm align-items-center">
|
||||||
<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">
|
<div
|
||||||
|
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">
|
||||||
{% 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,7 +28,7 @@
|
|||||||
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 %}
|
||||||
@@ -36,13 +37,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg col-12">
|
<div class="col-lg col-12">
|
||||||
{# 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 %}
|
||||||
@@ -50,18 +51,18 @@
|
|||||||
class="badge text-bg-secondary">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
|
class="badge text-bg-secondary">{{ 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"><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">{% 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 +70,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 +85,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>
|
||||||
@@ -121,7 +122,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 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"
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -91,4 +91,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
<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>
|
||||||
</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
|
||||||
|
|||||||
8
app/templates/cotton/ui/help_icon.html
Normal file
8
app/templates/cotton/ui/help_icon.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{% spaceless %}
|
||||||
|
{% load i18n %}
|
||||||
|
<span class="tw:text-xs text-white-50 mx-1"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{{ content }}">
|
||||||
|
<i class="{% if not icon %}fa-solid fa-circle-question{% else %}{{ icon }}{% endif %} fa-fw"></i>
|
||||||
|
</span>
|
||||||
|
{% endspaceless %}
|
||||||
@@ -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 %}{% include 'includes/help_icon.html' with content=help_text %}{% 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:0 }}%">
|
<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:0 }}%">
|
<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:0 }}%">
|
<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:0 }}%">
|
<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,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 .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' %}"
|
||||||
@@ -56,17 +56,17 @@
|
|||||||
|
|
||||||
<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 +91,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)
|
||||||
@@ -149,10 +149,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 +169,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 +189,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 +209,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 +229,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 +249,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
|
||||||
|
|||||||
60
app/templates/cotton/ui/transactions_fab.html
Normal file
60
app/templates/cotton/ui/transactions_fab.html
Normal file
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{% spaceless %}
|
|
||||||
<span class="tw-text-xs text-white-50 mx-1"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-title="{{ content }}">
|
|
||||||
<i class="fa-solid fa-circle-question fa-fw"></i>
|
|
||||||
</span>
|
|
||||||
{% endspaceless %}
|
|
||||||
@@ -12,7 +12,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 +50,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 +68,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' %}"
|
||||||
@@ -94,7 +96,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle {% 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' %}"
|
<a class="nav-link dropdown-toggle {% 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' %}"
|
||||||
href="#" role="button"
|
href="#" role="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
@@ -138,9 +140,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a class="dropdown-item {% active_link views='automatic_exchange_rates_index' %}"
|
<li><a class="dropdown-item {% active_link views='automatic_exchange_rates_index' %}"
|
||||||
href="{% url 'automatic_exchange_rates_index' %}">{% translate 'Automatic Exchange Rates' %}</a></li>
|
href="{% url 'automatic_exchange_rates_index' %}">{% translate 'Automatic Exchange Rates' %}</a></li>
|
||||||
|
{% if user.is_superuser %}
|
||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
|
<li><h6 class="dropdown-header">{% trans 'Admin' %}</h6></li>
|
||||||
|
<li><a class="dropdown-item {% active_link views='users_index' %}"
|
||||||
|
href="{% url 'users_index' %}">{% translate 'Users' %}</a></li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{% url 'admin:index' %}"
|
href="{% url 'admin:index' %}"
|
||||||
@@ -151,19 +157,20 @@
|
|||||||
{% translate 'Django Admin' %}
|
{% translate 'Django Admin' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</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">
|
<li class="nav-item">
|
||||||
<a class="nav-item tw-text-2xl tw-cursor-pointer me-lg-4"
|
<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,9 +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="nav-link tw:lg:text-2xl! tw:cursor-pointer" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fa-solid fa-user"></i>
|
<i class="fa-solid fa-user"></i>
|
||||||
</a>
|
<span class="d-lg-none d-inline">{% trans "Profile" %}</span>
|
||||||
|
</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 class="dropdown-item-text">{{ user.email }}</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
@@ -12,6 +13,11 @@
|
|||||||
hx-target="#generic-offcanvas"
|
hx-target="#generic-offcanvas"
|
||||||
role="button">
|
role="button">
|
||||||
<i class="fa-solid fa-gear me-2 fa-fw"></i>{% translate 'Settings' %}</a></li>
|
<i class="fa-solid fa-gear me-2 fa-fw"></i>{% translate 'Settings' %}</a></li>
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
hx-get="{% url 'user_edit' pk=request.user.id %}"
|
||||||
|
hx-target="#generic-offcanvas"
|
||||||
|
role="button">
|
||||||
|
<i class="fa-solid fa-user me-2 fa-fw"></i>{% translate 'Edit profile' %}</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{% load webpack_loader %}
|
{% load webpack_loader %}
|
||||||
|
|
||||||
{% stylesheet_pack 'style' %}
|
{% stylesheet_pack 'style' %}
|
||||||
{#{% stylesheet_pack 'select' %}#}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<div id="toasts">
|
<div id="toasts">
|
||||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" hx-trigger="load, updated from:window, toasts from:window" hx-get="{% url 'toasts' %}" hx-swap="beforeend">
|
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3" hx-trigger="load, updated from:window, toasts from:window" hx-get="{% url 'toasts' %}" hx-swap="beforeend">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% load formats %}
|
{% load formats %}
|
||||||
<div class="tw-hidden tw-w-[60vw] lg:tw-w-[30vw] xl:tw-w-[20vw] position-fixed shadow rounded-3 bg-body tw-border-gray-700 tw-border tw-border-solid tw-text-center tw-align-middle tw-z-[2000] tw-touch-none user-select-none"
|
<div class="tw:hidden tw:w-[60vw] tw:lg:w-[30vw] tw:xl:w-[20vw] position-fixed shadow rounded-3 bg-body tw:border-gray-700 tw:border tw:border-solid tw:text-center tw:align-middle tw:z-[2000] tw:touch-none user-select-none"
|
||||||
id="calculator"
|
id="calculator"
|
||||||
hx-preserve
|
hx-preserve
|
||||||
_="
|
_="
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
on focusin halt the event end -- this prevents bootstrap's static offcanvas from hijacking the focus from the input when open end
|
on focusin halt the event end -- this prevents bootstrap's static offcanvas from hijacking the focus from the input when open end
|
||||||
|
|
||||||
on show or keyup[code is 'KeyC' and altKey is true] from body
|
on show or keyup[code is 'KeyC' and altKey is true] from body
|
||||||
if my.classList.contains('tw-hidden')
|
if my.classList.contains('tw:hidden')
|
||||||
remove .tw-hidden from me
|
remove .{'tw:hidden'} from me
|
||||||
measure my width, height
|
measure my width, height
|
||||||
set xoff to (window.innerWidth/2) - (width/2)
|
set xoff to (window.innerWidth/2) - (width/2)
|
||||||
set yoff to (window.innerHeight/2) - (height)
|
set yoff to (window.innerHeight/2) - (height)
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
then call #calculator-input.focus()
|
then call #calculator-input.focus()
|
||||||
else
|
else
|
||||||
add .scale-out-center to me then wait for animationend then remove .scale-out-center from me
|
add .scale-out-center to me then wait for animationend then remove .scale-out-center from me
|
||||||
add .tw-hidden to me
|
add .{'tw:hidden'} to me
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
end">
|
end">
|
||||||
|
|
||||||
<div id="calculator-handle"
|
<div id="calculator-handle"
|
||||||
class="position-absolute bg-secondary rounded-top-2 tw-cursor-move d-flex align-items-center justify-content-center tw-top-[-20px] tw-left-[3px] tw-w-[2em] tw-h-[20px]">
|
class="position-absolute bg-secondary rounded-top-2 tw:cursor-move d-flex align-items-center justify-content-center tw:top-[-20px] tw:left-[3px] tw:w-[2em] tw:h-[20px]">
|
||||||
<i class="fa-solid fa-grip"></i>
|
<i class="fa-solid fa-grip"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,31 +73,31 @@
|
|||||||
end
|
end
|
||||||
then set localizedResult to it
|
then set localizedResult to it
|
||||||
set #calculator-result.innerText to localizedResult
|
set #calculator-result.innerText to localizedResult
|
||||||
then remove .tw-hidden from #calculator-result-container
|
then remove .{'tw:hidden'} from #calculator-result-container
|
||||||
then add .swing-in-top-fwd to #calculator-result-container
|
then add .swing-in-top-fwd to #calculator-result-container
|
||||||
then settle
|
then settle
|
||||||
then remove .swing-in-top-fwd from #calculator-result-container
|
then remove .swing-in-top-fwd from #calculator-result-container
|
||||||
else
|
else
|
||||||
add .swing-out-top-bck to #calculator-result-container
|
add .swing-out-top-bck to #calculator-result-container
|
||||||
then settle
|
then settle
|
||||||
then add .tw-hidden to #calculator-result-container
|
then add .{'tw:hidden'} to #calculator-result-container
|
||||||
then remove .swing-out-top-bck from #calculator-result-container
|
then remove .swing-out-top-bck from #calculator-result-container
|
||||||
end
|
end
|
||||||
catch e
|
catch e
|
||||||
add .swing-out-top-bck to #calculator-result-container
|
add .swing-out-top-bck to #calculator-result-container
|
||||||
then settle
|
then settle
|
||||||
then add .tw-hidden to #calculator-result-container
|
then add .{'tw:hidden'} to #calculator-result-container
|
||||||
then remove .swing-out-top-bck from #calculator-result-container
|
then remove .swing-out-top-bck from #calculator-result-container
|
||||||
end"
|
end"
|
||||||
placeholder="2 + 2">
|
placeholder="2 + 2">
|
||||||
<div class="tw-hidden" id="calculator-result-container">
|
<div class="tw:hidden" id="calculator-result-container">
|
||||||
<div class="d-flex flex-row p-2 justify-content-between">
|
<div class="d-flex flex-row p-2 justify-content-between">
|
||||||
<div class="tw-text-gray-400">=</div>
|
<div class="tw:text-gray-400">=</div>
|
||||||
<div id="calculator-result" class="user-select-all"></div>
|
<div id="calculator-result" class="user-select-all"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="position-absolute tw-cursor-pointer top-0 start-100 translate-middle tw-p-0 text-bg-primary border border-light rounded-circle tw-flex tw-items-center tw-justify-center tw-w-5 tw-h-5"
|
<div class="position-absolute tw:cursor-pointer top-0 start-100 translate-middle tw:p-0 text-bg-primary border border-light rounded-circle tw:flex tw:items-center tw:justify-center tw:w-5 tw:h-5"
|
||||||
_="on click trigger show on #calculator">
|
_="on click trigger show on #calculator">
|
||||||
<i class="fa-solid fa-xmark tw-flex tw-items-center tw-justify-center tw-w-full tw-h-full"></i>
|
<i class="fa-solid fa-xmark tw:flex tw:items-center tw:justify-center tw:w-full tw:h-full"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,68 +1,431 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div hx-get="{% url 'category_overview' %}" hx-trigger="updated from:window" class="show-loading" hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
|
<div hx-get="{% url 'category_overview' %}" hx-trigger="updated from:window" class="show-loading" hx-swap="outerHTML"
|
||||||
{% if total_table %}
|
hx-include="#picker-form, #picker-type, #view-type, #show-tags, #showing">
|
||||||
<div class="table-responsive">
|
<div class="h-100 text-center mb-4">
|
||||||
<table class="table table-striped table-hover">
|
<div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated">
|
||||||
<thead>
|
<input type="radio" class="btn-check"
|
||||||
<tr>
|
name="view_type"
|
||||||
<th scope="col">{% trans 'Category' %}</th>
|
id="table-view"
|
||||||
<th scope="col">{% trans 'Income' %}</th>
|
autocomplete="off"
|
||||||
<th scope="col">{% trans 'Expense' %}</th>
|
value="table"
|
||||||
<th scope="col">{% trans 'Total' %}</th>
|
{% if view_type == "table" %}checked{% endif %}>
|
||||||
</tr>
|
<label class="btn btn-outline-primary rounded-5" for="table-view"><i
|
||||||
</thead>
|
class="fa-solid fa-table fa-fw me-2"></i>{% trans 'Table' %}</label>
|
||||||
<tbody>
|
|
||||||
{% for category in total_table.values %}
|
<input type="radio"
|
||||||
<tr>
|
class="btn-check"
|
||||||
<th>{% if category.name %}{{ category.name }}{% else %}{% trans 'Uncategorized' %}{% endif %}</th>
|
name="view_type"
|
||||||
<td>
|
id="bars-view"
|
||||||
{% for currency in category.currencies.values %}
|
autocomplete="off"
|
||||||
{% if currency.total_income != 0 %}
|
value="bars"
|
||||||
<c-amount.display
|
{% if view_type == "bars" %}checked{% endif %}>
|
||||||
:amount="currency.total_income"
|
<label class="btn btn-outline-primary rounded-5" for="bars-view"><i
|
||||||
:prefix="currency.currency.prefix"
|
class="fa-solid fa-chart-bar fa-fw me-2"></i>{% trans 'Bars' %}</label>
|
||||||
:suffix="currency.currency.suffix"
|
</div>
|
||||||
:decimal_places="currency.currency.decimal_places"
|
|
||||||
color="green"></c-amount.display>
|
|
||||||
{% else %}
|
|
||||||
<div>-</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% for currency in category.currencies.values %}
|
|
||||||
{% if currency.total_expense != 0 %}
|
|
||||||
<c-amount.display
|
|
||||||
:amount="currency.total_expense"
|
|
||||||
:prefix="currency.currency.prefix"
|
|
||||||
:suffix="currency.currency.suffix"
|
|
||||||
:decimal_places="currency.currency.decimal_places"
|
|
||||||
color="red"></c-amount.display>
|
|
||||||
{% else %}
|
|
||||||
<div>-</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% for currency in category.currencies.values %}
|
|
||||||
{% if currency.total_final != 0 %}
|
|
||||||
<c-amount.display
|
|
||||||
:amount="currency.total_final"
|
|
||||||
:prefix="currency.currency.prefix"
|
|
||||||
:suffix="currency.currency.suffix"
|
|
||||||
:decimal_places="currency.currency.decimal_places"
|
|
||||||
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
|
||||||
{% else %}
|
|
||||||
<div>-</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-3 mb-1 d-flex flex-column flex-md-row justify-content-between">
|
||||||
|
<div class="form-check form-switch" id="show-tags">
|
||||||
|
{% if view_type == 'table' %}
|
||||||
|
<input type="hidden" name="show_tags" value="off">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="show-tags-switch" name="show_tags"
|
||||||
|
_="on change trigger updated" {% if show_tags %}checked{% endif %}>
|
||||||
|
{% spaceless %}
|
||||||
|
<label class="form-check-label" for="show-tags-switch">
|
||||||
|
{% trans 'Tags' %}
|
||||||
|
</label>
|
||||||
|
<c-ui.help-icon
|
||||||
|
content="{% trans 'Transaction amounts associated with multiple tags will be counted once for each tag' %}"
|
||||||
|
icon="fa-solid fa-circle-exclamation"></c-ui.help-icon>
|
||||||
|
{% endspaceless %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="showing" _="on change trigger updated">
|
||||||
|
<input type="radio" class="btn-check" name="showing" id="showing-projected" autocomplete="off"
|
||||||
|
value="projected" {% if showing == 'projected' %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-primary" for="showing-projected">{% trans "Projected" %}</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="showing" id="showing-current" autocomplete="off" value="current"
|
||||||
|
{% if showing == 'current' %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-primary" for="showing-current">{% trans "Current" %}</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="showing" id="showing-final" autocomplete="off" value="final"
|
||||||
|
{% if showing == 'final' %}checked{% endif %}>
|
||||||
|
<label class="btn btn-outline-primary" for="showing-final">{% trans "Final total" %}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if total_table %}
|
||||||
|
{% if view_type == "table" %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover table-bordered align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans 'Category' %}</th>
|
||||||
|
<th scope="col">{% trans 'Income' %}</th>
|
||||||
|
<th scope="col">{% trans 'Expense' %}</th>
|
||||||
|
<th scope="col">{% trans 'Total' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% for category in total_table.values %}
|
||||||
|
<!-- Category row -->
|
||||||
|
<tr class="table-group-header">
|
||||||
|
<th>{% if category.name %}{{ category.name }}{% else %}{% trans 'Uncategorized' %}{% endif %}</th>
|
||||||
|
<td> {# income #}
|
||||||
|
{% for currency in category.currencies.values %}
|
||||||
|
{% if showing == 'current' and currency.income_current != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.income_current"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="green"></c-amount.display>
|
||||||
|
{% elif showing == 'projected' and currency.income_projected != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.income_projected"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="green"></c-amount.display>
|
||||||
|
{% elif showing == 'final' and currency.total_income != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_income"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="green"></c-amount.display>
|
||||||
|
{% else %}
|
||||||
|
<div>-</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td> {# expenses #}
|
||||||
|
{% for currency in category.currencies.values %}
|
||||||
|
{% if showing == 'current' and currency.expense_current != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.expense_current"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="red"></c-amount.display>
|
||||||
|
{% elif showing == 'projected' and currency.expense_projected != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.expense_projected"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="red"></c-amount.display>
|
||||||
|
{% elif showing == 'final' and currency.total_expense != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_expense"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="red"></c-amount.display>
|
||||||
|
{% else %}
|
||||||
|
<div>-</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td> {# total #}
|
||||||
|
{% for currency in category.currencies.values %}
|
||||||
|
{% if showing == 'current' and currency.total_current != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_current"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
||||||
|
{% elif showing == 'projected' and currency.total_projected != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_projected"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
||||||
|
{% elif showing == 'final' and currency.total_final != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_final"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
||||||
|
{% else %}
|
||||||
|
<div>-</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Tag rows -->
|
||||||
|
{% if show_tags %}
|
||||||
|
{% for tag_id, tag in category.tags.items %}
|
||||||
|
{% if tag.name or not tag.name and category.tags.values|length > 1 %}
|
||||||
|
<tr class="table-row-nested">
|
||||||
|
<td class="ps-4">
|
||||||
|
<i class="fa-solid fa-hashtag fa-fw me-2 text-muted"></i>{% if tag.name %}{{ tag.name }}{% else %}{% trans 'Untagged' %}{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for currency in tag.currencies.values %}
|
||||||
|
{% if showing == 'current' and currency.income_current != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.income_current"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="green"></c-amount.display>
|
||||||
|
{% elif showing == 'projected' and currency.income_projected != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.income_projected"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="green"></c-amount.display>
|
||||||
|
{% elif showing == 'final' and currency.total_income != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_income"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="green"></c-amount.display>
|
||||||
|
{% else %}
|
||||||
|
<div>-</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for currency in tag.currencies.values %}
|
||||||
|
{% if showing == 'current' and currency.expense_current != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.expense_current"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="red"></c-amount.display>
|
||||||
|
{% elif showing == 'projected' and currency.expense_projected != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.expense_projected"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="red"></c-amount.display>
|
||||||
|
{% elif showing == 'final' and currency.total_expense != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_expense"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="red"></c-amount.display>
|
||||||
|
{% else %}
|
||||||
|
<div>-</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for currency in tag.currencies.values %}
|
||||||
|
{% if showing == 'current' and currency.total_current != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_current"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
||||||
|
{% elif showing == 'projected' and currency.total_projected != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_projected"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
||||||
|
{% elif showing == 'final' and currency.total_final != 0 %}
|
||||||
|
<c-amount.display
|
||||||
|
:amount="currency.total_final"
|
||||||
|
:prefix="currency.currency.prefix"
|
||||||
|
:suffix="currency.currency.suffix"
|
||||||
|
:decimal_places="currency.currency.decimal_places"
|
||||||
|
color="{% if currency.total_final < 0 %}red{% else %}green{% endif %}"></c-amount.display>
|
||||||
|
{% else %}
|
||||||
|
<div>-</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% elif view_type == "bars" %}
|
||||||
|
<div>
|
||||||
|
<div class="chart-container" _="init call setupChart() end" style="position: relative; height:78vh; width:100%">
|
||||||
|
<canvas id="categoryChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ total_table|json_script:"categoryOverviewData" }}
|
||||||
|
{{ showing|json_script:"showingString" }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setupChart() {
|
||||||
|
var rawData = JSON.parse(document.getElementById('categoryOverviewData').textContent);
|
||||||
|
var showing_string = JSON.parse(document.getElementById('showingString').textContent);
|
||||||
|
console.log(showing_string)
|
||||||
|
|
||||||
|
// --- Dynamic Data Processing ---
|
||||||
|
var categories = [];
|
||||||
|
var currencyDetails = {}; // Stores details like { BRL: {code: 'BRL', name: 'Real', ...}, ... }
|
||||||
|
var currencyData = {}; // Stores data arrays like { BRL: [val1, null, val3,...], ... }
|
||||||
|
|
||||||
|
// Pass 1: Collect categories and currency details
|
||||||
|
Object.values(rawData).forEach(cat => {
|
||||||
|
var categoryName = cat.name === null ? "{% trans 'Uncategorized' %}" : cat.name;
|
||||||
|
if (!categories.includes(categoryName)) {
|
||||||
|
categories.push(categoryName);
|
||||||
|
}
|
||||||
|
if (cat.currencies) {
|
||||||
|
Object.values(cat.currencies).forEach(curr => {
|
||||||
|
var details = curr.currency;
|
||||||
|
if (details && details.code && !currencyDetails[details.code]) {
|
||||||
|
var decimals = parseInt(details.decimal_places, 10);
|
||||||
|
currencyDetails[details.code] = {
|
||||||
|
code: details.code,
|
||||||
|
name: details.name || details.code,
|
||||||
|
prefix: details.prefix || '',
|
||||||
|
suffix: details.suffix || '',
|
||||||
|
// Ensure decimal_places is a non-negative integer
|
||||||
|
decimal_places: !isNaN(decimals) && decimals >= 0 ? decimals : 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize data structure for each currency with nulls
|
||||||
|
Object.keys(currencyDetails).forEach(code => {
|
||||||
|
currencyData[code] = new Array(categories.length).fill(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass 2: Populate data arrays (store all valid numbers now)
|
||||||
|
Object.values(rawData).forEach(cat => {
|
||||||
|
var categoryName = cat.name === null ? "{% trans 'Uncategorized' %}" : cat.name;
|
||||||
|
var catIndex = categories.indexOf(categoryName);
|
||||||
|
if (catIndex === -1) return;
|
||||||
|
|
||||||
|
if (cat.currencies) {
|
||||||
|
Object.values(cat.currencies).forEach(curr => {
|
||||||
|
var code = curr.currency?.code;
|
||||||
|
if (code && currencyData[code]) {
|
||||||
|
if (showing_string == 'current') {
|
||||||
|
var value = parseFloat(curr.total_current);
|
||||||
|
} else if (showing_string == 'projected') {
|
||||||
|
var value = parseFloat(curr.total_projected);
|
||||||
|
} else {
|
||||||
|
var value = parseFloat(curr.total_final);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the number if it's valid, otherwise keep null
|
||||||
|
currencyData[code][catIndex] = !isNaN(value) ? value : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Dynamic Chart Configuration ---
|
||||||
|
var datasets = Object.keys(currencyDetails).map((code, index) => {
|
||||||
|
return {
|
||||||
|
label: currencyDetails[code].name, // Use currency name for the legend label
|
||||||
|
data: currencyData[code],
|
||||||
|
currencyCode: code, // Store code for easy lookup in tooltip
|
||||||
|
borderWidth: 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('categoryChart'),
|
||||||
|
{
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: categories,
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: "y"
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (context) {
|
||||||
|
const dataset = context.dataset;
|
||||||
|
const currencyCode = dataset.currencyCode;
|
||||||
|
const details = currencyDetails[currencyCode];
|
||||||
|
const value = context.parsed.x; // Use 'x' because indexAxis is 'y'
|
||||||
|
|
||||||
|
if (value === null || value === undefined || !details) {
|
||||||
|
// Display the category name if the value is null/undefined
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formattedValue = '';
|
||||||
|
try {
|
||||||
|
// Use Intl.NumberFormat for ALL values, configured with locale and exact decimal places
|
||||||
|
formattedValue = new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: details.decimal_places,
|
||||||
|
maximumFractionDigits: details.decimal_places,
|
||||||
|
// Do NOT use style: 'currency' here, as we add prefix/suffix manually
|
||||||
|
}).format(value);
|
||||||
|
} catch (e) {
|
||||||
|
formattedValue = value.toFixed(details.decimal_places);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return label with currency name and formatted value including prefix/suffix
|
||||||
|
return `${details.prefix}${formattedValue}${details.suffix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
type: 'linear',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: '{% trans 'Final Total' %}'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
// Format ticks using the detected locale
|
||||||
|
callback: function (value, index, ticks) {
|
||||||
|
return value.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
text: '{% trans 'Category' %}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<c-msg.empty title="{% translate "No categories" %}"></c-msg.empty>
|
<c-msg.empty title="{% translate "No categories" %}"></c-msg.empty>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -12,17 +12,17 @@
|
|||||||
data-bs-target="#flush-collapse-{{ id }}" aria-expanded="false"
|
data-bs-target="#flush-collapse-{{ id }}" aria-expanded="false"
|
||||||
aria-controls="flush-collapse-{{ id }}">
|
aria-controls="flush-collapse-{{ id }}">
|
||||||
<span>
|
<span>
|
||||||
<span class="tw-text-gray-300">{% trans "You've spent an average of" %}</span>
|
<span class="tw:text-gray-300">{% trans "You've spent an average of" %}</span>
|
||||||
<c-amount.display
|
<c-amount.display
|
||||||
:amount="data.average"
|
:amount="data.average"
|
||||||
:prefix="data.currency.prefix"
|
:prefix="data.currency.prefix"
|
||||||
:suffix="data.currency.suffix"
|
:suffix="data.currency.suffix"
|
||||||
:decimal_places="data.currency.decimal_places"
|
:decimal_places="data.currency.decimal_places"
|
||||||
custom_class="tw-text-3xl"
|
custom_class="tw:text-3xl"
|
||||||
divless></c-amount.display>
|
divless></c-amount.display>
|
||||||
<span class="tw-text-gray-300">{% trans 'on the last 12 months, at this rate you could go by' %}</span>
|
<span class="tw:text-gray-300">{% trans 'on the last 12 months, at this rate you could go by' %}</span>
|
||||||
<span class="tw-text-3xl">{{ data.months }}</span>
|
<span class="tw:text-3xl">{{ data.months }}</span>
|
||||||
<span class="tw-text-gray-300">{% trans 'months without any income.' %}</span>
|
<span class="tw:text-gray-300">{% trans 'months without any income.' %}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<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 'average expenses' %}</div>
|
<div class="tw:text-gray-400">{% translate 'average expenses' %}</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">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<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 'liquid total' %}</div>
|
<div class="tw:text-gray-400">{% translate 'liquid 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">
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<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 'months left' %}</div>
|
<div class="tw:text-gray-400">{% translate 'months left' %}</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">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="show-loading" hx-get="{% url 'insights_sankey_by_currency' %}" hx-trigger="updated from:window"
|
<div class="show-loading" hx-get="{% url 'insights_sankey_by_currency' %}" hx-trigger="updated from:window"
|
||||||
hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
|
hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="chart-container position-relative tw-min-h-[85vh] tw-max-h-[85vh] tw-h-full tw-w-full"
|
<div class="chart-container position-relative tw:min-h-[85vh] tw:max-h-[85vh] tw:h-full tw:w-full"
|
||||||
id="sankeyContainer"
|
id="sankeyContainer"
|
||||||
_="init call setupSankeyChart() end">
|
_="init call setupSankeyChart() end">
|
||||||
<canvas id="sankeyChart"></canvas>
|
<canvas id="sankeyChart"></canvas>
|
||||||
|
|||||||
@@ -2,31 +2,33 @@
|
|||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Insights' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row my-3 h-100">
|
<div class="row my-3 h-100">
|
||||||
<div class="col-lg-2 col-md-3 mb-3 mb-md-0">
|
<div class="col-lg-2 col-md-3 mb-3 mb-md-0">
|
||||||
<div class="position-sticky tw-top-3">
|
<div class="position-sticky tw:top-3">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-2 w-100 d-lg-inline-flex d-grid gap-2 flex-wrap justify-content-lg-center" role="group"
|
<div class="mb-2 w-100 d-lg-inline-flex d-grid gap-2 flex-wrap justify-content-lg-center" role="group"
|
||||||
_="on change
|
_="on change
|
||||||
set type to event.target.value
|
set type to event.target.value
|
||||||
add .tw-hidden to <#picker-form > div:not(.tw-hidden)/>
|
add .tw:hidden to <#picker-form > div:not(.tw:hidden)/>
|
||||||
|
|
||||||
if type == 'month'
|
if type == 'month'
|
||||||
remove .tw-hidden from #month-form
|
remove .tw:hidden from #month-form
|
||||||
end
|
end
|
||||||
if type == 'year'
|
if type == 'year'
|
||||||
remove .tw-hidden from #year-form
|
remove .tw:hidden from #year-form
|
||||||
end
|
end
|
||||||
if type == 'month-range'
|
if type == 'month-range'
|
||||||
remove .tw-hidden from #month-range-form
|
remove .tw:hidden from #month-range-form
|
||||||
end
|
end
|
||||||
if type == 'year-range'
|
if type == 'year-range'
|
||||||
remove .tw-hidden from #year-range-form
|
remove .tw:hidden from #year-range-form
|
||||||
end
|
end
|
||||||
if type == 'date-range'
|
if type == 'date-range'
|
||||||
remove .tw-hidden from #date-range-form
|
remove .tw:hidden from #date-range-form
|
||||||
end
|
end
|
||||||
then trigger updated"
|
then trigger updated"
|
||||||
id="picker-type">
|
id="picker-type">
|
||||||
@@ -58,16 +60,16 @@
|
|||||||
<div id="month-form" class="">
|
<div id="month-form" class="">
|
||||||
{% crispy month_form %}
|
{% crispy month_form %}
|
||||||
</div>
|
</div>
|
||||||
<div id="year-form" class="tw-hidden">
|
<div id="year-form" class="tw:hidden">
|
||||||
{% crispy year_form %}
|
{% crispy year_form %}
|
||||||
</div>
|
</div>
|
||||||
<div id="month-range-form" class="tw-hidden">
|
<div id="month-range-form" class="tw:hidden">
|
||||||
{% crispy month_range_form %}
|
{% crispy month_range_form %}
|
||||||
</div>
|
</div>
|
||||||
<div id="year-range-form" class="tw-hidden">
|
<div id="year-range-form" class="tw:hidden">
|
||||||
{% crispy year_range_form %}
|
{% crispy year_range_form %}
|
||||||
</div>
|
</div>
|
||||||
<div id="date-range-form" class="tw-hidden">
|
<div id="date-range-form" class="tw:hidden">
|
||||||
{% crispy date_range_form %}
|
{% crispy date_range_form %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -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 'Installment Plans' %}<span>
|
<div>{% translate 'Installment Plans' %}<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" %}"
|
||||||
|
|||||||
@@ -64,10 +64,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="col">
|
<td class="col">
|
||||||
<div class="{% if installment_plan.type == 'EX' %}tw-text-red-400{% else %}tw-text-green-400{% endif %}">
|
<div class="{% if installment_plan.type == 'EX' %}tw:text-red-400{% else %}tw:text-green-400{% endif %}">
|
||||||
{{ installment_plan.description }}
|
{{ installment_plan.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-text-sm tw-text-gray-400">{{ installment_plan.notes|linebreaksbr }}</div>
|
<div class="tw:text-sm tw:text-gray-400">{{ installment_plan.notes|linebreaksbr }}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="container px-md-3 py-3 column-gap-5"
|
<div class="container px-md-3 py-3 column-gap-5"
|
||||||
_="install init_tom_select
|
_="install init_tom_select
|
||||||
install init_datepicker">
|
install init_datepicker">
|
||||||
<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">
|
||||||
<div>{% translate 'Currency Converter' %}</div>
|
<div>{% translate 'Currency Converter' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>{{ form.from_currency|as_crispy_field }}</div>
|
<div>{{ form.from_currency|as_crispy_field }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-primary tw-flex tw-items-center tw-justify-center my-3 my-lg-0">
|
<div class="col text-primary tw:flex tw:items-center tw:justify-center my-3 my-lg-0">
|
||||||
<i class="fa-solid fa-equals"></i>
|
<i class="fa-solid fa-equals"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-5">
|
<div class="col-12 col-lg-5">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="tw-cursor-pointer text-primary text-end"
|
<div class="tw:cursor-pointer text-primary text-end"
|
||||||
_="on click
|
_="on click
|
||||||
set from_value to #id_from_currency's value
|
set from_value to #id_from_currency's value
|
||||||
set to_value to #id_to_currency's value
|
set to_value to #id_to_currency's value
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{% for rate in data.rates.values %}
|
{% for rate in data.rates.values %}
|
||||||
<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">
|
<div class="tw:text-gray-400">
|
||||||
{# <c-amount.display#}
|
{# <c-amount.display#}
|
||||||
{# :amount="1"#}
|
{# :amount="1"#}
|
||||||
{# :prefix="data.prefix"#}
|
{# :prefix="data.prefix"#}
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</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="rate.rate"
|
:amount="rate.rate"
|
||||||
:prefix="rate.prefix"
|
:prefix="rate.prefix"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% 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="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">
|
||||||
<div>{% translate 'Unit Price Calculator' %}</div>
|
<div>{% translate 'Unit Price Calculator' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card mb-3 d-none" id="card-placeholder">
|
<div class="card mb-3 d-none" id="card-placeholder">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<label class="form-label">{% trans 'Unit price' %}</label>
|
<label class="form-label">{% trans 'Unit price' %}</label>
|
||||||
<div class="unit-price tw-text-xl" data-amount="0">0</div>
|
<div class="unit-price tw:text-xl" data-amount="0">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<label class="form-label">{% trans 'Unit price' %}</label>
|
<label class="form-label">{% trans 'Unit price' %}</label>
|
||||||
<div class="unit-price tw-text-xl" data-amount="0">0</div>
|
<div class="unit-price tw:text-xl" data-amount="0">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<label class="form-label">{% trans 'Unit price' %}</label>
|
<label class="form-label">{% trans 'Unit price' %}</label>
|
||||||
<div class="unit-price tw-text-xl" data-amount="0">0</div>
|
<div class="unit-price tw:text-xl" data-amount="0">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% for x in transactions_by_date %}
|
{% for x in transactions_by_date %}
|
||||||
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
|
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
|
||||||
_="on htmx:afterSwap from #transactions if sessionStorage.getItem(my id) is null then sessionStorage.setItem(my id, 'true')">
|
_="on htmx:afterSwap from #transactions if sessionStorage.getItem(my id) is null then sessionStorage.setItem(my id, 'true')">
|
||||||
<div class="mt-3 mb-1 w-100 tw-text-base border-bottom bg-body transactions-divider-title">
|
<div class="mt-3 mb-1 w-100 tw:text-base border-bottom bg-body transactions-divider-title">
|
||||||
<a class="text-decoration-none d-inline-block w-100"
|
<a class="text-decoration-none d-inline-block w-100"
|
||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<c-ui.info-card color="yellow" icon="fa-solid fa-calendar-day" title="{% trans 'Daily Spending Allowance' %}" help_text={% trans "This is the final total divided by the remaining days in the month" %}>
|
<c-ui.info-card color="yellow" icon="fa-solid fa-calendar-day" title="{% trans 'Daily Spending Allowance' %}" help_text={% trans "This is the final total divided by the remaining days in the month" %}>
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'today' %}</div>
|
<div class="tw:text-gray-400">{% translate 'today' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in daily_spending_allowance.values %}
|
{% for currency in daily_spending_allowance.values %}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<c-ui.info-card color="green" icon="fa-solid fa-arrow-right-to-bracket" title="{% trans 'Income' %}">
|
<c-ui.info-card color="green" icon="fa-solid fa-arrow-right-to-bracket" title="{% trans 'Income' %}">
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in income_current.values %}
|
{% for currency in income_current.values %}
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<hr class="my-1">
|
<hr class="my-1">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in income_projected.values %}
|
{% for currency in income_projected.values %}
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<c-ui.info-card color="red" icon="fa-solid fa-arrow-right-from-bracket" title="{% trans 'Expenses' %}">
|
<c-ui.info-card color="red" icon="fa-solid fa-arrow-right-from-bracket" title="{% trans 'Expenses' %}">
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in expense_current.values %}
|
{% for currency in expense_current.values %}
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<hr class="my-1">
|
<hr class="my-1">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in expense_projected.values %}
|
{% for currency in expense_projected.values %}
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<c-ui.info-card color="blue" icon="fa-solid fa-scale-balanced" title="{% trans 'Total' %}">
|
<c-ui.info-card color="blue" icon="fa-solid fa-scale-balanced" title="{% trans 'Total' %}">
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'current' %}</div>
|
<div class="tw:text-gray-400">{% translate 'current' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in total_current.values %}
|
{% for currency in total_current.values %}
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
<div class="tw-text-gray-400">{% translate 'projected' %}</div>
|
<div class="tw:text-gray-400">{% translate 'projected' %}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end font-monospace">
|
<div class="text-end font-monospace">
|
||||||
{% for currency in total_projected.values %}
|
{% for currency in total_projected.values %}
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<c-ui.info-card color="yellow" icon="fa-solid fa-percent" title="{% trans 'Distribution' %}">
|
<c-ui.info-card color="yellow" icon="fa-solid fa-percent" title="{% trans 'Distribution' %}">
|
||||||
{% for p in percentages.values %}
|
{% for p in percentages.values %}
|
||||||
<p class="tw-text-gray-400 mb-2 {% if not forloop.first %}mt-3{% endif %}">{{ p.currency.name }}</p>
|
<p class="tw:text-gray-400 mb-2 {% if not forloop.first %}mt-3{% endif %}">{{ p.currency.name }}</p>
|
||||||
<c-ui.percentage-distribution :percentage="p"></c-ui.percentage-distribution>
|
<c-ui.percentage-distribution :percentage="p"></c-ui.percentage-distribution>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</c-ui.info-card>
|
</c-ui.info-card>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user