Compare commits

..

34 Commits
0.9.0 ... 0.9.4

Author SHA1 Message Date
Herculino Trotta
851b34f07a Merge pull request #156 from eitchtee/dev
fix(transactions): paying transaction doesn't trigger update rules
2025-02-09 23:38:58 -03:00
Herculino Trotta
546ed5c6af fix(transactions): bulk (un)paying transactions doesn't trigger update rules 2025-02-09 23:38:22 -03:00
Herculino Trotta
04ae7337f5 fix(transactions): paying transaction doesn't trigger update rules 2025-02-09 23:33:57 -03:00
Herculino Trotta
63069f0ec9 Merge pull request #155 from eitchtee/dev
refactor: don't display currency code
2025-02-09 19:50:09 -03:00
Herculino Trotta
32b522dad2 refactor: don't display currency code 2025-02-09 19:49:47 -03:00
Herculino Trotta
0c20a079e3 Merge pull request #154 from eitchtee/dev
locale: update locales
2025-02-09 17:31:03 -03:00
Herculino Trotta
7c9697f683 locale: update locales 2025-02-09 17:30:39 -03:00
Herculino Trotta
15d04230ae Merge pull request #153
feat(monthly): add quick-search field
2025-02-09 17:14:44 -03:00
Herculino Trotta
ecc09ca6a6 feat(monthly): add quick-search field 2025-02-09 17:14:25 -03:00
Herculino Trotta
cd753c5dd5 Merge pull request #152 from luzpaz/readme-typos
fix: typos in README
2025-02-09 10:55:54 -03:00
luzpaz
a3b9952f80 fix: typos in README
Found via `codespell -q 3 -S "*.po" -L bu,nome,vew`
2025-02-09 09:47:03 +00:00
Herculino Trotta
e93969c035 Merge pull request #151
feat(import:v1): add XLS and XLSX support
2025-02-09 00:51:46 -03:00
Herculino Trotta
6ec5b5df1e feat(import:v1): add XLS and XLSX support
Closes #47
2025-02-09 00:51:26 -03:00
Herculino Trotta
93e7adeea8 Merge pull request #150 from eitchtee/dev
feat(import): add Cajamar preset
2025-02-09 00:50:38 -03:00
Herculino Trotta
37b5a43c1f feat(import): add Cajamar preset
Thanks to Pablo Hinojosa for sharing his file
2025-02-09 00:50:11 -03:00
Herculino Trotta
87a07c25d1 Merge pull request #149
feat(import:v1): add "add" and "subtract" transformations
2025-02-08 18:30:25 -03:00
Herculino Trotta
9e27fef5e5 feat(import:v1): add "add" and "subtract" transformations 2025-02-08 18:30:06 -03:00
Herculino Trotta
2cbba53e06 Merge pull request #148
feat(import:v1): allow to source previously mapped data by prefixing it with "__" on transformations
2025-02-08 16:38:57 -03:00
Herculino Trotta
d9e8be7efb feat(import:v1): allow to source previously mapped data by prefixing it with "__" on transformations 2025-02-08 16:38:36 -03:00
Herculino Trotta
7dc9ef9950 Merge pull request #147 from eitchtee/dev
refactor(import:v1): remove forced "required" from some fields
2025-02-08 16:36:48 -03:00
Herculino Trotta
00e83cf6a2 refactor(import:v1): remove forced "required" from some fields 2025-02-08 16:35:46 -03:00
Herculino Trotta
039242b48a Merge pull request #146 from eitchtee/dev
fix(dev): django-browser-reload not working
2025-02-08 16:01:06 -03:00
Herculino Trotta
94e2bdf93d fix(dev): django-browser-reload not working 2025-02-08 16:00:45 -03:00
Herculino Trotta
79b387ce60 Merge pull request #145 from eitchtee/dev
feat(import:v1): allow to source previously mapped data by prefixing it with "__"
2025-02-08 15:59:56 -03:00
Herculino Trotta
43eb87d3ba feat(import:v1): allow to source previously mapped data by prefixing it with "__" 2025-02-08 15:59:27 -03:00
Herculino Trotta
0110220b72 Merge pull request #144 from eitchtee/dev
feat: account and currency cards will no longer display unneeded zeros, only for totals
2025-02-08 11:43:24 -03:00
Herculino Trotta
f5c86f3d97 feat: account and currency cards will no longer display unneeded zeros, only for totals 2025-02-08 11:42:46 -03:00
Herculino Trotta
7b7f58d34d Merge pull request #143 from eitchtee/dev
fix(logging): procrastinate job logs not showing up
2025-02-08 04:19:03 -03:00
Herculino Trotta
86112931d9 fix(logging): procrastinate job logs not showing up 2025-02-08 04:18:33 -03:00
Herculino Trotta
e6e0e4caea Merge pull request #142
feat(rules): add Update or Create Transaction action
2025-02-08 04:18:00 -03:00
Herculino Trotta
942154480e feat(rules): add Update or Create Transaction action 2025-02-08 04:17:28 -03:00
Herculino Trotta
467131d9f1 feat(rules): add Update or Create Transaction action 2025-02-08 04:16:28 -03:00
Herculino Trotta
fee1db8660 Merge pull request #141
fix(automatic-exchange-rates): skipping hours due to minutes
2025-02-07 14:34:58 -03:00
Herculino Trotta
4f7fc1c9c8 fix(automatic-exchange-rates): skipping hours due to minutes 2025-02-07 14:34:38 -03:00
39 changed files with 3150 additions and 732 deletions

View File

