Files
WYGIWYH/app/apps/import_app/models.py
T
Asish Kumar 1ed5dc5d31 feat(import): pick the account when running an import
An import run can now be given an account, and every transaction it
creates goes into that account instead of the one the profile maps. This
lets a single profile serve several accounts at the same bank, for
statements that only name the account in a header cell or not at all,
instead of keeping one copy of the profile per account.

- ImportRun gets an optional `account` (SET_NULL), shown on the run card.
- The upload form offers the user's active accounts when the profile
  imports transactions, and leaves the field out for profiles that import
  accounts, currencies, categories, tags or entities.
- CSV and Excel rows take the run's account before deduplication, so
  rules comparing `account` check against the picked account. The
  profile's account mapping is skipped for these rows.
- QIF files use the run's account instead of the account named after the
  file.
- The import API accepts an optional `account_id`, limited to accounts
  the user can access, rejected for profiles that don't import
  transactions, and returns `account` on import runs.

Leaving the account empty keeps the current behaviour.

Closes #538
2026-11-11 22:41:37 +05:30

121 lines
3.7 KiB
Python

import yaml
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.common.models import (
OwnedObject,
OwnedObjectManager,
SharedObject,
SharedObjectManager,
)
from apps.import_app.schemas import version_1
class ImportProfile(SharedObject):
class Versions(models.IntegerChoices):
VERSION_1 = 1, "Version 1"
name = models.CharField(max_length=100, verbose_name=_("Name"))
yaml_config = models.TextField(verbose_name=_("YAML Configuration"))
version = models.IntegerField(
choices=Versions,
default=Versions.VERSION_1,
verbose_name=_("Version"),
)
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
def __str__(self):
return self.name
class Meta:
ordering = ["name"]
def get_version_display(self):
version_number = self.Versions(self.version).name.split("_")[1]
return _("Version {number}").format(number=version_number)
@property
def imports_transactions(self):
"""Whether this profile creates transactions, as opposed to accounts, tags, etc."""
try:
config = version_1.ImportProfileSchema(**yaml.safe_load(self.yaml_config))
except Exception:
return False
return config.settings.importing == "transactions"
def clean(self):
if self.version and self.version == self.Versions.VERSION_1:
try:
yaml_data = yaml.safe_load(self.yaml_config)
version_1.ImportProfileSchema(**yaml_data)
except Exception as e:
raise ValidationError(
{"yaml_config": _("Invalid YAML Configuration: ") + str(e)}
)
class ImportRun(OwnedObject):
class Status(models.TextChoices):
QUEUED = "QUEUED", _("Queued")
PROCESSING = "PROCESSING", _("Processing")
FAILED = "FAILED", _("Failed")
FINISHED = "FINISHED", _("Finished")
status = models.CharField(
max_length=10,
choices=Status,
default=Status.QUEUED,
verbose_name=_("Status"),
)
profile = models.ForeignKey(
ImportProfile,
on_delete=models.CASCADE,
)
file_name = models.CharField(
max_length=10000,
help_text=_("File name"),
)
account = models.ForeignKey(
"accounts.Account",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="import_runs",
verbose_name=_("Account"),
help_text=_(
"Import every transaction into this account. Leave empty to let the "
"profile pick the account."
),
)
transactions = models.ManyToManyField(
"transactions.Transaction", related_name="import_runs"
)
tags = models.ManyToManyField(
"transactions.TransactionTag", related_name="import_runs"
)
categories = models.ManyToManyField(
"transactions.TransactionCategory", related_name="import_runs"
)
entities = models.ManyToManyField(
"transactions.TransactionEntity", related_name="import_runs"
)
currencies = models.ManyToManyField(
"currencies.Currency", related_name="import_runs"
)
logs = models.TextField(blank=True)
processed_rows = models.IntegerField(default=0)
total_rows = models.IntegerField(default=0)
successful_rows = models.IntegerField(default=0)
skipped_rows = models.IntegerField(default=0)
failed_rows = models.IntegerField(default=0)
started_at = models.DateTimeField(null=True)
finished_at = models.DateTimeField(null=True)
objects = OwnedObjectManager()
all_objects = models.Manager() # Unfiltered manager