mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-06-06 22:52:51 +02:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6987b54dba | |||
| b44563b09b | |||
| e839f31104 | |||
| 2282625790 | |||
| aa8b559152 | |||
| e1862b8241 | |||
| eb6be8548c | |||
| 6ee4e21939 | |||
| bdd8aed891 | |||
| 801b2a9edd | |||
| 968499f1ab | |||
| 5b351821b1 | |||
| 7b49072848 | |||
| 0ee32724f1 | |||
| 6a19381672 | |||
| 248fec8b4c | |||
| b34c0557fa | |||
| 2af4066aab | |||
| d72ff3cdf5 | |||
| 63c69e5c6a | |||
| 78171183cc | |||
| 34a2b6bfd4 | |||
| 1dc24f855e | |||
| 1390aff07d | |||
| 8fc11b0acf | |||
| 9a30a0d3c0 | |||
| 10eecd09ff | |||
| 2cfb3fb12e | |||
| 47af8b135b | |||
| 39d0e63375 | |||
| 792154eba2 | |||
| dc76ed3156 | |||
| e627dd50be | |||
| 5527389196 | |||
| be24ca014e | |||
| 7c7056536e | |||
| 66d5d7a83b | |||
| 4d3ce087d6 | |||
| aeaf9fac43 | |||
| dcedf53b83 | |||
| 549648bd6b | |||
| 79149abdd2 | |||
| d66d1530bb | |||
| 4b9b6484d3 | |||
| a02944bdae | |||
| 3ede5304f1 | |||
| 27041695b8 | |||
| 0c927a2fe9 | |||
| c52db80c64 | |||
| 330ce8069c | |||
| 2989f11b01 | |||
| 738bb7fb74 | |||
| 79e50cd853 | |||
| 0a23c3ad5b | |||
| 43c7749102 | |||
| c1c4ccda8c | |||
| 615a689c61 | |||
| c5ccc42f99 | |||
| 2baa8b21e8 | |||
| 2e554141ba | |||
| 73ec6dc0fe | |||
| e19449ff99 | |||
| e81651119c | |||
| 55e9ef1b3f | |||
| c414179135 | |||
| 14c507de0f | |||
| 4722690fe9 | |||
| 493619a4ff | |||
| fb4aec88f1 | |||
| 4a35e770a4 | |||
| 83b81edbae | |||
| a7dc2c955e | |||
| ce2ae562c6 | |||
| de2881ffd4 | |||
| 838bf22498 | |||
| d3797ae4a5 | |||
| 0532397afd | |||
| 8106dc58e5 | |||
| 5986cf675b | |||
| 80da9142f1 | |||
| 766516d248 | |||
| 3fd0fba1b8 | |||
| c787565c04 | |||
| 0413921dbe | |||
| 9ecf8279b4 | |||
| 86cf625158 | |||
| ea097ab6f0 | |||
| b1201b51bb | |||
| 4c1d20215c | |||
| 27e85c4776 | |||
| 5a73cd20da | |||
| e305fab300 | |||
| c11f525373 | |||
| ea5d86dbf8 | |||
| a1d3539e3c | |||
| 1028a11c8b | |||
| e387a5e2a8 | |||
| 624dc382cf | |||
| f88699b333 | |||
| ca98dc073b | |||
| 63ba7af3c8 | |||
| 2d0dee4a9b | |||
| 0000a9ee03 | |||
| 41adb37fdb | |||
| 496651173e | |||
| 8836f06b80 | |||
| e98a48b3a7 | |||
| f9bc9f449b | |||
| 26eb1ae813 | |||
| 29a2cb9813 | |||
| be79e1b25a | |||
| 3fd08466a7 | |||
| 6896cdcdca | |||
| 2532930a64 | |||
| 24a1ef2d0a | |||
| 163f2f4e5b | |||
| ede63acf5f | |||
| a8ba3d8754 | |||
| e2f1156264 | |||
| d5bbad7887 | |||
| 7ebacff6e4 | |||
| df8ef5d04c | |||
| fa2a8b8c65 | |||
| e44ac5dab6 | |||
| f9261d1283 | |||
| 4c73c1cae5 | |||
| 0315a56f88 | |||
| 44d6b8b53c |
@@ -165,3 +165,6 @@ cython_debug/
|
||||
node_modules/
|
||||
postgres_data/
|
||||
.prod.env
|
||||
|
||||
# Private local uploads
|
||||
app/attachments/
|
||||
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Docker: Dev",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "docker compose --env-file .env -f docker-compose.dev.yml up --build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"postDebugTask": "Docker: Dev Down"
|
||||
},
|
||||
{
|
||||
"name": "Docker: Dev (no rebuild)",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "docker compose --env-file .env -f docker-compose.dev.yml up",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"postDebugTask": "Docker: Dev Down"
|
||||
},
|
||||
{
|
||||
"name": "Docker: Prod",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "docker compose --env-file .prod.env -f docker-compose.prod.yml up --build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"postDebugTask": "Docker: Prod Down"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Docker: Dev",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"compose",
|
||||
"--env-file",
|
||||
".env",
|
||||
"-f",
|
||||
"docker-compose.dev.yml",
|
||||
"up",
|
||||
"--build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Docker: Dev (no rebuild)",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"compose",
|
||||
"--env-file",
|
||||
".env",
|
||||
"-f",
|
||||
"docker-compose.dev.yml",
|
||||
"up"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Docker: Dev Refresh Vite Deps",
|
||||
"type": "shell",
|
||||
"command": "docker compose --env-file .env -f docker-compose.dev.yml rm -sfv vite; docker compose --env-file .env -f docker-compose.dev.yml up --build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Docker: Dev Down",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"compose",
|
||||
"--env-file",
|
||||
".env",
|
||||
"-f",
|
||||
"docker-compose.dev.yml",
|
||||
"down"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Docker: Prod",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"compose",
|
||||
"--env-file",
|
||||
".prod.env",
|
||||
"-f",
|
||||
"docker-compose.prod.yml",
|
||||
"up",
|
||||
"--build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Docker: Prod Down",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"compose",
|
||||
"--env-file",
|
||||
".prod.env",
|
||||
"-f",
|
||||
"docker-compose.prod.yml",
|
||||
"down"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Django: Runserver localhost:8000",
|
||||
"type": "shell",
|
||||
"command": "${command:python.interpreterPath}",
|
||||
"args": [
|
||||
"manage.py",
|
||||
"runserver",
|
||||
"localhost:8000"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app",
|
||||
"env": {
|
||||
"PYTHONUNBUFFERED": "1"
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -157,6 +157,13 @@ WYGIWYH supports login via OpenID Connect (OIDC) through `django-allauth`. This
|
||||
> [!NOTE]
|
||||
> Currently only OpenID Connect is supported as a provider, open an issue if you need something else.
|
||||
|
||||
> [!Caution]
|
||||
> WYGIWYH automatically connects OIDC accounts to existing local accounts with matching email addresses.
|
||||
> This means if a user already exists with email `user@example.com` and someone logs in via OIDC with the same email, the OIDC account will be automatically linked to the existing account without requiring user confirmation.
|
||||
> This is only recommended for trusted OIDC providers that verify email addresses and where you control who can create accounts.
|
||||
|
||||
### Configuration
|
||||
|
||||
To configure OIDC, you need to set the following environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
+28
-27
@@ -311,6 +311,7 @@ LOCALE_PATHS = [BASE_DIR / "locale"]
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "static_files"
|
||||
ATTACHMENT_MEDIA_ROOT = BASE_DIR / "attachments"
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
ROOT_DIR / "frontend" / "build",
|
||||
@@ -376,8 +377,10 @@ ACCOUNT_EMAIL_VERIFICATION = "none"
|
||||
SOCIALACCOUNT_LOGIN_ON_GET = True
|
||||
SOCIALACCOUNT_ONLY = True
|
||||
SOCIALACCOUNT_AUTO_SIGNUP = os.getenv("OIDC_ALLOW_SIGNUP", "true").lower() == "true"
|
||||
SOCIALACCOUNT_EMAIL_AUTHENTICATION = True
|
||||
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
|
||||
ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter"
|
||||
SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter"
|
||||
SOCIALACCOUNT_ADAPTER = "apps.users.adapters.AutoConnectSocialAccountAdapter"
|
||||
|
||||
# CRISPY FORMS
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = [
|
||||
@@ -390,6 +393,10 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
SESSION_COOKIE_AGE = int(os.getenv("SESSION_EXPIRY_TIME", 2678400)) # 31 days
|
||||
SESSION_COOKIE_SECURE = os.getenv("HTTPS_ENABLED", "false").lower() == "true"
|
||||
|
||||
HTTPS_ENABLED = os.getenv("HTTPS_ENABLED", "false").lower() == "true"
|
||||
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" if HTTPS_ENABLED else "http"
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") if HTTPS_ENABLED else None
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"ROOT_TAG_EXTRA_ATTRS": "hx-preserve",
|
||||
# "SHOW_TOOLBAR_CALLBACK": lambda r: False, # disables it
|
||||
@@ -434,14 +441,14 @@ REST_FRAMEWORK = {
|
||||
"apps.api.permissions.NotInDemoMode",
|
||||
"rest_framework.permissions.DjangoModelPermissions",
|
||||
],
|
||||
'DEFAULT_FILTER_BACKENDS': [
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
'rest_framework.filters.OrderingFilter',
|
||||
"DEFAULT_FILTER_BACKENDS": [
|
||||
"django_filters.rest_framework.DjangoFilterBackend",
|
||||
"rest_framework.filters.OrderingFilter",
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework.authentication.BasicAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
],
|
||||
"DEFAULT_PAGINATION_CLASS": "apps.api.custom.pagination.CustomPageNumberPagination",
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
@@ -458,7 +465,7 @@ SPECTACULAR_SETTINGS = {
|
||||
if "procrastinate" in sys.argv:
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"disable_existing_loggers": True,
|
||||
"formatters": {
|
||||
"standard": {
|
||||
"format": "[%(asctime)s] - %(levelname)s - %(name)s - %(message)s",
|
||||
@@ -466,26 +473,19 @@ if "procrastinate" in sys.argv:
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"procrastinate": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "standard",
|
||||
},
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "standard",
|
||||
"level": "INFO",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
},
|
||||
"loggers": {
|
||||
"procrastinate": {
|
||||
"handlers": ["procrastinate"],
|
||||
"propagate": False,
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -505,19 +505,20 @@ else:
|
||||
"formatter": "standard",
|
||||
"level": "INFO",
|
||||
},
|
||||
"procrastinate": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
},
|
||||
"loggers": {
|
||||
"procrastinate": {
|
||||
"handlers": None,
|
||||
"handlers": [],
|
||||
"propagate": False,
|
||||
},
|
||||
"root": {
|
||||
"allauth": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -90,10 +90,10 @@ class AccountBalanceAPITests(TestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_get_balance_unauthenticated(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
response = unauthenticated_client.get(
|
||||
f"/api/accounts/{self.account.id}/balance/"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -159,7 +159,7 @@ column_mapping:
|
||||
self.assertIn("import_run_id", response.data)
|
||||
|
||||
def test_unauthenticated_request(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
|
||||
csv_content = b"date,description,amount\n2025-01-01,Test,100"
|
||||
@@ -173,7 +173,7 @@ column_mapping:
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
@override_settings(
|
||||
@@ -266,11 +266,11 @@ column_mapping:
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_profiles_unauthenticated(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
response = unauthenticated_client.get("/api/import/profiles/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
@override_settings(
|
||||
@@ -397,8 +397,8 @@ column_mapping:
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_runs_unauthenticated(self):
|
||||
"""Test unauthenticated request returns 403"""
|
||||
"""Test unauthenticated request returns 401"""
|
||||
unauthenticated_client = APIClient()
|
||||
response = unauthenticated_client.get("/api/import/runs/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -1,6 +1,47 @@
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
import procrastinate
|
||||
from django.db import close_old_connections
|
||||
|
||||
|
||||
_CONNECTION_CLEANUP_WRAPPED = "_wygiwyh_connection_cleanup_wrapped"
|
||||
|
||||
|
||||
def _wrap_task_with_django_connection_cleanup(task):
|
||||
if getattr(task.func, _CONNECTION_CLEANUP_WRAPPED, False):
|
||||
return
|
||||
|
||||
func = task.func
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
async def async_wrapped(*args, **kwargs):
|
||||
close_old_connections()
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
finally:
|
||||
close_old_connections()
|
||||
|
||||
wrapped = async_wrapped
|
||||
else:
|
||||
|
||||
@functools.wraps(func)
|
||||
def sync_wrapped(*args, **kwargs):
|
||||
close_old_connections()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
close_old_connections()
|
||||
|
||||
wrapped = sync_wrapped
|
||||
|
||||
setattr(wrapped, _CONNECTION_CLEANUP_WRAPPED, True)
|
||||
task.func = wrapped
|
||||
|
||||
|
||||
def on_app_ready(app: procrastinate.App):
|
||||
"""This function is ran upon procrastinate initialization."""
|
||||
...
|
||||
for task in set(app.tasks.values()):
|
||||
_wrap_task_with_django_connection_cleanup(task)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import procrastinate
|
||||
from django.db import connection
|
||||
from django.test import SimpleTestCase, TransactionTestCase
|
||||
from procrastinate.testing import InMemoryConnector
|
||||
|
||||
from apps.common.procrastinate import on_app_ready
|
||||
|
||||
|
||||
def make_app_with_task(func):
|
||||
app = procrastinate.App(connector=InMemoryConnector())
|
||||
task = app.task(name="sample_task")(func)
|
||||
|
||||
return app, task
|
||||
|
||||
|
||||
class ProcrastinateConnectionCleanupTests(SimpleTestCase):
|
||||
def test_app_ready_closes_old_connections_around_sync_tasks(self):
|
||||
calls = []
|
||||
|
||||
def sample_task(value):
|
||||
calls.append(("task", value))
|
||||
return value * 2
|
||||
|
||||
app, task = make_app_with_task(sample_task)
|
||||
|
||||
with patch(
|
||||
"apps.common.procrastinate.close_old_connections",
|
||||
create=True,
|
||||
side_effect=lambda: calls.append(("cleanup", None)),
|
||||
):
|
||||
on_app_ready(app)
|
||||
|
||||
result = task.func(3)
|
||||
|
||||
self.assertEqual(result, 6)
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
("cleanup", None),
|
||||
("task", 3),
|
||||
("cleanup", None),
|
||||
],
|
||||
)
|
||||
|
||||
def test_app_ready_closes_old_connections_when_sync_task_raises(self):
|
||||
calls = []
|
||||
|
||||
def sample_task():
|
||||
calls.append(("task", None))
|
||||
raise RuntimeError("boom")
|
||||
|
||||
app, task = make_app_with_task(sample_task)
|
||||
|
||||
with patch(
|
||||
"apps.common.procrastinate.close_old_connections",
|
||||
create=True,
|
||||
side_effect=lambda: calls.append(("cleanup", None)),
|
||||
):
|
||||
on_app_ready(app)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
task.func()
|
||||
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
("cleanup", None),
|
||||
("task", None),
|
||||
("cleanup", None),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class ProcrastinateConnectionRecoveryTests(TransactionTestCase):
|
||||
def test_wrapped_task_recovers_from_closed_django_connection(self):
|
||||
def sample_task():
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
app, task = make_app_with_task(sample_task)
|
||||
on_app_ready(app)
|
||||
|
||||
connection.ensure_connection()
|
||||
connection.connection.close()
|
||||
|
||||
self.assertEqual(task.func(), 1)
|
||||
@@ -106,6 +106,17 @@ class ExcelImportSettings(BaseModel):
|
||||
sheets: list[str] | str = "*"
|
||||
|
||||
|
||||
class QIFImportSettings(BaseModel):
|
||||
skip_errors: bool = Field(
|
||||
default=False,
|
||||
description="If True, errors during import will be logged and skipped",
|
||||
)
|
||||
file_type: Literal["qif"] = "qif"
|
||||
importing: Literal["transactions"] = "transactions"
|
||||
encoding: str = Field(default="utf-8", description="File encoding")
|
||||
date_format: str = Field(..., description="Date format (e.g. %d/%m/%Y)")
|
||||
|
||||
|
||||
class ColumnMapping(BaseModel):
|
||||
source: Optional[str] | Optional[list[str]] = Field(
|
||||
default=None,
|
||||
@@ -342,7 +353,7 @@ class CurrencyExchangeMapping(ColumnMapping):
|
||||
|
||||
|
||||
class ImportProfileSchema(BaseModel):
|
||||
settings: CSVImportSettings | ExcelImportSettings
|
||||
settings: CSVImportSettings | ExcelImportSettings | QIFImportSettings
|
||||
mapping: Dict[
|
||||
str,
|
||||
TransactionAccountMapping
|
||||
|
||||
@@ -3,6 +3,8 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
from django.db import transaction
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Dict, Any, Literal, Union
|
||||
@@ -11,6 +13,7 @@ import openpyxl
|
||||
import xlrd
|
||||
import yaml
|
||||
from cachalot.api import cachalot_disabled
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.utils import timezone
|
||||
from openpyxl.utils.exceptions import InvalidFileException
|
||||
|
||||
@@ -363,7 +366,7 @@ class ImportService:
|
||||
try:
|
||||
if entities_mapping:
|
||||
if entities_mapping.type == "id":
|
||||
entity = TransactionTag.objects.filter(
|
||||
entity = TransactionEntity.objects.filter(
|
||||
id=entity_name
|
||||
).first()
|
||||
else: # name
|
||||
@@ -460,12 +463,12 @@ class ImportService:
|
||||
for field in rule.fields:
|
||||
if field in transaction_data:
|
||||
value = transaction_data[field]
|
||||
# Use __iexact only for string fields; non-string types
|
||||
# (date, Decimal, bool, int, etc.) don't support UPPER()
|
||||
if rule.match_type == "strict" or not isinstance(value, str):
|
||||
query = query.filter(**{field: value})
|
||||
else: # lax matching for strings only
|
||||
query = query.filter(**{f"{field}__iexact": value})
|
||||
query = self._apply_deduplication_filter(
|
||||
query=query,
|
||||
field=field,
|
||||
value=value,
|
||||
match_type=rule.match_type,
|
||||
)
|
||||
|
||||
# If we found any matching transaction, it's a duplicate
|
||||
if query.exists():
|
||||
@@ -473,6 +476,71 @@ class ImportService:
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_int_like(value: Any) -> bool:
|
||||
try:
|
||||
int(value)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _apply_deduplication_filter(
|
||||
self,
|
||||
query,
|
||||
field: str,
|
||||
value: Any,
|
||||
match_type: Literal["lax", "strict"],
|
||||
):
|
||||
if isinstance(value, list):
|
||||
return self._apply_list_deduplication_filter(
|
||||
query=query,
|
||||
field=field,
|
||||
values=value,
|
||||
match_type=match_type,
|
||||
)
|
||||
|
||||
# Use __iexact only for string fields; non-string types
|
||||
# (date, Decimal, bool, int, etc.) don't support UPPER()
|
||||
if match_type == "strict" or not isinstance(value, str):
|
||||
return query.filter(**{field: value})
|
||||
|
||||
return query.filter(**{f"{field}__iexact": value})
|
||||
|
||||
def _apply_list_deduplication_filter(
|
||||
self,
|
||||
query,
|
||||
field: str,
|
||||
values: list[Any],
|
||||
match_type: Literal["lax", "strict"],
|
||||
):
|
||||
clean_values = [v for v in values if v not in (None, "")]
|
||||
if not clean_values:
|
||||
return query
|
||||
|
||||
try:
|
||||
model_field = Transaction._meta.get_field(field)
|
||||
except FieldDoesNotExist:
|
||||
return query.filter(**{f"{field}__in": clean_values})
|
||||
|
||||
if getattr(model_field, "many_to_many", False):
|
||||
# For m2m fields (e.g., entities/tags), apply one filter per value so
|
||||
# all provided values must be present in the matched transaction.
|
||||
if all(self._is_int_like(v) for v in clean_values):
|
||||
for value in clean_values:
|
||||
query = query.filter(**{f"{field}__id": int(value)})
|
||||
else:
|
||||
for value in clean_values:
|
||||
lookup = (
|
||||
f"{field}__name"
|
||||
if match_type == "strict"
|
||||
else f"{field}__name__iexact"
|
||||
)
|
||||
query = query.filter(**{lookup: str(value).strip()})
|
||||
|
||||
return query.distinct()
|
||||
|
||||
return query.filter(**{f"{field}__in": clean_values})
|
||||
|
||||
def _coerce_type(
|
||||
self, value: str, mapping: version_1.ColumnMapping
|
||||
) -> Union[str, int, bool, Decimal, datetime, list, None]:
|
||||
@@ -845,6 +913,219 @@ class ImportService:
|
||||
f"Invalid {self.settings.file_type.upper()} file format: {str(e)}"
|
||||
)
|
||||
|
||||
def _parse_and_import_qif(self, content_lines: list[str], filename: str) -> None:
|
||||
# Infer account from filename (remove extension)
|
||||
account_name = os.path.splitext(os.path.basename(filename))[0]
|
||||
|
||||
current_transaction = {}
|
||||
raw_lines_buffer = []
|
||||
|
||||
account = Account.objects.filter(name=account_name).first()
|
||||
if not account:
|
||||
raise ValueError(f"Account '{account_name}' not found.")
|
||||
|
||||
row_number = 0
|
||||
for line in content_lines:
|
||||
row_number += 1
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
raw_lines_buffer.append(line)
|
||||
|
||||
if line == "^":
|
||||
if current_transaction:
|
||||
# Deduplication using hash of raw lines
|
||||
raw_content = "".join(raw_lines_buffer)
|
||||
internal_id = hashlib.sha256(
|
||||
raw_content.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
# Reset buffer for next transaction
|
||||
raw_lines_buffer = []
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if Transaction.objects.filter(
|
||||
internal_id=internal_id
|
||||
).exists():
|
||||
self._increment_totals("skipped", 1)
|
||||
self._log(
|
||||
"info",
|
||||
f"Skipped duplicate transaction from {filename}",
|
||||
)
|
||||
current_transaction = {}
|
||||
continue
|
||||
|
||||
# Handle Account
|
||||
if account:
|
||||
current_transaction["account"] = account
|
||||
else:
|
||||
acc = Account.objects.filter(name=account_name).first()
|
||||
if acc:
|
||||
current_transaction["account"] = acc
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Account '{account_name}' not found."
|
||||
)
|
||||
|
||||
current_transaction["internal_id"] = internal_id
|
||||
|
||||
# Handle Description/Memo mapping
|
||||
if "memo" in current_transaction:
|
||||
current_transaction["description"] = (
|
||||
current_transaction.pop("memo")
|
||||
)
|
||||
|
||||
# Handle Payee mapping
|
||||
entities = []
|
||||
if "payee" in current_transaction:
|
||||
payee_name = current_transaction.pop("payee")
|
||||
# "Treat the payee (P) as the entity. Use existing or create"
|
||||
entity, _ = TransactionEntity.objects.get_or_create(
|
||||
name=payee_name
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
# Handle Label/Category
|
||||
category = None
|
||||
tags = []
|
||||
if "label" in current_transaction:
|
||||
label = current_transaction.pop("label")
|
||||
if label.startswith("[") and label.endswith("]"):
|
||||
# Transfer: set label as description, ignore category/tags
|
||||
clean_label = label[1:-1]
|
||||
current_transaction["description"] = clean_label
|
||||
else:
|
||||
parts = label.split(":")
|
||||
if parts:
|
||||
cat_name = parts[0].strip()
|
||||
if cat_name:
|
||||
category, _ = (
|
||||
TransactionCategory.objects.get_or_create(
|
||||
name=cat_name
|
||||
)
|
||||
)
|
||||
|
||||
if len(parts) > 1:
|
||||
for tag_name in parts[1:]:
|
||||
tag_name = tag_name.strip()
|
||||
if tag_name:
|
||||
tag, _ = (
|
||||
TransactionTag.objects.get_or_create(
|
||||
name=tag_name
|
||||
)
|
||||
)
|
||||
tags.append(tag)
|
||||
|
||||
current_transaction["category"] = category
|
||||
|
||||
# Create transaction
|
||||
new_trans = Transaction.objects.create(
|
||||
**current_transaction
|
||||
)
|
||||
if entities:
|
||||
new_trans.entities.set(entities)
|
||||
if tags:
|
||||
new_trans.tags.set(tags)
|
||||
|
||||
self.import_run.transactions.add(new_trans)
|
||||
self._increment_totals("successful", 1)
|
||||
|
||||
except Exception as e:
|
||||
if not self.settings.skip_errors:
|
||||
raise e
|
||||
self._log(
|
||||
"warning",
|
||||
f"Error processing transaction in {filename}: {str(e)}",
|
||||
)
|
||||
self._increment_totals("failed", 1)
|
||||
|
||||
# Reset for next transaction
|
||||
current_transaction = {}
|
||||
else:
|
||||
# Empty transaction record (orphaned ^)
|
||||
raw_lines_buffer = []
|
||||
pass
|
||||
self._increment_totals("processed", 1)
|
||||
continue
|
||||
|
||||
if line.startswith("!"):
|
||||
continue
|
||||
|
||||
code = line[0]
|
||||
value = line[1:]
|
||||
|
||||
if code == "D":
|
||||
try:
|
||||
current_transaction["date"] = datetime.strptime(
|
||||
value, self.settings.date_format
|
||||
).date()
|
||||
except ValueError:
|
||||
self._log(
|
||||
"warning",
|
||||
f"Could not parse date '{value}' using format '{self.settings.date_format}' in {filename}",
|
||||
)
|
||||
if not self.settings.skip_errors:
|
||||
raise ValueError(f"Invalid date format '{value}'")
|
||||
|
||||
elif code == "T":
|
||||
try:
|
||||
cleaned_value = value.replace(",", "")
|
||||
amount = Decimal(cleaned_value)
|
||||
if amount < 0:
|
||||
current_transaction["type"] = Transaction.Type.EXPENSE
|
||||
current_transaction["amount"] = abs(amount)
|
||||
else:
|
||||
current_transaction["type"] = Transaction.Type.INCOME
|
||||
current_transaction["amount"] = amount
|
||||
except InvalidOperation:
|
||||
self._log(
|
||||
"warning", f"Could not parse amount '{value}' in {filename}"
|
||||
)
|
||||
if not self.settings.skip_errors:
|
||||
raise ValueError(f"Invalid amount format '{value}'")
|
||||
|
||||
elif code == "P":
|
||||
current_transaction["payee"] = value
|
||||
elif code == "M":
|
||||
current_transaction["memo"] = value
|
||||
elif code == "L":
|
||||
current_transaction["label"] = value
|
||||
elif code == "N":
|
||||
pass
|
||||
|
||||
def _process_qif(self, file_path):
|
||||
def process_logic():
|
||||
if zipfile.is_zipfile(file_path):
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
for filename in zf.namelist():
|
||||
if filename.lower().endswith(
|
||||
".qif"
|
||||
) and not filename.startswith("__MACOSX"):
|
||||
self._log(
|
||||
"info", f"Processing QIF from ZIP: {filename}"
|
||||
)
|
||||
with zf.open(filename) as f:
|
||||
content = f.read().decode(self.settings.encoding)
|
||||
self._parse_and_import_qif(
|
||||
content.splitlines(), filename
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error processing ZIP file: {str(e)}")
|
||||
else:
|
||||
with open(file_path, "r", encoding=self.settings.encoding) as f:
|
||||
self._parse_and_import_qif(
|
||||
f.readlines(), os.path.basename(file_path)
|
||||
)
|
||||
|
||||
if not self.settings.skip_errors:
|
||||
with transaction.atomic():
|
||||
process_logic()
|
||||
else:
|
||||
process_logic()
|
||||
|
||||
def _validate_file_path(self, file_path: str) -> str:
|
||||
"""
|
||||
Validates that the file path is within the allowed temporary directory.
|
||||
@@ -871,6 +1152,8 @@ class ImportService:
|
||||
self._process_csv(file_path)
|
||||
elif isinstance(self.settings, version_1.ExcelImportSettings):
|
||||
self._process_excel(file_path)
|
||||
elif isinstance(self.settings, version_1.QIFImportSettings):
|
||||
self._process_qif(file_path)
|
||||
|
||||
self._update_status("FINISHED")
|
||||
self._log(
|
||||
|
||||
@@ -15,7 +15,7 @@ from apps.accounts.models import Account, AccountGroup
|
||||
from apps.currencies.models import Currency
|
||||
from apps.import_app.models import ImportProfile, ImportRun
|
||||
from apps.import_app.services.v1 import ImportService
|
||||
from apps.transactions.models import Transaction
|
||||
from apps.transactions.models import Transaction, TransactionEntity
|
||||
|
||||
|
||||
class DeduplicationTests(TestCase):
|
||||
@@ -273,3 +273,39 @@ deduplication:
|
||||
}
|
||||
)
|
||||
self.assertTrue(is_duplicate)
|
||||
|
||||
def test_deduplication_with_entities_list_value(self):
|
||||
"""Test that list values for m2m entities deduplicate correctly."""
|
||||
entity = TransactionEntity.objects.create(name="DB Vertrieb GmbH")
|
||||
self.existing_transaction.entities.add(entity)
|
||||
|
||||
service = self._create_import_service_with_deduplication(
|
||||
fields=["date", "amount", "entities"], match_type="strict"
|
||||
)
|
||||
|
||||
is_duplicate = service._check_duplicate_transaction(
|
||||
{
|
||||
"date": date(2024, 1, 15),
|
||||
"amount": Decimal("100.00"),
|
||||
"entities": ["DB Vertrieb GmbH"],
|
||||
}
|
||||
)
|
||||
self.assertTrue(is_duplicate)
|
||||
|
||||
def test_deduplication_with_entities_list_value_not_matching(self):
|
||||
"""Test that non-matching entity list values are not marked duplicate."""
|
||||
entity = TransactionEntity.objects.create(name="DB Vertrieb GmbH")
|
||||
self.existing_transaction.entities.add(entity)
|
||||
|
||||
service = self._create_import_service_with_deduplication(
|
||||
fields=["date", "amount", "entities"], match_type="strict"
|
||||
)
|
||||
|
||||
is_duplicate = service._check_duplicate_transaction(
|
||||
{
|
||||
"date": date(2024, 1, 15),
|
||||
"amount": Decimal("100.00"),
|
||||
"entities": ["Different Entity"],
|
||||
}
|
||||
)
|
||||
self.assertFalse(is_duplicate)
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
from decimal import Decimal
|
||||
import os
|
||||
import shutil
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from apps.accounts.models import Account, AccountGroup
|
||||
from apps.currencies.models import Currency
|
||||
from apps.common.middleware.thread_local import write_current_user, delete_current_user
|
||||
from apps.import_app.models import ImportProfile, ImportRun
|
||||
from apps.import_app.services.v1 import ImportService
|
||||
from apps.transactions.models import (
|
||||
Transaction,
|
||||
)
|
||||
|
||||
|
||||
class QIFImportTests(TestCase):
|
||||
def setUp(self):
|
||||
# Patch TEMP_DIR for testing
|
||||
self.original_temp_dir = ImportService.TEMP_DIR
|
||||
self.test_dir = os.path.abspath("temp_test_import")
|
||||
ImportService.TEMP_DIR = self.test_dir
|
||||
os.makedirs(self.test_dir, exist_ok=True)
|
||||
|
||||
# Create user and set context
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(
|
||||
email="test@example.com", password="password"
|
||||
)
|
||||
write_current_user(self.user)
|
||||
|
||||
self.currency = Currency.objects.create(
|
||||
code="BRL", name="Real", decimal_places=2, prefix="R$ "
|
||||
)
|
||||
self.group = AccountGroup.objects.create(name="Test Group", owner=self.user)
|
||||
self.account = Account.objects.create(
|
||||
name="bradesco-checking",
|
||||
group=self.group,
|
||||
currency=self.currency,
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
delete_current_user()
|
||||
ImportService.TEMP_DIR = self.original_temp_dir
|
||||
if os.path.exists(self.test_dir):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
def test_import_single_qif_valid_mapping(self):
|
||||
content = """!Type:Bank
|
||||
D04/01/2015
|
||||
T8069.46
|
||||
PMy Payee -> Entity
|
||||
MNote -> Desc
|
||||
LOld Cat:New Tag
|
||||
^
|
||||
D05/01/2015
|
||||
T-100.00
|
||||
PSupermarket
|
||||
MWeekly shopping
|
||||
L[Transfer]
|
||||
^
|
||||
"""
|
||||
filename = "bradesco-checking.qif"
|
||||
file_path = os.path.join(self.test_dir, filename)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
yaml_config = """
|
||||
settings:
|
||||
file_type: qif
|
||||
importing: transactions
|
||||
date_format: "%d/%m/%Y"
|
||||
mapping: {}
|
||||
"""
|
||||
profile = ImportProfile.objects.create(
|
||||
name="QIF Profile",
|
||||
yaml_config=yaml_config,
|
||||
version=ImportProfile.Versions.VERSION_1,
|
||||
)
|
||||
run = ImportRun.objects.create(profile=profile, file_name=filename)
|
||||
service = ImportService(run)
|
||||
|
||||
service.process_file(file_path)
|
||||
|
||||
self.assertEqual(Transaction.objects.count(), 2)
|
||||
|
||||
# Transaction 1: Income, Category+Tag
|
||||
t1 = Transaction.objects.get(description="Note -> Desc")
|
||||
self.assertEqual(t1.amount, Decimal("8069.46"))
|
||||
self.assertEqual(t1.type, Transaction.Type.INCOME)
|
||||
self.assertEqual(t1.category.name, "Old Cat")
|
||||
self.assertTrue(t1.tags.filter(name="New Tag").exists())
|
||||
self.assertTrue(t1.entities.filter(name="My Payee -> Entity").exists())
|
||||
self.assertEqual(t1.account, self.account)
|
||||
|
||||
# Transaction 2: Expense, Transfer ([Transfer] -> Description)
|
||||
t2 = Transaction.objects.get(description="Transfer")
|
||||
self.assertEqual(t2.amount, Decimal("100.00"))
|
||||
self.assertEqual(t2.type, Transaction.Type.EXPENSE)
|
||||
self.assertIsNone(t2.category)
|
||||
self.assertFalse(t2.tags.exists())
|
||||
self.assertTrue(t2.entities.filter(name="Supermarket").exists())
|
||||
self.assertEqual(t2.description, "Transfer")
|
||||
|
||||
def test_import_deduplication_hash(self):
|
||||
# Same content twice. Should result in only 1 transaction due to hash deduplication.
|
||||
content = """!Type:Bank
|
||||
D04/01/2015
|
||||
T100.00
|
||||
POK
|
||||
^
|
||||
"""
|
||||
filename = "bradesco-checking.qif"
|
||||
file_path = os.path.join(self.test_dir, filename)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
yaml_config = """
|
||||
settings:
|
||||
file_type: qif
|
||||
importing: transactions
|
||||
date_format: "%d/%m/%Y"
|
||||
mapping: {}
|
||||
"""
|
||||
profile = ImportProfile.objects.create(
|
||||
name="QIF Profile",
|
||||
yaml_config=yaml_config,
|
||||
version=ImportProfile.Versions.VERSION_1,
|
||||
)
|
||||
run = ImportRun.objects.create(profile=profile, file_name=filename)
|
||||
service = ImportService(run)
|
||||
|
||||
# First run
|
||||
service.process_file(file_path)
|
||||
self.assertEqual(Transaction.objects.count(), 1)
|
||||
|
||||
# Service deletes file after processing, so recreate it for second run
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
# Second run - Duplicate content
|
||||
service.process_file(file_path)
|
||||
self.assertEqual(Transaction.objects.count(), 1)
|
||||
|
||||
def test_import_strict_error_rollback(self):
|
||||
# atomic check.
|
||||
# Transaction 1 valid, Transaction 2 invalid date.
|
||||
content = """!Type:Bank
|
||||
D04/01/2015
|
||||
T100.00
|
||||
POK
|
||||
^
|
||||
DINVALID
|
||||
T100.00
|
||||
PBad
|
||||
^
|
||||
"""
|
||||
filename = "bradesco-checking.qif"
|
||||
file_path = os.path.join(self.test_dir, filename)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
yaml_config = """
|
||||
settings:
|
||||
file_type: qif
|
||||
importing: transactions
|
||||
date_format: "%d/%m/%Y"
|
||||
skip_errors: false
|
||||
mapping: {}
|
||||
"""
|
||||
profile = ImportProfile.objects.create(
|
||||
name="QIF Profile",
|
||||
yaml_config=yaml_config,
|
||||
version=ImportProfile.Versions.VERSION_1,
|
||||
)
|
||||
run = ImportRun.objects.create(profile=profile, file_name=filename)
|
||||
service = ImportService(run)
|
||||
|
||||
with self.assertRaises(Exception) as cm:
|
||||
service.process_file(file_path)
|
||||
self.assertEqual(str(cm.exception), "Import failed")
|
||||
|
||||
# Should be 0 transactions because of atomic rollback
|
||||
self.assertEqual(Transaction.objects.count(), 0)
|
||||
|
||||
def test_import_missing_account(self):
|
||||
# File with account name that doesn't exist
|
||||
content = """!Type:Bank
|
||||
D04/01/2015
|
||||
T100.00
|
||||
POK
|
||||
^
|
||||
"""
|
||||
filename = "missing-account.qif"
|
||||
file_path = os.path.join(self.test_dir, filename)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
yaml_config = """
|
||||
settings:
|
||||
file_type: qif
|
||||
importing: transactions
|
||||
date_format: "%d/%m/%Y"
|
||||
mapping: {}
|
||||
"""
|
||||
profile = ImportProfile.objects.create(
|
||||
name="QIF Profile",
|
||||
yaml_config=yaml_config,
|
||||
version=ImportProfile.Versions.VERSION_1,
|
||||
)
|
||||
run = ImportRun.objects.create(profile=profile, file_name=filename)
|
||||
service = ImportService(run)
|
||||
|
||||
# Should fail because account doesn't exist
|
||||
with self.assertRaises(Exception) as cm:
|
||||
service.process_file(file_path)
|
||||
self.assertEqual(str(cm.exception), "Import failed")
|
||||
|
||||
def test_import_skip_errors(self):
|
||||
# skip_errors: true.
|
||||
# Transaction 1 valid, Transaction 2 invalid date.
|
||||
content = """!Type:Bank
|
||||
D04/01/2015
|
||||
T100.00
|
||||
POK
|
||||
^
|
||||
DINVALID
|
||||
T100.00
|
||||
PBad
|
||||
^
|
||||
"""
|
||||
filename = "bradesco-checking.qif"
|
||||
file_path = os.path.join(self.test_dir, filename)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
yaml_config = """
|
||||
settings:
|
||||
file_type: qif
|
||||
importing: transactions
|
||||
date_format: "%d/%m/%Y"
|
||||
skip_errors: true
|
||||
mapping: {}
|
||||
"""
|
||||
profile = ImportProfile.objects.create(
|
||||
name="QIF Profile",
|
||||
yaml_config=yaml_config,
|
||||
version=ImportProfile.Versions.VERSION_1,
|
||||
)
|
||||
run = ImportRun.objects.create(profile=profile, file_name=filename)
|
||||
service = ImportService(run)
|
||||
|
||||
service.process_file(file_path)
|
||||
|
||||
# Should be 1 transaction (valid one)
|
||||
self.assertEqual(Transaction.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
Transaction.objects.first().description, ""
|
||||
) # empty desc if no memo
|
||||
@@ -365,7 +365,9 @@ def check_for_transaction_rules(
|
||||
|
||||
if processed_action.set_category:
|
||||
value = simple.eval(processed_action.set_category)
|
||||
if isinstance(value, int):
|
||||
if value is None:
|
||||
transaction.category = None
|
||||
elif isinstance(value, int):
|
||||
transaction.category = TransactionCategory.objects.get(id=value)
|
||||
else:
|
||||
transaction.category = TransactionCategory.objects.get(name=value)
|
||||
@@ -458,7 +460,9 @@ def check_for_transaction_rules(
|
||||
transaction.account = account
|
||||
|
||||
elif field == TransactionRuleAction.Field.category:
|
||||
if isinstance(new_value, int):
|
||||
if new_value is None:
|
||||
transaction.category = None
|
||||
elif isinstance(new_value, int):
|
||||
category = TransactionCategory.objects.get(id=new_value)
|
||||
transaction.category = category
|
||||
elif isinstance(new_value, str):
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from apps.accounts.models import Account
|
||||
from apps.currencies.models import Currency
|
||||
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
|
||||
from apps.rules.tasks import check_for_transaction_rules
|
||||
from apps.transactions.models import Transaction
|
||||
|
||||
|
||||
def run_check_for_transaction_rules_without_worker_wrapper(**kwargs):
|
||||
task_func = check_for_transaction_rules.func
|
||||
task_func = getattr(task_func, "__wrapped__", task_func)
|
||||
|
||||
return task_func(**kwargs)
|
||||
|
||||
|
||||
class CheckForTransactionRulesTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(
|
||||
email="rules@example.com",
|
||||
password="testpass123",
|
||||
)
|
||||
self.currency = Currency.objects.create(
|
||||
code="USD",
|
||||
name="US Dollar",
|
||||
decimal_places=2,
|
||||
)
|
||||
self.account = Account.objects.create(
|
||||
name="Main Account",
|
||||
currency=self.currency,
|
||||
owner=self.user,
|
||||
)
|
||||
|
||||
@patch("apps.rules.signals.check_for_transaction_rules.defer")
|
||||
def test_update_or_create_action_can_clear_category_from_none_expression(
|
||||
self, mock_defer
|
||||
):
|
||||
source_transaction = Transaction.objects.create(
|
||||
account=self.account,
|
||||
type=Transaction.Type.EXPENSE,
|
||||
amount=Decimal("10.00"),
|
||||
date=date(2026, 5, 4),
|
||||
reference_date=date(2026, 5, 1),
|
||||
description="Source without category",
|
||||
category=None,
|
||||
owner=self.user,
|
||||
)
|
||||
rule = TransactionRule.objects.create(
|
||||
active=True,
|
||||
on_create=False,
|
||||
on_update=True,
|
||||
name="Copy transaction",
|
||||
trigger="True",
|
||||
owner=self.user,
|
||||
)
|
||||
UpdateOrCreateTransactionRuleAction.objects.create(
|
||||
rule=rule,
|
||||
set_account="account_id",
|
||||
set_type="'EX'",
|
||||
set_date="date",
|
||||
set_reference_date="reference_date",
|
||||
set_amount="amount",
|
||||
set_description="'Generated transaction'",
|
||||
set_category="category_name",
|
||||
)
|
||||
|
||||
run_check_for_transaction_rules_without_worker_wrapper(
|
||||
instance_id=source_transaction.id,
|
||||
user_id=self.user.id,
|
||||
signal="transaction_updated",
|
||||
)
|
||||
|
||||
generated_transaction = Transaction.objects.get(
|
||||
description="Generated transaction"
|
||||
)
|
||||
self.assertIsNone(generated_transaction.category)
|
||||
@@ -5,6 +5,7 @@ from apps.common.fields.forms.dynamic_select import (
|
||||
DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
from apps.common.widgets.crispy.daisyui import Switch
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput
|
||||
@@ -13,6 +14,7 @@ from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.rules.signals import transaction_created, transaction_updated
|
||||
from apps.transactions.models import (
|
||||
InstallmentPlan,
|
||||
TransactionAttachment,
|
||||
QuickTransaction,
|
||||
RecurringTransaction,
|
||||
Transaction,
|
||||
@@ -35,6 +37,22 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class MultipleFileInput(forms.ClearableFileInput):
|
||||
allow_multiple_selected = True
|
||||
|
||||
|
||||
class MultipleFileField(forms.FileField):
|
||||
widget = MultipleFileInput
|
||||
|
||||
def clean(self, data, initial=None):
|
||||
single_file_clean = super().clean
|
||||
if isinstance(data, (list, tuple)):
|
||||
return [single_file_clean(file, initial) for file in data]
|
||||
if data:
|
||||
return [single_file_clean(data, initial)]
|
||||
return []
|
||||
|
||||
|
||||
class TransactionForm(forms.ModelForm):
|
||||
category = DynamicModelChoiceField(
|
||||
create_field="name",
|
||||
@@ -116,6 +134,9 @@ class TransactionForm(forms.ModelForm):
|
||||
self.fields["account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
user_settings = get_current_user().settings
|
||||
if user_settings.default_account:
|
||||
self.fields["account"].initial = user_settings.default_account
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
@@ -243,6 +264,41 @@ class TransactionForm(forms.ModelForm):
|
||||
return instance
|
||||
|
||||
|
||||
class TransactionAttachmentForm(forms.Form):
|
||||
attachments = MultipleFileField(
|
||||
required=True,
|
||||
label=_("Attachments"),
|
||||
help_text=_("Files are private and only visible to users with access to this transaction."),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.form_method = "post"
|
||||
self.helper.layout = Layout(
|
||||
"attachments",
|
||||
FormActions(
|
||||
NoClassSubmit("submit", _("Upload"), css_class="btn btn-primary"),
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, transaction, uploaded_by):
|
||||
created = []
|
||||
for attachment in self.cleaned_data.get("attachments") or []:
|
||||
created.append(
|
||||
TransactionAttachment.objects.create(
|
||||
transaction=transaction,
|
||||
file=attachment,
|
||||
original_name=attachment.name,
|
||||
content_type=getattr(attachment, "content_type", ""),
|
||||
size=attachment.size,
|
||||
uploaded_by=uploaded_by,
|
||||
)
|
||||
)
|
||||
return created
|
||||
|
||||
|
||||
class QuickTransactionForm(forms.ModelForm):
|
||||
category = DynamicModelChoiceField(
|
||||
create_field="name",
|
||||
@@ -768,6 +824,9 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
).distinct()
|
||||
else:
|
||||
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
|
||||
user_settings = get_current_user().settings
|
||||
if user_settings.default_account:
|
||||
self.fields["account"].initial = user_settings.default_account
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
@@ -1010,6 +1069,10 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
).distinct()
|
||||
else:
|
||||
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
|
||||
|
||||
user_settings = get_current_user().settings
|
||||
if user_settings.default_account:
|
||||
self.fields["account"].initial = user_settings.default_account
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.13 on 2026-06-06 02:34
|
||||
|
||||
import apps.transactions.models
|
||||
import apps.transactions.storage
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0048_recurringtransaction_keep_at_most'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TransactionAttachment',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('file', models.FileField(storage=apps.transactions.storage.PrivateMediaStorage(), upload_to=apps.transactions.models.transaction_attachment_path, verbose_name='File')),
|
||||
('original_name', models.CharField(max_length=255, verbose_name='Original Name')),
|
||||
('content_type', models.CharField(blank=True, max_length=255, verbose_name='Content Type')),
|
||||
('size', models.PositiveBigIntegerField(default=0, verbose_name='Size')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='transactions.transaction', verbose_name='Transaction')),
|
||||
('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transaction_attachments', to=settings.AUTH_USER_MODEL, verbose_name='Uploaded By')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Transaction Attachment',
|
||||
'verbose_name_plural': 'Transaction Attachments',
|
||||
'db_table': 'transaction_attachments',
|
||||
'ordering': ['-created_at', 'original_name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
import decimal
|
||||
import logging
|
||||
import uuid
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from apps.common.fields.month_year import MonthYearModelField
|
||||
from apps.common.functions.decimals import truncate_decimal
|
||||
@@ -13,13 +15,15 @@ from apps.common.models import (
|
||||
)
|
||||
from apps.common.templatetags.decimal import drop_trailing_zeros, localize_number
|
||||
from apps.currencies.utils.convert import convert
|
||||
from apps.transactions.storage import PrivateMediaStorage
|
||||
from apps.transactions.validators import validate_decimal_places, validate_non_negative
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.dispatch import Signal
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.template.defaultfilters import date
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -32,6 +36,11 @@ transaction_updated = Signal()
|
||||
transaction_deleted = Signal()
|
||||
|
||||
|
||||
def transaction_attachment_path(instance, filename):
|
||||
extension = Path(filename).suffix
|
||||
return f"transaction_attachments/{instance.transaction_id}/{instance.id}{extension}"
|
||||
|
||||
|
||||
class SoftDeleteQuerySet(models.QuerySet):
|
||||
@staticmethod
|
||||
def _emit_signals(instances, created=False, old_data=None):
|
||||
@@ -526,6 +535,60 @@ class Transaction(OwnedObject):
|
||||
|
||||
return new_obj
|
||||
|
||||
class TransactionAttachment(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
transaction = models.ForeignKey(
|
||||
Transaction,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="attachments",
|
||||
verbose_name=_("Transaction"),
|
||||
)
|
||||
file = models.FileField(
|
||||
upload_to=transaction_attachment_path,
|
||||
storage=PrivateMediaStorage(),
|
||||
verbose_name=_("File"),
|
||||
)
|
||||
original_name = models.CharField(max_length=255, verbose_name=_("Original Name"))
|
||||
content_type = models.CharField(
|
||||
max_length=255, blank=True, verbose_name=_("Content Type")
|
||||
)
|
||||
size = models.PositiveBigIntegerField(default=0, verbose_name=_("Size"))
|
||||
uploaded_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="transaction_attachments",
|
||||
verbose_name=_("Uploaded By"),
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Transaction Attachment")
|
||||
verbose_name_plural = _("Transaction Attachments")
|
||||
db_table = "transaction_attachments"
|
||||
ordering = ["-created_at", "original_name"]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.file:
|
||||
if not self.original_name:
|
||||
self.original_name = Path(self.file.name).name
|
||||
if not self.size:
|
||||
self.size = self.file.size
|
||||
if not self.content_type:
|
||||
self.content_type = getattr(self.file.file, "content_type", "")
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.original_name
|
||||
|
||||
|
||||
@receiver(post_delete, sender=TransactionAttachment)
|
||||
def delete_transaction_attachment_file(sender, instance, **kwargs):
|
||||
if not instance.file.name:
|
||||
return
|
||||
|
||||
storage = instance.file.storage
|
||||
if storage.exists(instance.file.name):
|
||||
storage.delete(instance.file.name)
|
||||
|
||||
class InstallmentPlan(models.Model):
|
||||
class Recurrence(models.TextChoices):
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
|
||||
class PrivateMediaStorage(FileSystemStorage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("location", settings.ATTACHMENT_MEDIA_ROOT)
|
||||
kwargs.setdefault("base_url", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -0,0 +1,219 @@
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from apps.accounts.models import Account
|
||||
from apps.common.middleware.thread_local import delete_current_user, write_current_user
|
||||
from apps.currencies.models import Currency
|
||||
from apps.transactions.models import Transaction, TransactionAttachment
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@override_settings(
|
||||
STORAGES={
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||
},
|
||||
},
|
||||
WHITENOISE_AUTOREFRESH=True,
|
||||
)
|
||||
class TransactionAttachmentTests(TestCase):
|
||||
def setUp(self):
|
||||
self.attachment_media_root = tempfile.mkdtemp()
|
||||
self.override_private_media = override_settings(
|
||||
ATTACHMENT_MEDIA_ROOT=self.attachment_media_root
|
||||
)
|
||||
self.override_private_media.enable()
|
||||
self.addCleanup(self.override_private_media.disable)
|
||||
self.addCleanup(shutil.rmtree, self.attachment_media_root, ignore_errors=True)
|
||||
|
||||
self.attachment_storage = TransactionAttachment._meta.get_field("file").storage
|
||||
self.original_storage_location = self.attachment_storage._location
|
||||
self.attachment_storage._location = self.attachment_media_root
|
||||
self.attachment_storage.__dict__.pop("base_location", None)
|
||||
self.attachment_storage.__dict__.pop("location", None)
|
||||
self.addCleanup(self.restore_attachment_storage)
|
||||
|
||||
User = get_user_model()
|
||||
self.user1 = User.objects.create_user(
|
||||
email="user1@test.com", password="testpass123"
|
||||
)
|
||||
self.user2 = User.objects.create_user(
|
||||
email="user2@test.com", password="testpass123"
|
||||
)
|
||||
|
||||
self.currency = Currency.objects.create(
|
||||
code="USD", name="US Dollar", decimal_places=2, prefix="$ "
|
||||
)
|
||||
self.user1_account = Account.all_objects.create(
|
||||
name="User1 Account", currency=self.currency, owner=self.user1
|
||||
)
|
||||
self.user2_account = Account.all_objects.create(
|
||||
name="User2 Account", currency=self.currency, owner=self.user2
|
||||
)
|
||||
self.transaction = Transaction.userless_all_objects.create(
|
||||
account=self.user1_account,
|
||||
type=Transaction.Type.EXPENSE,
|
||||
amount=Decimal("12.34"),
|
||||
is_paid=True,
|
||||
date=date(2026, 6, 5),
|
||||
description="Receipt transaction",
|
||||
owner=self.user1,
|
||||
)
|
||||
self.other_transaction = Transaction.userless_all_objects.create(
|
||||
account=self.user2_account,
|
||||
type=Transaction.Type.EXPENSE,
|
||||
amount=Decimal("56.78"),
|
||||
is_paid=True,
|
||||
date=date(2026, 6, 5),
|
||||
description="Other receipt transaction",
|
||||
owner=self.user2,
|
||||
)
|
||||
|
||||
def restore_attachment_storage(self):
|
||||
self.attachment_storage._location = self.original_storage_location
|
||||
self.attachment_storage.__dict__.pop("base_location", None)
|
||||
self.attachment_storage.__dict__.pop("location", None)
|
||||
|
||||
def test_attachment_uses_uuid_and_preserves_original_download_name(self):
|
||||
attachment = TransactionAttachment.objects.create(
|
||||
transaction=self.transaction,
|
||||
file=SimpleUploadedFile(
|
||||
"receipt June.pdf", b"receipt bytes", content_type="application/pdf"
|
||||
),
|
||||
uploaded_by=self.user1,
|
||||
)
|
||||
|
||||
self.assertEqual(attachment.original_name, "receipt June.pdf")
|
||||
self.assertNotIn("receipt June.pdf", attachment.file.name)
|
||||
|
||||
self.client.force_login(self.user1)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"transaction_attachment_download",
|
||||
kwargs={"attachment_id": attachment.id},
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(b"".join(response.streaming_content), b"receipt bytes")
|
||||
self.assertIn('filename="receipt June.pdf"', response["Content-Disposition"])
|
||||
|
||||
def test_user_without_transaction_access_cannot_download_attachment(self):
|
||||
attachment = TransactionAttachment.objects.create(
|
||||
transaction=self.other_transaction,
|
||||
file=SimpleUploadedFile("private.txt", b"private"),
|
||||
uploaded_by=self.user2,
|
||||
)
|
||||
|
||||
self.client.force_login(self.user1)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"transaction_attachment_download",
|
||||
kwargs={"attachment_id": attachment.id},
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_attachment_button_lives_in_transaction_hover_toolbar(self):
|
||||
template = Path("templates/cotton/transaction/item.html").read_text()
|
||||
before_toolbar, toolbar = template.split("{# Item actions#}", 1)
|
||||
|
||||
self.assertNotIn("transaction_attachments", before_toolbar)
|
||||
self.assertLess(
|
||||
toolbar.index("transaction_edit"),
|
||||
toolbar.index("transaction_attachments"),
|
||||
)
|
||||
self.assertLess(
|
||||
toolbar.index("transaction_attachments"),
|
||||
toolbar.index("transaction_delete"),
|
||||
)
|
||||
|
||||
def test_transaction_edit_form_does_not_include_attachment_upload(self):
|
||||
self.client.force_login(self.user1)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("transaction_edit", kwargs={"transaction_id": self.transaction.id}),
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, "multipart/form-data")
|
||||
self.assertNotContains(response, 'type="file"')
|
||||
|
||||
def test_attachment_management_uploads_multiple_attachments(self):
|
||||
self.client.force_login(self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"transaction_attachments",
|
||||
kwargs={"transaction_id": self.transaction.id},
|
||||
),
|
||||
{
|
||||
"attachments": [
|
||||
SimpleUploadedFile("first.txt", b"first"),
|
||||
SimpleUploadedFile("second.txt", b"second"),
|
||||
],
|
||||
},
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "first.txt")
|
||||
self.assertContains(response, "second.txt")
|
||||
self.assertEqual(self.transaction.attachments.count(), 2)
|
||||
|
||||
def test_attachment_delete_returns_refreshed_attachment_list(self):
|
||||
attachment = TransactionAttachment.objects.create(
|
||||
transaction=self.transaction,
|
||||
file=SimpleUploadedFile("delete-me.txt", b"delete"),
|
||||
uploaded_by=self.user1,
|
||||
)
|
||||
|
||||
self.client.force_login(self.user1)
|
||||
response = self.client.delete(
|
||||
reverse(
|
||||
"transaction_attachment_delete",
|
||||
kwargs={"attachment_id": attachment.id},
|
||||
),
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, "delete-me.txt")
|
||||
self.assertContains(response, "No attachments yet")
|
||||
self.assertFalse(
|
||||
TransactionAttachment.objects.filter(id=attachment.id).exists()
|
||||
)
|
||||
|
||||
def test_hard_deleting_transaction_deletes_attachment_files(self):
|
||||
attachment = TransactionAttachment.objects.create(
|
||||
transaction=self.transaction,
|
||||
file=SimpleUploadedFile("hard-delete.txt", b"delete with transaction"),
|
||||
uploaded_by=self.user1,
|
||||
)
|
||||
file_path = Path(attachment.file.path)
|
||||
|
||||
self.assertTrue(file_path.exists())
|
||||
|
||||
write_current_user(self.user1)
|
||||
self.addCleanup(delete_current_user)
|
||||
|
||||
self.transaction.delete()
|
||||
|
||||
self.assertTrue(file_path.exists())
|
||||
self.assertTrue(TransactionAttachment.objects.filter(id=attachment.id).exists())
|
||||
|
||||
self.transaction.delete()
|
||||
|
||||
self.assertFalse(file_path.exists())
|
||||
self.assertFalse(
|
||||
TransactionAttachment.objects.filter(id=attachment.id).exists()
|
||||
)
|
||||
@@ -81,6 +81,26 @@ urlpatterns = [
|
||||
views.transaction_move_to_today,
|
||||
name="transaction_move_to_today",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/attachments/",
|
||||
views.transaction_attachments,
|
||||
name="transaction_attachments",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/attachments/list/",
|
||||
views.transaction_attachments_list,
|
||||
name="transaction_attachments_list",
|
||||
),
|
||||
path(
|
||||
"transaction/attachments/<uuid:attachment_id>/download/",
|
||||
views.transaction_attachment_download,
|
||||
name="transaction_attachment_download",
|
||||
),
|
||||
path(
|
||||
"transaction/attachments/<uuid:attachment_id>/delete/",
|
||||
views.transaction_attachment_delete,
|
||||
name="transaction_attachment_delete",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/delete/",
|
||||
views.transaction_delete,
|
||||
|
||||
@@ -1,32 +1,120 @@
|
||||
import datetime
|
||||
from copy import deepcopy
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q, When, Case, Value, IntegerField
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _, ngettext_lazy
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from apps.common.decorators.demo import disabled_on_demo
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.rules.signals import transaction_created, transaction_updated
|
||||
from apps.transactions.filters import TransactionsFilter
|
||||
from apps.transactions.forms import (
|
||||
BulkEditTransactionForm,
|
||||
TransactionAttachmentForm,
|
||||
TransactionForm,
|
||||
TransferForm,
|
||||
BulkEditTransactionForm,
|
||||
)
|
||||
from apps.transactions.models import Transaction
|
||||
from apps.transactions.models import Transaction, TransactionAttachment
|
||||
from apps.transactions.utils.calculations import (
|
||||
calculate_currency_totals,
|
||||
calculate_account_totals,
|
||||
calculate_currency_totals,
|
||||
calculate_percentage_distribution,
|
||||
)
|
||||
from apps.transactions.utils.default_ordering import default_order
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Case, IntegerField, Q, Value, When
|
||||
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext_lazy
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
|
||||
def _get_accessible_transaction_or_404(transaction_id):
|
||||
return get_object_or_404(Transaction.objects, id=transaction_id)
|
||||
|
||||
|
||||
def _get_accessible_attachment_or_404(attachment_id):
|
||||
attachment = get_object_or_404(
|
||||
TransactionAttachment.objects.select_related("transaction"),
|
||||
id=attachment_id,
|
||||
)
|
||||
if not Transaction.objects.filter(id=attachment.transaction_id).exists():
|
||||
raise Http404()
|
||||
return attachment
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def transaction_attachments(request, transaction_id):
|
||||
transaction = _get_accessible_transaction_or_404(transaction_id)
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionAttachmentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
form.save(transaction=transaction, uploaded_by=request.user)
|
||||
messages.success(request, _("Attachment uploaded successfully"))
|
||||
form = TransactionAttachmentForm()
|
||||
else:
|
||||
form = TransactionAttachmentForm()
|
||||
|
||||
response = render(
|
||||
request,
|
||||
"transactions/fragments/attachments_manage.html",
|
||||
{"form": form, "transaction": transaction},
|
||||
)
|
||||
|
||||
response["HX-Trigger"] = "toasts, updated"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_attachments_list(request, transaction_id):
|
||||
transaction = _get_accessible_transaction_or_404(transaction_id)
|
||||
return render(
|
||||
request,
|
||||
"transactions/fragments/attachments.html",
|
||||
{"transaction": transaction},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_attachment_download(request, attachment_id):
|
||||
attachment = _get_accessible_attachment_or_404(attachment_id)
|
||||
return FileResponse(
|
||||
attachment.file.open("rb"),
|
||||
as_attachment=False,
|
||||
filename=attachment.original_name,
|
||||
content_type=attachment.content_type or "application/octet-stream",
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@disabled_on_demo
|
||||
@require_http_methods(["DELETE"])
|
||||
def transaction_attachment_delete(request, attachment_id):
|
||||
attachment = _get_accessible_attachment_or_404(attachment_id)
|
||||
transaction = attachment.transaction
|
||||
attachment.file.delete(save=False)
|
||||
attachment.delete()
|
||||
messages.success(request, _("Attachment deleted successfully"))
|
||||
response = render(
|
||||
request,
|
||||
"transactions/fragments/attachments.html",
|
||||
{"transaction": transaction},
|
||||
)
|
||||
response["HX-Trigger"] = "toasts, updated"
|
||||
return response
|
||||
|
||||
|
||||
@only_htmx
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutoConnectSocialAccountAdapter(DefaultSocialAccountAdapter):
|
||||
"""
|
||||
Custom adapter to automatically connect social accounts to existing users
|
||||
with the same email address.
|
||||
|
||||
SECURITY WARNING:
|
||||
This adapter automatically connects OIDC accounts to existing local accounts
|
||||
based on email matching.
|
||||
|
||||
If your OIDC provider allows unverified emails, this could lead to
|
||||
ACCOUNT TAKEOVER attacks where an attacker creates an OIDC account
|
||||
with someone else's email and gains access to their account.
|
||||
"""
|
||||
|
||||
def pre_social_login(self, request, sociallogin):
|
||||
"""
|
||||
Invoked just after a user successfully authenticates via a
|
||||
social provider, but before the login is actually processed.
|
||||
|
||||
If a user with the same email already exists, connect the social
|
||||
account to that existing user instead of creating a new account.
|
||||
"""
|
||||
# If the social account is already connected to a user, do nothing
|
||||
if sociallogin.is_existing:
|
||||
return
|
||||
|
||||
# Check if we have an email from the social provider
|
||||
if not sociallogin.email_addresses:
|
||||
logger.warning(
|
||||
"OIDC login attempted without email address. "
|
||||
f"Provider: {sociallogin.account.provider}"
|
||||
)
|
||||
return
|
||||
|
||||
# Get the email from the social login
|
||||
email = sociallogin.email_addresses[0].email.lower()
|
||||
|
||||
# Try to find an existing user with this email
|
||||
try:
|
||||
user = User.objects.get(email__iexact=email)
|
||||
|
||||
# Log this connection for security audit trail
|
||||
logger.info(
|
||||
f"Auto-connecting OIDC account to existing user. "
|
||||
f"Email: {email}, Provider: {sociallogin.account.provider}, "
|
||||
f"User ID: {user.id}"
|
||||
)
|
||||
|
||||
# Connect the social account to the existing user
|
||||
sociallogin.connect(request, user)
|
||||
|
||||
except User.DoesNotExist:
|
||||
# No user with this email exists, proceed with normal signup flow
|
||||
logger.debug(
|
||||
f"No existing user found for email {email}. "
|
||||
"Proceeding with new account creation."
|
||||
)
|
||||
pass
|
||||
except User.MultipleObjectsReturned:
|
||||
# Multiple users with the same email (shouldn't happen with unique constraint)
|
||||
logger.error(
|
||||
f"Multiple users found with email {email}. "
|
||||
"This should not happen with unique constraint. "
|
||||
"Blocking auto-connect."
|
||||
)
|
||||
# Let the default behavior handle this
|
||||
pass
|
||||
@@ -1,6 +1,8 @@
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||
from apps.common.widgets.tom_select import TomSelect
|
||||
from apps.users.models import UserSettings
|
||||
from apps.accounts.models import Account
|
||||
from crispy_forms.bootstrap import (
|
||||
FormActions,
|
||||
)
|
||||
@@ -116,6 +118,15 @@ class UserSettingsForm(forms.ModelForm):
|
||||
label=_("Number Format"),
|
||||
)
|
||||
|
||||
default_account = forms.ModelChoiceField(
|
||||
queryset=Account.objects.filter(
|
||||
is_archived=False,
|
||||
),
|
||||
label=_("Default Account"),
|
||||
widget=TomSelect(clear_button=False, group_by="group"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
fields = [
|
||||
@@ -126,11 +137,19 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"datetime_format",
|
||||
"number_format",
|
||||
"volume",
|
||||
"default_account",
|
||||
]
|
||||
widgets = {
|
||||
"default_account": TomSelect(clear_button=False, group_by="group"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["default_account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
self.helper.form_method = "post"
|
||||
@@ -143,6 +162,7 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"number_format",
|
||||
HTML('<hr class="hr my-3" />'),
|
||||
"start_page",
|
||||
"default_account",
|
||||
HTML('<hr class="hr my-3" />'),
|
||||
"volume",
|
||||
FormActions(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.9 on 2026-02-15 21:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0016_account_untracked_by"),
|
||||
("users", "0023_alter_usersettings_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="usersettings",
|
||||
name="default_account",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="accounts.account",
|
||||
verbose_name="Default account",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.9 on 2026-02-16 01:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0016_account_untracked_by'),
|
||||
('users', '0024_usersettings_default_account'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usersettings',
|
||||
name='default_account',
|
||||
field=models.ForeignKey(blank=True, help_text='Selects the account by default when creating new transactions', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.account', verbose_name='Default account'),
|
||||
),
|
||||
]
|
||||
@@ -510,6 +510,14 @@ class UserSettings(models.Model):
|
||||
default=StartPage.MONTHLY,
|
||||
verbose_name=_("Start page"),
|
||||
)
|
||||
default_account = models.ForeignKey(
|
||||
"accounts.Account",
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Default account"),
|
||||
help_text=_("Selects the account by default when creating new transactions"),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email}'s settings"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
settings:
|
||||
file_type: qif
|
||||
importing: transactions
|
||||
encoding: cp1252
|
||||
date_format: "%d/%m/%Y"
|
||||
skip_errors: true
|
||||
|
||||
mapping: {}
|
||||
|
||||
deduplicate: []
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"author": "eitchtee",
|
||||
"description": "Standard QIF Import. Mapping is automatic.",
|
||||
"schema_version": 1,
|
||||
"name": "Standard QIF",
|
||||
"message": "Account is inferred from filename (e.g., 'Checking.qif' -> Account 'Checking').\nYou might need to change the date format to match the date format on your file."
|
||||
}
|
||||
+319
-219
File diff suppressed because it is too large
Load Diff
+272
-177
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
|
||||
"POT-Creation-Date: 2026-06-06 07:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -26,12 +26,12 @@ msgstr ""
|
||||
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
|
||||
#: apps/currencies/forms.py:136 apps/dca/forms.py:46 apps/dca/forms.py:205
|
||||
#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
|
||||
#: apps/rules/forms.py:385 apps/transactions/forms.py:197
|
||||
#: apps/transactions/forms.py:361 apps/transactions/forms.py:480
|
||||
#: apps/transactions/forms.py:821 apps/transactions/forms.py:860
|
||||
#: apps/transactions/forms.py:888 apps/transactions/forms.py:919
|
||||
#: apps/transactions/forms.py:1065 apps/users/forms.py:222
|
||||
#: apps/users/forms.py:380
|
||||
#: apps/rules/forms.py:385 apps/transactions/forms.py:218
|
||||
#: apps/transactions/forms.py:417 apps/transactions/forms.py:536
|
||||
#: apps/transactions/forms.py:880 apps/transactions/forms.py:919
|
||||
#: apps/transactions/forms.py:947 apps/transactions/forms.py:978
|
||||
#: apps/transactions/forms.py:1128 apps/users/forms.py:242
|
||||
#: apps/users/forms.py:400
|
||||
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
|
||||
#: templates/rules/fragments/transaction_rule/view.html:128
|
||||
msgid "Update"
|
||||
@@ -42,11 +42,11 @@ msgstr ""
|
||||
#: apps/currencies/forms.py:93 apps/currencies/forms.py:142
|
||||
#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
|
||||
#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
|
||||
#: apps/transactions/forms.py:184 apps/transactions/forms.py:204
|
||||
#: apps/transactions/forms.py:368 apps/transactions/forms.py:827
|
||||
#: apps/transactions/forms.py:866 apps/transactions/forms.py:894
|
||||
#: apps/transactions/forms.py:925 apps/transactions/forms.py:1071
|
||||
#: apps/users/forms.py:228 apps/users/forms.py:386
|
||||
#: apps/transactions/forms.py:205 apps/transactions/forms.py:225
|
||||
#: apps/transactions/forms.py:424 apps/transactions/forms.py:886
|
||||
#: apps/transactions/forms.py:925 apps/transactions/forms.py:953
|
||||
#: apps/transactions/forms.py:984 apps/transactions/forms.py:1134
|
||||
#: apps/users/forms.py:248 apps/users/forms.py:406
|
||||
#: templates/mini_tools/unit_price_calculator.html:168
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
@@ -62,12 +62,12 @@ msgstr ""
|
||||
#: apps/accounts/forms.py:125 apps/dca/forms.py:79 apps/dca/forms.py:86
|
||||
#: apps/insights/forms.py:117 apps/rules/forms.py:181 apps/rules/forms.py:197
|
||||
#: apps/rules/models.py:44 apps/rules/models.py:311
|
||||
#: apps/transactions/forms.py:43 apps/transactions/forms.py:251
|
||||
#: apps/transactions/forms.py:419 apps/transactions/forms.py:516
|
||||
#: apps/transactions/forms.py:523 apps/transactions/forms.py:707
|
||||
#: apps/transactions/forms.py:948 apps/transactions/models.py:322
|
||||
#: apps/transactions/models.py:578 apps/transactions/models.py:778
|
||||
#: apps/transactions/models.py:1026
|
||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
||||
#: apps/transactions/models.py:1089
|
||||
#: templates/insights/fragments/category_overview/index.html:86
|
||||
#: templates/insights/fragments/category_overview/index.html:542
|
||||
#: templates/insights/fragments/month_by_month.html:84
|
||||
@@ -79,12 +79,12 @@ msgstr ""
|
||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
||||
#: apps/transactions/forms.py:51 apps/transactions/forms.py:259
|
||||
#: apps/transactions/forms.py:427 apps/transactions/forms.py:532
|
||||
#: apps/transactions/forms.py:540 apps/transactions/forms.py:700
|
||||
#: apps/transactions/forms.py:941 apps/transactions/models.py:328
|
||||
#: apps/transactions/models.py:580 apps/transactions/models.py:782
|
||||
#: apps/transactions/models.py:1032 templates/includes/sidebar.html:150
|
||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
||||
#: templates/insights/fragments/category_overview/index.html:40
|
||||
#: templates/insights/fragments/month_by_month.html:29
|
||||
#: templates/insights/fragments/month_by_month.html:32
|
||||
@@ -96,8 +96,8 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||
#: apps/transactions/models.py:214 apps/transactions/models.py:239
|
||||
#: apps/transactions/models.py:263 apps/transactions/models.py:994
|
||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
||||
#: templates/account_groups/fragments/list.html:22
|
||||
#: templates/accounts/fragments/list.html:22
|
||||
#: templates/categories/fragments/table.html:17
|
||||
@@ -163,11 +163,11 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
|
||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||
#: apps/transactions/forms.py:63 apps/transactions/forms.py:271
|
||||
#: apps/transactions/forms.py:386 apps/transactions/forms.py:692
|
||||
#: apps/transactions/forms.py:933 apps/transactions/models.py:294
|
||||
#: apps/transactions/models.py:538 apps/transactions/models.py:760
|
||||
#: apps/transactions/models.py:1000
|
||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
||||
#: apps/transactions/models.py:1063
|
||||
#: templates/installment_plans/fragments/table.html:17
|
||||
#: templates/quick_transactions/fragments/list.html:14
|
||||
#: templates/recurring_transactions/fragments/table.html:19
|
||||
@@ -344,7 +344,7 @@ msgid ""
|
||||
"owner.<br/>Public: Shown for all users. Only editable by the owner."
|
||||
msgstr ""
|
||||
|
||||
#: apps/common/forms.py:76 apps/users/forms.py:149
|
||||
#: apps/common/forms.py:76 apps/users/forms.py:169
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@@ -480,8 +480,8 @@ msgstr ""
|
||||
|
||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
||||
#: apps/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||
#: apps/transactions/forms.py:67 apps/transactions/forms.py:391
|
||||
#: apps/transactions/forms.py:544 apps/transactions/models.py:304
|
||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
||||
#: templates/dca/fragments/strategy/details.html:49
|
||||
#: templates/exchange_rates/fragments/table.html:10
|
||||
#: templates/exchange_rates_services/fragments/table.html:11
|
||||
@@ -567,8 +567,8 @@ msgid "Service Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/currencies/models.py:118 apps/transactions/filters.py:27
|
||||
#: apps/transactions/models.py:218 apps/transactions/models.py:242
|
||||
#: apps/transactions/models.py:266 templates/categories/fragments/list.html:16
|
||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
||||
#: templates/entities/fragments/list.html:16
|
||||
#: templates/installment_plans/fragments/list.html:16
|
||||
#: templates/recurring_transactions/fragments/list.html:16
|
||||
@@ -696,11 +696,11 @@ msgstr ""
|
||||
msgid "Create transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:64 apps/transactions/forms.py:491
|
||||
#: apps/dca/forms.py:64 apps/transactions/forms.py:547
|
||||
msgid "From Account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:70 apps/transactions/forms.py:496
|
||||
#: apps/dca/forms.py:70 apps/transactions/forms.py:552
|
||||
msgid "To Account"
|
||||
msgstr ""
|
||||
|
||||
@@ -725,7 +725,7 @@ msgstr ""
|
||||
msgid "You must provide an account."
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:290 apps/transactions/forms.py:638
|
||||
#: apps/dca/forms.py:290 apps/transactions/forms.py:694
|
||||
msgid "From and To accounts must be different."
|
||||
msgstr ""
|
||||
|
||||
@@ -744,9 +744,9 @@ msgstr ""
|
||||
|
||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
||||
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||
#: apps/transactions/forms.py:413 apps/transactions/forms.py:560
|
||||
#: apps/transactions/models.py:318 apps/transactions/models.py:587
|
||||
#: apps/transactions/models.py:788 apps/transactions/models.py:1022
|
||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
@@ -809,7 +809,7 @@ msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||
#: apps/transactions/models.py:379 templates/includes/sidebar.html:81
|
||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
||||
#: templates/includes/sidebar.html:142
|
||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||
#: templates/recurring_transactions/fragments/table.html:37
|
||||
@@ -830,11 +830,11 @@ msgstr ""
|
||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
||||
#: apps/transactions/forms.py:59 apps/transactions/forms.py:267
|
||||
#: apps/transactions/forms.py:435 apps/transactions/forms.py:715
|
||||
#: apps/transactions/forms.py:956 apps/transactions/models.py:277
|
||||
#: apps/transactions/models.py:333 apps/transactions/models.py:583
|
||||
#: apps/transactions/models.py:785 apps/transactions/models.py:1037
|
||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
||||
#: templates/entities/fragments/list.html:9
|
||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||
#: templates/insights/fragments/category_overview/index.html:54
|
||||
@@ -846,14 +846,14 @@ msgid "Entities"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||
#: apps/transactions/models.py:825 templates/includes/sidebar.html:110
|
||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
||||
#: templates/recurring_transactions/fragments/list.html:9
|
||||
#: templates/recurring_transactions/pages/index.html:4
|
||||
msgid "Recurring Transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||
#: apps/transactions/models.py:601 templates/includes/sidebar.html:104
|
||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
||||
#: templates/installment_plans/fragments/list.html:9
|
||||
#: templates/installment_plans/pages/index.html:4
|
||||
msgid "Installment Plans"
|
||||
@@ -906,7 +906,7 @@ msgstr ""
|
||||
msgid "Update or create transaction actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:224
|
||||
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:230
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
|
||||
#: templates/export_app/fragments/restore.html:5
|
||||
#: templates/export_app/pages/index.html:19
|
||||
@@ -1102,16 +1102,16 @@ msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||
#: apps/rules/models.py:271 apps/transactions/forms.py:377
|
||||
#: apps/transactions/models.py:301 apps/transactions/models.py:543
|
||||
#: apps/transactions/models.py:766 apps/transactions/models.py:1007
|
||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
||||
#: apps/rules/models.py:275 apps/transactions/filters.py:22
|
||||
#: apps/transactions/forms.py:381 apps/transactions/models.py:303
|
||||
#: apps/transactions/models.py:1009 templates/cotton/transaction/item.html:20
|
||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
||||
#: templates/cotton/transaction/item.html:31
|
||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||
@@ -1119,17 +1119,17 @@ msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||
#: apps/rules/models.py:283 apps/transactions/forms.py:71
|
||||
#: apps/transactions/forms.py:397 apps/transactions/forms.py:547
|
||||
#: apps/transactions/forms.py:721 apps/transactions/models.py:305
|
||||
#: apps/transactions/models.py:561 apps/transactions/models.py:790
|
||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
||||
msgid "Reference Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||
#: apps/rules/models.py:287 apps/transactions/forms.py:404
|
||||
#: apps/transactions/models.py:311 apps/transactions/models.py:771
|
||||
#: apps/transactions/models.py:1015
|
||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
||||
#: apps/transactions/models.py:1078
|
||||
#: templates/insights/fragments/sankey.html:102
|
||||
#: templates/installment_plans/fragments/table.html:18
|
||||
#: templates/quick_transactions/fragments/list.html:15
|
||||
@@ -1139,28 +1139,28 @@ msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
||||
#: apps/rules/models.py:42 apps/rules/models.py:291
|
||||
#: apps/transactions/forms.py:408 apps/transactions/forms.py:551
|
||||
#: apps/transactions/models.py:316 apps/transactions/models.py:545
|
||||
#: apps/transactions/models.py:774 apps/transactions/models.py:1020
|
||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||
#: apps/rules/models.py:299 apps/transactions/models.py:355
|
||||
#: apps/transactions/models.py:1042
|
||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
||||
#: apps/transactions/models.py:1105
|
||||
msgid "Internal Note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||
#: apps/rules/models.py:303 apps/transactions/models.py:357
|
||||
#: apps/transactions/models.py:1044
|
||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
||||
#: apps/transactions/models.py:1107
|
||||
msgid "Internal ID"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||
#: apps/rules/models.py:319 apps/transactions/forms.py:564
|
||||
#: apps/transactions/models.py:215 apps/transactions/models.py:306
|
||||
#: apps/transactions/models.py:1010
|
||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
||||
#: apps/transactions/models.py:1073
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
|
||||
@@ -1173,7 +1173,7 @@ msgid "Set Values"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||
#: apps/transactions/models.py:378
|
||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
||||
msgid "Transaction"
|
||||
msgstr ""
|
||||
|
||||
@@ -1378,96 +1378,110 @@ msgstr ""
|
||||
msgid "No entity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:170
|
||||
#: apps/transactions/forms.py:191
|
||||
msgid "More"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:207
|
||||
#: apps/transactions/forms.py:228
|
||||
msgid "Save and add similar"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:212
|
||||
#: apps/transactions/forms.py:233
|
||||
msgid "Save and add another"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:295 apps/transactions/forms.py:567
|
||||
#: apps/transactions/forms.py:270 templates/cotton/transaction/item.html:158
|
||||
#: templates/transactions/fragments/attachments.html:4
|
||||
msgid "Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:271
|
||||
msgid ""
|
||||
"Files are private and only visible to users with access to this transaction."
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:282
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:351 apps/transactions/forms.py:623
|
||||
msgid "Muted transactions won't be displayed on monthly summaries"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:503
|
||||
#: apps/transactions/forms.py:559
|
||||
msgid "From Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:508
|
||||
#: apps/transactions/forms.py:564
|
||||
msgid "To Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:606
|
||||
#: apps/transactions/forms.py:662
|
||||
#: templates/cotton/ui/quick_transactions_buttons.html:40
|
||||
#: templates/cotton/ui/transactions_fab.html:44
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:847
|
||||
#: apps/transactions/forms.py:906
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:875
|
||||
#: apps/transactions/forms.py:934
|
||||
msgid "Entity name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:903
|
||||
#: apps/transactions/forms.py:962
|
||||
msgid "Category name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:905
|
||||
#: apps/transactions/forms.py:964
|
||||
msgid "Muted categories won't be displayed on monthly summaries"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:1055
|
||||
#: apps/transactions/forms.py:1118
|
||||
msgid "future transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:1081
|
||||
#: apps/transactions/forms.py:1144
|
||||
msgid "End date should be after the start date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:220
|
||||
#: apps/transactions/models.py:229
|
||||
msgid ""
|
||||
"Deactivated categories won't be able to be selected when creating new "
|
||||
"transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:228
|
||||
#: apps/transactions/models.py:237
|
||||
msgid "Transaction Category"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:229
|
||||
#: apps/transactions/models.py:238
|
||||
msgid "Transaction Categories"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:244
|
||||
#: apps/transactions/models.py:253
|
||||
msgid ""
|
||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:252 apps/transactions/models.py:253
|
||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
||||
msgid "Transaction Tags"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:268
|
||||
#: apps/transactions/models.py:277
|
||||
msgid ""
|
||||
"Deactivated entities won't be able to be selected when creating new "
|
||||
"transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:276
|
||||
#: apps/transactions/models.py:285
|
||||
#: templates/insights/fragments/month_by_month.html:88
|
||||
#: templates/insights/fragments/year_by_year.html:56
|
||||
msgid "Entity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:288 apps/transactions/models.py:987
|
||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
||||
#: templates/calendar_view/fragments/list.html:42
|
||||
#: templates/calendar_view/fragments/list.html:44
|
||||
#: templates/calendar_view/fragments/list.html:52
|
||||
@@ -1479,7 +1493,7 @@ msgstr ""
|
||||
msgid "Income"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:289 apps/transactions/models.py:988
|
||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
||||
#: templates/calendar_view/fragments/list.html:46
|
||||
#: templates/calendar_view/fragments/list.html:48
|
||||
#: templates/calendar_view/fragments/list.html:56
|
||||
@@ -1490,129 +1504,157 @@ msgstr ""
|
||||
msgid "Expense"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:344 apps/transactions/models.py:600
|
||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
||||
msgid "Installment Plan"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:353 apps/transactions/models.py:824
|
||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
||||
msgid "Recurring Transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:361
|
||||
#: apps/transactions/models.py:370
|
||||
msgid "Deleted"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:366
|
||||
#: apps/transactions/models.py:375
|
||||
msgid "Deleted At"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:480 templates/tags/fragments/table.html:69
|
||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
||||
msgid "No tags"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:482
|
||||
#: apps/transactions/models.py:491
|
||||
msgid "No category"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:484
|
||||
#: apps/transactions/models.py:493
|
||||
msgid "No description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:532 templates/includes/sidebar.html:57
|
||||
#: apps/transactions/models.py:549
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:551
|
||||
msgid "Original Name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:553
|
||||
msgid "Content Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:555
|
||||
msgid "Size"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:560
|
||||
msgid "Uploaded By"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:565
|
||||
msgid "Transaction Attachment"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:566
|
||||
msgid "Transaction Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
||||
msgid "Yearly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:533 apps/users/models.py:464
|
||||
#: apps/transactions/models.py:596 apps/users/models.py:464
|
||||
#: templates/includes/sidebar.html:51
|
||||
msgid "Monthly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:534
|
||||
#: apps/transactions/models.py:597
|
||||
msgid "Weekly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:535
|
||||
#: apps/transactions/models.py:598
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:548
|
||||
#: apps/transactions/models.py:611
|
||||
msgid "Number of Installments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:553
|
||||
#: apps/transactions/models.py:616
|
||||
msgid "Installment Start"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:554
|
||||
#: apps/transactions/models.py:617
|
||||
msgid "The installment number to start counting from"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:559 apps/transactions/models.py:794
|
||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
||||
msgid "Start Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:563 apps/transactions/models.py:795
|
||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
||||
msgid "End Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:568
|
||||
#: apps/transactions/models.py:631
|
||||
msgid "Recurrence"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:571
|
||||
#: apps/transactions/models.py:634
|
||||
msgid "Installment Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:590 apps/transactions/models.py:814
|
||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
||||
msgid "Add description to transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:593 apps/transactions/models.py:817
|
||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
||||
msgid "Add notes to transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:753
|
||||
#: apps/transactions/models.py:816
|
||||
msgid "day(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:754
|
||||
#: apps/transactions/models.py:817
|
||||
msgid "week(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:755
|
||||
#: apps/transactions/models.py:818
|
||||
msgid "month(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:756
|
||||
#: apps/transactions/models.py:819
|
||||
msgid "year(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:758
|
||||
#: apps/transactions/models.py:821
|
||||
#: templates/recurring_transactions/fragments/list.html:18
|
||||
msgid "Paused"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:797
|
||||
#: apps/transactions/models.py:860
|
||||
msgid "Recurrence Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:800
|
||||
#: apps/transactions/models.py:863
|
||||
msgid "Recurrence Interval"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:803
|
||||
#: apps/transactions/models.py:866
|
||||
msgid "Keep at most"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:807
|
||||
#: apps/transactions/models.py:870
|
||||
msgid "Last Generated Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:810
|
||||
#: apps/transactions/models.py:873
|
||||
msgid "Last Generated Reference Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:1054
|
||||
#: apps/transactions/models.py:1117
|
||||
#: apps/transactions/views/quick_transactions.py:178
|
||||
#: apps/transactions/views/quick_transactions.py:187
|
||||
#: apps/transactions/views/quick_transactions.py:189
|
||||
@@ -1621,7 +1663,7 @@ msgstr ""
|
||||
msgid "Quick Transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:1055 templates/includes/sidebar.html:98
|
||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
||||
#: templates/quick_transactions/pages/index.html:5
|
||||
#: templates/quick_transactions/pages/index.html:15
|
||||
msgid "Quick Transactions"
|
||||
@@ -1726,8 +1768,8 @@ msgid "Item deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/quick_transactions.py:156
|
||||
#: apps/transactions/views/transactions.py:53
|
||||
#: apps/transactions/views/transactions.py:238
|
||||
#: apps/transactions/views/transactions.py:141
|
||||
#: apps/transactions/views/transactions.py:326
|
||||
msgid "Transaction added successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1767,30 +1809,38 @@ msgstr ""
|
||||
msgid "Tag deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:262
|
||||
#: apps/transactions/views/transactions.py:59
|
||||
msgid "Attachment uploaded successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:110
|
||||
msgid "Attachment deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:350
|
||||
msgid "Transaction updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:313
|
||||
#: apps/transactions/views/transactions.py:401
|
||||
#, python-format
|
||||
msgid "%(count)s transaction updated successfully"
|
||||
msgid_plural "%(count)s transactions updated successfully"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:349
|
||||
#: apps/transactions/views/transactions.py:437
|
||||
msgid "Transaction duplicated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:391
|
||||
#: apps/transactions/views/transactions.py:479
|
||||
msgid "Transaction deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:409
|
||||
#: apps/transactions/views/transactions.py:497
|
||||
msgid "Transaction restored successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:435
|
||||
#: apps/transactions/views/transactions.py:523
|
||||
msgid "Transfer added successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1814,24 +1864,24 @@ msgstr ""
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:22 apps/users/forms.py:26 apps/users/models.py:451
|
||||
#: apps/users/forms.py:24 apps/users/forms.py:28 apps/users/models.py:451
|
||||
#: templates/users/login.html:18
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:33 apps/users/forms.py:38 templates/users/login.html:19
|
||||
#: apps/users/forms.py:35 apps/users/forms.py:40 templates/users/login.html:19
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:45
|
||||
#: apps/users/forms.py:47
|
||||
msgid "Invalid e-mail or password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:46
|
||||
#: apps/users/forms.py:48
|
||||
msgid "This account is deactivated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:62 apps/users/forms.py:75 apps/users/forms.py:97
|
||||
#: apps/users/forms.py:64 apps/users/forms.py:77 apps/users/forms.py:99
|
||||
#: templates/monthly_overview/pages/overview.html:98
|
||||
#: templates/monthly_overview/pages/overview.html:245
|
||||
#: templates/transactions/pages/transactions.html:47
|
||||
@@ -1839,19 +1889,23 @@ msgstr ""
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:105 apps/users/models.py:484
|
||||
#: apps/users/forms.py:107 apps/users/models.py:484
|
||||
msgid "Date Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:110 apps/users/models.py:489
|
||||
#: apps/users/forms.py:112 apps/users/models.py:489
|
||||
msgid "Datetime Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:116 apps/users/models.py:492
|
||||
#: apps/users/forms.py:118 apps/users/models.py:492
|
||||
msgid "Number Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:154
|
||||
#: apps/users/forms.py:125
|
||||
msgid "Default Account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:174
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This changes the language (if available) and how numbers and dates are "
|
||||
@@ -1859,59 +1913,59 @@ msgid ""
|
||||
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:163
|
||||
#: apps/users/forms.py:183
|
||||
msgid "New Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:166
|
||||
#: apps/users/forms.py:186
|
||||
msgid "Leave blank to keep the current password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:169
|
||||
#: apps/users/forms.py:189
|
||||
msgid "Confirm New Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:181 apps/users/forms.py:338
|
||||
#: apps/users/forms.py:201 apps/users/forms.py:358
|
||||
msgid ""
|
||||
"Designates whether this user should be treated as active. Unselect this "
|
||||
"instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:184 apps/users/forms.py:341
|
||||
#: apps/users/forms.py:204 apps/users/forms.py:361
|
||||
msgid ""
|
||||
"Designates that this user has all permissions without explicitly assigning "
|
||||
"them."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:251
|
||||
#: apps/users/forms.py:271
|
||||
msgid "This email address is already in use by another account."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:259
|
||||
#: apps/users/forms.py:279
|
||||
msgid "The two password fields didn't match."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:261
|
||||
#: apps/users/forms.py:281
|
||||
msgid "Please confirm your new password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:263
|
||||
#: apps/users/forms.py:283
|
||||
msgid "Please enter the new password first."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:283
|
||||
#: apps/users/forms.py:303
|
||||
msgid "You cannot deactivate your own account using this form."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:296
|
||||
#: apps/users/forms.py:316
|
||||
msgid "Cannot remove status from the last superuser."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:302
|
||||
#: apps/users/forms.py:322
|
||||
msgid "You cannot remove your own superuser status using this form."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:395
|
||||
#: apps/users/forms.py:415
|
||||
msgid "A user with this email address already exists."
|
||||
msgstr ""
|
||||
|
||||
@@ -1955,6 +2009,14 @@ msgstr ""
|
||||
msgid "Start page"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/models.py:516
|
||||
msgid "Default account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/models.py:517
|
||||
msgid "Selects the account by default when creating new transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/views.py:67
|
||||
msgid "Transaction amounts are now hidden"
|
||||
msgstr ""
|
||||
@@ -2049,8 +2111,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:54
|
||||
#: templates/accounts/fragments/list.html:71
|
||||
#: templates/categories/fragments/table.html:51
|
||||
#: templates/cotton/transaction/item.html:158
|
||||
#: templates/cotton/transaction/item.html:230
|
||||
#: templates/cotton/transaction/item.html:164
|
||||
#: templates/cotton/transaction/item.html:236
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
|
||||
#: templates/cotton/ui/transactions_action_bar.html:82
|
||||
#: templates/currencies/fragments/list.html:40
|
||||
@@ -2078,8 +2140,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:58
|
||||
#: templates/accounts/fragments/list.html:75
|
||||
#: templates/categories/fragments/table.html:56
|
||||
#: templates/cotton/transaction/item.html:160
|
||||
#: templates/cotton/transaction/item.html:236
|
||||
#: templates/cotton/transaction/item.html:166
|
||||
#: templates/cotton/transaction/item.html:242
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/currencies/fragments/list.html:44
|
||||
@@ -2108,8 +2170,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:59
|
||||
#: templates/accounts/fragments/list.html:76
|
||||
#: templates/categories/fragments/table.html:57
|
||||
#: templates/cotton/transaction/item.html:161
|
||||
#: templates/cotton/transaction/item.html:237
|
||||
#: templates/cotton/transaction/item.html:167
|
||||
#: templates/cotton/transaction/item.html:243
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/currencies/fragments/list.html:45
|
||||
@@ -2130,8 +2192,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:60
|
||||
#: templates/accounts/fragments/list.html:77
|
||||
#: templates/categories/fragments/table.html:58
|
||||
#: templates/cotton/transaction/item.html:162
|
||||
#: templates/cotton/transaction/item.html:238
|
||||
#: templates/cotton/transaction/item.html:168
|
||||
#: templates/cotton/transaction/item.html:244
|
||||
#: templates/currencies/fragments/list.html:46
|
||||
#: templates/dca/fragments/strategy/details.html:77
|
||||
#: templates/dca/fragments/strategy/list.html:44
|
||||
@@ -2148,6 +2210,7 @@ msgstr ""
|
||||
#: templates/rules/fragments/transaction_rule/view.html:65
|
||||
#: templates/rules/fragments/transaction_rule/view.html:98
|
||||
#: templates/tags/fragments/table.html:57
|
||||
#: templates/transactions/fragments/attachments.html:22
|
||||
msgid "Yes, delete it!"
|
||||
msgstr ""
|
||||
|
||||
@@ -2283,41 +2346,41 @@ msgstr ""
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:175
|
||||
#: templates/cotton/transaction/item.html:186
|
||||
#: templates/cotton/transaction/item.html:196
|
||||
#: templates/cotton/transaction/item.html:181
|
||||
#: templates/cotton/transaction/item.html:192
|
||||
#: templates/cotton/transaction/item.html:202
|
||||
msgid "Show on summaries"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:177
|
||||
#: templates/cotton/transaction/item.html:183
|
||||
msgid "Controlled by account"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:188
|
||||
#: templates/cotton/transaction/item.html:194
|
||||
msgid "Controlled by category"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:201
|
||||
#: templates/cotton/transaction/item.html:207
|
||||
msgid "Hide from summaries"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:205
|
||||
#: templates/cotton/transaction/item.html:211
|
||||
msgid "Add as quick transaction"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:210
|
||||
#: templates/cotton/transaction/item.html:216
|
||||
msgid "Move to previous month"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:214
|
||||
#: templates/cotton/transaction/item.html:220
|
||||
msgid "Move to next month"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:217
|
||||
#: templates/cotton/transaction/item.html:223
|
||||
msgid "Move to today"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:221
|
||||
#: templates/cotton/transaction/item.html:227
|
||||
#: templates/cotton/ui/transactions_action_bar.html:78
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
@@ -2798,6 +2861,10 @@ msgstr ""
|
||||
msgid "Try reloading the page or check the console for more information."
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/hyperscript/htmx_error_handler.html:24
|
||||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -2806,6 +2873,18 @@ msgstr ""
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:4
|
||||
msgid "Pull down to refresh"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:5
|
||||
msgid "Release to refresh"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:6
|
||||
msgid "Refreshing"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
|
||||
msgid "Insights"
|
||||
msgstr ""
|
||||
@@ -3449,6 +3528,22 @@ msgstr ""
|
||||
msgid "Add Installment Plan"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:20
|
||||
msgid "Delete this attachment?"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:21
|
||||
msgid "This file will be removed from the transaction."
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:30
|
||||
msgid "No attachments yet"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments_manage.html:5
|
||||
msgid "Transaction attachments"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/bulk_edit.html:5
|
||||
msgid "Bulk Editing"
|
||||
msgstr ""
|
||||
|
||||
+316
-219
File diff suppressed because it is too large
Load Diff
+334
-235
File diff suppressed because it is too large
Load Diff
+290
-177
File diff suppressed because it is too large
Load Diff
+272
-177
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
|
||||
"POT-Creation-Date: 2026-06-06 07:41+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -25,12 +25,12 @@ msgstr ""
|
||||
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
|
||||
#: apps/currencies/forms.py:136 apps/dca/forms.py:46 apps/dca/forms.py:205
|
||||
#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
|
||||
#: apps/rules/forms.py:385 apps/transactions/forms.py:197
|
||||
#: apps/transactions/forms.py:361 apps/transactions/forms.py:480
|
||||
#: apps/transactions/forms.py:821 apps/transactions/forms.py:860
|
||||
#: apps/transactions/forms.py:888 apps/transactions/forms.py:919
|
||||
#: apps/transactions/forms.py:1065 apps/users/forms.py:222
|
||||
#: apps/users/forms.py:380
|
||||
#: apps/rules/forms.py:385 apps/transactions/forms.py:218
|
||||
#: apps/transactions/forms.py:417 apps/transactions/forms.py:536
|
||||
#: apps/transactions/forms.py:880 apps/transactions/forms.py:919
|
||||
#: apps/transactions/forms.py:947 apps/transactions/forms.py:978
|
||||
#: apps/transactions/forms.py:1128 apps/users/forms.py:242
|
||||
#: apps/users/forms.py:400
|
||||
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
|
||||
#: templates/rules/fragments/transaction_rule/view.html:128
|
||||
msgid "Update"
|
||||
@@ -41,11 +41,11 @@ msgstr ""
|
||||
#: apps/currencies/forms.py:93 apps/currencies/forms.py:142
|
||||
#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
|
||||
#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
|
||||
#: apps/transactions/forms.py:184 apps/transactions/forms.py:204
|
||||
#: apps/transactions/forms.py:368 apps/transactions/forms.py:827
|
||||
#: apps/transactions/forms.py:866 apps/transactions/forms.py:894
|
||||
#: apps/transactions/forms.py:925 apps/transactions/forms.py:1071
|
||||
#: apps/users/forms.py:228 apps/users/forms.py:386
|
||||
#: apps/transactions/forms.py:205 apps/transactions/forms.py:225
|
||||
#: apps/transactions/forms.py:424 apps/transactions/forms.py:886
|
||||
#: apps/transactions/forms.py:925 apps/transactions/forms.py:953
|
||||
#: apps/transactions/forms.py:984 apps/transactions/forms.py:1134
|
||||
#: apps/users/forms.py:248 apps/users/forms.py:406
|
||||
#: templates/mini_tools/unit_price_calculator.html:168
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
@@ -61,12 +61,12 @@ msgstr ""
|
||||
#: apps/accounts/forms.py:125 apps/dca/forms.py:79 apps/dca/forms.py:86
|
||||
#: apps/insights/forms.py:117 apps/rules/forms.py:181 apps/rules/forms.py:197
|
||||
#: apps/rules/models.py:44 apps/rules/models.py:311
|
||||
#: apps/transactions/forms.py:43 apps/transactions/forms.py:251
|
||||
#: apps/transactions/forms.py:419 apps/transactions/forms.py:516
|
||||
#: apps/transactions/forms.py:523 apps/transactions/forms.py:707
|
||||
#: apps/transactions/forms.py:948 apps/transactions/models.py:322
|
||||
#: apps/transactions/models.py:578 apps/transactions/models.py:778
|
||||
#: apps/transactions/models.py:1026
|
||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
||||
#: apps/transactions/models.py:1089
|
||||
#: templates/insights/fragments/category_overview/index.html:86
|
||||
#: templates/insights/fragments/category_overview/index.html:542
|
||||
#: templates/insights/fragments/month_by_month.html:84
|
||||
@@ -78,12 +78,12 @@ msgstr ""
|
||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
||||
#: apps/transactions/forms.py:51 apps/transactions/forms.py:259
|
||||
#: apps/transactions/forms.py:427 apps/transactions/forms.py:532
|
||||
#: apps/transactions/forms.py:540 apps/transactions/forms.py:700
|
||||
#: apps/transactions/forms.py:941 apps/transactions/models.py:328
|
||||
#: apps/transactions/models.py:580 apps/transactions/models.py:782
|
||||
#: apps/transactions/models.py:1032 templates/includes/sidebar.html:150
|
||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
||||
#: templates/insights/fragments/category_overview/index.html:40
|
||||
#: templates/insights/fragments/month_by_month.html:29
|
||||
#: templates/insights/fragments/month_by_month.html:32
|
||||
@@ -95,8 +95,8 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||
#: apps/transactions/models.py:214 apps/transactions/models.py:239
|
||||
#: apps/transactions/models.py:263 apps/transactions/models.py:994
|
||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
||||
#: templates/account_groups/fragments/list.html:22
|
||||
#: templates/accounts/fragments/list.html:22
|
||||
#: templates/categories/fragments/table.html:17
|
||||
@@ -162,11 +162,11 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
|
||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||
#: apps/transactions/forms.py:63 apps/transactions/forms.py:271
|
||||
#: apps/transactions/forms.py:386 apps/transactions/forms.py:692
|
||||
#: apps/transactions/forms.py:933 apps/transactions/models.py:294
|
||||
#: apps/transactions/models.py:538 apps/transactions/models.py:760
|
||||
#: apps/transactions/models.py:1000
|
||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
||||
#: apps/transactions/models.py:1063
|
||||
#: templates/installment_plans/fragments/table.html:17
|
||||
#: templates/quick_transactions/fragments/list.html:14
|
||||
#: templates/recurring_transactions/fragments/table.html:19
|
||||
@@ -343,7 +343,7 @@ msgid ""
|
||||
"owner.<br/>Public: Shown for all users. Only editable by the owner."
|
||||
msgstr ""
|
||||
|
||||
#: apps/common/forms.py:76 apps/users/forms.py:149
|
||||
#: apps/common/forms.py:76 apps/users/forms.py:169
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@@ -479,8 +479,8 @@ msgstr ""
|
||||
|
||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
||||
#: apps/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||
#: apps/transactions/forms.py:67 apps/transactions/forms.py:391
|
||||
#: apps/transactions/forms.py:544 apps/transactions/models.py:304
|
||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
||||
#: templates/dca/fragments/strategy/details.html:49
|
||||
#: templates/exchange_rates/fragments/table.html:10
|
||||
#: templates/exchange_rates_services/fragments/table.html:11
|
||||
@@ -566,8 +566,8 @@ msgid "Service Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/currencies/models.py:118 apps/transactions/filters.py:27
|
||||
#: apps/transactions/models.py:218 apps/transactions/models.py:242
|
||||
#: apps/transactions/models.py:266 templates/categories/fragments/list.html:16
|
||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
||||
#: templates/entities/fragments/list.html:16
|
||||
#: templates/installment_plans/fragments/list.html:16
|
||||
#: templates/recurring_transactions/fragments/list.html:16
|
||||
@@ -695,11 +695,11 @@ msgstr ""
|
||||
msgid "Create transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:64 apps/transactions/forms.py:491
|
||||
#: apps/dca/forms.py:64 apps/transactions/forms.py:547
|
||||
msgid "From Account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:70 apps/transactions/forms.py:496
|
||||
#: apps/dca/forms.py:70 apps/transactions/forms.py:552
|
||||
msgid "To Account"
|
||||
msgstr ""
|
||||
|
||||
@@ -724,7 +724,7 @@ msgstr ""
|
||||
msgid "You must provide an account."
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:290 apps/transactions/forms.py:638
|
||||
#: apps/dca/forms.py:290 apps/transactions/forms.py:694
|
||||
msgid "From and To accounts must be different."
|
||||
msgstr ""
|
||||
|
||||
@@ -743,9 +743,9 @@ msgstr ""
|
||||
|
||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
||||
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||
#: apps/transactions/forms.py:413 apps/transactions/forms.py:560
|
||||
#: apps/transactions/models.py:318 apps/transactions/models.py:587
|
||||
#: apps/transactions/models.py:788 apps/transactions/models.py:1022
|
||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
@@ -808,7 +808,7 @@ msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||
#: apps/transactions/models.py:379 templates/includes/sidebar.html:81
|
||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
||||
#: templates/includes/sidebar.html:142
|
||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||
#: templates/recurring_transactions/fragments/table.html:37
|
||||
@@ -829,11 +829,11 @@ msgstr ""
|
||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
||||
#: apps/transactions/forms.py:59 apps/transactions/forms.py:267
|
||||
#: apps/transactions/forms.py:435 apps/transactions/forms.py:715
|
||||
#: apps/transactions/forms.py:956 apps/transactions/models.py:277
|
||||
#: apps/transactions/models.py:333 apps/transactions/models.py:583
|
||||
#: apps/transactions/models.py:785 apps/transactions/models.py:1037
|
||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
||||
#: templates/entities/fragments/list.html:9
|
||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||
#: templates/insights/fragments/category_overview/index.html:54
|
||||
@@ -845,14 +845,14 @@ msgid "Entities"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||
#: apps/transactions/models.py:825 templates/includes/sidebar.html:110
|
||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
||||
#: templates/recurring_transactions/fragments/list.html:9
|
||||
#: templates/recurring_transactions/pages/index.html:4
|
||||
msgid "Recurring Transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||
#: apps/transactions/models.py:601 templates/includes/sidebar.html:104
|
||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
||||
#: templates/installment_plans/fragments/list.html:9
|
||||
#: templates/installment_plans/pages/index.html:4
|
||||
msgid "Installment Plans"
|
||||
@@ -905,7 +905,7 @@ msgstr ""
|
||||
msgid "Update or create transaction actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:224
|
||||
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:230
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
|
||||
#: templates/export_app/fragments/restore.html:5
|
||||
#: templates/export_app/pages/index.html:19
|
||||
@@ -1101,16 +1101,16 @@ msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||
#: apps/rules/models.py:271 apps/transactions/forms.py:377
|
||||
#: apps/transactions/models.py:301 apps/transactions/models.py:543
|
||||
#: apps/transactions/models.py:766 apps/transactions/models.py:1007
|
||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
||||
#: apps/rules/models.py:275 apps/transactions/filters.py:22
|
||||
#: apps/transactions/forms.py:381 apps/transactions/models.py:303
|
||||
#: apps/transactions/models.py:1009 templates/cotton/transaction/item.html:20
|
||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
||||
#: templates/cotton/transaction/item.html:31
|
||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||
@@ -1118,17 +1118,17 @@ msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||
#: apps/rules/models.py:283 apps/transactions/forms.py:71
|
||||
#: apps/transactions/forms.py:397 apps/transactions/forms.py:547
|
||||
#: apps/transactions/forms.py:721 apps/transactions/models.py:305
|
||||
#: apps/transactions/models.py:561 apps/transactions/models.py:790
|
||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
||||
msgid "Reference Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||
#: apps/rules/models.py:287 apps/transactions/forms.py:404
|
||||
#: apps/transactions/models.py:311 apps/transactions/models.py:771
|
||||
#: apps/transactions/models.py:1015
|
||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
||||
#: apps/transactions/models.py:1078
|
||||
#: templates/insights/fragments/sankey.html:102
|
||||
#: templates/installment_plans/fragments/table.html:18
|
||||
#: templates/quick_transactions/fragments/list.html:15
|
||||
@@ -1138,28 +1138,28 @@ msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
||||
#: apps/rules/models.py:42 apps/rules/models.py:291
|
||||
#: apps/transactions/forms.py:408 apps/transactions/forms.py:551
|
||||
#: apps/transactions/models.py:316 apps/transactions/models.py:545
|
||||
#: apps/transactions/models.py:774 apps/transactions/models.py:1020
|
||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||
#: apps/rules/models.py:299 apps/transactions/models.py:355
|
||||
#: apps/transactions/models.py:1042
|
||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
||||
#: apps/transactions/models.py:1105
|
||||
msgid "Internal Note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||
#: apps/rules/models.py:303 apps/transactions/models.py:357
|
||||
#: apps/transactions/models.py:1044
|
||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
||||
#: apps/transactions/models.py:1107
|
||||
msgid "Internal ID"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||
#: apps/rules/models.py:319 apps/transactions/forms.py:564
|
||||
#: apps/transactions/models.py:215 apps/transactions/models.py:306
|
||||
#: apps/transactions/models.py:1010
|
||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
||||
#: apps/transactions/models.py:1073
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
|
||||
@@ -1172,7 +1172,7 @@ msgid "Set Values"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||
#: apps/transactions/models.py:378
|
||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
||||
msgid "Transaction"
|
||||
msgstr ""
|
||||
|
||||
@@ -1377,96 +1377,110 @@ msgstr ""
|
||||
msgid "No entity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:170
|
||||
#: apps/transactions/forms.py:191
|
||||
msgid "More"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:207
|
||||
#: apps/transactions/forms.py:228
|
||||
msgid "Save and add similar"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:212
|
||||
#: apps/transactions/forms.py:233
|
||||
msgid "Save and add another"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:295 apps/transactions/forms.py:567
|
||||
#: apps/transactions/forms.py:270 templates/cotton/transaction/item.html:158
|
||||
#: templates/transactions/fragments/attachments.html:4
|
||||
msgid "Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:271
|
||||
msgid ""
|
||||
"Files are private and only visible to users with access to this transaction."
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:282
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:351 apps/transactions/forms.py:623
|
||||
msgid "Muted transactions won't be displayed on monthly summaries"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:503
|
||||
#: apps/transactions/forms.py:559
|
||||
msgid "From Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:508
|
||||
#: apps/transactions/forms.py:564
|
||||
msgid "To Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:606
|
||||
#: apps/transactions/forms.py:662
|
||||
#: templates/cotton/ui/quick_transactions_buttons.html:40
|
||||
#: templates/cotton/ui/transactions_fab.html:44
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:847
|
||||
#: apps/transactions/forms.py:906
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:875
|
||||
#: apps/transactions/forms.py:934
|
||||
msgid "Entity name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:903
|
||||
#: apps/transactions/forms.py:962
|
||||
msgid "Category name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:905
|
||||
#: apps/transactions/forms.py:964
|
||||
msgid "Muted categories won't be displayed on monthly summaries"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:1055
|
||||
#: apps/transactions/forms.py:1118
|
||||
msgid "future transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:1081
|
||||
#: apps/transactions/forms.py:1144
|
||||
msgid "End date should be after the start date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:220
|
||||
#: apps/transactions/models.py:229
|
||||
msgid ""
|
||||
"Deactivated categories won't be able to be selected when creating new "
|
||||
"transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:228
|
||||
#: apps/transactions/models.py:237
|
||||
msgid "Transaction Category"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:229
|
||||
#: apps/transactions/models.py:238
|
||||
msgid "Transaction Categories"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:244
|
||||
#: apps/transactions/models.py:253
|
||||
msgid ""
|
||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:252 apps/transactions/models.py:253
|
||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
||||
msgid "Transaction Tags"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:268
|
||||
#: apps/transactions/models.py:277
|
||||
msgid ""
|
||||
"Deactivated entities won't be able to be selected when creating new "
|
||||
"transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:276
|
||||
#: apps/transactions/models.py:285
|
||||
#: templates/insights/fragments/month_by_month.html:88
|
||||
#: templates/insights/fragments/year_by_year.html:56
|
||||
msgid "Entity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:288 apps/transactions/models.py:987
|
||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
||||
#: templates/calendar_view/fragments/list.html:42
|
||||
#: templates/calendar_view/fragments/list.html:44
|
||||
#: templates/calendar_view/fragments/list.html:52
|
||||
@@ -1478,7 +1492,7 @@ msgstr ""
|
||||
msgid "Income"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:289 apps/transactions/models.py:988
|
||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
||||
#: templates/calendar_view/fragments/list.html:46
|
||||
#: templates/calendar_view/fragments/list.html:48
|
||||
#: templates/calendar_view/fragments/list.html:56
|
||||
@@ -1489,129 +1503,157 @@ msgstr ""
|
||||
msgid "Expense"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:344 apps/transactions/models.py:600
|
||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
||||
msgid "Installment Plan"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:353 apps/transactions/models.py:824
|
||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
||||
msgid "Recurring Transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:361
|
||||
#: apps/transactions/models.py:370
|
||||
msgid "Deleted"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:366
|
||||
#: apps/transactions/models.py:375
|
||||
msgid "Deleted At"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:480 templates/tags/fragments/table.html:69
|
||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
||||
msgid "No tags"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:482
|
||||
#: apps/transactions/models.py:491
|
||||
msgid "No category"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:484
|
||||
#: apps/transactions/models.py:493
|
||||
msgid "No description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:532 templates/includes/sidebar.html:57
|
||||
#: apps/transactions/models.py:549
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:551
|
||||
msgid "Original Name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:553
|
||||
msgid "Content Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:555
|
||||
msgid "Size"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:560
|
||||
msgid "Uploaded By"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:565
|
||||
msgid "Transaction Attachment"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:566
|
||||
msgid "Transaction Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
||||
msgid "Yearly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:533 apps/users/models.py:464
|
||||
#: apps/transactions/models.py:596 apps/users/models.py:464
|
||||
#: templates/includes/sidebar.html:51
|
||||
msgid "Monthly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:534
|
||||
#: apps/transactions/models.py:597
|
||||
msgid "Weekly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:535
|
||||
#: apps/transactions/models.py:598
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:548
|
||||
#: apps/transactions/models.py:611
|
||||
msgid "Number of Installments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:553
|
||||
#: apps/transactions/models.py:616
|
||||
msgid "Installment Start"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:554
|
||||
#: apps/transactions/models.py:617
|
||||
msgid "The installment number to start counting from"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:559 apps/transactions/models.py:794
|
||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
||||
msgid "Start Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:563 apps/transactions/models.py:795
|
||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
||||
msgid "End Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:568
|
||||
#: apps/transactions/models.py:631
|
||||
msgid "Recurrence"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:571
|
||||
#: apps/transactions/models.py:634
|
||||
msgid "Installment Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:590 apps/transactions/models.py:814
|
||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
||||
msgid "Add description to transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:593 apps/transactions/models.py:817
|
||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
||||
msgid "Add notes to transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:753
|
||||
#: apps/transactions/models.py:816
|
||||
msgid "day(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:754
|
||||
#: apps/transactions/models.py:817
|
||||
msgid "week(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:755
|
||||
#: apps/transactions/models.py:818
|
||||
msgid "month(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:756
|
||||
#: apps/transactions/models.py:819
|
||||
msgid "year(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:758
|
||||
#: apps/transactions/models.py:821
|
||||
#: templates/recurring_transactions/fragments/list.html:18
|
||||
msgid "Paused"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:797
|
||||
#: apps/transactions/models.py:860
|
||||
msgid "Recurrence Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:800
|
||||
#: apps/transactions/models.py:863
|
||||
msgid "Recurrence Interval"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:803
|
||||
#: apps/transactions/models.py:866
|
||||
msgid "Keep at most"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:807
|
||||
#: apps/transactions/models.py:870
|
||||
msgid "Last Generated Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:810
|
||||
#: apps/transactions/models.py:873
|
||||
msgid "Last Generated Reference Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:1054
|
||||
#: apps/transactions/models.py:1117
|
||||
#: apps/transactions/views/quick_transactions.py:178
|
||||
#: apps/transactions/views/quick_transactions.py:187
|
||||
#: apps/transactions/views/quick_transactions.py:189
|
||||
@@ -1620,7 +1662,7 @@ msgstr ""
|
||||
msgid "Quick Transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:1055 templates/includes/sidebar.html:98
|
||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
||||
#: templates/quick_transactions/pages/index.html:5
|
||||
#: templates/quick_transactions/pages/index.html:15
|
||||
msgid "Quick Transactions"
|
||||
@@ -1725,8 +1767,8 @@ msgid "Item deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/quick_transactions.py:156
|
||||
#: apps/transactions/views/transactions.py:53
|
||||
#: apps/transactions/views/transactions.py:238
|
||||
#: apps/transactions/views/transactions.py:141
|
||||
#: apps/transactions/views/transactions.py:326
|
||||
msgid "Transaction added successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1766,30 +1808,38 @@ msgstr ""
|
||||
msgid "Tag deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:262
|
||||
#: apps/transactions/views/transactions.py:59
|
||||
msgid "Attachment uploaded successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:110
|
||||
msgid "Attachment deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:350
|
||||
msgid "Transaction updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:313
|
||||
#: apps/transactions/views/transactions.py:401
|
||||
#, python-format
|
||||
msgid "%(count)s transaction updated successfully"
|
||||
msgid_plural "%(count)s transactions updated successfully"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:349
|
||||
#: apps/transactions/views/transactions.py:437
|
||||
msgid "Transaction duplicated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:391
|
||||
#: apps/transactions/views/transactions.py:479
|
||||
msgid "Transaction deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:409
|
||||
#: apps/transactions/views/transactions.py:497
|
||||
msgid "Transaction restored successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:435
|
||||
#: apps/transactions/views/transactions.py:523
|
||||
msgid "Transfer added successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1813,24 +1863,24 @@ msgstr ""
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:22 apps/users/forms.py:26 apps/users/models.py:451
|
||||
#: apps/users/forms.py:24 apps/users/forms.py:28 apps/users/models.py:451
|
||||
#: templates/users/login.html:18
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:33 apps/users/forms.py:38 templates/users/login.html:19
|
||||
#: apps/users/forms.py:35 apps/users/forms.py:40 templates/users/login.html:19
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:45
|
||||
#: apps/users/forms.py:47
|
||||
msgid "Invalid e-mail or password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:46
|
||||
#: apps/users/forms.py:48
|
||||
msgid "This account is deactivated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:62 apps/users/forms.py:75 apps/users/forms.py:97
|
||||
#: apps/users/forms.py:64 apps/users/forms.py:77 apps/users/forms.py:99
|
||||
#: templates/monthly_overview/pages/overview.html:98
|
||||
#: templates/monthly_overview/pages/overview.html:245
|
||||
#: templates/transactions/pages/transactions.html:47
|
||||
@@ -1838,19 +1888,23 @@ msgstr ""
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:105 apps/users/models.py:484
|
||||
#: apps/users/forms.py:107 apps/users/models.py:484
|
||||
msgid "Date Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:110 apps/users/models.py:489
|
||||
#: apps/users/forms.py:112 apps/users/models.py:489
|
||||
msgid "Datetime Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:116 apps/users/models.py:492
|
||||
#: apps/users/forms.py:118 apps/users/models.py:492
|
||||
msgid "Number Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:154
|
||||
#: apps/users/forms.py:125
|
||||
msgid "Default Account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:174
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This changes the language (if available) and how numbers and dates are "
|
||||
@@ -1858,59 +1912,59 @@ msgid ""
|
||||
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:163
|
||||
#: apps/users/forms.py:183
|
||||
msgid "New Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:166
|
||||
#: apps/users/forms.py:186
|
||||
msgid "Leave blank to keep the current password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:169
|
||||
#: apps/users/forms.py:189
|
||||
msgid "Confirm New Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:181 apps/users/forms.py:338
|
||||
#: apps/users/forms.py:201 apps/users/forms.py:358
|
||||
msgid ""
|
||||
"Designates whether this user should be treated as active. Unselect this "
|
||||
"instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:184 apps/users/forms.py:341
|
||||
#: apps/users/forms.py:204 apps/users/forms.py:361
|
||||
msgid ""
|
||||
"Designates that this user has all permissions without explicitly assigning "
|
||||
"them."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:251
|
||||
#: apps/users/forms.py:271
|
||||
msgid "This email address is already in use by another account."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:259
|
||||
#: apps/users/forms.py:279
|
||||
msgid "The two password fields didn't match."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:261
|
||||
#: apps/users/forms.py:281
|
||||
msgid "Please confirm your new password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:263
|
||||
#: apps/users/forms.py:283
|
||||
msgid "Please enter the new password first."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:283
|
||||
#: apps/users/forms.py:303
|
||||
msgid "You cannot deactivate your own account using this form."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:296
|
||||
#: apps/users/forms.py:316
|
||||
msgid "Cannot remove status from the last superuser."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:302
|
||||
#: apps/users/forms.py:322
|
||||
msgid "You cannot remove your own superuser status using this form."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:395
|
||||
#: apps/users/forms.py:415
|
||||
msgid "A user with this email address already exists."
|
||||
msgstr ""
|
||||
|
||||
@@ -1954,6 +2008,14 @@ msgstr ""
|
||||
msgid "Start page"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/models.py:516
|
||||
msgid "Default account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/models.py:517
|
||||
msgid "Selects the account by default when creating new transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/views.py:67
|
||||
msgid "Transaction amounts are now hidden"
|
||||
msgstr ""
|
||||
@@ -2048,8 +2110,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:54
|
||||
#: templates/accounts/fragments/list.html:71
|
||||
#: templates/categories/fragments/table.html:51
|
||||
#: templates/cotton/transaction/item.html:158
|
||||
#: templates/cotton/transaction/item.html:230
|
||||
#: templates/cotton/transaction/item.html:164
|
||||
#: templates/cotton/transaction/item.html:236
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
|
||||
#: templates/cotton/ui/transactions_action_bar.html:82
|
||||
#: templates/currencies/fragments/list.html:40
|
||||
@@ -2077,8 +2139,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:58
|
||||
#: templates/accounts/fragments/list.html:75
|
||||
#: templates/categories/fragments/table.html:56
|
||||
#: templates/cotton/transaction/item.html:160
|
||||
#: templates/cotton/transaction/item.html:236
|
||||
#: templates/cotton/transaction/item.html:166
|
||||
#: templates/cotton/transaction/item.html:242
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/currencies/fragments/list.html:44
|
||||
@@ -2107,8 +2169,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:59
|
||||
#: templates/accounts/fragments/list.html:76
|
||||
#: templates/categories/fragments/table.html:57
|
||||
#: templates/cotton/transaction/item.html:161
|
||||
#: templates/cotton/transaction/item.html:237
|
||||
#: templates/cotton/transaction/item.html:167
|
||||
#: templates/cotton/transaction/item.html:243
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/currencies/fragments/list.html:45
|
||||
@@ -2129,8 +2191,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:60
|
||||
#: templates/accounts/fragments/list.html:77
|
||||
#: templates/categories/fragments/table.html:58
|
||||
#: templates/cotton/transaction/item.html:162
|
||||
#: templates/cotton/transaction/item.html:238
|
||||
#: templates/cotton/transaction/item.html:168
|
||||
#: templates/cotton/transaction/item.html:244
|
||||
#: templates/currencies/fragments/list.html:46
|
||||
#: templates/dca/fragments/strategy/details.html:77
|
||||
#: templates/dca/fragments/strategy/list.html:44
|
||||
@@ -2147,6 +2209,7 @@ msgstr ""
|
||||
#: templates/rules/fragments/transaction_rule/view.html:65
|
||||
#: templates/rules/fragments/transaction_rule/view.html:98
|
||||
#: templates/tags/fragments/table.html:57
|
||||
#: templates/transactions/fragments/attachments.html:22
|
||||
msgid "Yes, delete it!"
|
||||
msgstr ""
|
||||
|
||||
@@ -2282,41 +2345,41 @@ msgstr ""
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:175
|
||||
#: templates/cotton/transaction/item.html:186
|
||||
#: templates/cotton/transaction/item.html:196
|
||||
#: templates/cotton/transaction/item.html:181
|
||||
#: templates/cotton/transaction/item.html:192
|
||||
#: templates/cotton/transaction/item.html:202
|
||||
msgid "Show on summaries"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:177
|
||||
#: templates/cotton/transaction/item.html:183
|
||||
msgid "Controlled by account"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:188
|
||||
#: templates/cotton/transaction/item.html:194
|
||||
msgid "Controlled by category"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:201
|
||||
#: templates/cotton/transaction/item.html:207
|
||||
msgid "Hide from summaries"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:205
|
||||
#: templates/cotton/transaction/item.html:211
|
||||
msgid "Add as quick transaction"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:210
|
||||
#: templates/cotton/transaction/item.html:216
|
||||
msgid "Move to previous month"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:214
|
||||
#: templates/cotton/transaction/item.html:220
|
||||
msgid "Move to next month"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:217
|
||||
#: templates/cotton/transaction/item.html:223
|
||||
msgid "Move to today"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:221
|
||||
#: templates/cotton/transaction/item.html:227
|
||||
#: templates/cotton/ui/transactions_action_bar.html:78
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
@@ -2796,6 +2859,10 @@ msgstr ""
|
||||
msgid "Try reloading the page or check the console for more information."
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/hyperscript/htmx_error_handler.html:24
|
||||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -2804,6 +2871,18 @@ msgstr ""
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:4
|
||||
msgid "Pull down to refresh"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:5
|
||||
msgid "Release to refresh"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:6
|
||||
msgid "Refreshing"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
|
||||
msgid "Insights"
|
||||
msgstr ""
|
||||
@@ -3447,6 +3526,22 @@ msgstr ""
|
||||
msgid "Add Installment Plan"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:20
|
||||
msgid "Delete this attachment?"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:21
|
||||
msgid "This file will be removed from the transaction."
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:30
|
||||
msgid "No attachments yet"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments_manage.html:5
|
||||
msgid "Transaction attachments"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/bulk_edit.html:5
|
||||
msgid "Bulk Editing"
|
||||
msgstr ""
|
||||
|
||||
+302
-177
File diff suppressed because it is too large
Load Diff
+295
-184
File diff suppressed because it is too large
Load Diff
+857
-694
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
+272
-177
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
|
||||
"POT-Creation-Date: 2026-06-06 07:41+0000\n"
|
||||
"PO-Revision-Date: 2025-04-14 06:16+0000\n"
|
||||
"Last-Translator: Emil <emil.bjorkroth@gmail.com>\n"
|
||||
"Language-Team: Swedish <https://translations.herculino.com/projects/wygiwyh/"
|
||||
@@ -27,12 +27,12 @@ msgstr ""
|
||||
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
|
||||
#: apps/currencies/forms.py:136 apps/dca/forms.py:46 apps/dca/forms.py:205
|
||||
#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
|
||||
#: apps/rules/forms.py:385 apps/transactions/forms.py:197
|
||||
#: apps/transactions/forms.py:361 apps/transactions/forms.py:480
|
||||
#: apps/transactions/forms.py:821 apps/transactions/forms.py:860
|
||||
#: apps/transactions/forms.py:888 apps/transactions/forms.py:919
|
||||
#: apps/transactions/forms.py:1065 apps/users/forms.py:222
|
||||
#: apps/users/forms.py:380
|
||||
#: apps/rules/forms.py:385 apps/transactions/forms.py:218
|
||||
#: apps/transactions/forms.py:417 apps/transactions/forms.py:536
|
||||
#: apps/transactions/forms.py:880 apps/transactions/forms.py:919
|
||||
#: apps/transactions/forms.py:947 apps/transactions/forms.py:978
|
||||
#: apps/transactions/forms.py:1128 apps/users/forms.py:242
|
||||
#: apps/users/forms.py:400
|
||||
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
|
||||
#: templates/rules/fragments/transaction_rule/view.html:128
|
||||
msgid "Update"
|
||||
@@ -43,11 +43,11 @@ msgstr "Uppdatera"
|
||||
#: apps/currencies/forms.py:93 apps/currencies/forms.py:142
|
||||
#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
|
||||
#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
|
||||
#: apps/transactions/forms.py:184 apps/transactions/forms.py:204
|
||||
#: apps/transactions/forms.py:368 apps/transactions/forms.py:827
|
||||
#: apps/transactions/forms.py:866 apps/transactions/forms.py:894
|
||||
#: apps/transactions/forms.py:925 apps/transactions/forms.py:1071
|
||||
#: apps/users/forms.py:228 apps/users/forms.py:386
|
||||
#: apps/transactions/forms.py:205 apps/transactions/forms.py:225
|
||||
#: apps/transactions/forms.py:424 apps/transactions/forms.py:886
|
||||
#: apps/transactions/forms.py:925 apps/transactions/forms.py:953
|
||||
#: apps/transactions/forms.py:984 apps/transactions/forms.py:1134
|
||||
#: apps/users/forms.py:248 apps/users/forms.py:406
|
||||
#: templates/mini_tools/unit_price_calculator.html:168
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
@@ -63,12 +63,12 @@ msgstr ""
|
||||
#: apps/accounts/forms.py:125 apps/dca/forms.py:79 apps/dca/forms.py:86
|
||||
#: apps/insights/forms.py:117 apps/rules/forms.py:181 apps/rules/forms.py:197
|
||||
#: apps/rules/models.py:44 apps/rules/models.py:311
|
||||
#: apps/transactions/forms.py:43 apps/transactions/forms.py:251
|
||||
#: apps/transactions/forms.py:419 apps/transactions/forms.py:516
|
||||
#: apps/transactions/forms.py:523 apps/transactions/forms.py:707
|
||||
#: apps/transactions/forms.py:948 apps/transactions/models.py:322
|
||||
#: apps/transactions/models.py:578 apps/transactions/models.py:778
|
||||
#: apps/transactions/models.py:1026
|
||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
||||
#: apps/transactions/models.py:1089
|
||||
#: templates/insights/fragments/category_overview/index.html:86
|
||||
#: templates/insights/fragments/category_overview/index.html:542
|
||||
#: templates/insights/fragments/month_by_month.html:84
|
||||
@@ -80,12 +80,12 @@ msgstr ""
|
||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
||||
#: apps/transactions/forms.py:51 apps/transactions/forms.py:259
|
||||
#: apps/transactions/forms.py:427 apps/transactions/forms.py:532
|
||||
#: apps/transactions/forms.py:540 apps/transactions/forms.py:700
|
||||
#: apps/transactions/forms.py:941 apps/transactions/models.py:328
|
||||
#: apps/transactions/models.py:580 apps/transactions/models.py:782
|
||||
#: apps/transactions/models.py:1032 templates/includes/sidebar.html:150
|
||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
||||
#: templates/insights/fragments/category_overview/index.html:40
|
||||
#: templates/insights/fragments/month_by_month.html:29
|
||||
#: templates/insights/fragments/month_by_month.html:32
|
||||
@@ -97,8 +97,8 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||
#: apps/transactions/models.py:214 apps/transactions/models.py:239
|
||||
#: apps/transactions/models.py:263 apps/transactions/models.py:994
|
||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
||||
#: templates/account_groups/fragments/list.html:22
|
||||
#: templates/accounts/fragments/list.html:22
|
||||
#: templates/categories/fragments/table.html:17
|
||||
@@ -164,11 +164,11 @@ msgstr ""
|
||||
|
||||
#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
|
||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||
#: apps/transactions/forms.py:63 apps/transactions/forms.py:271
|
||||
#: apps/transactions/forms.py:386 apps/transactions/forms.py:692
|
||||
#: apps/transactions/forms.py:933 apps/transactions/models.py:294
|
||||
#: apps/transactions/models.py:538 apps/transactions/models.py:760
|
||||
#: apps/transactions/models.py:1000
|
||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
||||
#: apps/transactions/models.py:1063
|
||||
#: templates/installment_plans/fragments/table.html:17
|
||||
#: templates/quick_transactions/fragments/list.html:14
|
||||
#: templates/recurring_transactions/fragments/table.html:19
|
||||
@@ -345,7 +345,7 @@ msgid ""
|
||||
"owner.<br/>Public: Shown for all users. Only editable by the owner."
|
||||
msgstr ""
|
||||
|
||||
#: apps/common/forms.py:76 apps/users/forms.py:149
|
||||
#: apps/common/forms.py:76 apps/users/forms.py:169
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
@@ -481,8 +481,8 @@ msgstr ""
|
||||
|
||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
||||
#: apps/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||
#: apps/transactions/forms.py:67 apps/transactions/forms.py:391
|
||||
#: apps/transactions/forms.py:544 apps/transactions/models.py:304
|
||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
||||
#: templates/dca/fragments/strategy/details.html:49
|
||||
#: templates/exchange_rates/fragments/table.html:10
|
||||
#: templates/exchange_rates_services/fragments/table.html:11
|
||||
@@ -568,8 +568,8 @@ msgid "Service Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/currencies/models.py:118 apps/transactions/filters.py:27
|
||||
#: apps/transactions/models.py:218 apps/transactions/models.py:242
|
||||
#: apps/transactions/models.py:266 templates/categories/fragments/list.html:16
|
||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
||||
#: templates/entities/fragments/list.html:16
|
||||
#: templates/installment_plans/fragments/list.html:16
|
||||
#: templates/recurring_transactions/fragments/list.html:16
|
||||
@@ -697,11 +697,11 @@ msgstr ""
|
||||
msgid "Create transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:64 apps/transactions/forms.py:491
|
||||
#: apps/dca/forms.py:64 apps/transactions/forms.py:547
|
||||
msgid "From Account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:70 apps/transactions/forms.py:496
|
||||
#: apps/dca/forms.py:70 apps/transactions/forms.py:552
|
||||
msgid "To Account"
|
||||
msgstr ""
|
||||
|
||||
@@ -726,7 +726,7 @@ msgstr ""
|
||||
msgid "You must provide an account."
|
||||
msgstr ""
|
||||
|
||||
#: apps/dca/forms.py:290 apps/transactions/forms.py:638
|
||||
#: apps/dca/forms.py:290 apps/transactions/forms.py:694
|
||||
msgid "From and To accounts must be different."
|
||||
msgstr ""
|
||||
|
||||
@@ -745,9 +745,9 @@ msgstr ""
|
||||
|
||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
||||
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||
#: apps/transactions/forms.py:413 apps/transactions/forms.py:560
|
||||
#: apps/transactions/models.py:318 apps/transactions/models.py:587
|
||||
#: apps/transactions/models.py:788 apps/transactions/models.py:1022
|
||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
@@ -810,7 +810,7 @@ msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||
#: apps/transactions/models.py:379 templates/includes/sidebar.html:81
|
||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
||||
#: templates/includes/sidebar.html:142
|
||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||
#: templates/recurring_transactions/fragments/table.html:37
|
||||
@@ -831,11 +831,11 @@ msgstr ""
|
||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
||||
#: apps/transactions/forms.py:59 apps/transactions/forms.py:267
|
||||
#: apps/transactions/forms.py:435 apps/transactions/forms.py:715
|
||||
#: apps/transactions/forms.py:956 apps/transactions/models.py:277
|
||||
#: apps/transactions/models.py:333 apps/transactions/models.py:583
|
||||
#: apps/transactions/models.py:785 apps/transactions/models.py:1037
|
||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
||||
#: templates/entities/fragments/list.html:9
|
||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||
#: templates/insights/fragments/category_overview/index.html:54
|
||||
@@ -847,14 +847,14 @@ msgid "Entities"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||
#: apps/transactions/models.py:825 templates/includes/sidebar.html:110
|
||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
||||
#: templates/recurring_transactions/fragments/list.html:9
|
||||
#: templates/recurring_transactions/pages/index.html:4
|
||||
msgid "Recurring Transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||
#: apps/transactions/models.py:601 templates/includes/sidebar.html:104
|
||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
||||
#: templates/installment_plans/fragments/list.html:9
|
||||
#: templates/installment_plans/pages/index.html:4
|
||||
msgid "Installment Plans"
|
||||
@@ -907,7 +907,7 @@ msgstr ""
|
||||
msgid "Update or create transaction actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:224
|
||||
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:230
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
|
||||
#: templates/export_app/fragments/restore.html:5
|
||||
#: templates/export_app/pages/index.html:19
|
||||
@@ -1103,16 +1103,16 @@ msgid "Operator"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||
#: apps/rules/models.py:271 apps/transactions/forms.py:377
|
||||
#: apps/transactions/models.py:301 apps/transactions/models.py:543
|
||||
#: apps/transactions/models.py:766 apps/transactions/models.py:1007
|
||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
||||
#: apps/rules/models.py:275 apps/transactions/filters.py:22
|
||||
#: apps/transactions/forms.py:381 apps/transactions/models.py:303
|
||||
#: apps/transactions/models.py:1009 templates/cotton/transaction/item.html:20
|
||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
||||
#: templates/cotton/transaction/item.html:31
|
||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||
@@ -1120,17 +1120,17 @@ msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||
#: apps/rules/models.py:283 apps/transactions/forms.py:71
|
||||
#: apps/transactions/forms.py:397 apps/transactions/forms.py:547
|
||||
#: apps/transactions/forms.py:721 apps/transactions/models.py:305
|
||||
#: apps/transactions/models.py:561 apps/transactions/models.py:790
|
||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
||||
msgid "Reference Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||
#: apps/rules/models.py:287 apps/transactions/forms.py:404
|
||||
#: apps/transactions/models.py:311 apps/transactions/models.py:771
|
||||
#: apps/transactions/models.py:1015
|
||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
||||
#: apps/transactions/models.py:1078
|
||||
#: templates/insights/fragments/sankey.html:102
|
||||
#: templates/installment_plans/fragments/table.html:18
|
||||
#: templates/quick_transactions/fragments/list.html:15
|
||||
@@ -1140,28 +1140,28 @@ msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
||||
#: apps/rules/models.py:42 apps/rules/models.py:291
|
||||
#: apps/transactions/forms.py:408 apps/transactions/forms.py:551
|
||||
#: apps/transactions/models.py:316 apps/transactions/models.py:545
|
||||
#: apps/transactions/models.py:774 apps/transactions/models.py:1020
|
||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||
#: apps/rules/models.py:299 apps/transactions/models.py:355
|
||||
#: apps/transactions/models.py:1042
|
||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
||||
#: apps/transactions/models.py:1105
|
||||
msgid "Internal Note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||
#: apps/rules/models.py:303 apps/transactions/models.py:357
|
||||
#: apps/transactions/models.py:1044
|
||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
||||
#: apps/transactions/models.py:1107
|
||||
msgid "Internal ID"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||
#: apps/rules/models.py:319 apps/transactions/forms.py:564
|
||||
#: apps/transactions/models.py:215 apps/transactions/models.py:306
|
||||
#: apps/transactions/models.py:1010
|
||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
||||
#: apps/transactions/models.py:1073
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
|
||||
@@ -1174,7 +1174,7 @@ msgid "Set Values"
|
||||
msgstr ""
|
||||
|
||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||
#: apps/transactions/models.py:378
|
||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
||||
msgid "Transaction"
|
||||
msgstr ""
|
||||
|
||||
@@ -1379,96 +1379,110 @@ msgstr ""
|
||||
msgid "No entity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:170
|
||||
#: apps/transactions/forms.py:191
|
||||
msgid "More"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:207
|
||||
#: apps/transactions/forms.py:228
|
||||
msgid "Save and add similar"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:212
|
||||
#: apps/transactions/forms.py:233
|
||||
msgid "Save and add another"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:295 apps/transactions/forms.py:567
|
||||
#: apps/transactions/forms.py:270 templates/cotton/transaction/item.html:158
|
||||
#: templates/transactions/fragments/attachments.html:4
|
||||
msgid "Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:271
|
||||
msgid ""
|
||||
"Files are private and only visible to users with access to this transaction."
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:282
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:351 apps/transactions/forms.py:623
|
||||
msgid "Muted transactions won't be displayed on monthly summaries"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:503
|
||||
#: apps/transactions/forms.py:559
|
||||
msgid "From Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:508
|
||||
#: apps/transactions/forms.py:564
|
||||
msgid "To Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:606
|
||||
#: apps/transactions/forms.py:662
|
||||
#: templates/cotton/ui/quick_transactions_buttons.html:40
|
||||
#: templates/cotton/ui/transactions_fab.html:44
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:847
|
||||
#: apps/transactions/forms.py:906
|
||||
msgid "Tag name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:875
|
||||
#: apps/transactions/forms.py:934
|
||||
msgid "Entity name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:903
|
||||
#: apps/transactions/forms.py:962
|
||||
msgid "Category name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:905
|
||||
#: apps/transactions/forms.py:964
|
||||
msgid "Muted categories won't be displayed on monthly summaries"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:1055
|
||||
#: apps/transactions/forms.py:1118
|
||||
msgid "future transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/forms.py:1081
|
||||
#: apps/transactions/forms.py:1144
|
||||
msgid "End date should be after the start date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:220
|
||||
#: apps/transactions/models.py:229
|
||||
msgid ""
|
||||
"Deactivated categories won't be able to be selected when creating new "
|
||||
"transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:228
|
||||
#: apps/transactions/models.py:237
|
||||
msgid "Transaction Category"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:229
|
||||
#: apps/transactions/models.py:238
|
||||
msgid "Transaction Categories"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:244
|
||||
#: apps/transactions/models.py:253
|
||||
msgid ""
|
||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:252 apps/transactions/models.py:253
|
||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
||||
msgid "Transaction Tags"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:268
|
||||
#: apps/transactions/models.py:277
|
||||
msgid ""
|
||||
"Deactivated entities won't be able to be selected when creating new "
|
||||
"transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:276
|
||||
#: apps/transactions/models.py:285
|
||||
#: templates/insights/fragments/month_by_month.html:88
|
||||
#: templates/insights/fragments/year_by_year.html:56
|
||||
msgid "Entity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:288 apps/transactions/models.py:987
|
||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
||||
#: templates/calendar_view/fragments/list.html:42
|
||||
#: templates/calendar_view/fragments/list.html:44
|
||||
#: templates/calendar_view/fragments/list.html:52
|
||||
@@ -1480,7 +1494,7 @@ msgstr ""
|
||||
msgid "Income"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:289 apps/transactions/models.py:988
|
||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
||||
#: templates/calendar_view/fragments/list.html:46
|
||||
#: templates/calendar_view/fragments/list.html:48
|
||||
#: templates/calendar_view/fragments/list.html:56
|
||||
@@ -1491,129 +1505,157 @@ msgstr ""
|
||||
msgid "Expense"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:344 apps/transactions/models.py:600
|
||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
||||
msgid "Installment Plan"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:353 apps/transactions/models.py:824
|
||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
||||
msgid "Recurring Transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:361
|
||||
#: apps/transactions/models.py:370
|
||||
msgid "Deleted"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:366
|
||||
#: apps/transactions/models.py:375
|
||||
msgid "Deleted At"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:480 templates/tags/fragments/table.html:69
|
||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
||||
msgid "No tags"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:482
|
||||
#: apps/transactions/models.py:491
|
||||
msgid "No category"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:484
|
||||
#: apps/transactions/models.py:493
|
||||
msgid "No description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:532 templates/includes/sidebar.html:57
|
||||
#: apps/transactions/models.py:549
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:551
|
||||
msgid "Original Name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:553
|
||||
msgid "Content Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:555
|
||||
msgid "Size"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:560
|
||||
msgid "Uploaded By"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:565
|
||||
msgid "Transaction Attachment"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:566
|
||||
msgid "Transaction Attachments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
||||
msgid "Yearly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:533 apps/users/models.py:464
|
||||
#: apps/transactions/models.py:596 apps/users/models.py:464
|
||||
#: templates/includes/sidebar.html:51
|
||||
msgid "Monthly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:534
|
||||
#: apps/transactions/models.py:597
|
||||
msgid "Weekly"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:535
|
||||
#: apps/transactions/models.py:598
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:548
|
||||
#: apps/transactions/models.py:611
|
||||
msgid "Number of Installments"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:553
|
||||
#: apps/transactions/models.py:616
|
||||
msgid "Installment Start"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:554
|
||||
#: apps/transactions/models.py:617
|
||||
msgid "The installment number to start counting from"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:559 apps/transactions/models.py:794
|
||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
||||
msgid "Start Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:563 apps/transactions/models.py:795
|
||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
||||
msgid "End Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:568
|
||||
#: apps/transactions/models.py:631
|
||||
msgid "Recurrence"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:571
|
||||
#: apps/transactions/models.py:634
|
||||
msgid "Installment Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:590 apps/transactions/models.py:814
|
||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
||||
msgid "Add description to transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:593 apps/transactions/models.py:817
|
||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
||||
msgid "Add notes to transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:753
|
||||
#: apps/transactions/models.py:816
|
||||
msgid "day(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:754
|
||||
#: apps/transactions/models.py:817
|
||||
msgid "week(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:755
|
||||
#: apps/transactions/models.py:818
|
||||
msgid "month(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:756
|
||||
#: apps/transactions/models.py:819
|
||||
msgid "year(s)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:758
|
||||
#: apps/transactions/models.py:821
|
||||
#: templates/recurring_transactions/fragments/list.html:18
|
||||
msgid "Paused"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:797
|
||||
#: apps/transactions/models.py:860
|
||||
msgid "Recurrence Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:800
|
||||
#: apps/transactions/models.py:863
|
||||
msgid "Recurrence Interval"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:803
|
||||
#: apps/transactions/models.py:866
|
||||
msgid "Keep at most"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:807
|
||||
#: apps/transactions/models.py:870
|
||||
msgid "Last Generated Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:810
|
||||
#: apps/transactions/models.py:873
|
||||
msgid "Last Generated Reference Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:1054
|
||||
#: apps/transactions/models.py:1117
|
||||
#: apps/transactions/views/quick_transactions.py:178
|
||||
#: apps/transactions/views/quick_transactions.py:187
|
||||
#: apps/transactions/views/quick_transactions.py:189
|
||||
@@ -1622,7 +1664,7 @@ msgstr ""
|
||||
msgid "Quick Transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/models.py:1055 templates/includes/sidebar.html:98
|
||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
||||
#: templates/quick_transactions/pages/index.html:5
|
||||
#: templates/quick_transactions/pages/index.html:15
|
||||
msgid "Quick Transactions"
|
||||
@@ -1727,8 +1769,8 @@ msgid "Item deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/quick_transactions.py:156
|
||||
#: apps/transactions/views/transactions.py:53
|
||||
#: apps/transactions/views/transactions.py:238
|
||||
#: apps/transactions/views/transactions.py:141
|
||||
#: apps/transactions/views/transactions.py:326
|
||||
msgid "Transaction added successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1768,30 +1810,38 @@ msgstr ""
|
||||
msgid "Tag deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:262
|
||||
#: apps/transactions/views/transactions.py:59
|
||||
msgid "Attachment uploaded successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:110
|
||||
msgid "Attachment deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:350
|
||||
msgid "Transaction updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:313
|
||||
#: apps/transactions/views/transactions.py:401
|
||||
#, python-format
|
||||
msgid "%(count)s transaction updated successfully"
|
||||
msgid_plural "%(count)s transactions updated successfully"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:349
|
||||
#: apps/transactions/views/transactions.py:437
|
||||
msgid "Transaction duplicated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:391
|
||||
#: apps/transactions/views/transactions.py:479
|
||||
msgid "Transaction deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:409
|
||||
#: apps/transactions/views/transactions.py:497
|
||||
msgid "Transaction restored successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/transactions/views/transactions.py:435
|
||||
#: apps/transactions/views/transactions.py:523
|
||||
msgid "Transfer added successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1815,24 +1865,24 @@ msgstr ""
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:22 apps/users/forms.py:26 apps/users/models.py:451
|
||||
#: apps/users/forms.py:24 apps/users/forms.py:28 apps/users/models.py:451
|
||||
#: templates/users/login.html:18
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:33 apps/users/forms.py:38 templates/users/login.html:19
|
||||
#: apps/users/forms.py:35 apps/users/forms.py:40 templates/users/login.html:19
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:45
|
||||
#: apps/users/forms.py:47
|
||||
msgid "Invalid e-mail or password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:46
|
||||
#: apps/users/forms.py:48
|
||||
msgid "This account is deactivated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:62 apps/users/forms.py:75 apps/users/forms.py:97
|
||||
#: apps/users/forms.py:64 apps/users/forms.py:77 apps/users/forms.py:99
|
||||
#: templates/monthly_overview/pages/overview.html:98
|
||||
#: templates/monthly_overview/pages/overview.html:245
|
||||
#: templates/transactions/pages/transactions.html:47
|
||||
@@ -1840,19 +1890,23 @@ msgstr ""
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:105 apps/users/models.py:484
|
||||
#: apps/users/forms.py:107 apps/users/models.py:484
|
||||
msgid "Date Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:110 apps/users/models.py:489
|
||||
#: apps/users/forms.py:112 apps/users/models.py:489
|
||||
msgid "Datetime Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:116 apps/users/models.py:492
|
||||
#: apps/users/forms.py:118 apps/users/models.py:492
|
||||
msgid "Number Format"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:154
|
||||
#: apps/users/forms.py:125
|
||||
msgid "Default Account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:174
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This changes the language (if available) and how numbers and dates are "
|
||||
@@ -1860,59 +1914,59 @@ msgid ""
|
||||
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:163
|
||||
#: apps/users/forms.py:183
|
||||
msgid "New Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:166
|
||||
#: apps/users/forms.py:186
|
||||
msgid "Leave blank to keep the current password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:169
|
||||
#: apps/users/forms.py:189
|
||||
msgid "Confirm New Password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:181 apps/users/forms.py:338
|
||||
#: apps/users/forms.py:201 apps/users/forms.py:358
|
||||
msgid ""
|
||||
"Designates whether this user should be treated as active. Unselect this "
|
||||
"instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:184 apps/users/forms.py:341
|
||||
#: apps/users/forms.py:204 apps/users/forms.py:361
|
||||
msgid ""
|
||||
"Designates that this user has all permissions without explicitly assigning "
|
||||
"them."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:251
|
||||
#: apps/users/forms.py:271
|
||||
msgid "This email address is already in use by another account."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:259
|
||||
#: apps/users/forms.py:279
|
||||
msgid "The two password fields didn't match."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:261
|
||||
#: apps/users/forms.py:281
|
||||
msgid "Please confirm your new password."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:263
|
||||
#: apps/users/forms.py:283
|
||||
msgid "Please enter the new password first."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:283
|
||||
#: apps/users/forms.py:303
|
||||
msgid "You cannot deactivate your own account using this form."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:296
|
||||
#: apps/users/forms.py:316
|
||||
msgid "Cannot remove status from the last superuser."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:302
|
||||
#: apps/users/forms.py:322
|
||||
msgid "You cannot remove your own superuser status using this form."
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/forms.py:395
|
||||
#: apps/users/forms.py:415
|
||||
msgid "A user with this email address already exists."
|
||||
msgstr ""
|
||||
|
||||
@@ -1956,6 +2010,14 @@ msgstr ""
|
||||
msgid "Start page"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/models.py:516
|
||||
msgid "Default account"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/models.py:517
|
||||
msgid "Selects the account by default when creating new transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/users/views.py:67
|
||||
msgid "Transaction amounts are now hidden"
|
||||
msgstr ""
|
||||
@@ -2050,8 +2112,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:54
|
||||
#: templates/accounts/fragments/list.html:71
|
||||
#: templates/categories/fragments/table.html:51
|
||||
#: templates/cotton/transaction/item.html:158
|
||||
#: templates/cotton/transaction/item.html:230
|
||||
#: templates/cotton/transaction/item.html:164
|
||||
#: templates/cotton/transaction/item.html:236
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
|
||||
#: templates/cotton/ui/transactions_action_bar.html:82
|
||||
#: templates/currencies/fragments/list.html:40
|
||||
@@ -2079,8 +2141,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:58
|
||||
#: templates/accounts/fragments/list.html:75
|
||||
#: templates/categories/fragments/table.html:56
|
||||
#: templates/cotton/transaction/item.html:160
|
||||
#: templates/cotton/transaction/item.html:236
|
||||
#: templates/cotton/transaction/item.html:166
|
||||
#: templates/cotton/transaction/item.html:242
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/currencies/fragments/list.html:44
|
||||
@@ -2109,8 +2171,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:59
|
||||
#: templates/accounts/fragments/list.html:76
|
||||
#: templates/categories/fragments/table.html:57
|
||||
#: templates/cotton/transaction/item.html:161
|
||||
#: templates/cotton/transaction/item.html:237
|
||||
#: templates/cotton/transaction/item.html:167
|
||||
#: templates/cotton/transaction/item.html:243
|
||||
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
|
||||
#: templates/cotton/ui/transactions_action_bar.html:83
|
||||
#: templates/currencies/fragments/list.html:45
|
||||
@@ -2131,8 +2193,8 @@ msgstr ""
|
||||
#: templates/account_groups/fragments/list.html:60
|
||||
#: templates/accounts/fragments/list.html:77
|
||||
#: templates/categories/fragments/table.html:58
|
||||
#: templates/cotton/transaction/item.html:162
|
||||
#: templates/cotton/transaction/item.html:238
|
||||
#: templates/cotton/transaction/item.html:168
|
||||
#: templates/cotton/transaction/item.html:244
|
||||
#: templates/currencies/fragments/list.html:46
|
||||
#: templates/dca/fragments/strategy/details.html:77
|
||||
#: templates/dca/fragments/strategy/list.html:44
|
||||
@@ -2149,6 +2211,7 @@ msgstr ""
|
||||
#: templates/rules/fragments/transaction_rule/view.html:65
|
||||
#: templates/rules/fragments/transaction_rule/view.html:98
|
||||
#: templates/tags/fragments/table.html:57
|
||||
#: templates/transactions/fragments/attachments.html:22
|
||||
msgid "Yes, delete it!"
|
||||
msgstr ""
|
||||
|
||||
@@ -2284,41 +2347,41 @@ msgstr ""
|
||||
msgid "Select"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:175
|
||||
#: templates/cotton/transaction/item.html:186
|
||||
#: templates/cotton/transaction/item.html:196
|
||||
#: templates/cotton/transaction/item.html:181
|
||||
#: templates/cotton/transaction/item.html:192
|
||||
#: templates/cotton/transaction/item.html:202
|
||||
msgid "Show on summaries"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:177
|
||||
#: templates/cotton/transaction/item.html:183
|
||||
msgid "Controlled by account"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:188
|
||||
#: templates/cotton/transaction/item.html:194
|
||||
msgid "Controlled by category"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:201
|
||||
#: templates/cotton/transaction/item.html:207
|
||||
msgid "Hide from summaries"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:205
|
||||
#: templates/cotton/transaction/item.html:211
|
||||
msgid "Add as quick transaction"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:210
|
||||
#: templates/cotton/transaction/item.html:216
|
||||
msgid "Move to previous month"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:214
|
||||
#: templates/cotton/transaction/item.html:220
|
||||
msgid "Move to next month"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:217
|
||||
#: templates/cotton/transaction/item.html:223
|
||||
msgid "Move to today"
|
||||
msgstr ""
|
||||
|
||||
#: templates/cotton/transaction/item.html:221
|
||||
#: templates/cotton/transaction/item.html:227
|
||||
#: templates/cotton/ui/transactions_action_bar.html:78
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
@@ -2799,6 +2862,10 @@ msgstr ""
|
||||
msgid "Try reloading the page or check the console for more information."
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/hyperscript/htmx_error_handler.html:24
|
||||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -2807,6 +2874,18 @@ msgstr ""
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:4
|
||||
msgid "Pull down to refresh"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:5
|
||||
msgid "Release to refresh"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/scripts/pull_to_refresh_i18n.html:6
|
||||
msgid "Refreshing"
|
||||
msgstr ""
|
||||
|
||||
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
|
||||
msgid "Insights"
|
||||
msgstr ""
|
||||
@@ -3450,6 +3529,22 @@ msgstr ""
|
||||
msgid "Add Installment Plan"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:20
|
||||
msgid "Delete this attachment?"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:21
|
||||
msgid "This file will be removed from the transaction."
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments.html:30
|
||||
msgid "No attachments yet"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/attachments_manage.html:5
|
||||
msgid "Transaction attachments"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transactions/fragments/bulk_edit.html:5
|
||||
msgid "Bulk Editing"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+289
-177
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -147,6 +147,12 @@
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML"
|
||||
data-tippy-content="{% translate "Edit" %}">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||
<a class="btn btn-soft btn-sm transaction-action gap-1"
|
||||
role="button"
|
||||
hx-get="{% url 'transaction_attachments' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML"
|
||||
data-tippy-content="{% translate "Attachments" %}">
|
||||
<i class="fa-solid fa-paperclip fa-fw"></i><span>{{ transaction.attachments.count }}</span></a>
|
||||
<a class="btn btn-error btn-soft btn-sm transaction-action"
|
||||
role="button"
|
||||
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
{% include 'includes/scripts/hyperscript/sounds.html' %}
|
||||
{% include 'includes/scripts/hyperscript/swal.html' %}
|
||||
{% include 'includes/scripts/hyperscript/autosize.html' %}
|
||||
{% include 'includes/scripts/pull_to_refresh_i18n.html' %}
|
||||
|
||||
<script defer>
|
||||
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (!tz) {
|
||||
tz = "UTC"
|
||||
}
|
||||
|
||||
@@ -20,11 +20,16 @@ behavior htmx_error_handler
|
||||
text: '{% trans "Try reloading the page or check the console for more information." %}',
|
||||
icon: 'error',
|
||||
timer: 60000,
|
||||
showDenyButton: true,
|
||||
denyButtonText: '{% trans "Reload" %}',
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-primary'
|
||||
confirmButton: 'btn btn-primary',
|
||||
denyButton: 'btn btn-error',
|
||||
actions: 'gap-2'
|
||||
},
|
||||
buttonsStyling: false
|
||||
})
|
||||
buttonsStyling: false,
|
||||
reverseButtons: true
|
||||
}) then if it.isDenied call location.reload()
|
||||
end
|
||||
then log event
|
||||
then halt the event
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
<span id="ptr-i18n"
|
||||
class="hidden"
|
||||
data-pull="{% translate 'Pull down to refresh' %}"
|
||||
data-release="{% translate 'Release to refresh' %}"
|
||||
data-refreshing="{% translate 'Refreshing' %}"></span>
|
||||
@@ -268,7 +268,7 @@
|
||||
</div>
|
||||
|
||||
{# Filter transactions form #}
|
||||
<div class="z-1" x-show="filterOpen" x-collapse>
|
||||
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
|
||||
<div class="card card-body bg-base-200 mt-2">
|
||||
<div class="text-right">
|
||||
<button class="btn btn-outline btn-error btn-sm w-fit"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="transaction-attachments-list" class="mt-4">
|
||||
<h3 class="font-semibold mb-2">{% translate 'Attachments' %}</h3>
|
||||
{% if transaction.attachments.exists %}
|
||||
<ul class="flex flex-col gap-2">
|
||||
{% for attachment in transaction.attachments.all %}
|
||||
<li class="flex items-center justify-between gap-3 rounded-box border border-base-content/20 p-3">
|
||||
<a class="link link-primary truncate"
|
||||
target="_blank"
|
||||
href="{% url 'transaction_attachment_download' attachment_id=attachment.id %}">
|
||||
{{ attachment.original_name }}
|
||||
</a>
|
||||
<button class="btn btn-error btn-sm btn-soft"
|
||||
type="button"
|
||||
hx-delete="{% url 'transaction_attachment_delete' attachment_id=attachment.id %}"
|
||||
hx-trigger="confirmed"
|
||||
hx-target="#transaction-attachments-list"
|
||||
hx-swap="outerHTML"
|
||||
data-title="{% translate 'Delete this attachment?' %}"
|
||||
data-text="{% translate 'This file will be removed from the transaction.' %}"
|
||||
data-confirm-text="{% translate 'Yes, delete it!' %}"
|
||||
_="install prompt_swal">
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-base-content/60">{% translate 'No attachments yet' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends 'extends/offcanvas.html' %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Transaction attachments' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'transaction_attachments' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
hx-swap="innerHTML"
|
||||
enctype="multipart/form-data"
|
||||
novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
|
||||
{% include 'transactions/fragments/attachments.html' %}
|
||||
{% endblock %}
|
||||
@@ -218,7 +218,7 @@
|
||||
</div>
|
||||
|
||||
{# Filter transactions form #}
|
||||
<div class="z-1" x-show="filterOpen" x-collapse>
|
||||
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
|
||||
<div class="card card-body bg-base-200 mt-2">
|
||||
<div class="text-right">
|
||||
<button class="btn btn-outline btn-error btn-sm w-fit"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
volumes:
|
||||
wygiwyh_dev_postgres_data: {}
|
||||
wygiwyh_temp:
|
||||
wygiwyh_attachments:
|
||||
|
||||
|
||||
services:
|
||||
@@ -14,6 +15,7 @@ services:
|
||||
- ./app/:/usr/src/app/:z
|
||||
- ./frontend/:/usr/src/frontend:z
|
||||
- wygiwyh_temp:/usr/src/app/temp/
|
||||
- wygiwyh_attachments:/usr/src/app/attachments/
|
||||
ports:
|
||||
- "${OUTBOUND_PORT:-8000}:${INTERNAL_PORT:-8000}"
|
||||
env_file:
|
||||
|
||||
@@ -6,6 +6,8 @@ services:
|
||||
- "${OUTBOUND_PORT:-8000}:${INTERNAL_PORT:-8000}"
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./media:/usr/src/app/attachments/
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -2,9 +2,9 @@ FROM node:lts-alpine
|
||||
|
||||
WORKDIR /usr/src/frontend
|
||||
|
||||
COPY ./frontend/package.json .
|
||||
COPY ./frontend/package.json ./frontend/package-lock.json ./
|
||||
|
||||
RUN npm install --verbose && npm cache clean --force
|
||||
RUN npm ci --verbose && npm cache clean --force
|
||||
|
||||
ENV PATH ./node_modules/.bin/:$PATH
|
||||
|
||||
|
||||
Generated
+802
-735
File diff suppressed because it is too large
Load Diff
+21
-20
@@ -16,34 +16,35 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@alpinejs/collapse": "^3.15.1",
|
||||
"@alpinejs/mask": "^3.15.1",
|
||||
"@alpinejs/collapse": "^3.15.12",
|
||||
"@alpinejs/mask": "^3.15.12",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@rollup/plugin-commonjs": "^29.0.0",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@rollup/plugin-commonjs": "^29.0.3",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"air-datepicker": "^3.6.0",
|
||||
"alpinejs": "^3.15.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"alpinejs": "^3.15.12",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"autosize": "^6.0.1",
|
||||
"bootstrap": "^5.3.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-chart-sankey": "^0.14.0",
|
||||
"daisyui": "^5.5.5",
|
||||
"htmx.org": "^2.0.8",
|
||||
"hyperscript.org": "^0.9.14",
|
||||
"mathjs": "^15.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.94.0",
|
||||
"sweetalert2": "^11.26.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"chartjs-chart-sankey": "^0.14.3",
|
||||
"daisyui": "5.5.20",
|
||||
"htmx.org": "^2.0.10",
|
||||
"hyperscript.org": "^0.9.91",
|
||||
"mathjs": "^15.2.0",
|
||||
"postcss": "^8.5.15",
|
||||
"pulltorefreshjs": "^0.1.22",
|
||||
"sass": "^1.100.0",
|
||||
"sweetalert2": "^11.26.25",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tom-select": "^2.4.3",
|
||||
"tw-bootstrap-grid": "^1.3.2",
|
||||
"vite": "7.2.2"
|
||||
"tom-select": "^2.6.1",
|
||||
"tw-bootstrap-grid": "^1.4.0",
|
||||
"vite": "7.3.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "npm:@rollup/wasm-node"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import _hyperscript from 'hyperscript.org/dist/_hyperscript.min';
|
||||
import 'hyperscript.org';
|
||||
import './_htmx.js';
|
||||
import Alpine from "alpinejs";
|
||||
import mask from '@alpinejs/mask';
|
||||
@@ -6,7 +6,6 @@ import collapse from '@alpinejs/collapse'
|
||||
import { create, all } from 'mathjs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
window._hyperscript = _hyperscript;
|
||||
window.math = create(all, {
|
||||
number: 'BigNumber',
|
||||
});
|
||||
@@ -15,8 +14,6 @@ Alpine.plugin(mask);
|
||||
Alpine.plugin(collapse);
|
||||
Alpine.start();
|
||||
|
||||
_hyperscript.browserInit();
|
||||
|
||||
const successAudio = new Audio("/static/sounds/success.mp3");
|
||||
const popAudio = new Audio("/static/sounds/pop.mp3");
|
||||
window.paidSound = successAudio;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import PullToRefresh from 'pulltorefreshjs';
|
||||
|
||||
const isOverlayOpen = () => !!document.querySelector('.offcanvas.show, .swal2-container');
|
||||
|
||||
const isIosPwa = () => {
|
||||
const ua = window.navigator.userAgent.toLowerCase();
|
||||
const isIos = /iphone|ipad|ipod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
|
||||
return isIos && isStandalone;
|
||||
};
|
||||
|
||||
const ptrMarkup = `
|
||||
<div class="__PREFIX__box">
|
||||
<div class="__PREFIX__content">
|
||||
<div class="__PREFIX__icon"></div>
|
||||
<div class="__PREFIX__text"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const ptrStyles = `
|
||||
.__PREFIX__ptr {
|
||||
box-shadow: inset 0 -3px 5px rgba(0, 0, 0, 0.12);
|
||||
pointer-events: none;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
top: 0;
|
||||
height: 0;
|
||||
transition: height 0.3s, min-height 0.3s;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-content: stretch;
|
||||
}
|
||||
|
||||
.__PREFIX__box {
|
||||
padding: 10px;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.__PREFIX__pull {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.__PREFIX__text {
|
||||
margin-top: .33em;
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
|
||||
.__PREFIX__icon {
|
||||
color: var(--color-base-content);
|
||||
transition: transform .3s;
|
||||
}
|
||||
|
||||
/*
|
||||
When at the top of the page, disable vertical overscroll so passive touch
|
||||
listeners can take over.
|
||||
*/
|
||||
.__PREFIX__top {
|
||||
touch-action: pan-x pan-down pinch-zoom;
|
||||
}
|
||||
|
||||
.__PREFIX__release .__PREFIX__icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const getPtrStrings = () => {
|
||||
const ptrStringsEl = document.querySelector('#ptr-i18n');
|
||||
return {
|
||||
pull: ptrStringsEl?.dataset.pull,
|
||||
release: ptrStringsEl?.dataset.release,
|
||||
refreshing: ptrStringsEl?.dataset.refreshing
|
||||
};
|
||||
};
|
||||
|
||||
const initPullToRefresh = () => {
|
||||
const ptrStrings = getPtrStrings();
|
||||
|
||||
PullToRefresh.destroyAll();
|
||||
let ptr = PullToRefresh.init({
|
||||
mainElement: 'body',
|
||||
triggerElement: '#content',
|
||||
getMarkup() {
|
||||
return ptrMarkup;
|
||||
},
|
||||
getStyles() {
|
||||
return ptrStyles;
|
||||
},
|
||||
instructionsPullToRefresh: ptrStrings.pull || 'Pull down to refresh',
|
||||
instructionsReleaseToRefresh: ptrStrings.release || 'Release to refresh',
|
||||
instructionsRefreshing: ptrStrings.refreshing || 'Refreshing',
|
||||
shouldPullToRefresh() {
|
||||
return isIosPwa() && !isOverlayOpen() && window.scrollY === 0;
|
||||
},
|
||||
onRefresh() {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isIosPwa()) {
|
||||
initPullToRefresh();
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
if (event.detail.target === document.body) {
|
||||
initPullToRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,15 @@ import '../styles/_tom-select.scss'
|
||||
|
||||
|
||||
window.TomSelect = function createDynamicTomSelect(element) {
|
||||
const schedulePopperUpdate = function (instance) {
|
||||
// Wait for TomSelect DOM updates before recalculating dropdown position.
|
||||
requestAnimationFrame(() => {
|
||||
if (instance.popper) {
|
||||
instance.popper.update();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Basic configuration
|
||||
const config = {
|
||||
plugins: {},
|
||||
@@ -27,10 +36,16 @@ window.TomSelect = function createDynamicTomSelect(element) {
|
||||
this.popper = Popper.createPopper(this.control, this.dropdown, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sameWidth",
|
||||
enabled: true,
|
||||
fn: ({state}) => {
|
||||
fn: ({ state }) => {
|
||||
state.styles.popper.width = `${state.rects.reference.width}px`;
|
||||
},
|
||||
phase: "beforeWrite",
|
||||
@@ -48,8 +63,17 @@ window.TomSelect = function createDynamicTomSelect(element) {
|
||||
|
||||
},
|
||||
onDropdownOpen: function () {
|
||||
this.popper.update();
|
||||
}
|
||||
schedulePopperUpdate(this);
|
||||
},
|
||||
onItemAdd: function () {
|
||||
schedulePopperUpdate(this);
|
||||
},
|
||||
onItemRemove: function () {
|
||||
schedulePopperUpdate(this);
|
||||
},
|
||||
onClear: function () {
|
||||
schedulePopperUpdate(this);
|
||||
},
|
||||
};
|
||||
|
||||
if (element.dataset.checkboxes === 'true') {
|
||||
|
||||
@@ -10,3 +10,4 @@ import './js/sweetalert2.js';
|
||||
import './js/style.js';
|
||||
import './js/_utils.js';
|
||||
import './js/hide_amounts.js';
|
||||
import './js/pulltorefresh.js';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import twBootstrapGrid from "tw-bootstrap-grid";
|
||||
|
||||
export default twBootstrapGrid;
|
||||
@@ -49,6 +49,10 @@ div:where(.swal2-container) {
|
||||
z-index: 1101 !important;
|
||||
}
|
||||
|
||||
#toasts .toast-container {
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
/* Set the background-color to DaisyUI CSS variable */
|
||||
background-color: var(--color-primary);
|
||||
@@ -77,4 +81,4 @@ div:where(.swal2-container) {
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
themes: wygiwyh_dark --default, wygiwyh_light;
|
||||
logs: true;
|
||||
}
|
||||
@plugin "tw-bootstrap-grid";
|
||||
@plugin "../plugins/tw-bootstrap-grid-plugin.js";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "wygiwyh_light";
|
||||
|
||||
@@ -51,10 +51,44 @@ export default defineConfig({
|
||||
manifest: 'manifest.json',
|
||||
emptyOutDir: true,
|
||||
target: 'es2017',
|
||||
chunkSizeWarningLimit: 800,
|
||||
rollupOptions: {
|
||||
input: rollupInputs,
|
||||
output: {
|
||||
chunkFileNames: undefined,
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id.includes('/chart.js/') || id.includes('/chartjs-chart-sankey/')) {
|
||||
return 'vendor-chart';
|
||||
}
|
||||
|
||||
if (id.includes('/mathjs/')) {
|
||||
return 'vendor-math';
|
||||
}
|
||||
|
||||
if (
|
||||
id.includes('/alpinejs/') ||
|
||||
id.includes('/@alpinejs/') ||
|
||||
id.includes('/htmx.org/') ||
|
||||
id.includes('/hyperscript.org/')
|
||||
) {
|
||||
return 'vendor-interaction';
|
||||
}
|
||||
|
||||
if (
|
||||
id.includes('/bootstrap/') ||
|
||||
id.includes('/@popperjs/') ||
|
||||
id.includes('/sweetalert2/') ||
|
||||
id.includes('/tippy.js/') ||
|
||||
id.includes('/tom-select/') ||
|
||||
id.includes('/air-datepicker/')
|
||||
) {
|
||||
return 'vendor-ui';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+18
-18
@@ -5,34 +5,34 @@ description = "An opinionated and powerful finance tracker."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"crispy-bootstrap5==2025.6",
|
||||
"django~=5.2.9",
|
||||
"django-allauth[socialaccount]~=65.13.1",
|
||||
"crispy-bootstrap5==2026.3",
|
||||
"django~=5.2.15",
|
||||
"django-allauth[socialaccount]~=65.18.0",
|
||||
"django-browser-reload==1.21.0",
|
||||
"django-cachalot~=2.8.0",
|
||||
"django-cotton<2.3.0",
|
||||
"django-crispy-forms==2.5",
|
||||
"django-debug-toolbar==6.1.0",
|
||||
"django-cachalot~=2.9.0",
|
||||
"django-cotton~=2.7.2",
|
||||
"django-crispy-forms==2.6",
|
||||
"django-debug-toolbar==6.3.0",
|
||||
"django-filter==25.2",
|
||||
"django-hijack==3.7.4",
|
||||
"django-import-export~=4.3.9",
|
||||
"django-hijack==3.7.8",
|
||||
"django-import-export~=4.4.1",
|
||||
"django-pwa~=2.0.1",
|
||||
"django-vite==3.1.0",
|
||||
"djangorestframework~=3.16.0",
|
||||
"djangorestframework~=3.17.1",
|
||||
"drf-spectacular~=0.29.0",
|
||||
"gunicorn==23.0.0",
|
||||
"mistune~=3.1.3",
|
||||
"gunicorn==26.0.0",
|
||||
"mistune~=3.2.1",
|
||||
"openpyxl~=3.1.5",
|
||||
"procrastinate[django]~=3.5.3",
|
||||
"psycopg[binary,pool]==3.2.9",
|
||||
"pydantic~=2.12.3",
|
||||
"procrastinate[django]~=3.8.1",
|
||||
"psycopg[binary,pool]==3.3.4",
|
||||
"pydantic~=2.13.4",
|
||||
"python-dateutil~=2.9.0.post0",
|
||||
"pytz>=2025.2",
|
||||
"pyyaml~=6.0.2",
|
||||
"requests~=2.32.5",
|
||||
"requests~=2.34.2",
|
||||
"simpleeval~=1.0.3",
|
||||
"watchfiles==1.1.1",
|
||||
"whitenoise[brotli]==6.11.0",
|
||||
"watchfiles==1.2.0",
|
||||
"whitenoise[brotli]==6.12.0",
|
||||
"xlrd~=2.0.1",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user