Compare commits

..

14 Commits

Author SHA1 Message Date
Herculino Trotta
24a1ef2d0a fix: add encoding to qif preset 2026-01-25 16:57:00 -03:00
Herculino Trotta
163f2f4e5b Merge pull request #504 from eitchtee/dev
feat: add QIF import
2026-01-25 16:54:02 -03:00
Herculino Trotta
ede63acf5f Merge pull request #503 from eitchtee/dependabot/uv/urllib3-2.6.3
build(deps): bump urllib3 from 2.6.2 to 2.6.3
2026-01-25 16:53:41 -03:00
Herculino Trotta
a8ba3d8754 Merge pull request #500 from eitchtee/weblate
Translations update from Weblate
2026-01-25 16:53:09 -03:00
dependabot[bot]
e2f1156264 build(deps): bump urllib3 from 2.6.2 to 2.6.3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.2 to 2.6.3.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.2...2.6.3)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 19:49:42 +00:00
Herculino Trotta
d5bbad7887 feat: add QIF import 2026-01-25 16:46:56 -03:00
Andrei Kamianets
7ebacff6e4 locale(Russian): update translation
Currently translated at 25.1% (180 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-19 10:24:31 +00:00
Andrei Kamianets
df8ef5d04c locale(Russian): update translation
Currently translated at 12.6% (91 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-19 09:24:31 +00:00
Andrei Kamianets
fa2a8b8c65 locale((Russian)): added translation using Weblate 2026-01-19 08:57:52 +00:00
Juan David Afanador
e44ac5dab6 locale(Spanish): update translation
Currently translated at 100.0% (717 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/es/
2026-01-18 21:24:30 +00:00
Ebrahim Tayabali
f9261d1283 locale(Swahili): update translation
Currently translated at 0.9% (7 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/sw/
2026-01-18 19:24:30 +00:00
Ebrahim Tayabali
4c73c1cae5 locale(Swahili): update translation
Currently translated at 0.0% (0 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/sw/
2026-01-15 21:24:30 +00:00
Ebrahim Tayabali
0315a56f88 locale((Swahili)): added translation using Weblate 2026-01-15 20:56:34 +00:00
sorcierwax
44d6b8b53c locale(French): update translation
Currently translated at 100.0% (717 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2026-01-14 22:24:30 +00:00
10 changed files with 7668 additions and 103 deletions

View File

@@ -106,6 +106,17 @@ class ExcelImportSettings(BaseModel):
sheets: list[str] | str = "*"
class QIFImportSettings(BaseModel):
skip_errors: bool = Field(
default=False,
description="If True, errors during import will be logged and skipped",
)
file_type: Literal["qif"] = "qif"
importing: Literal["transactions"] = "transactions"
encoding: str = Field(default="utf-8", description="File encoding")
date_format: str = Field(..., description="Date format (e.g. %d/%m/%Y)")
class ColumnMapping(BaseModel):
source: Optional[str] | Optional[list[str]] = Field(
default=None,
@@ -342,7 +353,7 @@ class CurrencyExchangeMapping(ColumnMapping):
class ImportProfileSchema(BaseModel):
settings: CSVImportSettings | ExcelImportSettings
settings: CSVImportSettings | ExcelImportSettings | QIFImportSettings
mapping: Dict[
str,
TransactionAccountMapping

View File

@@ -3,6 +3,8 @@ import hashlib
import logging
import os
import re
import zipfile
from django.db import transaction
from datetime import datetime, date
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Literal, Union
@@ -845,6 +847,219 @@ class ImportService:
f"Invalid {self.settings.file_type.upper()} file format: {str(e)}"
)
def _parse_and_import_qif(self, content_lines: list[str], filename: str) -> None:
# Infer account from filename (remove extension)
account_name = os.path.splitext(os.path.basename(filename))[0]
current_transaction = {}
raw_lines_buffer = []
account = Account.objects.filter(name=account_name).first()
if not account:
raise ValueError(f"Account '{account_name}' not found.")
row_number = 0
for line in content_lines:
row_number += 1
line = line.strip()
if not line:
continue
raw_lines_buffer.append(line)
if line == "^":
if current_transaction:
# Deduplication using hash of raw lines
raw_content = "".join(raw_lines_buffer)
internal_id = hashlib.sha256(
raw_content.encode("utf-8")
).hexdigest()
# Reset buffer for next transaction
raw_lines_buffer = []
try:
with transaction.atomic():
if Transaction.objects.filter(
internal_id=internal_id
).exists():
self._increment_totals("skipped", 1)
self._log(
"info",
f"Skipped duplicate transaction from {filename}",
)
current_transaction = {}
continue
# Handle Account
if account:
current_transaction["account"] = account
else:
acc = Account.objects.filter(name=account_name).first()
if acc:
current_transaction["account"] = acc
else:
raise ValueError(
f"Account '{account_name}' not found."
)
current_transaction["internal_id"] = internal_id
# Handle Description/Memo mapping
if "memo" in current_transaction:
current_transaction["description"] = (
current_transaction.pop("memo")
)
# Handle Payee mapping
entities = []
if "payee" in current_transaction:
payee_name = current_transaction.pop("payee")
# "Treat the payee (P) as the entity. Use existing or create"
entity, _ = TransactionEntity.objects.get_or_create(
name=payee_name
)
entities.append(entity)
# Handle Label/Category
category = None
tags = []
if "label" in current_transaction:
label = current_transaction.pop("label")
if label.startswith("[") and label.endswith("]"):
# Transfer: set label as description, ignore category/tags
clean_label = label[1:-1]
current_transaction["description"] = clean_label
else:
parts = label.split(":")
if parts:
cat_name = parts[0].strip()
if cat_name:
category, _ = (
TransactionCategory.objects.get_or_create(
name=cat_name
)
)
if len(parts) > 1:
for tag_name in parts[1:]:
tag_name = tag_name.strip()
if tag_name:
tag, _ = (
TransactionTag.objects.get_or_create(
name=tag_name
)
)
tags.append(tag)
current_transaction["category"] = category
# Create transaction
new_trans = Transaction.objects.create(
**current_transaction
)
if entities:
new_trans.entities.set(entities)
if tags:
new_trans.tags.set(tags)
self.import_run.transactions.add(new_trans)
self._increment_totals("successful", 1)
except Exception as e:
if not self.settings.skip_errors:
raise e
self._log(
"warning",
f"Error processing transaction in {filename}: {str(e)}",
)
self._increment_totals("failed", 1)
# Reset for next transaction
current_transaction = {}
else:
# Empty transaction record (orphaned ^)
raw_lines_buffer = []
pass
self._increment_totals("processed", 1)
continue
if line.startswith("!"):
continue
code = line[0]
value = line[1:]
if code == "D":
try:
current_transaction["date"] = datetime.strptime(
value, self.settings.date_format
).date()
except ValueError:
self._log(
"warning",
f"Could not parse date '{value}' using format '{self.settings.date_format}' in {filename}",
)
if not self.settings.skip_errors:
raise ValueError(f"Invalid date format '{value}'")
elif code == "T":
try:
cleaned_value = value.replace(",", "")
amount = Decimal(cleaned_value)
if amount < 0:
current_transaction["type"] = Transaction.Type.EXPENSE
current_transaction["amount"] = abs(amount)
else:
current_transaction["type"] = Transaction.Type.INCOME
current_transaction["amount"] = amount
except InvalidOperation:
self._log(
"warning", f"Could not parse amount '{value}' in {filename}"
)
if not self.settings.skip_errors:
raise ValueError(f"Invalid amount format '{value}'")
elif code == "P":
current_transaction["payee"] = value
elif code == "M":
current_transaction["memo"] = value
elif code == "L":
current_transaction["label"] = value
elif code == "N":
pass
def _process_qif(self, file_path):
def process_logic():
if zipfile.is_zipfile(file_path):
try:
with zipfile.ZipFile(file_path, "r") as zf:
for filename in zf.namelist():
if filename.lower().endswith(
".qif"
) and not filename.startswith("__MACOSX"):
self._log(
"info", f"Processing QIF from ZIP: {filename}"
)
with zf.open(filename) as f:
content = f.read().decode(self.settings.encoding)
self._parse_and_import_qif(
content.splitlines(), filename
)
except Exception as e:
raise ValueError(f"Error processing ZIP file: {str(e)}")
else:
with open(file_path, "r", encoding=self.settings.encoding) as f:
self._parse_and_import_qif(
f.readlines(), os.path.basename(file_path)
)
if not self.settings.skip_errors:
with transaction.atomic():
process_logic()
else:
process_logic()
def _validate_file_path(self, file_path: str) -> str:
"""
Validates that the file path is within the allowed temporary directory.
@@ -871,6 +1086,8 @@ class ImportService:
self._process_csv(file_path)
elif isinstance(self.settings, version_1.ExcelImportSettings):
self._process_excel(file_path)
elif isinstance(self.settings, version_1.QIFImportSettings):
self._process_qif(file_path)
self._update_status("FINISHED")
self._log(

View File

@@ -0,0 +1,259 @@
from decimal import Decimal
import os
import shutil
from django.test import TestCase
from django.contrib.auth import get_user_model
from apps.accounts.models import Account, AccountGroup
from apps.currencies.models import Currency
from apps.common.middleware.thread_local import write_current_user, delete_current_user
from apps.import_app.models import ImportProfile, ImportRun
from apps.import_app.services.v1 import ImportService
from apps.transactions.models import (
Transaction,
)
class QIFImportTests(TestCase):
def setUp(self):
# Patch TEMP_DIR for testing
self.original_temp_dir = ImportService.TEMP_DIR
self.test_dir = os.path.abspath("temp_test_import")
ImportService.TEMP_DIR = self.test_dir
os.makedirs(self.test_dir, exist_ok=True)
# Create user and set context
User = get_user_model()
self.user = User.objects.create_user(
email="test@example.com", password="password"
)
write_current_user(self.user)
self.currency = Currency.objects.create(
code="BRL", name="Real", decimal_places=2, prefix="R$ "
)
self.group = AccountGroup.objects.create(name="Test Group", owner=self.user)
self.account = Account.objects.create(
name="bradesco-checking",
group=self.group,
currency=self.currency,
owner=self.user,
)
def tearDown(self):
delete_current_user()
ImportService.TEMP_DIR = self.original_temp_dir
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir)
def test_import_single_qif_valid_mapping(self):
content = """!Type:Bank
D04/01/2015
T8069.46
PMy Payee -> Entity
MNote -> Desc
LOld Cat:New Tag
^
D05/01/2015
T-100.00
PSupermarket
MWeekly shopping
L[Transfer]
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
service.process_file(file_path)
self.assertEqual(Transaction.objects.count(), 2)
# Transaction 1: Income, Category+Tag
t1 = Transaction.objects.get(description="Note -> Desc")
self.assertEqual(t1.amount, Decimal("8069.46"))
self.assertEqual(t1.type, Transaction.Type.INCOME)
self.assertEqual(t1.category.name, "Old Cat")
self.assertTrue(t1.tags.filter(name="New Tag").exists())
self.assertTrue(t1.entities.filter(name="My Payee -> Entity").exists())
self.assertEqual(t1.account, self.account)
# Transaction 2: Expense, Transfer ([Transfer] -> Description)
t2 = Transaction.objects.get(description="Transfer")
self.assertEqual(t2.amount, Decimal("100.00"))
self.assertEqual(t2.type, Transaction.Type.EXPENSE)
self.assertIsNone(t2.category)
self.assertFalse(t2.tags.exists())
self.assertTrue(t2.entities.filter(name="Supermarket").exists())
self.assertEqual(t2.description, "Transfer")
def test_import_deduplication_hash(self):
# Same content twice. Should result in only 1 transaction due to hash deduplication.
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
# First run
service.process_file(file_path)
self.assertEqual(Transaction.objects.count(), 1)
# Service deletes file after processing, so recreate it for second run
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
# Second run - Duplicate content
service.process_file(file_path)
self.assertEqual(Transaction.objects.count(), 1)
def test_import_strict_error_rollback(self):
# atomic check.
# Transaction 1 valid, Transaction 2 invalid date.
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
DINVALID
T100.00
PBad
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
skip_errors: false
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
with self.assertRaises(Exception) as cm:
service.process_file(file_path)
self.assertEqual(str(cm.exception), "Import failed")
# Should be 0 transactions because of atomic rollback
self.assertEqual(Transaction.objects.count(), 0)
def test_import_missing_account(self):
# File with account name that doesn't exist
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
"""
filename = "missing-account.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
# Should fail because account doesn't exist
with self.assertRaises(Exception) as cm:
service.process_file(file_path)
self.assertEqual(str(cm.exception), "Import failed")
def test_import_skip_errors(self):
# skip_errors: true.
# Transaction 1 valid, Transaction 2 invalid date.
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
DINVALID
T100.00
PBad
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
skip_errors: true
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
service.process_file(file_path)
# Should be 1 transaction (valid one)
self.assertEqual(Transaction.objects.count(), 1)
self.assertEqual(
Transaction.objects.first().description, ""
) # empty desc if no memo

View File

@@ -0,0 +1,10 @@
settings:
file_type: qif
importing: transactions
encoding: cp1252
date_format: "%d/%m/%Y"
skip_errors: true
mapping: {}
deduplicate: []

View File

@@ -0,0 +1,7 @@
{
"author": "eitchtee",
"description": "Standard QIF Import. Mapping is automatic.",
"schema_version": 1,
"name": "Standard QIF",
"message": "Account is inferred from filename (e.g., 'Checking.qif' -> Account 'Checking').\nYou might need to change the date format to match the date format on your file."
}

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
"PO-Revision-Date: 2025-12-16 05:24+0000\n"
"Last-Translator: BRodolfo <simplysmartbydesign@gmail.com>\n"
"PO-Revision-Date: 2026-01-18 21:24+0000\n"
"Last-Translator: Juan David Afanador <juanafanador07@gmail.com>\n"
"Language-Team: Spanish <https://translations.herculino.com/projects/wygiwyh/"
"app/es/>\n"
"Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.14.3\n"
"X-Generator: Weblate 5.15.1\n"
#: apps/accounts/forms.py:24
msgid "Group name"
@@ -1353,10 +1353,8 @@ msgid "Transaction Type"
msgstr "Tipo de Transacción"
#: apps/transactions/filters.py:89
#, fuzzy
#| msgid "Status"
msgid "Mute Status"
msgstr "Estado"
msgstr "Silenciada"
#: apps/transactions/filters.py:94
msgid "Date from"
@@ -2696,8 +2694,8 @@ msgstr "Última sincronización"
#, python-format
msgid "%(counter)s consecutive failure"
msgid_plural "%(counter)s consecutive failures"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(counter)s fallo consecutivo"
msgstr[1] "%(counter)s fallos consecutivos"
#: templates/exchange_rates_services/fragments/list.html:69
msgid "currencies"
@@ -3012,72 +3010,60 @@ msgstr "No hay transacciones recientes"
#: templates/insights/fragments/month_by_month.html:86
#: templates/insights/fragments/year_by_year.html:54
#, fuzzy
#| msgid "Tags"
msgid "Tag"
msgstr "Etiquetas"
msgstr "Etiqueta"
#: templates/insights/fragments/month_by_month.html:94
msgid "Jan"
msgstr ""
msgstr "Ene"
#: templates/insights/fragments/month_by_month.html:95
msgid "Feb"
msgstr ""
msgstr "Feb"
#: templates/insights/fragments/month_by_month.html:96
#, fuzzy
#| msgid "Max"
msgid "Mar"
msgstr "Máximo"
msgstr "Mar"
#: templates/insights/fragments/month_by_month.html:97
msgid "Apr"
msgstr ""
msgstr "Abr"
#: templates/insights/fragments/month_by_month.html:98
#, fuzzy
#| msgid "Max"
msgid "May"
msgstr "Máximo"
msgstr "May"
#: templates/insights/fragments/month_by_month.html:99
msgid "Jun"
msgstr ""
msgstr "Jun"
#: templates/insights/fragments/month_by_month.html:100
msgid "Jul"
msgstr ""
msgstr "Jul"
#: templates/insights/fragments/month_by_month.html:101
msgid "Aug"
msgstr ""
msgstr "Ago"
#: templates/insights/fragments/month_by_month.html:102
#, fuzzy
#| msgid "Set"
msgid "Sep"
msgstr "Establecer"
msgstr "Sep"
#: templates/insights/fragments/month_by_month.html:103
msgid "Oct"
msgstr ""
msgstr "Oct"
#: templates/insights/fragments/month_by_month.html:104
#, fuzzy
#| msgid "Now"
msgid "Nov"
msgstr "Ahora"
msgstr "Nov"
#: templates/insights/fragments/month_by_month.html:105
msgid "Dec"
msgstr ""
msgstr "Dic"
#: templates/insights/fragments/month_by_month.html:248
#, fuzzy
#| msgid "No transactions on this date"
msgid "No transactions for this year"
msgstr "No hay transacciones en esta fecha"
msgstr "No hay transacciones en este año"
#: templates/insights/fragments/sankey.html:100
msgid "From"
@@ -3088,10 +3074,8 @@ msgid "Percentage"
msgstr "Porcentaje"
#: templates/insights/fragments/year_by_year.html:202
#, fuzzy
#| msgid "transactions"
msgid "No transactions"
msgstr "transacciones"
msgstr "No hay transacciones"
#: templates/insights/pages/index.html:37
msgid "Month"
@@ -3143,14 +3127,12 @@ msgid "Emergency Fund"
msgstr "Fondo de Emergencia"
#: templates/insights/pages/index.html:127
#, fuzzy
#| msgid "Yearly by account"
msgid "Year by Year"
msgstr "Anual por cuenta"
msgstr "Año por año"
#: templates/insights/pages/index.html:132
msgid "Month by Month"
msgstr ""
msgstr "Mes por mes"
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
@@ -3228,7 +3210,7 @@ msgstr "Ítem"
#: templates/monthly_overview/fragments/list.html:15
#: templates/transactions/fragments/list_all.html:15
msgid "late"
msgstr ""
msgstr "Transacciones tardías"
#: templates/monthly_overview/fragments/list.html:58
msgid "No transactions this month"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
"PO-Revision-Date: 2025-10-07 20:17+0000\n"
"Last-Translator: Erwan Colin <zephone@protonmail.com>\n"
"PO-Revision-Date: 2026-01-14 22:24+0000\n"
"Last-Translator: sorcierwax <freakywax@gmail.com>\n"
"Language-Team: French <https://translations.herculino.com/projects/wygiwyh/"
"app/fr/>\n"
"Language: fr\n"
@@ -17,11 +17,11 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.13.3\n"
"X-Generator: Weblate 5.15.1\n"
#: apps/accounts/forms.py:24
msgid "Group name"
msgstr "Nom de groupe"
msgstr "Nom du groupe"
#: apps/accounts/forms.py:39 apps/accounts/forms.py:105
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
@@ -1097,7 +1097,7 @@ msgstr "Si..."
#: apps/rules/forms.py:53
msgid "You can add actions to this rule in the next screen."
msgstr ""
msgstr "Vous pouvez ajouter des actions à cette règle dans le prochain écran."
#: apps/rules/forms.py:76
msgid "Set field"
@@ -1357,10 +1357,8 @@ msgid "Transaction Type"
msgstr "Type de transaction"
#: apps/transactions/filters.py:89
#, fuzzy
#| msgid "Status"
msgid "Mute Status"
msgstr "Statut"
msgstr "Ignorer le statut"
#: apps/transactions/filters.py:94
msgid "Date from"
@@ -2420,12 +2418,10 @@ msgstr "Tout désélectionner"
#: templates/cotton/ui/deleted_transactions_action_bar.html:46
msgid "Invert election"
msgstr ""
msgstr "Inverser la sélection"
#: templates/cotton/ui/deleted_transactions_action_bar.html:59
#: templates/cotton/ui/transactions_action_bar.html:84
#, fuzzy
#| msgid "Yes, delete them!"
msgid "Yes, delete them!"
msgstr "Oui, supprime !"
@@ -2443,8 +2439,6 @@ msgstr "Oui, supprime !"
#: templates/cotton/ui/transactions_action_bar.html:191
#: templates/cotton/ui/transactions_action_bar.html:207
#: templates/cotton/ui/transactions_action_bar.html:223
#, fuzzy
#| msgid "copied!"
msgid "copied!"
msgstr "Copié !"
@@ -2479,10 +2473,8 @@ msgid "Count"
msgstr "Compteur"
#: templates/cotton/ui/percentage_distribution.html:4
#, fuzzy
#| msgid "Income/Expense by Account"
msgid "Income and Expense Percentages"
msgstr "Revenu/Dépense par comptes"
msgstr "Pourcentage de revenus et dépenses"
#: templates/cotton/ui/quick_transactions_buttons.html:25
#: templates/cotton/ui/transactions_fab.html:27
@@ -2501,7 +2493,7 @@ msgstr "Balance"
#: templates/cotton/ui/transactions_action_bar.html:46
msgid "Invert selection"
msgstr ""
msgstr "Inverser la sélection"
#: templates/cotton/ui/transactions_action_bar.html:66
msgid "Mark as unpaid"
@@ -2711,8 +2703,8 @@ msgstr "Dernière récupération"
#, python-format
msgid "%(counter)s consecutive failure"
msgid_plural "%(counter)s consecutive failures"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(counter)s erreur consécutive"
msgstr[1] "%(counter)s erreurs consécutives"
#: templates/exchange_rates_services/fragments/list.html:69
msgid "currencies"
@@ -2823,7 +2815,7 @@ msgstr "Activer la navigation"
#: templates/includes/navbar/user_menu.html:4
msgid "Toggle theme"
msgstr ""
msgstr "Basculer le thème"
#: templates/includes/navbar/user_menu.html:31
msgid "Settings"
@@ -3031,72 +3023,60 @@ msgstr "Aucunes transactions récentes"
#: templates/insights/fragments/month_by_month.html:86
#: templates/insights/fragments/year_by_year.html:54
#, fuzzy
#| msgid "Tags"
msgid "Tag"
msgstr "Etiquettes"
msgstr "Etiquette"
#: templates/insights/fragments/month_by_month.html:94
msgid "Jan"
msgstr ""
msgstr "Jan"
#: templates/insights/fragments/month_by_month.html:95
msgid "Feb"
msgstr ""
msgstr "Fév"
#: templates/insights/fragments/month_by_month.html:96
#, fuzzy
#| msgid "Max"
msgid "Mar"
msgstr "Max"
msgstr "Mar"
#: templates/insights/fragments/month_by_month.html:97
msgid "Apr"
msgstr ""
msgstr "Avr"
#: templates/insights/fragments/month_by_month.html:98
#, fuzzy
#| msgid "Max"
msgid "May"
msgstr "Max"
msgstr "Mai"
#: templates/insights/fragments/month_by_month.html:99
msgid "Jun"
msgstr ""
msgstr "Juin"
#: templates/insights/fragments/month_by_month.html:100
msgid "Jul"
msgstr ""
msgstr "Jui"
#: templates/insights/fragments/month_by_month.html:101
msgid "Aug"
msgstr ""
msgstr "Aou"
#: templates/insights/fragments/month_by_month.html:102
#, fuzzy
#| msgid "Set"
msgid "Sep"
msgstr "Définir"
msgstr "Sep"
#: templates/insights/fragments/month_by_month.html:103
msgid "Oct"
msgstr ""
msgstr "Oct"
#: templates/insights/fragments/month_by_month.html:104
#, fuzzy
#| msgid "Now"
msgid "Nov"
msgstr "Maintenant"
msgstr "Nov"
#: templates/insights/fragments/month_by_month.html:105
msgid "Dec"
msgstr ""
msgstr "Déc"
#: templates/insights/fragments/month_by_month.html:248
#, fuzzy
#| msgid "No transactions on this date"
msgid "No transactions for this year"
msgstr "Aucunes transactions à cette date"
msgstr "Aucunes transactions pour cette année"
#: templates/insights/fragments/sankey.html:100
msgid "From"
@@ -3107,10 +3087,8 @@ msgid "Percentage"
msgstr "Pourcentage"
#: templates/insights/fragments/year_by_year.html:202
#, fuzzy
#| msgid "transactions"
msgid "No transactions"
msgstr "transactions"
msgstr "Pas de transactions"
#: templates/insights/pages/index.html:37
msgid "Month"
@@ -3162,14 +3140,12 @@ msgid "Emergency Fund"
msgstr "Fonds de secours"
#: templates/insights/pages/index.html:127
#, fuzzy
#| msgid "Yearly by account"
msgid "Year by Year"
msgstr "Annuel par comptes"
msgstr "Année par année"
#: templates/insights/pages/index.html:132
msgid "Month by Month"
msgstr ""
msgstr "Mois par mois"
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
@@ -3249,7 +3225,7 @@ msgstr "Eléments"
#: templates/monthly_overview/fragments/list.html:15
#: templates/transactions/fragments/list_all.html:15
msgid "late"
msgstr ""
msgstr "En retard"
#: templates/monthly_overview/fragments/list.html:58
msgid "No transactions this month"
@@ -3515,10 +3491,8 @@ msgid "This rule has no actions"
msgstr "Cette règle n'a pas d'actions"
#: templates/rules/fragments/transaction_rule/view.html:140
#, fuzzy
#| msgid "Add notes to transactions"
msgid "Add new action"
msgstr "Ajouter des notes aux transactions"
msgstr "Ajouter une nouvelle action"
#: templates/rules/fragments/transaction_rule/view.html:145
msgid "Edit Transaction"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

6
uv.lock generated
View File

@@ -1151,11 +1151,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.6.2"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]