Source code for richy.transactions.models

import functools
import shutil
from pathlib import Path

from django.conf import settings
from django.db import models
from django.template.defaultfilters import date
from django.utils.functional import cached_property

from ..core.math import calc_percentage_change
from ..core.models import (
    Item,
    UserItem,
    UserRelatedManager,
    UserRelatedModel,
    UserRelatedQuerySet,
)
from ..etfs.models import Dividend as EtfDividend
from ..shares.models import Dividend as ShareDividend


def get_attachment_dir(instance, filename):
    return Path("transactions") / str(instance.transaction.pk) / filename


def get_staking_attachment_dir(instance, filename):
    return Path("stakings") / str(instance.staking.pk) / filename


[docs] class Exchange(models.Model): """ Exchange model. """ title = models.CharField(max_length=50, unique=True) def __str__(self): return self.title
[docs] class TransactionQuerySet(UserRelatedQuerySet): def positive_balance(self): """ Returns only those transactions it's item balance is higher than 0 - currently hold items. Not closed (is_closed = False) transaction can have 0 balance in case the item was bought and then exchanged for another one (cryptocurrencies). :return: Queryset filtering only open transactions. :rtype: models.QuerySet """ return self.extra( where=[ "(select sum(amount) from transactions_transaction tr where tr.item_id = transactions_transaction.item_id) > 0" ] ).all()
[docs] class TransactionManager(UserRelatedManager): """ ORM manager for Transaction model. """ def get_queryset(self): return TransactionQuerySet(self.model) def positive_balance(self): return self.get_queryset().positive_balance()
[docs] class Transaction(UserRelatedModel): """ Transaction model. """ # TODO: item shouldnt be allowed to delete in case of related transactions. parents = models.ManyToManyField("self", symmetrical=False, blank=True) item = models.ForeignKey(Item, on_delete=models.CASCADE) price = models.FloatField() amount = models.FloatField() fee = models.FloatField() date = models.DateField() exchange = models.ForeignKey( Exchange, blank=True, null=True, on_delete=models.PROTECT ) currency = models.CharField(max_length=50, default="USD") target = models.CharField(max_length=50, blank=True) is_closing = models.BooleanField(default=False, db_index=True) is_closed = models.BooleanField(default=False, db_index=True) is_deposit = models.BooleanField(default=False, db_index=True) note = models.TextField(blank=True, null=True) closed_on = models.DateField(blank=True, null=True) objects = TransactionManager() @functools.cache def __str__(self): base = f"{date(self.date)} {self.amount:+} {self.item.symbol}" if self.exchange: base += f" ({self.exchange})" return base
[docs] def get_market_value(self): """ Calculates current market price of assets gained (bought) in this transaction (applies only for positive (buy) transactions). :raises Exception: In case of negative transaction. :return: Market price based on last known item price :rtype: int or float """ if 0 < self.amount: last_price = self.item.price_set.last() if last_price: return self.amount * last_price.price return 0 raise Exception( "Asking for market value for negative transaction doesn't make sense." )
[docs] def get_value(self): """ Calculates market price of assets bought or sold. The price is negative for sell transactions. :return: Price of whole transaction. :rtype: int or float """ return self.amount * self.price
[docs] def delete(self, *args, **kwargs): """ Removes whole attachments dir. """ shutil.rmtree( Path(settings.MEDIA_ROOT) / "transactions" / str(self.pk), ignore_errors=True, ) return super().delete(*args, **kwargs)
@cached_property def is_positive(self): return 0 < self.amount @cached_property def target_as_absolute_value(self): if not self.target: return if "%" in self.target: return self.price * (1 + (float(self.target.strip("%")) / 100)) return float(self.target) @cached_property def target_as_percents(self): if not self.target: return if "%" not in self.target: return calc_percentage_change(float(self.target.strip("%")), self.price) return float(self.target.strip("%"))
[docs] def get_user_item(self): return UserItem.objects.get(user=self.user, item=self.item)
[docs] class BaseDividendTransaction(UserRelatedModel): shares = models.PositiveIntegerField() amount = models.FloatField() transactions = models.ManyToManyField(Transaction) class Meta: abstract = True
[docs] class ShareDividendTransaction(BaseDividendTransaction): dividend = models.ForeignKey(ShareDividend, on_delete=models.CASCADE)
[docs] class EtfDividendTransaction(BaseDividendTransaction): dividend = models.ForeignKey(EtfDividend, on_delete=models.CASCADE)
[docs] class AttachmentMixin: def delete(self, *args, **kwargs): self.file.delete() return super().delete(*args, **kwargs)
[docs] class Attachment(models.Model, AttachmentMixin): transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE) file = models.FileField(upload_to=get_attachment_dir)