@@ -80,7 +80,7 @@ $ docker compose exec -it web python manage.py createsuperuser
```
> [!NOTE]
> If you're using Unraid, you don't need to follow these steps, use the app on the store. Make sure to read the [Unraid section](#unraid) and [Enviroment Variables](#enviroment-variables) for an explanation of all available variables
> If you're using Unraid, you don't need to follow these steps, use the app on the store. Make sure to read the [Unraid section](#unraid) and [Environment Variables](#environment-variables) for an explanation of all available variables
## Running locally
@@ -110,7 +110,7 @@ WYGIWYH is available on the Unraid Store. You'll need to provision your own post
To create the first user, open the container's console using Unraid's UI, by clicking on WYGIWYH icon on the Docker page and selecting `Console`, then type `python manage.py createsuperuser`, you'll them be prompted to input your e-mail and password.
## Enviroment Variables
## Environment Variables
| variable | type | default | explanation |
|-------------------------------|-------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -122,7 +122,7 @@ To create the first user, open the container's console using Unraid's UI, by cli
| SQL_DATABASE | string | None *required | The name of your postgres database |
| SQL_USER | string | user | The username used to connect to your postgres database |
| SQL_PASSWORD | string | password | The password used to connect to your postgres database |
| SQL_HOST | string | localhost | The adress used to connect to your postgres database |
| SQL_HOST | string | localhost | The address used to connect to your postgres database |
| SQL_PORT | string | 5432 | The port used to connect to your postgres database |
| SESSION_EXPIRY_TIME | int | 2678400 (31 days) | The age of session cookies, in seconds. E.g. how long you will stay logged in |
| ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. |

View File

@@ -1,19 +0,0 @@
import logging
class ProcrastinateFilter(logging.Filter):
# from https://github.com/madzak/python-json-logger/blob/master/src/pythonjsonlogger/jsonlogger.py#L19
_reserved_log_keys = frozenset(
"""args asctime created exc_info exc_text filename
funcName levelname levelno lineno module msecs message msg name pathname
process processName relativeCreated stack_info thread threadName""".split()
)
def filter(self, record: logging.LogRecord):
record.procrastinate = {}
for key, value in vars(record).items():
if not key.startswith("_") and key not in self._reserved_log_keys | {
"procrastinate"
}:
record.procrastinate[key] = value # type: ignore
return True

View File

@@ -75,6 +75,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
"django_browser_reload.middleware.BrowserReloadMiddleware",
"apps.common.middleware.thread_local.ThreadLocalMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.security.SecurityMiddleware",
@@ -87,7 +88,6 @@ MIDDLEWARE = [
"apps.common.middleware.localization.LocalizationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_browser_reload.middleware.BrowserReloadMiddleware",
"hijack.middleware.HijackUserMiddleware",
]
@@ -277,27 +277,16 @@ if "procrastinate" in sys.argv:
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"procrastinate": {
"format": "[%(asctime)s] - %(levelname)s - %(name)s - %(message)s -> %(procrastinate)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"standard": {
"format": "[%(asctime)s] - %(levelname)s - %(name)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"filters": {
"procrastinate": {
"()": "WYGIWYH.logs.ProcrastinateFilter.ProcrastinateFilter",
"name": "procrastinate",
},
},
"handlers": {
"procrastinate": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "procrastinate",
"filters": ["procrastinate"],
"formatter": "standard",
},
"console": {
"class": "logging.StreamHandler",
@@ -308,10 +297,10 @@ if "procrastinate" in sys.argv:
"loggers": {
"procrastinate": {
"handlers": ["procrastinate"],
"propagate": True,
"propagate": False,
},
"root": {
"handlers": None,
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},

View File

@@ -31,7 +31,7 @@ class ExchangeRateFetcher:
service.fetch_interval
)
should_fetch = current_hour not in blocked_hours
logger.debug(
logger.info(
f"NOT_ON check for {service.name}: "
f"current_hour={current_hour}, "
f"blocked_hours={blocked_hours}, "
@@ -43,18 +43,35 @@ class ExchangeRateFetcher:
allowed_hours = ExchangeRateService._parse_hour_ranges(
service.fetch_interval
)
return current_hour in allowed_hours
should_fetch = current_hour in allowed_hours
logger.info(
f"ON check for {service.name}: "
f"current_hour={current_hour}, "
f"allowed_hours={allowed_hours}, "
f"should_fetch={should_fetch}"
)
return should_fetch
if service.interval_type == ExchangeRateService.IntervalType.EVERY:
try:
interval_hours = int(service.fetch_interval)
if service.last_fetch is None:
return True
hours_since_last = (
timezone.now() - service.last_fetch
).total_seconds() / 3600
# Round down to nearest hour
now = timezone.now().replace(minute=0, second=0, microsecond=0)
last_fetch = service.last_fetch.replace(
minute=0, second=0, microsecond=0
)
hours_since_last = (now - last_fetch).total_seconds() / 3600
should_fetch = hours_since_last >= interval_hours
logger.debug(
logger.info(
f"EVERY check for {service.name}: "
f"hours_since_last={hours_since_last:.1f}, "
f"interval={interval_hours}, "

View File

@@ -47,6 +47,34 @@ class SplitTransformationRule(BaseModel):
)
class AddTransformationRule(BaseModel):
type: Literal["add"]
field: str = Field(..., description="Field to add to the source value")
absolute_values: bool = Field(
default=False, description="Use absolute values for addition"
)
thousand_separator: str = Field(
default="", description="Thousand separator character"
)
decimal_separator: str = Field(
default=".", description="Decimal separator character"
)
class SubtractTransformationRule(BaseModel):
type: Literal["subtract"]
field: str = Field(..., description="Field to subtract from the source value")
absolute_values: bool = Field(
default=False, description="Use absolute values for subtraction"
)
thousand_separator: str = Field(
default="", description="Thousand separator character"
)
decimal_separator: str = Field(
default=".", description="Decimal separator character"
)
class CSVImportSettings(BaseModel):
skip_errors: bool = Field(
default=False,
@@ -64,6 +92,20 @@ class CSVImportSettings(BaseModel):
]
class ExcelImportSettings(BaseModel):
skip_errors: bool = Field(
default=False,
description="If True, errors during import will be logged and skipped",
)
file_type: Literal["xls", "xlsx"]
trigger_transaction_rules: bool = True
importing: Literal[
"transactions", "accounts", "currencies", "categories", "tags", "entities"
]
start_row: int = Field(default=1, description="Where your header is located")
sheets: list[str] | str = "*"
class ColumnMapping(BaseModel):
source: Optional[str] | Optional[list[str]] = Field(
default=None,
@@ -78,6 +120,8 @@ class ColumnMapping(BaseModel):
| HashTransformationRule
| MergeTransformationRule
| SplitTransformationRule
| AddTransformationRule
| SubtractTransformationRule
]
] = Field(default_factory=list)
@@ -86,7 +130,6 @@ class TransactionAccountMapping(ColumnMapping):
target: Literal["account"] = Field(..., description="Transaction field to map to")
type: Literal["id", "name"] = "name"
coerce_to: Literal["str|int"] = Field("str|int", frozen=True)
required: bool = Field(True, frozen=True)
class TransactionTypeMapping(ColumnMapping):
@@ -105,7 +148,6 @@ class TransactionDateMapping(ColumnMapping):
target: Literal["date"] = Field(..., description="Transaction field to map to")
format: List[str] | str
coerce_to: Literal["date"] = Field("date", frozen=True)
required: bool = Field(True, frozen=True)
class TransactionReferenceDateMapping(ColumnMapping):
@@ -119,7 +161,6 @@ class TransactionReferenceDateMapping(ColumnMapping):
class TransactionAmountMapping(ColumnMapping):
target: Literal["amount"] = Field(..., description="Transaction field to map to")
coerce_to: Literal["positive_decimal"] = Field("positive_decimal", frozen=True)
required: bool = Field(True, frozen=True)
class TransactionDescriptionMapping(ColumnMapping):
@@ -301,7 +342,7 @@ class CurrencyExchangeMapping(ColumnMapping):
class ImportProfileSchema(BaseModel):
settings: CSVImportSettings
settings: CSVImportSettings | ExcelImportSettings
mapping: Dict[
str,
TransactionAccountMapping

View File

@@ -3,14 +3,16 @@ import hashlib
import logging
import os
import re
from datetime import datetime
from decimal import Decimal
from datetime import datetime, date
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Literal, Union
import cachalot.api
import openpyxl
import xlrd
import yaml
from cachalot.api import cachalot_disabled
from django.utils import timezone
from openpyxl.utils.exceptions import InvalidFileException
from apps.accounts.models import Account, AccountGroup
from apps.currencies.models import Currency
@@ -40,7 +42,9 @@ class ImportService:
self.import_run: ImportRun = import_run
self.profile: ImportProfile = import_run.profile
self.config: version_1.ImportProfileSchema = self._load_config()
self.settings: version_1.CSVImportSettings = self.config.settings
self.settings: version_1.CSVImportSettings | version_1.ExcelImportSettings = (
self.config.settings
)
self.deduplication: list[version_1.CompareDeduplicationRule] = (
self.config.deduplication
)
@@ -75,6 +79,13 @@ class ImportService:
self.import_run.logs += log_line
self.import_run.save(update_fields=["logs"])
if level == "info":
logger.info(log_line)
elif level == "warning":
logger.warning(log_line)
elif level == "error":
logger.error(log_line, exc_info=True)
def _update_totals(
self,
field: Literal["total", "processed", "successful", "skipped", "failed"],
@@ -129,9 +140,12 @@ class ImportService:
self.import_run.save(update_fields=["status"])
@staticmethod
def _transform_value(
value: str, mapping: version_1.ColumnMapping, row: Dict[str, str] = None
self,
value: str,
mapping: version_1.ColumnMapping,
row: Dict[str, str] = None,
mapped_data: Dict[str, Any] = None,
) -> Any:
transformed = value
@@ -142,8 +156,12 @@ class ImportService:
for field in transform.fields:
if field in row:
values_to_hash.append(str(row[field]))
# Create hash from concatenated values
elif (
field.startswith("__")
and mapped_data
and field[2:] in mapped_data
):
values_to_hash.append(str(mapped_data[field[2:]]))
if values_to_hash:
concatenated = "|".join(values_to_hash)
transformed = hashlib.sha256(concatenated.encode()).hexdigest()
@@ -157,6 +175,7 @@ class ImportService:
transformed = transformed.replace(
transform.pattern, transform.replacement
)
elif transform.type == "regex":
if transform.exclusive:
transformed = re.sub(
@@ -166,16 +185,25 @@ class ImportService:
transformed = re.sub(
transform.pattern, transform.replacement, transformed
)
elif transform.type == "date_format":
transformed = datetime.strptime(
transformed, transform.original_format
).strftime(transform.new_format)
elif transform.type == "merge":
values_to_merge = []
for field in transform.fields:
if field in row:
values_to_merge.append(str(row[field]))
elif (
field.startswith("__")
and mapped_data
and field[2:] in mapped_data
):
values_to_merge.append(str(mapped_data[field[2:]]))
transformed = transform.separator.join(values_to_merge)
elif transform.type == "split":
parts = transformed.split(transform.separator)
if transform.index is not None:
@@ -183,6 +211,38 @@ class ImportService:
else:
transformed = parts
elif transform.type in ["add", "subtract"]:
try:
source_value = Decimal(transformed)
# First check row data, then mapped data if not found
field_value = row.get(transform.field)
if field_value is None and transform.field.startswith("__"):
field_value = mapped_data.get(transform.field[2:])
if field_value is None:
raise KeyError(
f"Field '{transform.field}' not found in row or mapped data"
)
field_value = self._prepare_numeric_value(
str(field_value),
transform.thousand_separator,
transform.decimal_separator,
)
if transform.absolute_values:
source_value = abs(source_value)
field_value = abs(field_value)
if transform.type == "add":
transformed = str(source_value + field_value)
else: # subtract
transformed = str(source_value - field_value)
except (InvalidOperation, KeyError, AttributeError) as e:
logger.warning(
f"Error in {transform.type} transformation: {e}. Values: {transformed}, {transform.field}"
)
return transformed
def _create_transaction(self, data: Dict[str, Any]) -> Transaction:
@@ -399,7 +459,7 @@ class ImportService:
def _coerce_type(
self, value: str, mapping: version_1.ColumnMapping
) -> Union[str, int, bool, Decimal, datetime, list]:
) -> Union[str, int, bool, Decimal, datetime, list, None]:
if not value:
return None
@@ -434,6 +494,11 @@ class ImportService:
version_1.TransactionReferenceDateMapping,
),
):
if isinstance(value, datetime):
return value.date()
elif isinstance(value, date):
return value
formats = (
mapping.format
if isinstance(mapping.format, list)
@@ -484,28 +549,30 @@ class ImportService:
def _map_row(self, row: Dict[str, str]) -> Dict[str, Any]:
mapped_data = {}
for field, mapping in self.mapping.items():
value = None
if isinstance(mapping.source, str):
value = row.get(mapping.source, None)
if mapping.source in row:
value = row[mapping.source]
elif (
mapping.source.startswith("__")
and mapping.source[2:] in mapped_data
):
value = mapped_data[mapping.source[2:]]
elif isinstance(mapping.source, list):
for source in mapping.source:
value = row.get(source, None)
if value:
if source in row:
value = row[source]
break
elif source.startswith("__") and source[2:] in mapped_data:
value = mapped_data[source[2:]]
break
else:
# If source is None, use None as the initial value
value = None
# Use default_value if value is None
if not value:
if value is None:
value = mapping.default
# Apply transformations
if mapping.transformations:
value = self._transform_value(value, mapping, row)
value = self._transform_value(value, mapping, row, mapped_data)
value = self._coerce_type(value, mapping)
@@ -513,17 +580,29 @@ class ImportService:
raise ValueError(f"Required field {field} is missing")
if value is not None:
# Remove the prefix from the target field
target = mapping.target
if self.settings.importing == "transactions":
mapped_data[target] = value
else:
# Remove the model prefix (e.g., "account_" from "account_name")
field_name = target.split("_", 1)[1]
mapped_data[field_name] = value
return mapped_data
@staticmethod
def _prepare_numeric_value(
value: str, thousand_separator: str, decimal_separator: str
) -> Decimal:
# Remove thousand separators
if thousand_separator:
value = value.replace(thousand_separator, "")
# Replace decimal separator with dot
if decimal_separator != ".":
value = value.replace(decimal_separator, ".")
return Decimal(value)
def _process_row(self, row: Dict[str, str], row_number: int) -> None:
try:
mapped_data = self._map_row(row)
@@ -589,6 +668,151 @@ class ImportService:
for row_number, row in enumerate(reader, start=1):
self._process_row(row, row_number)
def _process_excel(self, file_path):
try:
if self.settings.file_type == "xlsx":
workbook = openpyxl.load_workbook(
file_path, read_only=True, data_only=True
)
sheets_to_process = (
workbook.sheetnames
if self.settings.sheets == "*"
else (
self.settings.sheets
if isinstance(self.settings.sheets, list)
else [self.settings.sheets]
)
)
# Calculate total rows
total_rows = sum(
max(0, workbook[sheet_name].max_row - self.settings.start_row)
for sheet_name in sheets_to_process
if sheet_name in workbook.sheetnames
)
self._update_totals("total", value=total_rows)
# Process sheets
for sheet_name in sheets_to_process:
if sheet_name not in workbook.sheetnames:
self._log(
"warning",
f"Sheet '{sheet_name}' not found in the Excel file. Skipping.",
)
continue
sheet = workbook[sheet_name]
self._log("info", f"Processing sheet: {sheet_name}")
headers = [
str(cell.value or "") for cell in sheet[self.settings.start_row]
]
for row_number, row in enumerate(
sheet.iter_rows(
min_row=self.settings.start_row + 1, values_only=True
),
start=1,
):
try:
row_data = {
key: str(value) if value is not None else None
for key, value in zip(headers, row)
}
self._process_row(row_data, row_number)
except Exception as e:
if self.settings.skip_errors:
self._log(
"warning",
f"Error processing row {row_number} in sheet '{sheet_name}': {str(e)}",
)
self._increment_totals("failed", value=1)
else:
raise
workbook.close()
else: # xls
workbook = xlrd.open_workbook(file_path)
sheets_to_process = (
workbook.sheet_names()
if self.settings.sheets == "*"
else (
self.settings.sheets
if isinstance(self.settings.sheets, list)
else [self.settings.sheets]
)
)
# Calculate total rows
total_rows = sum(
max(
0,
workbook.sheet_by_name(sheet_name).nrows
- self.settings.start_row,
)
for sheet_name in sheets_to_process
if sheet_name in workbook.sheet_names()
)
self._update_totals("total", value=total_rows)
# Process sheets
for sheet_name in sheets_to_process:
if sheet_name not in workbook.sheet_names():
self._log(
"warning",
f"Sheet '{sheet_name}' not found in the Excel file. Skipping.",
)
continue
sheet = workbook.sheet_by_name(sheet_name)
self._log("info", f"Processing sheet: {sheet_name}")
headers = [
str(sheet.cell_value(self.settings.start_row - 1, col) or "")
for col in range(sheet.ncols)
]
for row_number in range(self.settings.start_row, sheet.nrows):
try:
row_data = {}
for col, key in enumerate(headers):
cell_type = sheet.cell_type(row_number, col)
cell_value = sheet.cell_value(row_number, col)
if cell_type == xlrd.XL_CELL_DATE:
# Convert Excel date to Python datetime
try:
python_date = datetime(
*xlrd.xldate_as_tuple(
cell_value, workbook.datemode
)
)
row_data[key] = python_date
except Exception:
# If date conversion fails, use the original value
row_data[key] = (
str(cell_value)
if cell_value is not None
else None
)
elif cell_value is None:
row_data[key] = None
else:
row_data[key] = str(cell_value)
self._process_row(
row_data, row_number - self.settings.start_row + 1
)
except Exception as e:
if self.settings.skip_errors:
self._log(
"warning",
f"Error processing row {row_number} in sheet '{sheet_name}': {str(e)}",
)
self._increment_totals("failed", value=1)
else:
raise
except (InvalidFileException, xlrd.XLRDError) as e:
raise ValueError(
f"Invalid {self.settings.file_type.upper()} file format: {str(e)}"
)
def _validate_file_path(self, file_path: str) -> str:
"""
Validates that the file path is within the allowed temporary directory.
@@ -611,8 +835,10 @@ class ImportService:
self._log("info", "Starting import process")
try:
if self.settings.file_type == "csv":
if isinstance(self.settings, version_1.CSVImportSettings):
self._process_csv(file_path)
elif isinstance(self.settings, version_1.ExcelImportSettings):
self._process_excel(file_path)
self._update_status("FINISHED")
self._log(
@@ -639,4 +865,3 @@ class ImportService:
self.import_run.finished_at = timezone.now()
self.import_run.save(update_fields=["finished_at"])
cachalot.api.invalidate()

View File

@@ -1,6 +1,5 @@
import logging
import cachalot.api
from procrastinate.contrib.django import app
from apps.import_app.models import ImportRun
@@ -15,7 +14,5 @@ def process_import(import_run_id: int, file_path: str):
import_run = ImportRun.objects.get(id=import_run_id)
import_service = ImportServiceV1(import_run)
import_service.process_file(file_path)
cachalot.api.invalidate()
except ImportRun.DoesNotExist:
cachalot.api.invalidate()
raise ValueError(f"ImportRun with id {import_run_id} not found")

View File

@@ -1,7 +1,12 @@
from django.contrib import admin
from apps.rules.models import TransactionRule, TransactionRuleAction
from apps.rules.models import (
TransactionRule,
TransactionRuleAction,
UpdateOrCreateTransactionRuleAction,
)
# Register your models here.
admin.site.register(TransactionRule)
admin.site.register(TransactionRuleAction)
admin.site.register(UpdateOrCreateTransactionRuleAction)

View File

@@ -1,15 +1,15 @@
from crispy_bootstrap5.bootstrap5 import Switch
from crispy_forms.bootstrap import FormActions
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Row, Column
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from apps.rules.models import TransactionRule
from apps.rules.models import TransactionRuleAction
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.tom_select import TomSelect
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
from apps.rules.models import TransactionRuleAction
class TransactionRuleForm(forms.ModelForm):
@@ -123,3 +123,255 @@ class TransactionRuleActionForm(forms.ModelForm):
if commit:
instance.save()
return instance
class UpdateOrCreateTransactionRuleActionForm(forms.ModelForm):
class Meta:
model = UpdateOrCreateTransactionRuleAction
exclude = ("rule",)
widgets = {
"search_account_operator": TomSelect(clear_button=False),
"search_type_operator": TomSelect(clear_button=False),
"search_is_paid_operator": TomSelect(clear_button=False),
"search_date_operator": TomSelect(clear_button=False),
"search_reference_date_operator": TomSelect(clear_button=False),
"search_amount_operator": TomSelect(clear_button=False),
"search_description_operator": TomSelect(clear_button=False),
"search_notes_operator": TomSelect(clear_button=False),
"search_category_operator": TomSelect(clear_button=False),
"search_internal_note_operator": TomSelect(clear_button=False),
"search_internal_id_operator": TomSelect(clear_button=False),
}
labels = {
"search_account_operator": _("Operator"),
"search_type_operator": _("Operator"),
"search_is_paid_operator": _("Operator"),
"search_date_operator": _("Operator"),
"search_reference_date_operator": _("Operator"),
"search_amount_operator": _("Operator"),
"search_description_operator": _("Operator"),
"search_notes_operator": _("Operator"),
"search_category_operator": _("Operator"),
"search_internal_note_operator": _("Operator"),
"search_internal_id_operator": _("Operator"),
"search_tags_operator": _("Operator"),
"search_entities_operator": _("Operator"),
"search_account": _("Account"),
"search_type": _("Type"),
"search_is_paid": _("Paid"),
"search_date": _("Date"),
"search_reference_date": _("Reference Date"),
"search_amount": _("Amount"),
"search_description": _("Description"),
"search_notes": _("Notes"),
"search_category": _("Category"),
"search_internal_note": _("Internal Note"),
"search_internal_id": _("Internal ID"),
"search_tags": _("Tags"),
"search_entities": _("Entities"),
"set_account": _("Account"),
"set_type": _("Type"),
"set_is_paid": _("Paid"),
"set_date": _("Date"),
"set_reference_date": _("Reference Date"),
"set_amount": _("Amount"),
"set_description": _("Description"),
"set_tags": _("Tags"),
"set_entities": _("Entities"),
"set_notes": _("Notes"),
"set_category": _("Category"),
"set_internal_note": _("Internal Note"),
"set_internal_id": _("Internal ID"),
}
def __init__(self, *args, **kwargs):
self.rule = kwargs.pop("rule", None)
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
BS5Accordion(
AccordionGroup(
_("Search Criteria"),
Field("filter", rows=1),
Row(
Column(
Field("search_type_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_type", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_is_paid_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_is_paid", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_account_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_account", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_entities_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_entities", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_date_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_date", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_reference_date_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_reference_date", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_description_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_description", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_amount_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_amount", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_category_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_category", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_tags_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_tags", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_notes_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_notes", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_internal_note_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_internal_note", rows=1),
css_class="form-group col-md-8",
),
),
Row(
Column(
Field("search_internal_id_operator"),
css_class="form-group col-md-4",
),
Column(
Field("search_internal_id", rows=1),
css_class="form-group col-md-8",
),
),
active=True,
),
AccordionGroup(
_("Set Values"),
Field("set_type", rows=1),
Field("set_is_paid", rows=1),
Field("set_account", rows=1),
Field("set_entities", rows=1),
Field("set_date", rows=1),
Field("set_reference_date", rows=1),
Field("set_description", rows=1),
Field("set_amount", rows=1),
Field("set_category", rows=1),
Field("set_tags", rows=1),
Field("set_notes", rows=1),
Field("set_internal_note", rows=1),
Field("set_internal_id", rows=1),
css_class="mb-3",
active=True,
),
always_open=True,
),
)
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit(
"submit", _("Update"), css_class="btn btn-outline-primary w-100"
),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit(
"submit", _("Add"), css_class="btn btn-outline-primary w-100"
),
),
)
def save(self, commit=True):
instance = super().save(commit=False)
instance.rule = self.rule
if commit:
instance.save()
return instance

View File

@@ -0,0 +1,60 @@
# Generated by Django 5.1.5 on 2025-02-08 03:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0005_alter_transactionruleaction_rule'),
]
operations = [
migrations.CreateModel(
name='UpdateOrCreateTransactionRuleAction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('search_account', models.TextField(blank=True, help_text='Expression to match transaction account (ID or name)', verbose_name='Search Account')),
('search_account_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Account Operator')),
('search_type', models.TextField(blank=True, help_text="Expression to match transaction type ('IN' or 'EX')", verbose_name='Search Type')),
('search_type_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Type Operator')),
('search_is_paid', models.TextField(blank=True, help_text='Expression to match transaction paid status', verbose_name='Search Is Paid')),
('search_is_paid_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Is Paid Operator')),
('search_date', models.TextField(blank=True, help_text='Expression to match transaction date', verbose_name='Search Date')),
('search_date_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Date Operator')),
('search_reference_date', models.TextField(blank=True, help_text='Expression to match transaction reference date', verbose_name='Search Reference Date')),
('search_reference_date_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Reference Date Operator')),
('search_amount', models.TextField(blank=True, help_text='Expression to match transaction amount', verbose_name='Search Amount')),
('search_amount_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Amount Operator')),
('search_description', models.TextField(blank=True, help_text='Expression to match transaction description', verbose_name='Search Description')),
('search_description_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='contains', max_length=10, verbose_name='Description Operator')),
('search_notes', models.TextField(blank=True, help_text='Expression to match transaction notes', verbose_name='Search Notes')),
('search_notes_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='contains', max_length=10, verbose_name='Notes Operator')),
('search_category', models.TextField(blank=True, help_text='Expression to match transaction category (ID or name)', verbose_name='Search Category')),
('search_category_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Category Operator')),
('search_internal_note', models.TextField(blank=True, help_text='Expression to match transaction internal note', verbose_name='Search Internal Note')),
('search_internal_note_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Internal Note Operator')),
('search_internal_id', models.TextField(blank=True, help_text='Expression to match transaction internal ID', verbose_name='Search Internal ID')),
('search_internal_id_operator', models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='exact', max_length=10, verbose_name='Internal ID Operator')),
('set_account', models.TextField(blank=True, help_text='Expression for account to set (ID or name)', verbose_name='Set Account')),
('set_type', models.TextField(blank=True, help_text="Expression for type to set ('IN' or 'EX')", verbose_name='Set Type')),
('set_is_paid', models.TextField(blank=True, help_text='Expression for paid status to set', verbose_name='Set Is Paid')),
('set_date', models.TextField(blank=True, help_text='Expression for date to set', verbose_name='Set Date')),
('set_reference_date', models.TextField(blank=True, help_text='Expression for reference date to set', verbose_name='Set Reference Date')),
('set_amount', models.TextField(blank=True, help_text='Expression for amount to set', verbose_name='Set Amount')),
('set_description', models.TextField(blank=True, help_text='Expression for description to set', verbose_name='Set Description')),
('set_notes', models.TextField(blank=True, help_text='Expression for notes to set', verbose_name='Set Notes')),
('set_internal_note', models.TextField(blank=True, help_text='Expression for internal note to set', verbose_name='Set Internal Note')),
('set_internal_id', models.TextField(blank=True, help_text='Expression for internal ID to set', verbose_name='Set Internal ID')),
('set_category', models.TextField(blank=True, help_text='Expression for category to set (ID or name)', verbose_name='Set Category')),
('set_tags', models.TextField(blank=True, help_text='Expression for tags to set (list of IDs or names)', verbose_name='Set Tags')),
('set_entities', models.TextField(blank=True, help_text='Expression for entities to set (list of IDs or names)', verbose_name='Set Entities')),
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='update_or_create_transaction_actions', to='rules.transactionrule', verbose_name='Rule')),
],
options={
'verbose_name': 'pdate or Create Transaction Action',
'verbose_name_plural': 'pdate or Create Transaction Action Actions',
},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.5 on 2025-02-08 04:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0006_updateorcreatetransactionruleaction'),
]
operations = [
migrations.AlterModelOptions(
name='updateorcreatetransactionruleaction',
options={'verbose_name': 'Update or Create Transaction Action', 'verbose_name_plural': 'Update or Create Transaction Action Actions'},
),
migrations.AddField(
model_name='updateorcreatetransactionruleaction',
name='filter',
field=models.TextField(blank=True, help_text='Generic expression to enable or disable execution. Should evaluate to True or False', verbose_name='Filter'),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.5 on 2025-02-08 06:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0007_alter_updateorcreatetransactionruleaction_options_and_more'),
]
operations = [
migrations.AddField(
model_name='updateorcreatetransactionruleaction',
name='search_entities',
field=models.TextField(blank=True, help_text='Expression to match transaction entities (list of IDs or names)', verbose_name='Search Entities'),
),
migrations.AddField(
model_name='updateorcreatetransactionruleaction',
name='search_entities_operator',
field=models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='contains', max_length=10, verbose_name='Entities Operator'),
),
migrations.AddField(
model_name='updateorcreatetransactionruleaction',
name='search_tags',
field=models.TextField(blank=True, help_text='Expression to match transaction tags (list of IDs or names)', verbose_name='Search Tags'),
),
migrations.AddField(
model_name='updateorcreatetransactionruleaction',
name='search_tags_operator',
field=models.CharField(choices=[('exact', 'is exactly'), ('contains', 'contains'), ('startswith', 'starts with'), ('endswith', 'ends with'), ('eq', 'equals'), ('gt', 'greater than'), ('lt', 'less than'), ('gte', 'greater than or equal'), ('lte', 'less than or equal')], default='contains', max_length=10, verbose_name='Tags Operator'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.1.5 on 2025-02-08 06:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('rules', '0008_updateorcreatetransactionruleaction_search_entities_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='transactionrule',
options={'verbose_name': 'Transaction rule', 'verbose_name_plural': 'Transaction rules'},
),
migrations.AlterModelOptions(
name='transactionruleaction',
options={'verbose_name': 'Edit transaction action', 'verbose_name_plural': 'Edit transaction actions'},
),
migrations.AlterModelOptions(
name='updateorcreatetransactionruleaction',
options={'verbose_name': 'Update or create transaction action', 'verbose_name_plural': 'Update or create transaction actions'},
),
]

View File

@@ -0,0 +1,138 @@
# Generated by Django 5.1.6 on 2025-02-09 20:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0009_alter_transactionrule_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_account',
field=models.TextField(blank=True, verbose_name='Search Account'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_amount',
field=models.TextField(blank=True, verbose_name='Search Amount'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_category',
field=models.TextField(blank=True, verbose_name='Search Category'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_description',
field=models.TextField(blank=True, verbose_name='Search Description'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_entities',
field=models.TextField(blank=True, verbose_name='Search Entities'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_internal_id',
field=models.TextField(blank=True, verbose_name='Search Internal ID'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_internal_note',
field=models.TextField(blank=True, verbose_name='Search Internal Note'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_is_paid',
field=models.TextField(blank=True, verbose_name='Search Is Paid'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_notes',
field=models.TextField(blank=True, verbose_name='Search Notes'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_reference_date',
field=models.TextField(blank=True, verbose_name='Search Reference Date'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_tags',
field=models.TextField(blank=True, verbose_name='Search Tags'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='search_type',
field=models.TextField(blank=True, verbose_name='Search Type'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_account',
field=models.TextField(blank=True, verbose_name='Account'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_amount',
field=models.TextField(blank=True, verbose_name='Amount'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_category',
field=models.TextField(blank=True, verbose_name='Category'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_date',
field=models.TextField(blank=True, verbose_name='Date'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_description',
field=models.TextField(blank=True, verbose_name='Description'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_entities',
field=models.TextField(blank=True, verbose_name='Entities'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_internal_id',
field=models.TextField(blank=True, verbose_name='Internal ID'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_internal_note',
field=models.TextField(blank=True, verbose_name='Internal Note'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_is_paid',
field=models.TextField(blank=True, verbose_name='Is Paid'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_notes',
field=models.TextField(blank=True, verbose_name='Notes'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_reference_date',
field=models.TextField(blank=True, verbose_name='Reference Date'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_tags',
field=models.TextField(blank=True, verbose_name='Tags'),
),
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_type',
field=models.TextField(blank=True, verbose_name='Type'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-02-09 20:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0010_alter_updateorcreatetransactionruleaction_search_account_and_more'),
]
operations = [
migrations.AlterField(
model_name='updateorcreatetransactionruleaction',
name='set_is_paid',
field=models.TextField(blank=True, verbose_name='Paid'),
),
]

View File

@@ -1,4 +1,5 @@
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -10,6 +11,10 @@ class TransactionRule(models.Model):
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
trigger = models.TextField(verbose_name=_("Trigger"))
class Meta:
verbose_name = _("Transaction rule")
verbose_name_plural = _("Transaction rules")
def __str__(self):
return self.name
@@ -45,4 +50,350 @@ class TransactionRuleAction(models.Model):
return f"{self.rule} - {self.field} - {self.value}"
class Meta:
verbose_name = _("Edit transaction action")
verbose_name_plural = _("Edit transaction actions")
unique_together = (("rule", "field"),)
class UpdateOrCreateTransactionRuleAction(models.Model):
"""
Will attempt to find and update latest matching transaction, or create new if none found.
"""
class SearchOperator(models.TextChoices):
EXACT = "exact", _("is exactly")
CONTAINS = "contains", _("contains")
STARTSWITH = "startswith", _("starts with")
ENDSWITH = "endswith", _("ends with")
EQ = "eq", _("equals")
GT = "gt", _("greater than")
LT = "lt", _("less than")
GTE = "gte", _("greater than or equal")
LTE = "lte", _("less than or equal")
rule = models.ForeignKey(
TransactionRule,
on_delete=models.CASCADE,
related_name="update_or_create_transaction_actions",
verbose_name=_("Rule"),
)
filter = models.TextField(
verbose_name=_("Filter"),
blank=True,
help_text=_(
"Generic expression to enable or disable execution. Should evaluate to True or False"
),
)
# Search fields with operators
search_account = models.TextField(
verbose_name="Search Account",
blank=True,
)
search_account_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Account Operator",
)
search_type = models.TextField(
verbose_name="Search Type",
blank=True,
)
search_type_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Type Operator",
)
search_is_paid = models.TextField(
verbose_name="Search Is Paid",
blank=True,
)
search_is_paid_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Is Paid Operator",
)
search_date = models.TextField(
verbose_name="Search Date",
blank=True,
help_text="Expression to match transaction date",
)
search_date_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Date Operator",
)
search_reference_date = models.TextField(
verbose_name="Search Reference Date",
blank=True,
)
search_reference_date_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Reference Date Operator",
)
search_amount = models.TextField(
verbose_name="Search Amount",
blank=True,
)
search_amount_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Amount Operator",
)
search_description = models.TextField(
verbose_name="Search Description",
blank=True,
)
search_description_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.CONTAINS,
verbose_name="Description Operator",
)
search_notes = models.TextField(
verbose_name="Search Notes",
blank=True,
)
search_notes_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.CONTAINS,
verbose_name="Notes Operator",
)
search_category = models.TextField(
verbose_name="Search Category",
blank=True,
)
search_category_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Category Operator",
)
search_tags = models.TextField(
verbose_name="Search Tags",
blank=True,
)
search_tags_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.CONTAINS,
verbose_name="Tags Operator",
)
search_entities = models.TextField(
verbose_name="Search Entities",
blank=True,
)
search_entities_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.CONTAINS,
verbose_name="Entities Operator",
)
search_internal_note = models.TextField(
verbose_name="Search Internal Note",
blank=True,
)
search_internal_note_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Internal Note Operator",
)
search_internal_id = models.TextField(
verbose_name="Search Internal ID",
blank=True,
)
search_internal_id_operator = models.CharField(
max_length=10,
choices=SearchOperator.choices,
default=SearchOperator.EXACT,
verbose_name="Internal ID Operator",
)
# Set fields
set_account = models.TextField(
verbose_name=_("Account"),
blank=True,
)
set_type = models.TextField(
verbose_name=_("Type"),
blank=True,
)
set_is_paid = models.TextField(
verbose_name=_("Paid"),
blank=True,
)
set_date = models.TextField(
verbose_name=_("Date"),
blank=True,
)
set_reference_date = models.TextField(
verbose_name=_("Reference Date"),
blank=True,
)
set_amount = models.TextField(
verbose_name=_("Amount"),
blank=True,
)
set_description = models.TextField(
verbose_name=_("Description"),
blank=True,
)
set_notes = models.TextField(
verbose_name=_("Notes"),
blank=True,
)
set_internal_note = models.TextField(
verbose_name=_("Internal Note"),
blank=True,
)
set_internal_id = models.TextField(
verbose_name=_("Internal ID"),
blank=True,
)
set_entities = models.TextField(
verbose_name=_("Entities"),
blank=True,
)
set_category = models.TextField(
verbose_name=_("Category"),
blank=True,
)
set_tags = models.TextField(
verbose_name=_("Tags"),
blank=True,
)
class Meta:
verbose_name = _("Update or create transaction action")
verbose_name_plural = _("Update or create transaction actions")
def __str__(self):
return f"Update or create transaction action for {self.rule}"
def build_search_query(self, simple):
"""Builds Q objects based on search fields and their operators"""
search_query = Q()
def add_to_query(field_name, value, operator):
if isinstance(value, (int, str)):
lookup = f"{field_name}__{operator}"
return Q(**{lookup: value})
return Q()
if self.search_account:
value = simple.eval(self.search_account)
if isinstance(value, int):
search_query &= add_to_query(
"account_id", value, self.search_account_operator
)
else:
search_query &= add_to_query(
"account__name", value, self.search_account_operator
)
if self.search_type:
value = simple.eval(self.search_type)
search_query &= add_to_query("type", value, self.search_type_operator)
if self.search_is_paid:
value = simple.eval(self.search_is_paid)
search_query &= add_to_query("is_paid", value, self.search_is_paid_operator)
if self.search_date:
value = simple.eval(self.search_date)
search_query &= add_to_query("date", value, self.search_date_operator)
if self.search_reference_date:
value = simple.eval(self.search_reference_date)
search_query &= add_to_query(
"reference_date", value, self.search_reference_date_operator
)
if self.search_amount:
value = simple.eval(self.search_amount)
search_query &= add_to_query("amount", value, self.search_amount_operator)
if self.search_description:
value = simple.eval(self.search_description)
search_query &= add_to_query(
"description", value, self.search_description_operator
)
if self.search_notes:
value = simple.eval(self.search_notes)
search_query &= add_to_query("notes", value, self.search_notes_operator)
if self.search_internal_note:
value = simple.eval(self.search_internal_note)
search_query &= add_to_query(
"internal_note", value, self.search_internal_note_operator
)
if self.search_internal_id:
value = simple.eval(self.search_internal_id)
search_query &= add_to_query(
"internal_id", value, self.search_internal_id_operator
)
if self.search_category:
value = simple.eval(self.search_category)
if isinstance(value, int):
search_query &= add_to_query(
"category_id", value, self.search_category_operator
)
else:
search_query &= add_to_query(
"category__name", value, self.search_category_operator
)
if self.search_tags:
tags_value = simple.eval(self.search_tags)
if isinstance(tags_value, (list, tuple)):
for tag in tags_value:
if isinstance(tag, int):
search_query &= Q(tags__id=tag)
else:
search_query &= Q(tags__name__iexact=tag)
elif isinstance(tags_value, (int, str)):
if isinstance(tags_value, int):
search_query &= Q(tags__id=tags_value)
else:
search_query &= Q(tags__name__iexact=tags_value)
if self.search_entities:
entities_value = simple.eval(self.search_entities)
if isinstance(entities_value, (list, tuple)):
for entity in entities_value:
if isinstance(entity, int):
search_query &= Q(entities__id=entity)
else:
search_query &= Q(entities__name__iexact=entity)
elif isinstance(entities_value, (int, str)):
if isinstance(entities_value, int):
search_query &= Q(entities__id=entities_value)
else:
search_query &= Q(entities__name__iexact=entities_value)
return search_query

View File

@@ -1,4 +1,6 @@
import decimal
import logging
from datetime import datetime, date
from cachalot.api import cachalot_disabled
from dateutil.relativedelta import relativedelta
@@ -6,7 +8,10 @@ from procrastinate.contrib.django import app
from simpleeval import EvalWithCompoundTypes
from apps.accounts.models import Account
from apps.rules.models import TransactionRule, TransactionRuleAction
from apps.rules.models import (
TransactionRule,
TransactionRuleAction,
)
from apps.transactions.models import (
Transaction,
TransactionCategory,
@@ -14,7 +19,6 @@ from apps.transactions.models import (
TransactionEntity,
)
logger = logging.getLogger(__name__)
@@ -25,137 +29,332 @@ def check_for_transaction_rules(
):
try:
with cachalot_disabled():
instance = Transaction.objects.get(id=instance_id)
context = {
"account_name": instance.account.name,
"account_id": instance.account.id,
"account_group_name": (
instance.account.group.name if instance.account.group else None
),
"account_group_id": (
instance.account.group.id if instance.account.group else None
),
"is_asset_account": instance.account.is_asset,
"is_archived_account": instance.account.is_archived,
"category_name": instance.category.name if instance.category else None,
"category_id": instance.category.id if instance.category else None,
"tag_names": [x.name for x in instance.tags.all()],
"tag_ids": [x.id for x in instance.tags.all()],
"entities_names": [x.name for x in instance.entities.all()],
"entities_ids": [x.id for x in instance.entities.all()],
"is_expense": instance.type == Transaction.Type.EXPENSE,
"is_income": instance.type == Transaction.Type.INCOME,
"is_paid": instance.is_paid,
"description": instance.description,
"amount": instance.amount,
"notes": instance.notes,
"date": instance.date,
"reference_date": instance.reference_date,
functions = {
"relativedelta": relativedelta,
"str": str,
"int": int,
"float": float,
"decimal": decimal.Decimal,
"datetime": datetime,
"date": date,
}
functions = {"relativedelta": relativedelta}
simple = EvalWithCompoundTypes(names=context, functions=functions)
simple = EvalWithCompoundTypes(
names=_get_names(instance), functions=functions
)
if signal == "transaction_created":
rules = TransactionRule.objects.filter(active=True, on_create=True)
rules = TransactionRule.objects.filter(
active=True, on_create=True
).order_by("id")
elif signal == "transaction_updated":
rules = TransactionRule.objects.filter(active=True, on_update=True)
rules = TransactionRule.objects.filter(
active=True, on_update=True
).order_by("id")
else:
rules = TransactionRule.objects.filter(active=True)
rules = TransactionRule.objects.filter(active=True).order_by("id")
for rule in rules:
if simple.eval(rule.trigger):
for action in rule.transaction_actions.all():
if action.field in [
TransactionRuleAction.Field.type,
TransactionRuleAction.Field.is_paid,
TransactionRuleAction.Field.date,
TransactionRuleAction.Field.reference_date,
TransactionRuleAction.Field.amount,
TransactionRuleAction.Field.description,
TransactionRuleAction.Field.notes,
]:
setattr(
instance,
action.field,
simple.eval(action.value),
try:
instance = _process_edit_transaction_action(
instance=instance, action=action, simple_eval=simple
)
except Exception as e:
logger.error(
f"Error processing edit transaction action {action.id}",
exc_info=True,
)
# else:
# simple.names.update(_get_names(instance))
# instance.save()
simple.names.update(_get_names(instance))
instance.save()
for action in rule.update_or_create_transaction_actions.all():
try:
_process_update_or_create_transaction_action(
action=action, simple_eval=simple
)
except Exception as e:
logger.error(
f"Error processing update or create transaction action {action.id}",
exc_info=True,
)
elif action.field == TransactionRuleAction.Field.account:
value = simple.eval(action.value)
if isinstance(value, int):
account = Account.objects.get(id=value)
instance.account = account
elif isinstance(value, str):
account = Account.objects.filter(name=value).first()
instance.account = account
elif action.field == TransactionRuleAction.Field.category:
value = simple.eval(action.value)
if isinstance(value, int):
category = TransactionCategory.objects.get(id=value)
instance.category = category
elif isinstance(value, str):
category = TransactionCategory.objects.get(name=value)
instance.category = category
elif action.field == TransactionRuleAction.Field.tags:
value = simple.eval(action.value)
if isinstance(value, list):
# Clear existing tags
instance.tags.clear()
for tag_value in value:
if isinstance(tag_value, int):
tag = TransactionTag.objects.get(id=tag_value)
instance.tags.add(tag)
elif isinstance(tag_value, str):
tag = TransactionTag.objects.get(name=tag_value)
instance.tags.add(tag)
elif isinstance(value, (int, str)):
# If a single value is provided, treat it as a single tag
instance.tags.clear()
if isinstance(value, int):
tag = TransactionTag.objects.get(id=value)
else:
tag = TransactionTag.objects.get(name=value)
instance.tags.add(tag)
elif action.field == TransactionRuleAction.Field.entities:
value = simple.eval(action.value)
if isinstance(value, list):
# Clear existing entities
instance.entities.clear()
for entity_value in value:
if isinstance(entity_value, int):
entity = TransactionEntity.objects.get(
id=entity_value
)
instance.entities.add(entity)
elif isinstance(entity_value, str):
entity = TransactionEntity.objects.get(
name=entity_value
)
instance.entities.add(entity)
elif isinstance(value, (int, str)):
# If a single value is provided, treat it as a single entity
instance.entities.clear()
if isinstance(value, int):
entity = TransactionEntity.objects.get(id=value)
else:
entity = TransactionEntity.objects.get(name=value)
instance.entities.add(entity)
instance.save()
except Exception as e:
logger.error(
"Error while executing 'check_for_transaction_rules' task",
exc_info=True,
)
raise e
def _get_names(instance):
return {
"id": instance.id,
"account_name": instance.account.name,
"account_id": instance.account.id,
"account_group_name": (
instance.account.group.name if instance.account.group else None
),
"account_group_id": (
instance.account.group.id if instance.account.group else None
),
"is_asset_account": instance.account.is_asset,
"is_archived_account": instance.account.is_archived,
"category_name": instance.category.name if instance.category else None,
"category_id": instance.category.id if instance.category else None,
"tag_names": [x.name for x in instance.tags.all()],
"tag_ids": [x.id for x in instance.tags.all()],
"entities_names": [x.name for x in instance.entities.all()],
"entities_ids": [x.id for x in instance.entities.all()],
"is_expense": instance.type == Transaction.Type.EXPENSE,
"is_income": instance.type == Transaction.Type.INCOME,
"is_paid": instance.is_paid,
"description": instance.description,
"amount": instance.amount,
"notes": instance.notes,
"date": instance.date,
"reference_date": instance.reference_date,
"internal_note": instance.internal_note,
"internal_id": instance.internal_id,
}
def _process_update_or_create_transaction_action(action, simple_eval):
"""Helper to process a single linked transaction action"""
# Build search query using the helper method
search_query = action.build_search_query(simple_eval)
# Find latest matching transaction or create new
if search_query:
transaction = (
Transaction.objects.filter(search_query).order_by("-date", "-id").first()
)
else:
transaction = None
if not transaction:
transaction = Transaction()
simple_eval.names.update(
{
"my_account_name": (transaction.account.name if transaction.id else None),
"my_account_id": transaction.account.id if transaction.id else None,
"my_account_group_name": (
transaction.account.group.name
if transaction.id and transaction.account.group
else None
),
"my_account_group_id": (
transaction.account.group.id
if transaction.id and transaction.account.group
else None
),
"my_is_asset_account": (
transaction.account.is_asset if transaction.id else None
),
"my_is_archived_account": (
transaction.account.is_archived if transaction.id else None
),
"my_category_name": (
transaction.category.name if transaction.category else None
),
"my_category_id": transaction.category.id if transaction.category else None,
"my_tag_names": (
[x.name for x in transaction.tags.all()] if transaction.id else []
),
"my_tag_ids": (
[x.id for x in transaction.tags.all()] if transaction.id else []
),
"my_entities_names": (
[x.name for x in transaction.entities.all()] if transaction.id else []
),
"my_entities_ids": (
[x.id for x in transaction.entities.all()] if transaction.id else []
),
"my_is_expense": transaction.type == Transaction.Type.EXPENSE,
"my_is_income": transaction.type == Transaction.Type.INCOME,
"my_is_paid": transaction.is_paid,
"my_description": transaction.description,
"my_amount": transaction.amount or 0,
"my_notes": transaction.notes,
"my_date": transaction.date,
"my_reference_date": transaction.reference_date,
"my_internal_note": transaction.internal_note,
"my_internal_id": transaction.reference_date,
}
)
if action.filter:
value = simple_eval.eval(action.filter)
if not value:
return # Short-circuit execution if filter evaluates to false
# Set fields if provided
if action.set_account:
value = simple_eval.eval(action.set_account)
if isinstance(value, int):
transaction.account = Account.objects.get(id=value)
else:
transaction.account = Account.objects.get(name=value)
if action.set_type:
transaction.type = simple_eval.eval(action.set_type)
if action.set_is_paid:
transaction.is_paid = simple_eval.eval(action.set_is_paid)
if action.set_date:
transaction.date = simple_eval.eval(action.set_date)
if action.set_reference_date:
transaction.reference_date = simple_eval.eval(action.set_reference_date)
if action.set_amount:
transaction.amount = simple_eval.eval(action.set_amount)
if action.set_description:
transaction.description = simple_eval.eval(action.set_description)
if action.set_internal_note:
transaction.internal_note = simple_eval.eval(action.set_internal_note)
if action.set_internal_id:
transaction.internal_id = simple_eval.eval(action.set_internal_id)
if action.set_notes:
transaction.notes = simple_eval.eval(action.set_notes)
if action.set_category:
value = simple_eval.eval(action.set_category)
if isinstance(value, int):
transaction.category = TransactionCategory.objects.get(id=value)
else:
transaction.category = TransactionCategory.objects.get(name=value)
transaction.save()
# Handle M2M fields after save
if action.set_tags:
tags_value = simple_eval.eval(action.set_tags)
transaction.tags.clear()
if isinstance(tags_value, (list, tuple)):
for tag in tags_value:
if isinstance(tag, int):
transaction.tags.add(TransactionTag.objects.get(id=tag))
else:
transaction.tags.add(TransactionTag.objects.get(name=tag))
elif isinstance(tags_value, (int, str)):
if isinstance(tags_value, int):
transaction.tags.add(TransactionTag.objects.get(id=tags_value))
else:
transaction.tags.add(TransactionTag.objects.get(name=tags_value))
if action.set_entities:
entities_value = simple_eval.eval(action.set_entities)
transaction.entities.clear()
if isinstance(entities_value, (list, tuple)):
for entity in entities_value:
if isinstance(entity, int):
transaction.entities.add(TransactionEntity.objects.get(id=entity))
else:
transaction.entities.add(TransactionEntity.objects.get(name=entity))
elif isinstance(entities_value, (int, str)):
if isinstance(entities_value, int):
transaction.entities.add(
TransactionEntity.objects.get(id=entities_value)
)
else:
transaction.entities.add(
TransactionEntity.objects.get(name=entities_value)
)
def _process_edit_transaction_action(instance, action, simple_eval) -> Transaction:
if action.field in [
TransactionRuleAction.Field.type,
TransactionRuleAction.Field.is_paid,
TransactionRuleAction.Field.date,
TransactionRuleAction.Field.reference_date,
TransactionRuleAction.Field.amount,
TransactionRuleAction.Field.description,
TransactionRuleAction.Field.notes,
]:
setattr(
instance,
action.field,
simple_eval.eval(action.value),
)
elif action.field == TransactionRuleAction.Field.account:
value = simple_eval.eval(action.value)
if isinstance(value, int):
account = Account.objects.get(id=value)
instance.account = account
elif isinstance(value, str):
account = Account.objects.filter(name=value).first()
instance.account = account
elif action.field == TransactionRuleAction.Field.category:
value = simple_eval.eval(action.value)
if isinstance(value, int):
category = TransactionCategory.objects.get(id=value)
instance.category = category
elif isinstance(value, str):
category = TransactionCategory.objects.get(name=value)
instance.category = category
elif action.field == TransactionRuleAction.Field.tags:
value = simple_eval.eval(action.value)
if isinstance(value, list):
# Clear existing tags
instance.tags.clear()
for tag_value in value:
if isinstance(tag_value, int):
tag = TransactionTag.objects.get(id=tag_value)
instance.tags.add(tag)
elif isinstance(tag_value, str):
tag = TransactionTag.objects.get(name=tag_value)
instance.tags.add(tag)
elif isinstance(value, (int, str)):
# If a single value is provided, treat it as a single tag
instance.tags.clear()
if isinstance(value, int):
tag = TransactionTag.objects.get(id=value)
else:
tag = TransactionTag.objects.get(name=value)
instance.tags.add(tag)
elif action.field == TransactionRuleAction.Field.entities:
value = simple_eval.eval(action.value)
if isinstance(value, list):
# Clear existing entities
instance.entities.clear()
for entity_value in value:
if isinstance(entity_value, int):
entity = TransactionEntity.objects.get(id=entity_value)
instance.entities.add(entity)
elif isinstance(entity_value, str):
entity = TransactionEntity.objects.get(name=entity_value)
instance.entities.add(entity)
elif isinstance(value, (int, str)):
# If a single value is provided, treat it as a single entity
instance.entities.clear()
if isinstance(value, int):
entity = TransactionEntity.objects.get(id=value)
else:
entity = TransactionEntity.objects.get(name=value)
instance.entities.add(entity)
return instance

View File

@@ -38,18 +38,33 @@ urlpatterns = [
name="transaction_rule_delete",
),
path(
"rules/transaction/<int:transaction_rule_id>/action/add/",
"rules/transaction/<int:transaction_rule_id>/transaction-action/add/",
views.transaction_rule_action_add,
name="transaction_rule_action_add",
),
path(
"rules/transaction/action/<int:transaction_rule_action_id>/edit/",
"rules/transaction/transaction-action/<int:transaction_rule_action_id>/edit/",
views.transaction_rule_action_edit,
name="transaction_rule_action_edit",
),
path(
"rules/transaction/action/<int:transaction_rule_action_id>/delete/",
"rules/transaction/transaction-action/<int:transaction_rule_action_id>/delete/",
views.transaction_rule_action_delete,
name="transaction_rule_action_delete",
),
path(
"rules/transaction/<int:transaction_rule_id>/update-or-create-transaction-action/add/",
views.update_or_create_transaction_rule_action_add,
name="update_or_create_transaction_rule_action_add",
),
path(
"rules/transaction/update-or-create-transaction-action/<int:pk>/edit/",
views.update_or_create_transaction_rule_action_edit,
name="update_or_create_transaction_rule_action_edit",
),
path(
"rules/transaction/update-or-create-transaction-action/<int:pk>/delete/",
views.update_or_create_transaction_rule_action_delete,
name="update_or_create_transaction_rule_action_delete",
),
]

View File

@@ -6,8 +6,16 @@ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.rules.forms import TransactionRuleForm, TransactionRuleActionForm
from apps.rules.models import TransactionRule, TransactionRuleAction
from apps.rules.forms import (
TransactionRuleForm,
TransactionRuleActionForm,
UpdateOrCreateTransactionRuleActionForm,
)
from apps.rules.models import (
TransactionRule,
TransactionRuleAction,
UpdateOrCreateTransactionRuleAction,
)
@login_required
@@ -60,10 +68,15 @@ def transaction_rule_add(request, **kwargs):
if request.method == "POST":
form = TransactionRuleForm(request.POST)
if form.is_valid():
instance = form.save()
form.save()
messages.success(request, _("Rule added successfully"))
return redirect("transaction_rule_action_add", instance.id)
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
else:
form = TransactionRuleForm()
@@ -215,3 +228,88 @@ def transaction_rule_action_delete(request, transaction_rule_action_id):
"HX-Trigger": "updated, hide_offcanvas",
},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
if request.method == "POST":
form = UpdateOrCreateTransactionRuleActionForm(
request.POST, rule=transaction_rule
)
if form.is_valid():
form.save()
messages.success(
request, _("Update or Create Transaction action added successfully")
)
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
else:
form = UpdateOrCreateTransactionRuleActionForm(rule=transaction_rule)
return render(
request,
"rules/fragments/transaction_rule/update_or_create_transaction_rule_action/add.html",
{"form": form, "transaction_rule_id": transaction_rule_id},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def update_or_create_transaction_rule_action_edit(request, pk):
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
transaction_rule = linked_action.rule
if request.method == "POST":
form = UpdateOrCreateTransactionRuleActionForm(
request.POST, instance=linked_action, rule=transaction_rule
)
if form.is_valid():
form.save()
messages.success(
request, _("Update or Create Transaction action updated successfully")
)
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)
else:
form = UpdateOrCreateTransactionRuleActionForm(
instance=linked_action, rule=transaction_rule
)
return render(
request,
"rules/fragments/transaction_rule/update_or_create_transaction_rule_action/edit.html",
{"form": form, "action": linked_action},
)
@only_htmx
@login_required
@require_http_methods(["DELETE"])
def update_or_create_transaction_rule_action_delete(request, pk):
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
linked_action.delete()
messages.success(
request, _("Update or Create Transaction action deleted successfully")
)
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated, hide_offcanvas",
},
)

View File

@@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _, ngettext_lazy
from apps.common.decorators.htmx import only_htmx
from apps.transactions.models import Transaction
from apps.rules.signals import transaction_updated
@only_htmx
@@ -17,6 +18,9 @@ def bulk_pay_transactions(request):
count = transactions.count()
transactions.update(is_paid=True)
for transaction in transactions:
transaction_updated.send(sender=transaction)
messages.success(
request,
ngettext_lazy(
@@ -41,6 +45,9 @@ def bulk_unpay_transactions(request):
count = transactions.count()
transactions.update(is_paid=False)
for transaction in transactions:
transaction_updated.send(sender=transaction)
messages.success(
request,
ngettext_lazy(

View File

@@ -316,6 +316,7 @@ def transaction_pay(request, transaction_id):
new_is_paid = False if transaction.is_paid else True
transaction.is_paid = new_is_paid
transaction.save()
transaction_updated.send(sender=transaction)
response = render(
request,

View File

@@ -0,0 +1,46 @@
settings:
file_type: xls
skip_errors: true
trigger_transaction_rules: true
importing: transactions
start_row: 1
sheets: "*"
mapping:
account:
target: account
default: "<TU NOMBRE DE CUENTA>"
type: name
type:
source: Importe
target: type
detection_method: sign
internal_id:
target: internal_id
transformations:
- type: hash
fields: ["Fecha", "Concepto", "Importe", "Saldo"]
date:
source: "Fecha"
target: date
format: "%d-%m-%Y"
description:
source: Concepto
target: description
amount:
source: Importe
target: amount
is_paid:
target: is_paid
detection_method: always_paid
deduplication:
- type: compare
fields:
- internal_id
match_type: strict

View File

@@ -0,0 +1,7 @@
{
"author": "eitchtee,Pablo Hinojosa",
"description": "Importe sus movimientos desde su cuenta de Cajamar",
"schema_version": 1,
"name": "Grupo Cooperativo Cajamar",
"message": "Cambia '<TU NOMBRE DE CUENTA>' por el nombre de tu cuenta de Cajamar dentro de WYGIWYH"
}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-07 11:45-0300\n"
"POT-Creation-Date: 2025-02-09 17:27-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -26,10 +26,10 @@ msgstr ""
#: apps/currencies/forms.py:53 apps/currencies/forms.py:91
#: apps/currencies/forms.py:142 apps/dca/forms.py:41 apps/dca/forms.py:93
#: apps/import_app/forms.py:34 apps/rules/forms.py:45 apps/rules/forms.py:87
#: apps/transactions/forms.py:190 apps/transactions/forms.py:257
#: apps/transactions/forms.py:581 apps/transactions/forms.py:624
#: apps/transactions/forms.py:656 apps/transactions/forms.py:691
#: apps/transactions/forms.py:827
#: apps/rules/forms.py:359 apps/transactions/forms.py:190
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
#: apps/transactions/forms.py:624 apps/transactions/forms.py:656
#: apps/transactions/forms.py:691 apps/transactions/forms.py:827
msgid "Update"
msgstr ""
@@ -37,10 +37,11 @@ msgstr ""
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:61
#: apps/currencies/forms.py:99 apps/currencies/forms.py:150
#: apps/dca/forms.py:49 apps/dca/forms.py:102 apps/import_app/forms.py:42
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/transactions/forms.py:174
#: apps/transactions/forms.py:199 apps/transactions/forms.py:589
#: apps/transactions/forms.py:632 apps/transactions/forms.py:664
#: apps/transactions/forms.py:699 apps/transactions/forms.py:835
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/rules/forms.py:367
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
#: apps/transactions/forms.py:664 apps/transactions/forms.py:699
#: apps/transactions/forms.py:835
#: templates/account_groups/fragments/list.html:9
#: templates/accounts/fragments/list.html:9
#: templates/categories/fragments/list.html:9
@@ -67,7 +68,8 @@ msgstr ""
msgid "New balance"
msgstr ""
#: apps/accounts/forms.py:119 apps/rules/models.py:27
#: apps/accounts/forms.py:119 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
@@ -75,7 +77,8 @@ msgstr ""
msgid "Category"
msgstr ""
#: apps/accounts/forms.py:126 apps/rules/models.py:28
#: apps/accounts/forms.py:126 apps/rules/forms.py:171 apps/rules/forms.py:180
#: apps/rules/models.py:33 apps/rules/models.py:284
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
@@ -86,7 +89,7 @@ msgid "Tags"
msgstr ""
#: apps/accounts/models.py:9 apps/accounts/models.py:21 apps/dca/models.py:14
#: apps/import_app/models.py:14 apps/rules/models.py:9
#: apps/import_app/models.py:14 apps/rules/models.py:10
#: apps/transactions/models.py:67 apps/transactions/models.py:87
#: apps/transactions/models.py:106
#: templates/account_groups/fragments/list.html:25
@@ -147,7 +150,8 @@ msgstr ""
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
#: apps/accounts/models.py:59 apps/rules/models.py:19
#: apps/accounts/models.py:59 apps/rules/forms.py:160 apps/rules/forms.py:173
#: apps/rules/models.py:24 apps/rules/models.py:236
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
#: apps/transactions/models.py:288 apps/transactions/models.py:490
@@ -356,7 +360,8 @@ msgstr ""
msgid "Suffix"
msgstr ""
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/models.py:22
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/forms.py:163
#: apps/rules/forms.py:176 apps/rules/models.py:27 apps/rules/models.py:248
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
#: apps/transactions/models.py:142
#: templates/dca/fragments/strategy/details.html:52
@@ -562,7 +567,8 @@ msgstr ""
msgid "Payment Currency"
msgstr ""
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/models.py:26
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/forms.py:167
#: apps/rules/forms.py:182 apps/rules/models.py:31 apps/rules/models.py:264
#: apps/transactions/forms.py:333 apps/transactions/models.py:155
#: apps/transactions/models.py:337 apps/transactions/models.py:518
msgid "Notes"
@@ -726,42 +732,62 @@ msgstr ""
msgid "A value for this field already exists in the rule."
msgstr ""
#: apps/rules/models.py:10 apps/rules/models.py:25
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
msgid "Description"
#: apps/rules/forms.py:147 apps/rules/forms.py:148 apps/rules/forms.py:149
#: apps/rules/forms.py:150 apps/rules/forms.py:151 apps/rules/forms.py:152
#: apps/rules/forms.py:153 apps/rules/forms.py:154 apps/rules/forms.py:155
#: apps/rules/forms.py:156 apps/rules/forms.py:157 apps/rules/forms.py:158
#: apps/rules/forms.py:159
msgid "Operator"
msgstr ""
#: apps/rules/models.py:11
msgid "Trigger"
msgstr ""
#: apps/rules/models.py:20 apps/transactions/models.py:139
#: apps/rules/forms.py:161 apps/rules/forms.py:174 apps/rules/models.py:25
#: apps/rules/models.py:240 apps/transactions/models.py:139
#: apps/transactions/models.py:293 apps/transactions/models.py:496
msgid "Type"
msgstr ""
#: apps/rules/models.py:21 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:30
#: apps/rules/forms.py:162 apps/rules/forms.py:175 apps/rules/models.py:26
#: apps/rules/models.py:244 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:12
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:16
msgid "Paid"
msgstr ""
#: apps/rules/models.py:23 apps/transactions/forms.py:66
#: apps/rules/forms.py:164 apps/rules/forms.py:177 apps/rules/models.py:28
#: apps/rules/models.py:252 apps/transactions/forms.py:66
#: apps/transactions/forms.py:322 apps/transactions/forms.py:492
#: apps/transactions/models.py:143 apps/transactions/models.py:311
#: apps/transactions/models.py:520
msgid "Reference Date"
msgstr ""
#: apps/rules/models.py:24 apps/transactions/models.py:148
#: apps/rules/forms.py:165 apps/rules/forms.py:178 apps/rules/models.py:29
#: apps/rules/models.py:256 apps/transactions/models.py:148
#: apps/transactions/models.py:501
msgid "Amount"
msgstr ""
#: apps/rules/models.py:29 apps/transactions/filters.py:81
#: apps/rules/forms.py:166 apps/rules/forms.py:179 apps/rules/models.py:11
#: apps/rules/models.py:30 apps/rules/models.py:260
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
msgid "Description"
msgstr ""
#: apps/rules/forms.py:169 apps/rules/forms.py:184 apps/rules/models.py:268
#: apps/transactions/models.py:192
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:170 apps/rules/forms.py:185 apps/rules/models.py:272
#: apps/transactions/models.py:194
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:172 apps/rules/forms.py:181 apps/rules/models.py:34
#: apps/rules/models.py:276 apps/transactions/filters.py:81
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
#: apps/transactions/models.py:170 apps/transactions/models.py:333
@@ -770,48 +796,142 @@ msgstr ""
msgid "Entities"
msgstr ""
#: apps/rules/models.py:35
#: apps/rules/forms.py:199
msgid "Search Criteria"
msgstr ""
#: apps/rules/forms.py:334
msgid "Set Values"
msgstr ""
#: apps/rules/models.py:12
msgid "Trigger"
msgstr ""
#: apps/rules/models.py:15
msgid "Transaction rule"
msgstr ""
#: apps/rules/models.py:16
msgid "Transaction rules"
msgstr ""
#: apps/rules/models.py:40 apps/rules/models.py:78
msgid "Rule"
msgstr ""
#: apps/rules/models.py:40
#: apps/rules/models.py:45
msgid "Field"
msgstr ""
#: apps/rules/models.py:42
#: apps/rules/models.py:47
msgid "Value"
msgstr ""
#: apps/rules/views.py:44
#: apps/rules/models.py:53
msgid "Edit transaction action"
msgstr ""
#: apps/rules/models.py:54
msgid "Edit transaction actions"
msgstr ""
#: apps/rules/models.py:64
msgid "is exactly"
msgstr ""
#: apps/rules/models.py:65
msgid "contains"
msgstr ""
#: apps/rules/models.py:66
msgid "starts with"
msgstr ""
#: apps/rules/models.py:67
msgid "ends with"
msgstr ""
#: apps/rules/models.py:68
msgid "equals"
msgstr ""
#: apps/rules/models.py:69
msgid "greater than"
msgstr ""
#: apps/rules/models.py:70
msgid "less than"
msgstr ""
#: apps/rules/models.py:71
msgid "greater than or equal"
msgstr ""
#: apps/rules/models.py:72
msgid "less than or equal"
msgstr ""
#: apps/rules/models.py:82 templates/transactions/pages/transactions.html:15
msgid "Filter"
msgstr ""
#: apps/rules/models.py:85
msgid ""
"Generic expression to enable or disable execution. Should evaluate to True "
"or False"
msgstr ""
#: apps/rules/models.py:289
msgid "Update or create transaction action"
msgstr ""
#: apps/rules/models.py:290
msgid "Update or create transaction actions"
msgstr ""
#: apps/rules/views.py:52
msgid "Rule deactivated successfully"
msgstr ""
#: apps/rules/views.py:46
#: apps/rules/views.py:54
msgid "Rule activated successfully"
msgstr ""
#: apps/rules/views.py:64
#: apps/rules/views.py:72
msgid "Rule added successfully"
msgstr ""
#: apps/rules/views.py:87
#: apps/rules/views.py:100
msgid "Rule updated successfully"
msgstr ""
#: apps/rules/views.py:126
#: apps/rules/views.py:139
msgid "Rule deleted successfully"
msgstr ""
#: apps/rules/views.py:180
#: apps/rules/views.py:193
msgid "Action updated successfully"
msgstr ""
#: apps/rules/views.py:210
#: apps/rules/views.py:223
msgid "Action deleted successfully"
msgstr ""
#: apps/transactions/filters.py:24 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:30 templates/includes/navbar.html:46
#: apps/rules/views.py:246
msgid "Update or Create Transaction action added successfully"
msgstr ""
#: apps/rules/views.py:278
msgid "Update or Create Transaction action updated successfully"
msgstr ""
#: apps/rules/views.py:307
msgid "Update or Create Transaction action deleted successfully"
msgstr ""
#: apps/transactions/filters.py:24 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31 templates/includes/navbar.html:46
#: templates/transactions/widgets/paid_toggle_button.html:8
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:12
msgid "Projected"
@@ -963,14 +1083,6 @@ msgstr ""
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:192
msgid "Internal Note"
msgstr ""
#: apps/transactions/models.py:194
msgid "Internal ID"
msgstr ""
#: apps/transactions/models.py:198
msgid "Deleted"
msgstr ""
@@ -1384,7 +1496,7 @@ msgstr ""
#: templates/account_groups/fragments/list.html:36
#: templates/accounts/fragments/list.html:41
#: templates/categories/fragments/table.html:29
#: templates/cotton/transaction/item.html:126
#: templates/cotton/transaction/item.html:127
#: templates/cotton/ui/transactions_action_bar.html:49
#: templates/currencies/fragments/list.html:37
#: templates/dca/fragments/strategy/details.html:67
@@ -1396,8 +1508,9 @@ msgstr ""
#: templates/import_app/fragments/profiles/list.html:48
#: templates/installment_plans/fragments/table.html:27
#: templates/recurring_transactions/fragments/table.html:29
#: templates/rules/fragments/transaction_rule/view.html:22
#: templates/rules/fragments/transaction_rule/view.html:48
#: templates/rules/fragments/transaction_rule/view.html:23
#: templates/rules/fragments/transaction_rule/view.html:47
#: templates/rules/fragments/transaction_rule/view.html:80
#: templates/tags/fragments/table.html:28
msgid "Edit"
msgstr ""
@@ -1405,8 +1518,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:43
#: templates/accounts/fragments/list.html:48
#: templates/categories/fragments/table.html:36
#: templates/cotton/transaction/item.html:141
#: templates/cotton/transaction/item.html:160
#: templates/cotton/transaction/item.html:142
#: templates/cotton/transaction/item.html:161
#: templates/cotton/ui/deleted_transactions_action_bar.html:55
#: templates/cotton/ui/transactions_action_bar.html:86
#: templates/currencies/fragments/list.html:44
@@ -1422,7 +1535,8 @@ msgstr ""
#: templates/mini_tools/unit_price_calculator.html:18
#: templates/recurring_transactions/fragments/table.html:91
#: templates/rules/fragments/list.html:44
#: templates/rules/fragments/transaction_rule/view.html:56
#: templates/rules/fragments/transaction_rule/view.html:55
#: templates/rules/fragments/transaction_rule/view.html:88
#: templates/tags/fragments/table.html:36
msgid "Delete"
msgstr ""
@@ -1430,8 +1544,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:47
#: templates/accounts/fragments/list.html:52
#: templates/categories/fragments/table.html:41
#: templates/cotton/transaction/item.html:145
#: templates/cotton/transaction/item.html:164
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:88
#: templates/currencies/fragments/list.html:48
@@ -1450,7 +1564,8 @@ msgstr ""
#: templates/recurring_transactions/fragments/table.html:82
#: templates/recurring_transactions/fragments/table.html:96
#: templates/rules/fragments/list.html:48
#: templates/rules/fragments/transaction_rule/view.html:60
#: templates/rules/fragments/transaction_rule/view.html:59
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/tags/fragments/table.html:40
msgid "Are you sure?"
msgstr ""
@@ -1458,8 +1573,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:48
#: templates/accounts/fragments/list.html:53
#: templates/categories/fragments/table.html:42
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:89
#: templates/currencies/fragments/list.html:49
@@ -1471,7 +1586,8 @@ msgstr ""
#: templates/exchange_rates_services/fragments/table.html:37
#: templates/import_app/fragments/profiles/list.html:74
#: templates/rules/fragments/list.html:49
#: templates/rules/fragments/transaction_rule/view.html:61
#: templates/rules/fragments/transaction_rule/view.html:60
#: templates/rules/fragments/transaction_rule/view.html:93
#: templates/tags/fragments/table.html:41
msgid "You won't be able to revert this!"
msgstr ""
@@ -1479,8 +1595,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:49
#: templates/accounts/fragments/list.html:54
#: templates/categories/fragments/table.html:43
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:148
#: templates/cotton/transaction/item.html:167
#: templates/currencies/fragments/list.html:50
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/list.html:48
@@ -1493,7 +1609,8 @@ msgstr ""
#: templates/installment_plans/fragments/table.html:62
#: templates/recurring_transactions/fragments/table.html:98
#: templates/rules/fragments/list.html:50
#: templates/rules/fragments/transaction_rule/view.html:62
#: templates/rules/fragments/transaction_rule/view.html:61
#: templates/rules/fragments/transaction_rule/view.html:94
#: templates/tags/fragments/table.html:42
msgid "Yes, delete it!"
msgstr ""
@@ -1604,16 +1721,16 @@ msgstr ""
msgid "Search"
msgstr ""
#: templates/cotton/transaction/item.html:7
#: templates/cotton/transaction/item.html:8
msgid "Select"
msgstr ""
#: templates/cotton/transaction/item.html:133
#: templates/cotton/transaction/item.html:134
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr ""
#: templates/cotton/transaction/item.html:154
#: templates/cotton/transaction/item.html:155
#: templates/cotton/ui/deleted_transactions_action_bar.html:47
msgid "Restore"
msgstr ""
@@ -1623,33 +1740,33 @@ msgstr ""
msgid "projected income"
msgstr ""
#: templates/cotton/ui/account_card.html:37
#: templates/cotton/ui/currency_card.html:32
#: templates/cotton/ui/account_card.html:41
#: templates/cotton/ui/currency_card.html:36
msgid "projected expenses"
msgstr ""
#: templates/cotton/ui/account_card.html:61
#: templates/cotton/ui/currency_card.html:56
#: templates/cotton/ui/account_card.html:69
#: templates/cotton/ui/currency_card.html:64
msgid "projected total"
msgstr ""
#: templates/cotton/ui/account_card.html:86
#: templates/cotton/ui/currency_card.html:81
#: templates/cotton/ui/account_card.html:94
#: templates/cotton/ui/currency_card.html:88
msgid "current income"
msgstr ""
#: templates/cotton/ui/account_card.html:108
#: templates/cotton/ui/currency_card.html:103
#: templates/cotton/ui/account_card.html:120
#: templates/cotton/ui/currency_card.html:114
msgid "current expenses"
msgstr ""
#: templates/cotton/ui/account_card.html:130
#: templates/cotton/ui/currency_card.html:125
#: templates/cotton/ui/account_card.html:146
#: templates/cotton/ui/currency_card.html:140
msgid "current total"
msgstr ""
#: templates/cotton/ui/account_card.html:156
#: templates/cotton/ui/currency_card.html:151
#: templates/cotton/ui/account_card.html:171
#: templates/cotton/ui/currency_card.html:165
msgid "final total"
msgstr ""
@@ -1810,7 +1927,7 @@ msgid "No entries for this DCA"
msgstr ""
#: templates/dca/fragments/strategy/details.html:125
#: templates/monthly_overview/fragments/list.html:41
#: templates/monthly_overview/fragments/list.html:47
#: templates/transactions/fragments/list_all.html:40
msgid "Try adding one"
msgstr ""
@@ -2220,7 +2337,7 @@ msgstr ""
msgid "Item"
msgstr ""
#: templates/monthly_overview/fragments/list.html:40
#: templates/monthly_overview/fragments/list.html:46
msgid "No transactions this month"
msgstr ""
@@ -2391,10 +2508,12 @@ msgid "Edit transaction rule"
msgstr ""
#: templates/rules/fragments/transaction_rule/transaction_rule_action/add.html:5
#: templates/rules/fragments/transaction_rule/update_or_create_transaction_rule_action/add.html:5
msgid "Add action to transaction rule"
msgstr ""
#: templates/rules/fragments/transaction_rule/transaction_rule_action/edit.html:5
#: templates/rules/fragments/transaction_rule/update_or_create_transaction_rule_action/edit.html:5
msgid "Edit transaction rule action"
msgstr ""
@@ -2402,15 +2521,21 @@ msgstr ""
msgid "Transaction Rule"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:13
#: templates/rules/fragments/transaction_rule/view.html:14
msgid "If transaction..."
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:31
#: templates/rules/fragments/transaction_rule/view.html:32
msgid "Then..."
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:36
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:39
msgid "Set"
msgstr ""
@@ -2419,13 +2544,29 @@ msgid "to"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:71
msgid "Update or create transaction"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:74
msgid "Edit to view"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:104
msgid "This rule has no actions"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:80
#: templates/rules/fragments/transaction_rule/view.html:112
msgid "Add new"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:117
msgid "Edit Transaction"
msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:120
msgid "Update or Create Transaction"
msgstr ""
#: templates/tags/fragments/add.html:5
msgid "Add tag"
msgstr ""
@@ -2459,11 +2600,6 @@ msgstr ""
msgid "transactions"
msgstr ""
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
msgstr ""
#: templates/transactions/fragments/list_all.html:39
msgid "No transactions found"
msgstr ""
@@ -2476,10 +2612,6 @@ msgstr ""
msgid "No deleted transactions to show"
msgstr ""
#: templates/transactions/pages/transactions.html:15
msgid "Filter"
msgstr ""
#: templates/transactions/pages/trash.html:4
#: templates/transactions/pages/trash.html:9
msgid "Deleted transactions"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-07 11:45-0300\n"
"POT-Creation-Date: 2025-02-09 17:27-0300\n"
"PO-Revision-Date: 2025-01-29 06:12+0100\n"
"Last-Translator: Dimitri Decrock <dimitri@fam-decrock.eu>\n"
"Language-Team: \n"
@@ -27,10 +27,10 @@ msgstr "Groepsnaam"
#: apps/currencies/forms.py:53 apps/currencies/forms.py:91
#: apps/currencies/forms.py:142 apps/dca/forms.py:41 apps/dca/forms.py:93
#: apps/import_app/forms.py:34 apps/rules/forms.py:45 apps/rules/forms.py:87
#: apps/transactions/forms.py:190 apps/transactions/forms.py:257
#: apps/transactions/forms.py:581 apps/transactions/forms.py:624
#: apps/transactions/forms.py:656 apps/transactions/forms.py:691
#: apps/transactions/forms.py:827
#: apps/rules/forms.py:359 apps/transactions/forms.py:190
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
#: apps/transactions/forms.py:624 apps/transactions/forms.py:656
#: apps/transactions/forms.py:691 apps/transactions/forms.py:827
msgid "Update"
msgstr "Bijwerken"
@@ -38,10 +38,11 @@ msgstr "Bijwerken"
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:61
#: apps/currencies/forms.py:99 apps/currencies/forms.py:150
#: apps/dca/forms.py:49 apps/dca/forms.py:102 apps/import_app/forms.py:42
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/transactions/forms.py:174
#: apps/transactions/forms.py:199 apps/transactions/forms.py:589
#: apps/transactions/forms.py:632 apps/transactions/forms.py:664
#: apps/transactions/forms.py:699 apps/transactions/forms.py:835
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/rules/forms.py:367
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
#: apps/transactions/forms.py:664 apps/transactions/forms.py:699
#: apps/transactions/forms.py:835
#: templates/account_groups/fragments/list.html:9
#: templates/accounts/fragments/list.html:9
#: templates/categories/fragments/list.html:9
@@ -68,7 +69,8 @@ msgstr "Groep"
msgid "New balance"
msgstr "Nieuw saldo"
#: apps/accounts/forms.py:119 apps/rules/models.py:27
#: apps/accounts/forms.py:119 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
@@ -76,7 +78,8 @@ msgstr "Nieuw saldo"
msgid "Category"
msgstr "Categorie"
#: apps/accounts/forms.py:126 apps/rules/models.py:28
#: apps/accounts/forms.py:126 apps/rules/forms.py:171 apps/rules/forms.py:180
#: apps/rules/models.py:33 apps/rules/models.py:284
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
@@ -87,7 +90,7 @@ msgid "Tags"
msgstr "Labels"
#: apps/accounts/models.py:9 apps/accounts/models.py:21 apps/dca/models.py:14
#: apps/import_app/models.py:14 apps/rules/models.py:9
#: apps/import_app/models.py:14 apps/rules/models.py:10
#: apps/transactions/models.py:67 apps/transactions/models.py:87
#: apps/transactions/models.py:106
#: templates/account_groups/fragments/list.html:25
@@ -152,7 +155,8 @@ msgstr ""
"Gearchiveerde rekeningen worden niet weergegeven en tellen niet mee voor je "
"\"Netto Waarde\"."
#: apps/accounts/models.py:59 apps/rules/models.py:19
#: apps/accounts/models.py:59 apps/rules/forms.py:160 apps/rules/forms.py:173
#: apps/rules/models.py:24 apps/rules/models.py:236
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
#: apps/transactions/models.py:288 apps/transactions/models.py:490
@@ -362,7 +366,8 @@ msgstr "Voorvoegsel"
msgid "Suffix"
msgstr "Achtervoegsel"
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/models.py:22
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/forms.py:163
#: apps/rules/forms.py:176 apps/rules/models.py:27 apps/rules/models.py:248
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
#: apps/transactions/models.py:142
#: templates/dca/fragments/strategy/details.html:52
@@ -592,7 +597,8 @@ msgstr "Doel Munteenheid"
msgid "Payment Currency"
msgstr "Betaal Munteenheid"
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/models.py:26
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/forms.py:167
#: apps/rules/forms.py:182 apps/rules/models.py:31 apps/rules/models.py:264
#: apps/transactions/forms.py:333 apps/transactions/models.py:155
#: apps/transactions/models.py:337 apps/transactions/models.py:518
msgid "Notes"
@@ -756,42 +762,62 @@ msgstr "Naar"
msgid "A value for this field already exists in the rule."
msgstr "Een waarde voor dit veld bestaat al in de regel."
#: apps/rules/models.py:10 apps/rules/models.py:25
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
msgid "Description"
msgstr "Beschrijving"
#: apps/rules/forms.py:147 apps/rules/forms.py:148 apps/rules/forms.py:149
#: apps/rules/forms.py:150 apps/rules/forms.py:151 apps/rules/forms.py:152
#: apps/rules/forms.py:153 apps/rules/forms.py:154 apps/rules/forms.py:155
#: apps/rules/forms.py:156 apps/rules/forms.py:157 apps/rules/forms.py:158
#: apps/rules/forms.py:159
msgid "Operator"
msgstr ""
#: apps/rules/models.py:11
msgid "Trigger"
msgstr "Trigger"
#: apps/rules/models.py:20 apps/transactions/models.py:139
#: apps/rules/forms.py:161 apps/rules/forms.py:174 apps/rules/models.py:25
#: apps/rules/models.py:240 apps/transactions/models.py:139
#: apps/transactions/models.py:293 apps/transactions/models.py:496
msgid "Type"
msgstr "Soort"
#: apps/rules/models.py:21 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:30
#: apps/rules/forms.py:162 apps/rules/forms.py:175 apps/rules/models.py:26
#: apps/rules/models.py:244 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:12
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:16
msgid "Paid"
msgstr "Betaald"
#: apps/rules/models.py:23 apps/transactions/forms.py:66
#: apps/rules/forms.py:164 apps/rules/forms.py:177 apps/rules/models.py:28
#: apps/rules/models.py:252 apps/transactions/forms.py:66
#: apps/transactions/forms.py:322 apps/transactions/forms.py:492
#: apps/transactions/models.py:143 apps/transactions/models.py:311
#: apps/transactions/models.py:520
msgid "Reference Date"
msgstr "Referentiedatum"
#: apps/rules/models.py:24 apps/transactions/models.py:148
#: apps/rules/forms.py:165 apps/rules/forms.py:178 apps/rules/models.py:29
#: apps/rules/models.py:256 apps/transactions/models.py:148
#: apps/transactions/models.py:501
msgid "Amount"
msgstr "Bedrag"
#: apps/rules/models.py:29 apps/transactions/filters.py:81
#: apps/rules/forms.py:166 apps/rules/forms.py:179 apps/rules/models.py:11
#: apps/rules/models.py:30 apps/rules/models.py:260
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
msgid "Description"
msgstr "Beschrijving"
#: apps/rules/forms.py:169 apps/rules/forms.py:184 apps/rules/models.py:268
#: apps/transactions/models.py:192
msgid "Internal Note"
msgstr "Interne opmerking"
#: apps/rules/forms.py:170 apps/rules/forms.py:185 apps/rules/models.py:272
#: apps/transactions/models.py:194
msgid "Internal ID"
msgstr "Interne ID"
#: apps/rules/forms.py:172 apps/rules/forms.py:181 apps/rules/models.py:34
#: apps/rules/models.py:276 apps/transactions/filters.py:81
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
#: apps/transactions/models.py:170 apps/transactions/models.py:333
@@ -800,48 +826,162 @@ msgstr "Bedrag"
msgid "Entities"
msgstr "Bedrijven"
#: apps/rules/models.py:35
#: apps/rules/forms.py:199
msgid "Search Criteria"
msgstr ""
#: apps/rules/forms.py:334
#, fuzzy
#| msgid "Current Value"
msgid "Set Values"
msgstr "Actuele waarde"
#: apps/rules/models.py:12
msgid "Trigger"
msgstr "Trigger"
#: apps/rules/models.py:15
#, fuzzy
#| msgid "Transaction Rule"
msgid "Transaction rule"
msgstr "Verrichtingsregel"
#: apps/rules/models.py:16
#, fuzzy
#| msgid "Transaction Rule"
msgid "Transaction rules"
msgstr "Verrichtingsregel"
#: apps/rules/models.py:40 apps/rules/models.py:78
msgid "Rule"
msgstr "Regel"
#: apps/rules/models.py:40
#: apps/rules/models.py:45
msgid "Field"
msgstr "Veld"
#: apps/rules/models.py:42
#: apps/rules/models.py:47
msgid "Value"
msgstr "Waarde"
#: apps/rules/views.py:44
#: apps/rules/models.py:53
#, fuzzy
#| msgid "Edit transaction rule action"
msgid "Edit transaction action"
msgstr "Bewerk verrichtingsregel actie"
#: apps/rules/models.py:54
#, fuzzy
#| msgid "Edit transaction rule action"
msgid "Edit transaction actions"
msgstr "Bewerk verrichtingsregel actie"
#: apps/rules/models.py:64
msgid "is exactly"
msgstr ""
#: apps/rules/models.py:65
msgid "contains"
msgstr ""
#: apps/rules/models.py:66
msgid "starts with"
msgstr ""
#: apps/rules/models.py:67
msgid "ends with"
msgstr ""
#: apps/rules/models.py:68
msgid "equals"
msgstr ""
#: apps/rules/models.py:69
msgid "greater than"
msgstr ""
#: apps/rules/models.py:70
msgid "less than"
msgstr ""
#: apps/rules/models.py:71
msgid "greater than or equal"
msgstr ""
#: apps/rules/models.py:72
msgid "less than or equal"
msgstr ""
#: apps/rules/models.py:82 templates/transactions/pages/transactions.html:15
msgid "Filter"
msgstr "Filter"
#: apps/rules/models.py:85
msgid ""
"Generic expression to enable or disable execution. Should evaluate to True "
"or False"
msgstr ""
#: apps/rules/models.py:289
#, fuzzy
#| msgid "Edit transaction rule action"
msgid "Update or create transaction action"
msgstr "Bewerk verrichtingsregel actie"
#: apps/rules/models.py:290
#, fuzzy
#| msgid "Edit transaction rule action"
msgid "Update or create transaction actions"
msgstr "Bewerk verrichtingsregel actie"
#: apps/rules/views.py:52
msgid "Rule deactivated successfully"
msgstr "Regel succesvol uitgeschakeld"
#: apps/rules/views.py:46
#: apps/rules/views.py:54
msgid "Rule activated successfully"
msgstr "Regel succesvol ingeschakeld"
#: apps/rules/views.py:64
#: apps/rules/views.py:72
msgid "Rule added successfully"
msgstr "Regel succesvol toegevoegd"
#: apps/rules/views.py:87
#: apps/rules/views.py:100
msgid "Rule updated successfully"
msgstr "Regel succesvol bijgewerkt"
#: apps/rules/views.py:126
#: apps/rules/views.py:139
msgid "Rule deleted successfully"
msgstr "Regel succesvol verwijderd"
#: apps/rules/views.py:180
#: apps/rules/views.py:193
msgid "Action updated successfully"
msgstr "Actie succesvol bijgewerkt"
#: apps/rules/views.py:210
#: apps/rules/views.py:223
msgid "Action deleted successfully"
msgstr "Actie succesvol verwijderd"
#: apps/transactions/filters.py:24 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:30 templates/includes/navbar.html:46
#: apps/rules/views.py:246
#, fuzzy
#| msgid "Recurring Transaction added successfully"
msgid "Update or Create Transaction action added successfully"
msgstr "Terugkerende Verrichting succesvol toegevoegd"
#: apps/rules/views.py:278
#, fuzzy
#| msgid "Recurring Transaction updated successfully"
msgid "Update or Create Transaction action updated successfully"
msgstr "Terugkerende Verrichting succesvol bijgewerkt"
#: apps/rules/views.py:307
#, fuzzy
#| msgid "Recurring Transaction deleted successfully"
msgid "Update or Create Transaction action deleted successfully"
msgstr "Terugkerende Verrichting succesvol verwijderd"
#: apps/transactions/filters.py:24 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31 templates/includes/navbar.html:46
#: templates/transactions/widgets/paid_toggle_button.html:8
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:12
msgid "Projected"
@@ -999,14 +1139,6 @@ msgstr "Afbetalingsplan"
msgid "Recurring Transaction"
msgstr "Terugkerende verrichting"
#: apps/transactions/models.py:192
msgid "Internal Note"
msgstr "Interne opmerking"
#: apps/transactions/models.py:194
msgid "Internal ID"
msgstr "Interne ID"
#: apps/transactions/models.py:198
msgid "Deleted"
msgstr "Verwijderd"
@@ -1424,7 +1556,7 @@ msgstr "Acties"
#: templates/account_groups/fragments/list.html:36
#: templates/accounts/fragments/list.html:41
#: templates/categories/fragments/table.html:29
#: templates/cotton/transaction/item.html:126
#: templates/cotton/transaction/item.html:127
#: templates/cotton/ui/transactions_action_bar.html:49
#: templates/currencies/fragments/list.html:37
#: templates/dca/fragments/strategy/details.html:67
@@ -1436,8 +1568,9 @@ msgstr "Acties"
#: templates/import_app/fragments/profiles/list.html:48
#: templates/installment_plans/fragments/table.html:27
#: templates/recurring_transactions/fragments/table.html:29
#: templates/rules/fragments/transaction_rule/view.html:22
#: templates/rules/fragments/transaction_rule/view.html:48
#: templates/rules/fragments/transaction_rule/view.html:23
#: templates/rules/fragments/transaction_rule/view.html:47
#: templates/rules/fragments/transaction_rule/view.html:80
#: templates/tags/fragments/table.html:28
msgid "Edit"
msgstr "Bijwerken"
@@ -1445,8 +1578,8 @@ msgstr "Bijwerken"
#: templates/account_groups/fragments/list.html:43
#: templates/accounts/fragments/list.html:48
#: templates/categories/fragments/table.html:36
#: templates/cotton/transaction/item.html:141
#: templates/cotton/transaction/item.html:160
#: templates/cotton/transaction/item.html:142
#: templates/cotton/transaction/item.html:161
#: templates/cotton/ui/deleted_transactions_action_bar.html:55
#: templates/cotton/ui/transactions_action_bar.html:86
#: templates/currencies/fragments/list.html:44
@@ -1462,7 +1595,8 @@ msgstr "Bijwerken"
#: templates/mini_tools/unit_price_calculator.html:18
#: templates/recurring_transactions/fragments/table.html:91
#: templates/rules/fragments/list.html:44
#: templates/rules/fragments/transaction_rule/view.html:56
#: templates/rules/fragments/transaction_rule/view.html:55
#: templates/rules/fragments/transaction_rule/view.html:88
#: templates/tags/fragments/table.html:36
msgid "Delete"
msgstr "Verwijderen"
@@ -1470,8 +1604,8 @@ msgstr "Verwijderen"
#: templates/account_groups/fragments/list.html:47
#: templates/accounts/fragments/list.html:52
#: templates/categories/fragments/table.html:41
#: templates/cotton/transaction/item.html:145
#: templates/cotton/transaction/item.html:164
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:88
#: templates/currencies/fragments/list.html:48
@@ -1490,7 +1624,8 @@ msgstr "Verwijderen"
#: templates/recurring_transactions/fragments/table.html:82
#: templates/recurring_transactions/fragments/table.html:96
#: templates/rules/fragments/list.html:48
#: templates/rules/fragments/transaction_rule/view.html:60
#: templates/rules/fragments/transaction_rule/view.html:59
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/tags/fragments/table.html:40
msgid "Are you sure?"
msgstr "Weet je het zeker?"
@@ -1498,8 +1633,8 @@ msgstr "Weet je het zeker?"
#: templates/account_groups/fragments/list.html:48
#: templates/accounts/fragments/list.html:53
#: templates/categories/fragments/table.html:42
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:89
#: templates/currencies/fragments/list.html:49
@@ -1511,7 +1646,8 @@ msgstr "Weet je het zeker?"
#: templates/exchange_rates_services/fragments/table.html:37
#: templates/import_app/fragments/profiles/list.html:74
#: templates/rules/fragments/list.html:49
#: templates/rules/fragments/transaction_rule/view.html:61
#: templates/rules/fragments/transaction_rule/view.html:60
#: templates/rules/fragments/transaction_rule/view.html:93
#: templates/tags/fragments/table.html:41
msgid "You won't be able to revert this!"
msgstr "Je kunt dit niet meer terugdraaien!"
@@ -1519,8 +1655,8 @@ msgstr "Je kunt dit niet meer terugdraaien!"
#: templates/account_groups/fragments/list.html:49
#: templates/accounts/fragments/list.html:54
#: templates/categories/fragments/table.html:43
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:148
#: templates/cotton/transaction/item.html:167
#: templates/currencies/fragments/list.html:50
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/list.html:48
@@ -1533,7 +1669,8 @@ msgstr "Je kunt dit niet meer terugdraaien!"
#: templates/installment_plans/fragments/table.html:62
#: templates/recurring_transactions/fragments/table.html:98
#: templates/rules/fragments/list.html:50
#: templates/rules/fragments/transaction_rule/view.html:62
#: templates/rules/fragments/transaction_rule/view.html:61
#: templates/rules/fragments/transaction_rule/view.html:94
#: templates/tags/fragments/table.html:42
msgid "Yes, delete it!"
msgstr "Ja, verwijder het!"
@@ -1644,16 +1781,16 @@ msgstr "Sluiten"
msgid "Search"
msgstr "Zoeken"
#: templates/cotton/transaction/item.html:7
#: templates/cotton/transaction/item.html:8
msgid "Select"
msgstr "Selecteer"
#: templates/cotton/transaction/item.html:133
#: templates/cotton/transaction/item.html:134
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr "Dupliceren"
#: templates/cotton/transaction/item.html:154
#: templates/cotton/transaction/item.html:155
#: templates/cotton/ui/deleted_transactions_action_bar.html:47
msgid "Restore"
msgstr ""
@@ -1663,33 +1800,33 @@ msgstr ""
msgid "projected income"
msgstr "verwachte inkomsten"
#: templates/cotton/ui/account_card.html:37
#: templates/cotton/ui/currency_card.html:32
#: templates/cotton/ui/account_card.html:41
#: templates/cotton/ui/currency_card.html:36
msgid "projected expenses"
msgstr "verwachte uitgaven"
#: templates/cotton/ui/account_card.html:61
#: templates/cotton/ui/currency_card.html:56
#: templates/cotton/ui/account_card.html:69
#: templates/cotton/ui/currency_card.html:64
msgid "projected total"
msgstr "verwachte totaal"
#: templates/cotton/ui/account_card.html:86
#: templates/cotton/ui/currency_card.html:81
#: templates/cotton/ui/account_card.html:94
#: templates/cotton/ui/currency_card.html:88
msgid "current income"
msgstr "huidige inkomsten"
#: templates/cotton/ui/account_card.html:108
#: templates/cotton/ui/currency_card.html:103
#: templates/cotton/ui/account_card.html:120
#: templates/cotton/ui/currency_card.html:114
msgid "current expenses"
msgstr "huidige uitgaven"
#: templates/cotton/ui/account_card.html:130
#: templates/cotton/ui/currency_card.html:125
#: templates/cotton/ui/account_card.html:146
#: templates/cotton/ui/currency_card.html:140
msgid "current total"
msgstr "huidige totaal"
#: templates/cotton/ui/account_card.html:156
#: templates/cotton/ui/currency_card.html:151
#: templates/cotton/ui/account_card.html:171
#: templates/cotton/ui/currency_card.html:165
msgid "final total"
msgstr "eindtotaal"
@@ -1850,7 +1987,7 @@ msgid "No entries for this DCA"
msgstr "Geen idems in deze DCA"
#: templates/dca/fragments/strategy/details.html:125
#: templates/monthly_overview/fragments/list.html:41
#: templates/monthly_overview/fragments/list.html:47
#: templates/transactions/fragments/list_all.html:40
msgid "Try adding one"
msgstr "Probeer er een toe te voegen"
@@ -2273,7 +2410,7 @@ msgstr "Eenheidsprijs"
msgid "Item"
msgstr "Artikel"
#: templates/monthly_overview/fragments/list.html:40
#: templates/monthly_overview/fragments/list.html:46
msgid "No transactions this month"
msgstr "Geen verrichtingen deze maand"
@@ -2450,10 +2587,12 @@ msgid "Edit transaction rule"
msgstr "Verrichtingsregel bewerken"
#: templates/rules/fragments/transaction_rule/transaction_rule_action/add.html:5
#: templates/rules/fragments/transaction_rule/update_or_create_transaction_rule_action/add.html:5
msgid "Add action to transaction rule"
msgstr "Actie toevoegen aan verrichtingsregel"
#: templates/rules/fragments/transaction_rule/transaction_rule_action/edit.html:5
#: templates/rules/fragments/transaction_rule/update_or_create_transaction_rule_action/edit.html:5
msgid "Edit transaction rule action"
msgstr "Bewerk verrichtingsregel actie"
@@ -2461,15 +2600,21 @@ msgstr "Bewerk verrichtingsregel actie"
msgid "Transaction Rule"
msgstr "Verrichtingsregel"
#: templates/rules/fragments/transaction_rule/view.html:13
#: templates/rules/fragments/transaction_rule/view.html:14
msgid "If transaction..."
msgstr "Als verrichting..."
#: templates/rules/fragments/transaction_rule/view.html:31
#: templates/rules/fragments/transaction_rule/view.html:32
msgid "Then..."
msgstr "Dan..."
#: templates/rules/fragments/transaction_rule/view.html:36
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
msgstr "Bewerk verrichting"
#: templates/rules/fragments/transaction_rule/view.html:39
msgid "Set"
msgstr "Stel"
@@ -2478,13 +2623,37 @@ msgid "to"
msgstr "naar"
#: templates/rules/fragments/transaction_rule/view.html:71
#, fuzzy
#| msgid "Edit recurring transaction"
msgid "Update or create transaction"
msgstr "Bewerk terugkerende verrichtingen"
#: templates/rules/fragments/transaction_rule/view.html:74
#, fuzzy
#| msgid "Edit entity"
msgid "Edit to view"
msgstr "Bewerk bedrijf"
#: templates/rules/fragments/transaction_rule/view.html:104
msgid "This rule has no actions"
msgstr "Deze regel heeft geen acties"
#: templates/rules/fragments/transaction_rule/view.html:80
#: templates/rules/fragments/transaction_rule/view.html:112
msgid "Add new"
msgstr "Nieuwe toevoegen"
#: templates/rules/fragments/transaction_rule/view.html:117
#, fuzzy
#| msgid "Edit transaction"
msgid "Edit Transaction"
msgstr "Bewerk verrichting"
#: templates/rules/fragments/transaction_rule/view.html:120
#, fuzzy
#| msgid "Expense Transaction"
msgid "Update or Create Transaction"
msgstr "Uitgave Transactie"
#: templates/tags/fragments/add.html:5
msgid "Add tag"
msgstr "Label toevoegen"
@@ -2518,11 +2687,6 @@ msgstr "Bewerking"
msgid "transactions"
msgstr "verrichtingen"
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
msgstr "Bewerk verrichting"
#: templates/transactions/fragments/list_all.html:39
msgid "No transactions found"
msgstr "Geen Verrichtingen gevonden"
@@ -2537,10 +2701,6 @@ msgstr "Nieuwe overschrijving"
msgid "No deleted transactions to show"
msgstr "Geen verrichtingen deze maand"
#: templates/transactions/pages/transactions.html:15
msgid "Filter"
msgstr "Filter"
#: templates/transactions/pages/trash.html:4
#: templates/transactions/pages/trash.html:9
#, fuzzy
@@ -2579,6 +2739,191 @@ msgstr "Jaaroverzicht"
msgid "Year"
msgstr "Jaar"
#, fuzzy
#~| msgid "Start Date"
#~ msgid "Search Date"
#~ msgstr "Startdatum"
#, fuzzy
#~| msgid "Internal Note"
#~ msgid "Search Internal Note"
#~ msgstr "Interne opmerking"
#, fuzzy
#~| msgid "Set field"
#~ msgid "Is Paid"
#~ msgstr "Veld instellen"
#, fuzzy
#~| msgid "From Account"
#~ msgid "Search Account"
#~ msgstr "Van rekening"
#, fuzzy
#~| msgid "Account Group"
#~ msgid "Account Operator"
#~ msgstr "Accountgroep"
#, fuzzy
#~| msgid "Recurrence Type"
#~ msgid "Search Type"
#~ msgstr "Type Terugkeerpatroon"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Is Paid"
#~ msgstr "Zoeken"
#, fuzzy
#~| msgid "Add action to transaction rule"
#~ msgid "Expression to match transaction date"
#~ msgstr "Actie toevoegen aan verrichtingsregel"
#, fuzzy
#~| msgid "Date Format"
#~ msgid "Date Operator"
#~ msgstr "Datumnotatie"
#, fuzzy
#~| msgid "Reference Date"
#~ msgid "Search Reference Date"
#~ msgstr "Referentiedatum"
#, fuzzy
#~| msgid "Reference date from"
#~ msgid "Reference Date Operator"
#~ msgstr "Referentiedatum vanaf"
#, fuzzy
#~| msgid "From Amount"
#~ msgid "Search Amount"
#~ msgstr "Van Bedrag"
#, fuzzy
#~| msgid "Amount max"
#~ msgid "Amount Operator"
#~ msgstr "Maximaal bedrag"
#, fuzzy
#~| msgid "Description"
#~ msgid "Search Description"
#~ msgstr "Beschrijving"
#, fuzzy
#~| msgid "Edit transaction rule action"
#~ msgid "Expression to match transaction description"
#~ msgstr "Bewerk verrichtingsregel actie"
#, fuzzy
#~| msgid "Description"
#~ msgid "Description Operator"
#~ msgstr "Beschrijving"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Notes"
#~ msgstr "Zoeken"
#, fuzzy
#~| msgid "Category"
#~ msgid "Search Category"
#~ msgstr "Categorie"
#, fuzzy
#~| msgid "Category name"
#~ msgid "Category Operator"
#~ msgstr "Naam van categorie"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Tags"
#~ msgstr "Zoeken"
#, fuzzy
#~| msgid "Entities"
#~ msgid "Search Entities"
#~ msgstr "Bedrijven"
#, fuzzy
#~| msgid "Entities"
#~ msgid "Entities Operator"
#~ msgstr "Bedrijven"
#, fuzzy
#~| msgid "Internal Note"
#~ msgid "Internal Note Operator"
#~ msgstr "Interne opmerking"
#, fuzzy
#~| msgid "Internal ID"
#~ msgid "Search Internal ID"
#~ msgstr "Interne ID"
#, fuzzy
#~| msgid "Internal ID"
#~ msgid "Internal ID Operator"
#~ msgstr "Interne ID"
#, fuzzy
#~| msgid "Account"
#~ msgid "Set Account"
#~ msgstr "Rekening"
#, fuzzy
#~| msgid "Recurrence Type"
#~ msgid "Set Type"
#~ msgstr "Type Terugkeerpatroon"
#, fuzzy
#~| msgid "Start Date"
#~ msgid "Set Date"
#~ msgstr "Startdatum"
#, fuzzy
#~| msgid "Reference Date"
#~ msgid "Set Reference Date"
#~ msgstr "Referentiedatum"
#, fuzzy
#~| msgid "Amount"
#~ msgid "Set Amount"
#~ msgstr "Bedrag"
#, fuzzy
#~| msgid "Description"
#~ msgid "Set Description"
#~ msgstr "Beschrijving"
#, fuzzy
#~| msgid "Notes"
#~ msgid "Set Notes"
#~ msgstr "Opmerkingen"
#, fuzzy
#~| msgid "Internal Note"
#~ msgid "Set Internal Note"
#~ msgstr "Interne opmerking"
#, fuzzy
#~| msgid "Internal ID"
#~ msgid "Set Internal ID"
#~ msgstr "Interne ID"
#, fuzzy
#~| msgid "Entities"
#~ msgid "Set Entities"
#~ msgstr "Bedrijven"
#, fuzzy
#~| msgid "Category"
#~ msgid "Set Category"
#~ msgstr "Categorie"
#, fuzzy
#~| msgid "Tags"
#~ msgid "Set Tags"
#~ msgstr "Labels"
#, fuzzy
#~| msgid "Exchange rate deleted successfully"
#~ msgid "Exchange rates queued to be fetched successfully"

View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-07 11:45-0300\n"
"PO-Revision-Date: 2025-02-07 11:46-0300\n"
"POT-Creation-Date: 2025-02-09 17:27-0300\n"
"PO-Revision-Date: 2025-02-09 17:30-0300\n"
"Last-Translator: Herculino Trotta\n"
"Language-Team: \n"
"Language: pt_BR\n"
@@ -27,10 +27,10 @@ msgstr "Nome do grupo"
#: apps/currencies/forms.py:53 apps/currencies/forms.py:91
#: apps/currencies/forms.py:142 apps/dca/forms.py:41 apps/dca/forms.py:93
#: apps/import_app/forms.py:34 apps/rules/forms.py:45 apps/rules/forms.py:87
#: apps/transactions/forms.py:190 apps/transactions/forms.py:257
#: apps/transactions/forms.py:581 apps/transactions/forms.py:624
#: apps/transactions/forms.py:656 apps/transactions/forms.py:691
#: apps/transactions/forms.py:827
#: apps/rules/forms.py:359 apps/transactions/forms.py:190
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
#: apps/transactions/forms.py:624 apps/transactions/forms.py:656
#: apps/transactions/forms.py:691 apps/transactions/forms.py:827
msgid "Update"
msgstr "Atualizar"
@@ -38,10 +38,11 @@ msgstr "Atualizar"
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:61
#: apps/currencies/forms.py:99 apps/currencies/forms.py:150
#: apps/dca/forms.py:49 apps/dca/forms.py:102 apps/import_app/forms.py:42
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/transactions/forms.py:174
#: apps/transactions/forms.py:199 apps/transactions/forms.py:589
#: apps/transactions/forms.py:632 apps/transactions/forms.py:664
#: apps/transactions/forms.py:699 apps/transactions/forms.py:835
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/rules/forms.py:367
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
#: apps/transactions/forms.py:664 apps/transactions/forms.py:699
#: apps/transactions/forms.py:835
#: templates/account_groups/fragments/list.html:9
#: templates/accounts/fragments/list.html:9
#: templates/categories/fragments/list.html:9
@@ -68,7 +69,8 @@ msgstr "Grupo da Conta"
msgid "New balance"
msgstr "Novo saldo"
#: apps/accounts/forms.py:119 apps/rules/models.py:27
#: apps/accounts/forms.py:119 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
@@ -76,7 +78,8 @@ msgstr "Novo saldo"
msgid "Category"
msgstr "Categoria"
#: apps/accounts/forms.py:126 apps/rules/models.py:28
#: apps/accounts/forms.py:126 apps/rules/forms.py:171 apps/rules/forms.py:180
#: apps/rules/models.py:33 apps/rules/models.py:284
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
@@ -87,7 +90,7 @@ msgid "Tags"
msgstr "Tags"
#: apps/accounts/models.py:9 apps/accounts/models.py:21 apps/dca/models.py:14
#: apps/import_app/models.py:14 apps/rules/models.py:9
#: apps/import_app/models.py:14 apps/rules/models.py:10
#: apps/transactions/models.py:67 apps/transactions/models.py:87
#: apps/transactions/models.py:106
#: templates/account_groups/fragments/list.html:25
@@ -151,7 +154,8 @@ msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Contas arquivadas não aparecem nem contam para o seu patrimônio líquido"
#: apps/accounts/models.py:59 apps/rules/models.py:19
#: apps/accounts/models.py:59 apps/rules/forms.py:160 apps/rules/forms.py:173
#: apps/rules/models.py:24 apps/rules/models.py:236
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
#: apps/transactions/models.py:288 apps/transactions/models.py:490
@@ -360,7 +364,8 @@ msgstr "Prefixo"
msgid "Suffix"
msgstr "Sufixo"
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/models.py:22
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/forms.py:163
#: apps/rules/forms.py:176 apps/rules/models.py:27 apps/rules/models.py:248
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
#: apps/transactions/models.py:142
#: templates/dca/fragments/strategy/details.html:52
@@ -576,7 +581,8 @@ msgstr "Moeda de destino"
msgid "Payment Currency"
msgstr "Moeda de pagamento"
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/models.py:26
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/forms.py:167
#: apps/rules/forms.py:182 apps/rules/models.py:31 apps/rules/models.py:264
#: apps/transactions/forms.py:333 apps/transactions/models.py:155
#: apps/transactions/models.py:337 apps/transactions/models.py:518
msgid "Notes"
@@ -740,42 +746,62 @@ msgstr "Para"
msgid "A value for this field already exists in the rule."
msgstr "Já existe um valor para esse campo na regra."
#: apps/rules/models.py:10 apps/rules/models.py:25
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
msgid "Description"
msgstr "Descrição"
#: apps/rules/forms.py:147 apps/rules/forms.py:148 apps/rules/forms.py:149
#: apps/rules/forms.py:150 apps/rules/forms.py:151 apps/rules/forms.py:152
#: apps/rules/forms.py:153 apps/rules/forms.py:154 apps/rules/forms.py:155
#: apps/rules/forms.py:156 apps/rules/forms.py:157 apps/rules/forms.py:158
#: apps/rules/forms.py:159
msgid "Operator"
msgstr "Operador"
#: apps/rules/models.py:11
msgid "Trigger"
msgstr "Gatilho"
#: apps/rules/models.py:20 apps/transactions/models.py:139
#: apps/rules/forms.py:161 apps/rules/forms.py:174 apps/rules/models.py:25
#: apps/rules/models.py:240 apps/transactions/models.py:139
#: apps/transactions/models.py:293 apps/transactions/models.py:496
msgid "Type"
msgstr "Tipo"
#: apps/rules/models.py:21 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:30
#: apps/rules/forms.py:162 apps/rules/forms.py:175 apps/rules/models.py:26
#: apps/rules/models.py:244 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:12
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:16
msgid "Paid"
msgstr "Pago"
#: apps/rules/models.py:23 apps/transactions/forms.py:66
#: apps/rules/forms.py:164 apps/rules/forms.py:177 apps/rules/models.py:28
#: apps/rules/models.py:252 apps/transactions/forms.py:66
#: apps/transactions/forms.py:322 apps/transactions/forms.py:492
#: apps/transactions/models.py:143 apps/transactions/models.py:311
#: apps/transactions/models.py:520
msgid "Reference Date"
msgstr "Data de Referência"
#: apps/rules/models.py:24 apps/transactions/models.py:148
#: apps/rules/forms.py:165 apps/rules/forms.py:178 apps/rules/models.py:29
#: apps/rules/models.py:256 apps/transactions/models.py:148
#: apps/transactions/models.py:501
msgid "Amount"
msgstr "Quantia"
#: apps/rules/models.py:29 apps/transactions/filters.py:81
#: apps/rules/forms.py:166 apps/rules/forms.py:179 apps/rules/models.py:11
#: apps/rules/models.py:30 apps/rules/models.py:260
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
msgid "Description"
msgstr "Descrição"
#: apps/rules/forms.py:169 apps/rules/forms.py:184 apps/rules/models.py:268
#: apps/transactions/models.py:192
msgid "Internal Note"
msgstr "Nota Interna"
#: apps/rules/forms.py:170 apps/rules/forms.py:185 apps/rules/models.py:272
#: apps/transactions/models.py:194
msgid "Internal ID"
msgstr "ID Interna"
#: apps/rules/forms.py:172 apps/rules/forms.py:181 apps/rules/models.py:34
#: apps/rules/models.py:276 apps/transactions/filters.py:81
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
#: apps/transactions/models.py:170 apps/transactions/models.py:333
@@ -784,48 +810,144 @@ msgstr "Quantia"
msgid "Entities"
msgstr "Entidades"
#: apps/rules/models.py:35
#: apps/rules/forms.py:199
msgid "Search Criteria"
msgstr "Critério de Busca"
#: apps/rules/forms.py:334
msgid "Set Values"
msgstr "Definir valores"
#: apps/rules/models.py:12
msgid "Trigger"
msgstr "Gatilho"
#: apps/rules/models.py:15
msgid "Transaction rule"
msgstr "Regra da transação"
#: apps/rules/models.py:16
msgid "Transaction rules"
msgstr "Regra da transação"
#: apps/rules/models.py:40 apps/rules/models.py:78
msgid "Rule"
msgstr "Regra"
#: apps/rules/models.py:40
#: apps/rules/models.py:45
msgid "Field"
msgstr "Campo"
#: apps/rules/models.py:42
#: apps/rules/models.py:47
msgid "Value"
msgstr "Valor"
#: apps/rules/views.py:44
#: apps/rules/models.py:53
msgid "Edit transaction action"
msgstr "Editar ação de transação"
#: apps/rules/models.py:54
msgid "Edit transaction actions"
msgstr "Editar ações de transação"
#: apps/rules/models.py:64
msgid "is exactly"
msgstr "é exatamete"
#: apps/rules/models.py:65
msgid "contains"
msgstr "contém"
#: apps/rules/models.py:66
msgid "starts with"
msgstr "começa com"
#: apps/rules/models.py:67
msgid "ends with"
msgstr "termina em"
#: apps/rules/models.py:68
msgid "equals"
msgstr "igual"
#: apps/rules/models.py:69
msgid "greater than"
msgstr "maior que"
#: apps/rules/models.py:70
msgid "less than"
msgstr "menos de"
#: apps/rules/models.py:71
msgid "greater than or equal"
msgstr "maior ou igual"
#: apps/rules/models.py:72
msgid "less than or equal"
msgstr "menor ou igual"
#: apps/rules/models.py:82 templates/transactions/pages/transactions.html:15
msgid "Filter"
msgstr "Filtro"
#: apps/rules/models.py:85
msgid ""
"Generic expression to enable or disable execution. Should evaluate to True "
"or False"
msgstr ""
"Expressão genérica para ativar ou desativar a execução. Deve retornar True "
"ou False"
#: apps/rules/models.py:289
msgid "Update or create transaction action"
msgstr "Atualizar ou criar transação ação"
#: apps/rules/models.py:290
msgid "Update or create transaction actions"
msgstr "Atualizar ou criar transação ações"
#: apps/rules/views.py:52
msgid "Rule deactivated successfully"
msgstr "Regra desativada com sucesso"
#: apps/rules/views.py:46
#: apps/rules/views.py:54
msgid "Rule activated successfully"
msgstr "Regra ativada com sucesso"
#: apps/rules/views.py:64
#: apps/rules/views.py:72
msgid "Rule added successfully"
msgstr "Regra adicionada com sucesso"
#: apps/rules/views.py:87
#: apps/rules/views.py:100
msgid "Rule updated successfully"
msgstr "Regra atualizada com sucesso"
#: apps/rules/views.py:126
#: apps/rules/views.py:139
msgid "Rule deleted successfully"
msgstr "Regra apagada com sucesso"
#: apps/rules/views.py:180
#: apps/rules/views.py:193
msgid "Action updated successfully"
msgstr "Ação atualizada com sucesso"
#: apps/rules/views.py:210
#: apps/rules/views.py:223
msgid "Action deleted successfully"
msgstr "Ação apagada com sucesso"
#: apps/transactions/filters.py:24 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:30 templates/includes/navbar.html:46
#: apps/rules/views.py:246
msgid "Update or Create Transaction action added successfully"
msgstr "Ação Atualizar ou Criar Transação adicionada com sucesso"
#: apps/rules/views.py:278
msgid "Update or Create Transaction action updated successfully"
msgstr "Ação Atualizar ou Criar Transação atualizada com sucesso"
#: apps/rules/views.py:307
msgid "Update or Create Transaction action deleted successfully"
msgstr "Ação Atualizar ou Criar Transação apagada com sucesso"
#: apps/transactions/filters.py:24 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31 templates/includes/navbar.html:46
#: templates/transactions/widgets/paid_toggle_button.html:8
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:12
msgid "Projected"
@@ -982,14 +1104,6 @@ msgstr "Parcelamento"
msgid "Recurring Transaction"
msgstr "Transação Recorrente"
#: apps/transactions/models.py:192
msgid "Internal Note"
msgstr "Nota Interna"
#: apps/transactions/models.py:194
msgid "Internal ID"
msgstr "ID Interna"
#: apps/transactions/models.py:198
msgid "Deleted"
msgstr "Apagado"
@@ -1403,7 +1517,7 @@ msgstr "Ações"
#: templates/account_groups/fragments/list.html:36
#: templates/accounts/fragments/list.html:41
#: templates/categories/fragments/table.html:29
#: templates/cotton/transaction/item.html:126
#: templates/cotton/transaction/item.html:127
#: templates/cotton/ui/transactions_action_bar.html:49
#: templates/currencies/fragments/list.html:37
#: templates/dca/fragments/strategy/details.html:67
@@ -1415,8 +1529,9 @@ msgstr "Ações"
#: templates/import_app/fragments/profiles/list.html:48
#: templates/installment_plans/fragments/table.html:27
#: templates/recurring_transactions/fragments/table.html:29
#: templates/rules/fragments/transaction_rule/view.html:22
#: templates/rules/fragments/transaction_rule/view.html:48
#: templates/rules/fragments/transaction_rule/view.html:23
#: templates/rules/fragments/transaction_rule/view.html:47
#: templates/rules/fragments/transaction_rule/view.html:80
#: templates/tags/fragments/table.html:28
msgid "Edit"
msgstr "Editar"
@@ -1424,8 +1539,8 @@ msgstr "Editar"
#: templates/account_groups/fragments/list.html:43
#: templates/accounts/fragments/list.html:48
#: templates/categories/fragments/table.html:36
#: templates/cotton/transaction/item.html:141
#: templates/cotton/transaction/item.html:160
#: templates/cotton/transaction/item.html:142
#: templates/cotton/transaction/item.html:161
#: templates/cotton/ui/deleted_transactions_action_bar.html:55
#: templates/cotton/ui/transactions_action_bar.html:86
#: templates/currencies/fragments/list.html:44
@@ -1441,7 +1556,8 @@ msgstr "Editar"
#: templates/mini_tools/unit_price_calculator.html:18
#: templates/recurring_transactions/fragments/table.html:91
#: templates/rules/fragments/list.html:44
#: templates/rules/fragments/transaction_rule/view.html:56
#: templates/rules/fragments/transaction_rule/view.html:55
#: templates/rules/fragments/transaction_rule/view.html:88
#: templates/tags/fragments/table.html:36
msgid "Delete"
msgstr "Apagar"
@@ -1449,8 +1565,8 @@ msgstr "Apagar"
#: templates/account_groups/fragments/list.html:47
#: templates/accounts/fragments/list.html:52
#: templates/categories/fragments/table.html:41
#: templates/cotton/transaction/item.html:145
#: templates/cotton/transaction/item.html:164
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:88
#: templates/currencies/fragments/list.html:48
@@ -1469,7 +1585,8 @@ msgstr "Apagar"
#: templates/recurring_transactions/fragments/table.html:82
#: templates/recurring_transactions/fragments/table.html:96
#: templates/rules/fragments/list.html:48
#: templates/rules/fragments/transaction_rule/view.html:60
#: templates/rules/fragments/transaction_rule/view.html:59
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/tags/fragments/table.html:40
msgid "Are you sure?"
msgstr "Tem certeza?"
@@ -1477,8 +1594,8 @@ msgstr "Tem certeza?"
#: templates/account_groups/fragments/list.html:48
#: templates/accounts/fragments/list.html:53
#: templates/categories/fragments/table.html:42
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:89
#: templates/currencies/fragments/list.html:49
@@ -1490,7 +1607,8 @@ msgstr "Tem certeza?"
#: templates/exchange_rates_services/fragments/table.html:37
#: templates/import_app/fragments/profiles/list.html:74
#: templates/rules/fragments/list.html:49
#: templates/rules/fragments/transaction_rule/view.html:61
#: templates/rules/fragments/transaction_rule/view.html:60
#: templates/rules/fragments/transaction_rule/view.html:93
#: templates/tags/fragments/table.html:41
msgid "You won't be able to revert this!"
msgstr "Você não será capaz de reverter isso!"
@@ -1498,8 +1616,8 @@ msgstr "Você não será capaz de reverter isso!"
#: templates/account_groups/fragments/list.html:49
#: templates/accounts/fragments/list.html:54
#: templates/categories/fragments/table.html:43
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:148
#: templates/cotton/transaction/item.html:167
#: templates/currencies/fragments/list.html:50
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/list.html:48
@@ -1512,7 +1630,8 @@ msgstr "Você não será capaz de reverter isso!"
#: templates/installment_plans/fragments/table.html:62
#: templates/recurring_transactions/fragments/table.html:98
#: templates/rules/fragments/list.html:50
#: templates/rules/fragments/transaction_rule/view.html:62
#: templates/rules/fragments/transaction_rule/view.html:61
#: templates/rules/fragments/transaction_rule/view.html:94
#: templates/tags/fragments/table.html:42
msgid "Yes, delete it!"
msgstr "Sim, apague!"
@@ -1623,16 +1742,16 @@ msgstr "Fechar"
msgid "Search"
msgstr "Buscar"
#: templates/cotton/transaction/item.html:7
#: templates/cotton/transaction/item.html:8
msgid "Select"
msgstr "Selecionar"
#: templates/cotton/transaction/item.html:133
#: templates/cotton/transaction/item.html:134
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr "Duplicar"
#: templates/cotton/transaction/item.html:154
#: templates/cotton/transaction/item.html:155
#: templates/cotton/ui/deleted_transactions_action_bar.html:47
msgid "Restore"
msgstr "Restaurar"
@@ -1642,33 +1761,33 @@ msgstr "Restaurar"
msgid "projected income"
msgstr "renda prevista"
#: templates/cotton/ui/account_card.html:37
#: templates/cotton/ui/currency_card.html:32
#: templates/cotton/ui/account_card.html:41
#: templates/cotton/ui/currency_card.html:36
msgid "projected expenses"
msgstr "despesas previstas"
#: templates/cotton/ui/account_card.html:61
#: templates/cotton/ui/currency_card.html:56
#: templates/cotton/ui/account_card.html:69
#: templates/cotton/ui/currency_card.html:64
msgid "projected total"
msgstr "total previsto"
#: templates/cotton/ui/account_card.html:86
#: templates/cotton/ui/currency_card.html:81
#: templates/cotton/ui/account_card.html:94
#: templates/cotton/ui/currency_card.html:88
msgid "current income"
msgstr "renda atual"
#: templates/cotton/ui/account_card.html:108
#: templates/cotton/ui/currency_card.html:103
#: templates/cotton/ui/account_card.html:120
#: templates/cotton/ui/currency_card.html:114
msgid "current expenses"
msgstr "despesas atuais"
#: templates/cotton/ui/account_card.html:130
#: templates/cotton/ui/currency_card.html:125
#: templates/cotton/ui/account_card.html:146
#: templates/cotton/ui/currency_card.html:140
msgid "current total"
msgstr "total atual"
#: templates/cotton/ui/account_card.html:156
#: templates/cotton/ui/currency_card.html:151
#: templates/cotton/ui/account_card.html:171
#: templates/cotton/ui/currency_card.html:165
msgid "final total"
msgstr "total final"
@@ -1829,7 +1948,7 @@ msgid "No entries for this DCA"
msgstr "Nenhuma entrada neste CMP"
#: templates/dca/fragments/strategy/details.html:125
#: templates/monthly_overview/fragments/list.html:41
#: templates/monthly_overview/fragments/list.html:47
#: templates/transactions/fragments/list_all.html:40
msgid "Try adding one"
msgstr "Tente adicionar uma"
@@ -2245,7 +2364,7 @@ msgstr "Preço unitário"
msgid "Item"
msgstr "Item"
#: templates/monthly_overview/fragments/list.html:40
#: templates/monthly_overview/fragments/list.html:46
msgid "No transactions this month"
msgstr "Nenhuma transação neste mês"
@@ -2419,10 +2538,12 @@ msgid "Edit transaction rule"
msgstr "Editar regra de transação"
#: templates/rules/fragments/transaction_rule/transaction_rule_action/add.html:5
#: templates/rules/fragments/transaction_rule/update_or_create_transaction_rule_action/add.html:5
msgid "Add action to transaction rule"
msgstr "Adicionar ação à regra de transação"
#: templates/rules/fragments/transaction_rule/transaction_rule_action/edit.html:5
#: templates/rules/fragments/transaction_rule/update_or_create_transaction_rule_action/edit.html:5
msgid "Edit transaction rule action"
msgstr "Editar ação de regra de transação"
@@ -2430,15 +2551,21 @@ msgstr "Editar ação de regra de transação"
msgid "Transaction Rule"
msgstr "Regra de transação"
#: templates/rules/fragments/transaction_rule/view.html:13
#: templates/rules/fragments/transaction_rule/view.html:14
msgid "If transaction..."
msgstr "Se a transação..."
#: templates/rules/fragments/transaction_rule/view.html:31
#: templates/rules/fragments/transaction_rule/view.html:32
msgid "Then..."
msgstr "Então..."
#: templates/rules/fragments/transaction_rule/view.html:36
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
msgstr "Editar transação"
#: templates/rules/fragments/transaction_rule/view.html:39
msgid "Set"
msgstr "Definir"
@@ -2447,13 +2574,29 @@ msgid "to"
msgstr "para"
#: templates/rules/fragments/transaction_rule/view.html:71
msgid "Update or create transaction"
msgstr "Atualizar ou criar transação"
#: templates/rules/fragments/transaction_rule/view.html:74
msgid "Edit to view"
msgstr "Edite para ver"
#: templates/rules/fragments/transaction_rule/view.html:104
msgid "This rule has no actions"
msgstr "Essa regra não tem ações"
#: templates/rules/fragments/transaction_rule/view.html:80
#: templates/rules/fragments/transaction_rule/view.html:112
msgid "Add new"
msgstr "Adicionar novo"
#: templates/rules/fragments/transaction_rule/view.html:117
msgid "Edit Transaction"
msgstr "Editar Transação"
#: templates/rules/fragments/transaction_rule/view.html:120
msgid "Update or Create Transaction"
msgstr "Atualizar ou Criar Transação"
#: templates/tags/fragments/add.html:5
msgid "Add tag"
msgstr "Adicionar tag"
@@ -2487,11 +2630,6 @@ msgstr "Editando"
msgid "transactions"
msgstr "transações"
#: templates/transactions/fragments/edit.html:5
#: templates/transactions/fragments/edit_installment_plan.html:5
msgid "Edit transaction"
msgstr "Editar transação"
#: templates/transactions/fragments/list_all.html:39
msgid "No transactions found"
msgstr "Nenhuma transação encontrada"
@@ -2504,10 +2642,6 @@ msgstr "Nova transferência"
msgid "No deleted transactions to show"
msgstr "Nenhuma transação apaga para mostrar"
#: templates/transactions/pages/transactions.html:15
msgid "Filter"
msgstr "Filtro"
#: templates/transactions/pages/trash.html:4
#: templates/transactions/pages/trash.html:9
msgid "Deleted transactions"
@@ -2544,6 +2678,171 @@ msgstr "Visão Anual"
msgid "Year"
msgstr "Ano"
#, fuzzy
#~| msgid "Start Date"
#~ msgid "Search Date"
#~ msgstr "Data de Início"
#, fuzzy
#~| msgid "Internal Note"
#~ msgid "Search Internal Note"
#~ msgstr "Nota Interna"
#, fuzzy
#~| msgid "Set field"
#~ msgid "Is Paid"
#~ msgstr "Definir campo"
#, fuzzy
#~| msgid "From Account"
#~ msgid "Search Account"
#~ msgstr "Conta de origem"
#, fuzzy
#~| msgid "Account Group"
#~ msgid "Account Operator"
#~ msgstr "Grupo da Conta"
#, fuzzy
#~| msgid "Service Type"
#~ msgid "Search Type"
#~ msgstr "Tipo de Serviço"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Is Paid"
#~ msgstr "Buscar"
#, fuzzy
#~| msgid "Add action to transaction rule"
#~ msgid "Expression to match transaction date"
#~ msgstr "Adicionar ação à regra de transação"
#, fuzzy
#~| msgid "Date Format"
#~ msgid "Date Operator"
#~ msgstr "Formato de Data"
#, fuzzy
#~| msgid "Reference Date"
#~ msgid "Search Reference Date"
#~ msgstr "Data de Referência"
#, fuzzy
#~| msgid "Reference date from"
#~ msgid "Reference Date Operator"
#~ msgstr "Data de Referência de"
#, fuzzy
#~| msgid "From Amount"
#~ msgid "Search Amount"
#~ msgstr "Quantia de origem"
#, fuzzy
#~| msgid "Amount max"
#~ msgid "Amount Operator"
#~ msgstr "Quantia máxima"
#, fuzzy
#~| msgid "Description"
#~ msgid "Search Description"
#~ msgstr "Descrição"
#, fuzzy
#~| msgid "Edit transaction rule action"
#~ msgid "Expression to match transaction description"
#~ msgstr "Editar ação de regra de transação"
#, fuzzy
#~| msgid "Description"
#~ msgid "Description Operator"
#~ msgstr "Descrição"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Notes"
#~ msgstr "Buscar"
#, fuzzy
#~| msgid "Category"
#~ msgid "Search Category"
#~ msgstr "Categoria"
#, fuzzy
#~| msgid "Category name"
#~ msgid "Category Operator"
#~ msgstr "Nome da Categoria"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Tags"
#~ msgstr "Buscar"
#, fuzzy
#~| msgid "Entities"
#~ msgid "Search Entities"
#~ msgstr "Entidades"
#, fuzzy
#~| msgid "Entities"
#~ msgid "Entities Operator"
#~ msgstr "Entidades"
#, fuzzy
#~| msgid "Internal Note"
#~ msgid "Internal Note Operator"
#~ msgstr "Nota Interna"
#, fuzzy
#~| msgid "Internal ID"
#~ msgid "Search Internal ID"
#~ msgstr "ID Interna"
#, fuzzy
#~| msgid "Internal ID"
#~ msgid "Internal ID Operator"
#~ msgstr "ID Interna"
#, fuzzy
#~| msgid "Account"
#~ msgid "Set Account"
#~ msgstr "Conta"
#, fuzzy
#~| msgid "Service Type"
#~ msgid "Set Type"
#~ msgstr "Tipo de Serviço"
#~ msgid "Set Date"
#~ msgstr "Definir Data de Início"
#~ msgid "Set Reference Date"
#~ msgstr "Definir Data de Referência"
#~ msgid "Set Amount"
#~ msgstr "Definir Quantia"
#~ msgid "Set Description"
#~ msgstr "Definir Descrição"
#~ msgid "Set Notes"
#~ msgstr "Definir Notas"
#~ msgid "Set Internal Note"
#~ msgstr "Definir Nota Interna"
#~ msgid "Set Internal ID"
#~ msgstr "Definir ID Interna"
#~ msgid "Set Entities"
#~ msgstr "Definir Entidades"
#~ msgid "Set Category"
#~ msgstr "Definir Categoria"
#~ msgid "Set Tags"
#~ msgstr "Definir Tags"
#~ msgid "Fetch Interval (hours)"
#~ msgstr "Intervalo de busca (horas)"

View File

@@ -57,9 +57,8 @@
</td>
<td class="col">{{ account.name }}</td>
<td class="col">{{ account.group.name }}</td>
<td class="col">{{ account.currency }} ({{ account.currency.code }})</td>
<td class="col">{% if account.exchange_currency %}{{ account.exchange_currency }} (
{{ account.exchange_currency.code }}){% else %}-{% endif %}</td>
<td class="col">{{ account.currency }}</td>
<td class="col">{% if account.exchange_currency %}{{ account.exchange_currency }}{% else %}-{% endif %}</td>
<td class="col">{% if account.is_asset %}<i class="fa-solid fa-solid fa-check text-success"></i>{% endif %}</td>
<td class="col">{% if account.is_archived %}<i class="fa-solid fa-solid fa-check text-success"></i>{% endif %}</td>
</tr>

View File

@@ -1,172 +1,174 @@
{% load markdown %}
{% load i18n %}
<div class="transaction d-flex my-1 {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
{% if not disable_selection %}
<label class="px-3 d-flex align-items-center justify-content-center">
<input class="form-check-input" type="checkbox" name="transactions" value="{{ transaction.id }}"
id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>
</label>
{% endif %}
<div class="tw-border-s-6 tw-border-e-0 tw-border-t-0 tw-border-b-0 border-bottom
<div class="transaction">
<div class="d-flex my-1 {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
{% if not disable_selection %}
<label class="px-3 d-flex align-items-center justify-content-center">
<input class="form-check-input" type="checkbox" name="transactions" value="{{ transaction.id }}"
id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>
</label>
{% endif %}
<div class="tw-border-s-6 tw-border-e-0 tw-border-t-0 tw-border-b-0 border-bottom
hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{% else %}tw-border-solid{% endif %}
{% if transaction.type == "EX" %}tw-border-red-500{% else %}tw-border-green-500{% endif %} tw-relative
w-100 transaction-item"
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
on mouseout add .tw-invisible to the first .transaction-actions in me end">
<div class="row font-monospace tw-text-sm align-items-center">
<div class="col-lg-1 col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center text-center">
{% if not transaction.deleted %}
<a class="text-decoration-none my-lg-3 mx-lg-3 mx-2 my-2 tw-text-gray-500"
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
role="button"
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
hx-target="closest .transaction"
hx-swap="outerHTML">
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
class="fa-regular fa-circle"></i>{% endif %}
</a>
{% else %}
<div class="text-decoration-none my-lg-3 mx-lg-3 mx-2 my-2 tw-text-gray-500"
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}">
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
class="fa-regular fa-circle"></i>{% endif %}
<div class="row font-monospace tw-text-sm align-items-center">
<div class="col-lg-1 col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center text-center">
{% if not transaction.deleted %}
<a class="text-decoration-none my-lg-3 mx-lg-3 mx-2 my-2 tw-text-gray-500"
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
role="button"
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
hx-target="closest .transaction"
hx-swap="outerHTML">
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
class="fa-regular fa-circle"></i>{% endif %}
</a>
{% else %}
<div class="text-decoration-none my-lg-3 mx-lg-3 mx-2 my-2 tw-text-gray-500"
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}">
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
class="fa-regular fa-circle"></i>{% endif %}
</div>
{% endif %}
</div>
<div class="col-lg-8 col-12">
{# Date#}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
<div
class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
</div>
{% endif %}
</div>
<div class="col-lg-8 col-12">
{# Date#}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
<div
class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
</div>
{# Description#}
<div class="mb-2 mb-lg-1 text-white tw-text-base">
{% spaceless %}
<span>{{ transaction.description }}</span>
{% if transaction.installment_plan and transaction.installment_id %}
<span
class="badge text-bg-secondary ms-2">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
{% endif %}
{% if transaction.recurring_transaction %}
<span class="text-primary tw-text-xs ms-2"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
{% endif %}
{% endspaceless %}
</div>
<div class="tw-text-gray-400 tw-text-sm">
{# Entities #}
{% with transaction.entities.all as entities %}
{% if entities %}
{# Description#}
<div class="mb-2 mb-lg-1 text-white tw-text-base">
{% spaceless %}
<span>{{ transaction.description }}</span>
{% if transaction.installment_plan and transaction.installment_id %}
<span
class="badge text-bg-secondary ms-2">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
{% endif %}
{% if transaction.recurring_transaction %}
<span class="text-primary tw-text-xs ms-2"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
{% endif %}
{% endspaceless %}
</div>
<div class="tw-text-gray-400 tw-text-sm">
{# Entities #}
{% with transaction.entities.all as entities %}
{% if entities %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ entities|join:", " }}</div>
</div>
{% endif %}
{% endwith %}
{# Notes#}
{% if transaction.notes %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ entities|join:", " }}</div>
</div>
{% endif %}
{% endwith %}
{# Notes#}
{% if transaction.notes %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
</div>
{% endif %}
{# Category#}
{% if transaction.category %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-icons fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.category.name }}</div>
</div>
{% endif %}
{# Tags#}
{% with transaction.tags.all as tags %}
{% if tags %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ tags|join:", " }}</div>
<div class="col-auto pe-1"><i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
</div>
{% endif %}
{# Category#}
{% if transaction.category %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-icons fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.category.name }}</div>
</div>
{% endif %}
{# Tags#}
{% with transaction.tags.all as tags %}
{% if tags %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ tags|join:", " }}</div>
</div>
{% endif %}
{% endwith %}
</div>
</div>
<div class="col-lg-3 col-12 text-lg-end align-self-end">
<div class="main-amount mb-2 mb-lg-0">
<c-amount.display
:amount="transaction.amount"
:prefix="transaction.account.currency.prefix"
:suffix="transaction.account.currency.suffix"
:decimal_places="transaction.account.currency.decimal_places"
color="{% if transaction.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
</div>
{# Exchange Rate#}
{% with exchanged=transaction.exchanged_amount %}
{% if exchanged %}
<div class="exchanged-amount mb-2 mb-lg-0">
<c-amount.display
:amount="exchanged.amount"
:prefix="exchanged.prefix"
:suffix="exchanged.suffix"
:decimal_places="exchanged.decimal_places"
color="grey"></c-amount.display>
</div>
{% endif %}
{% endwith %}
<div>
{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
</div>
</div>
</div>
<div class="col-lg-3 col-12 text-lg-end align-self-end">
<div class="main-amount mb-2 mb-lg-0">
<c-amount.display
:amount="transaction.amount"
:prefix="transaction.account.currency.prefix"
:suffix="transaction.account.currency.suffix"
:decimal_places="transaction.account.currency.decimal_places"
color="{% if transaction.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
</div>
{# Exchange Rate#}
{% with exchanged=transaction.exchanged_amount %}
{% if exchanged %}
<div class="exchanged-amount mb-2 mb-lg-0">
<c-amount.display
:amount="exchanged.amount"
:prefix="exchanged.prefix"
:suffix="exchanged.suffix"
:decimal_places="exchanged.decimal_places"
color="grey"></c-amount.display>
</div>
{% endif %}
{% endwith %}
<div>
{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
</div>
</div>
<div>
{# Item actions#}
<div
class="transaction-actions !tw-absolute tw-left-1/2 tw-top-0 tw--translate-x-1/2 tw--translate-y-1/2 tw-invisible d-flex flex-row card">
<div class="card-body p-1 shadow-lg">
{% if not transaction.deleted %}
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas" hx-swap="innerHTML">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Duplicate" %}"
hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"
_="on click if event.ctrlKey set @hx-get to `{% url 'transaction_clone' transaction_id=transaction.id %}?edit=true` then call htmx.process(me) end then trigger ready"
hx-trigger="ready">
<i class="fa-solid fa-clone fa-fw"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
</a>
{% else %}
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Restore" %}"
hx-get="{% url 'transaction_undelete' transaction_id=transaction.id %}"><i
class="fa-solid fa-trash-arrow-up"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
</a>
{% endif %}
{# Item actions#}
<div
class="transaction-actions !tw-absolute tw-left-1/2 tw-top-0 tw--translate-x-1/2 tw--translate-y-1/2 tw-invisible d-flex flex-row card">
<div class="card-body p-1 shadow-lg">
{% if not transaction.deleted %}
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas" hx-swap="innerHTML">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Duplicate" %}"
hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"
_="on click if event.ctrlKey set @hx-get to `{% url 'transaction_clone' transaction_id=transaction.id %}?edit=true` then call htmx.process(me) end then trigger ready"
hx-trigger="ready">
<i class="fa-solid fa-clone fa-fw"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
</a>
{% else %}
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Restore" %}"
hx-get="{% url 'transaction_undelete' transaction_id=transaction.id %}"><i
class="fa-solid fa-trash-arrow-up"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
</a>
{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -15,6 +15,7 @@
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if account.income_projected != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="account.income_projected"
@@ -22,6 +23,9 @@
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
{% if account.exchanged and account.exchanged.income_projected %}
<div class="text-end font-monospace tw-text-gray-500">
@@ -38,6 +42,7 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div>
{% if account.expense_projected != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="account.expense_projected"
@@ -45,6 +50,9 @@
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
</div>
{% if account.exchanged and account.exchanged.expense_projected %}
@@ -86,6 +94,7 @@
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if account.income_current != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="account.income_current"
@@ -93,6 +102,9 @@
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
{% if account.exchanged and account.exchanged.income_current %}
<div class="text-end font-monospace tw-text-gray-500">
@@ -108,6 +120,7 @@
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if account.expense_current != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="account.expense_current"
@@ -115,6 +128,9 @@
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
{% if account.exchanged and account.exchanged.expense_current %}
<div class="text-end font-monospace tw-text-gray-500">
@@ -130,8 +146,7 @@
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
class="text-end font-monospace">
<div class="text-end font-monospace">
<c-amount.display
:amount="account.total_current"
:prefix="account.currency.prefix"

View File

@@ -10,6 +10,7 @@
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if currency.income_projected != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="currency.income_projected"
@@ -17,6 +18,9 @@
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
{% if currency.exchanged and currency.exchanged.income_projected %}
<div class="text-end font-monospace tw-text-gray-500">
@@ -33,6 +37,7 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div>
{% if currency.expense_projected != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="currency.expense_projected"
@@ -40,6 +45,9 @@
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
</div>
{% if currency.exchanged and currency.exchanged.expense_projected %}
@@ -56,8 +64,7 @@
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
class="text-end font-monospace">
<div class="text-end font-monospace">
<c-amount.display
:amount="currency.total_projected"
:prefix="currency.currency.prefix"
@@ -81,6 +88,7 @@
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if currency.income_current != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<c-amount.display
:amount="currency.income_current"
@@ -88,6 +96,9 @@
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
{% if currency.exchanged and currency.exchanged.income_current %}
<div class="text-end font-monospace tw-text-gray-500">
@@ -103,6 +114,7 @@
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if currency.expense_current != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<c-amount.display
:amount="currency.expense_current"
@@ -110,6 +122,9 @@
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"></c-amount.display>
</div>
{% else %}
<div class="text-end font-monospace">-</div>
{% endif %}
</div>
{% if currency.exchanged and currency.exchanged.expense_current %}
<div class="text-end font-monospace tw-text-gray-500">
@@ -125,8 +140,7 @@
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
class="text-end font-monospace">
<div class="text-end font-monospace">
<c-amount.display
:amount="currency.total_current"
:prefix="currency.currency.prefix"

View File

@@ -4,9 +4,9 @@
<div id="transactions-list">
{% for x in transactions_by_date %}
<div id="{{ x.grouper|slugify }}"
_="on htmx:afterSettle from #transactions if sessionStorage.getItem(my id) is null then sessionStorage.setItem(my id, 'true')">
<div class="mt-3 mb-1 w-100 tw-text-base border-bottom bg-body">
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
_="on htmx:afterSwap from #transactions if sessionStorage.getItem(my id) is null then sessionStorage.setItem(my id, 'true')">
<div class="mt-3 mb-1 w-100 tw-text-base border-bottom bg-body transactions-divider-title">
<a class="text-decoration-none d-inline-block w-100"
role="button"
data-bs-toggle="collapse"
@@ -17,15 +17,21 @@
{{ x.grouper }}
</a>
</div>
<div class="collapse" id="c-{{ x.grouper|slugify }}-collapse"
<div class="collapse transactions-divider-collapse" id="c-{{ x.grouper|slugify }}-collapse"
_="on shown.bs.collapse sessionStorage.setItem(the closest parent @id, 'true')
on hidden.bs.collapse sessionStorage.setItem(the closest parent @id, 'false')
on htmx:afterSettle from #transactions
on htmx:afterSettle from #transactions or toggle
set state to sessionStorage.getItem(the closest parent @id)
if state is 'true' or state is null
add .show to me
set @aria-expanded of #c-{{ x.grouper|slugify }}-collapsible to true
end">
else
remove .show from me
set @aria-expanded of #c-{{ x.grouper|slugify }}-collapsible to false
end
on show
add .show to me
set @aria-expanded of #c-{{ x.grouper|slugify }}-collapsible to true">
<div class="d-flex flex-column">
{% for transaction in x.list %}
<c-transaction.item

View File

@@ -256,7 +256,7 @@
<div class="col">
<c-ui.info-card color="yellow" icon="fa-solid fa-percent" title="{% trans 'Distribution' %}">
{% for p in percentages.values %}
<p class="tw-text-gray-400 mb-2 {% if not forloop.first %}mt-3{% endif %}">{{ p.currency.name }} ({{ p.currency.code }})</p>
<p class="tw-text-gray-400 mb-2 {% if not forloop.first %}mt-3{% endif %}">{{ p.currency.name }}</p>
<c-ui.percentage-distribution :percentage="p"></c-ui.percentage-distribution>
{% endfor %}
</c-ui.info-card>

View File

@@ -172,6 +172,20 @@
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
</div>
</div>
<div id="search" class="my-3">
<label class="w-100">
<input type="search" class="form-control" placeholder="Buscar" hx-preserve id="quick-search"
_="on input or search or htmx:afterSwap from window
if my value is empty
trigger toggle on <.transactions-divider-collapse/>
else
trigger show on <.transactions-divider-collapse/>
end
show <.transactions-divider-title/> when my value is empty
show <.transaction/> in <#transactions-list/>
when its textContent.toLowerCase() contains my value.toLowerCase()">
</label>
</div>
{# Transactions list#}
<div id="transactions"
class="show-loading"

View File

@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add action to transaction rule' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'update_or_create_transaction_rule_action_add' transaction_rule_id=transaction_rule_id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit transaction rule action' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'update_or_create_transaction_rule_action_edit' pk=action.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}

View File

@@ -5,80 +5,121 @@
{% block title %}{% translate 'Transaction Rule' %}{% endblock %}
{% block body %}
<div hx-get="{% url 'transaction_rule_view' transaction_rule_id=transaction_rule.id %}" hx-trigger="updated from:window" hx-target="closest .offcanvas" class="show-loading">
<div class="tw-text-2xl">{{ transaction_rule.name }}</div>
<div class="tw-text-base tw-text-gray-400">{{ transaction_rule.description }}</div>
<hr>
<div class="my-3">
<div class="tw-text-xl">{% translate 'If transaction...' %}</div>
<div class="card">
<div class="card-body">
{{ transaction_rule.trigger }}
</div>
<div class="card-footer text-end">
<a class="text-decoration-none tw-text-gray-400 p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_rule_edit' transaction_rule_id=transaction_rule.id %}"
hx-target="#generic-offcanvas">
<div hx-get="{% url 'transaction_rule_view' transaction_rule_id=transaction_rule.id %}"
hx-trigger="updated from:window" hx-target="closest .offcanvas" class="show-loading">
<div class="tw-text-2xl">{{ transaction_rule.name }}</div>
<div class="tw-text-base tw-text-gray-400">{{ transaction_rule.description }}</div>
<hr>
<div class="my-3">
<div class="tw-text-xl mb-2">{% translate 'If transaction...' %}</div>
<div class="card">
<div class="card-body">
{{ transaction_rule.trigger }}
</div>
<div class="card-footer text-end">
<a class="text-decoration-none tw-text-gray-400 p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_rule_edit' transaction_rule_id=transaction_rule.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i></a>
</div>
</div>
</div>
</div>
</div>
<div class="my-3">
<div class="tw-text-xl">{% translate 'Then...' %}</div>
{% for action in transaction_rule.transaction_actions.all %}
<div class="card mb-3">
<div class="card-body">
<div class="card mb-3">
<div class="card-header">{% translate 'Set' %}</div>
<div class="card-body">{{ action.get_field_display }}</div>
<div class="my-3">
<div class="tw-text-xl mb-2">{% translate 'Then...' %}</div>
{% for action in transaction_rule.transaction_actions.all %}
<div class="card mb-3">
<div class="card-header">
<div><span class="badge text-bg-primary">{% trans 'Edit transaction' %}</span></div>
</div>
<div class="card-body">
<div>{% translate 'Set' %} <span
class="badge text-bg-secondary">{{ action.get_field_display }}</span> {% translate 'to' %}</div>
<div class="text-bg-secondary rounded-3 mt-3 p-2">{{ action.value }}</div>
</div>
<div class="card-footer text-end">
<a class="text-decoration-none tw-text-gray-400 p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_rule_action_edit' transaction_rule_action_id=action.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i>
</a>
<a class="text-danger text-decoration-none p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'transaction_rule_action_delete' transaction_rule_action_id=action.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal">
<i class="fa-solid fa-trash fa-fw"></i>
</a>
</div>
</div>
{% endfor %}
{% for action in transaction_rule.update_or_create_transaction_actions.all %}
<div class="card mb-3">
<div class="card-header">
<div><span class="badge text-bg-primary">{% trans 'Update or create transaction' %}</span></div>
</div>
<div class="card-body">
<div>{% trans 'Edit to view' %}</div>
</div>
<div class="card-footer text-end">
<a class="text-decoration-none tw-text-gray-400 p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'update_or_create_transaction_rule_action_edit' pk=action.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i>
</a>
<a class="text-danger text-decoration-none p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'update_or_create_transaction_rule_action_delete' pk=action.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal">
<i class="fa-solid fa-trash fa-fw"></i>
</a>
</div>
</div>
{% endfor %}
{% if not transaction_rule.update_or_create_transaction_actions.all and not transaction_rule.transaction_actions.all %}
<div class="card">
<div class="card-body">
{% translate 'This rule has no actions' %}
</div>
</div>
{% endif %}
<hr>
<div class="dropdown">
<button class="btn btn-outline-primary text-decoration-none w-100" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fa-solid fa-circle-plus me-2"></i>{% translate 'Add new' %}
</button>
<ul class="dropdown-menu dropdown-menu-end w-100">
<li><a class="dropdown-item" role="link"
hx-get="{% url 'transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
hx-target="#generic-offcanvas">{% trans 'Edit Transaction' %}</a></li>
<li><a class="dropdown-item" role="link"
hx-get="{% url 'update_or_create_transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
hx-target="#generic-offcanvas">{% trans 'Update or Create Transaction' %}</a></li>
</ul>
</div>
<div class="card mb-3">
<div class="card-header">{% translate 'to' %}</div>
<div class="card-body">{{ action.value }}</div>
</div>
</div>
<div class="card-footer text-end">
<a class="text-decoration-none tw-text-gray-400 p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_rule_action_edit' transaction_rule_action_id=action.id %}"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-pencil fa-fw"></i>
</a>
<a class="text-danger text-decoration-none p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'transaction_rule_action_delete' transaction_rule_action_id=action.id %}"
hx-trigger='confirmed'
data-bypass-on-ctrl="true"
data-title="{% translate "Are you sure?" %}"
data-text="{% translate "You won't be able to revert this!" %}"
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal">
<i class="fa-solid fa-trash fa-fw"></i>
</a>
</div>
</div>
{% empty %}
<div class="card">
<div class="card-body">
{% translate 'This rule has no actions' %}
</div>
</div>
{% endfor %}
<hr>
<a class="btn btn-outline-primary text-decoration-none w-100"
hx-get="{% url 'transaction_rule_action_add' transaction_rule_id=transaction_rule.id %}"
role="button"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-circle-plus me-2"></i>{% translate 'Add new' %}
</a>
</div>
</div>
{% endblock %}

View File

@@ -12,4 +12,4 @@ done
rm -f /tmp/migrations_complete
exec python manage.py procrastinate worker
exec watchfiles --filter python "python manage.py procrastinate worker"

View File

@@ -27,3 +27,5 @@ simpleeval~=1.0.0
pydantic~=2.10.5
PyYAML~=6.0.2
mistune~=3.1.1
openpyxl~=3.1
xlrd~=2.0