feat: update finance models
Browse files- finance/models.py +147 -1
finance/models.py
CHANGED
|
@@ -1,3 +1,149 @@
|
|
| 1 |
from django.db import models
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from django.db import models
|
| 2 |
+
from django.contrib.auth import get_user_model
|
| 3 |
|
| 4 |
+
User = get_user_model()
|
| 5 |
+
|
| 6 |
+
class Transaction(models.Model):
|
| 7 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions')
|
| 8 |
+
date = models.DateField()
|
| 9 |
+
merchant = models.CharField(max_length=255)
|
| 10 |
+
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
| 11 |
+
category = models.CharField(max_length=100)
|
| 12 |
+
category_key = models.CharField(max_length=50)
|
| 13 |
+
time = models.TimeField(null=True, blank=True)
|
| 14 |
+
payment_method = models.CharField(max_length=100, null=True, blank=True)
|
| 15 |
+
note = models.TextField(null=True, blank=True)
|
| 16 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 17 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 18 |
+
|
| 19 |
+
class Meta:
|
| 20 |
+
ordering = ['-date', '-time', '-created_at']
|
| 21 |
+
|
| 22 |
+
def __str__(self):
|
| 23 |
+
return f"{self.merchant} - {self.amount}"
|
| 24 |
+
|
| 25 |
+
class Budget(models.Model):
|
| 26 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='budgets')
|
| 27 |
+
name = models.CharField(max_length=100)
|
| 28 |
+
category_key = models.CharField(max_length=50)
|
| 29 |
+
budget_amount = models.DecimalField(max_digits=12, decimal_places=2)
|
| 30 |
+
spent_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
| 31 |
+
remaining_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
| 32 |
+
status = models.CharField(max_length=50, default='good') # good, warning, danger
|
| 33 |
+
warning_message = models.TextField(null=True, blank=True)
|
| 34 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 35 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 36 |
+
|
| 37 |
+
def save(self, *args, **kwargs):
|
| 38 |
+
self.remaining_amount = self.budget_amount - self.spent_amount
|
| 39 |
+
percentage = (self.spent_amount / self.budget_amount) * 100 if self.budget_amount > 0 else 0
|
| 40 |
+
|
| 41 |
+
if percentage >= 90:
|
| 42 |
+
self.status = 'danger'
|
| 43 |
+
elif percentage >= 70:
|
| 44 |
+
self.status = 'warning'
|
| 45 |
+
else:
|
| 46 |
+
self.status = 'good'
|
| 47 |
+
|
| 48 |
+
super().save(*args, **kwargs)
|
| 49 |
+
|
| 50 |
+
def __str__(self):
|
| 51 |
+
return f"{self.name} Budget - {self.user.email}"
|
| 52 |
+
|
| 53 |
+
class Goal(models.Model):
|
| 54 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='goals')
|
| 55 |
+
name = models.CharField(max_length=255)
|
| 56 |
+
current_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
| 57 |
+
target_amount = models.DecimalField(max_digits=12, decimal_places=2)
|
| 58 |
+
deadline = models.DateField(null=True, blank=True)
|
| 59 |
+
monthly_target = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
| 60 |
+
priority = models.CharField(max_length=50, default='medium') # low, medium, high
|
| 61 |
+
category = models.CharField(max_length=100, null=True, blank=True)
|
| 62 |
+
auto_save = models.BooleanField(default=False)
|
| 63 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 64 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 65 |
+
|
| 66 |
+
def __str__(self):
|
| 67 |
+
return f"{self.name} - {self.user.email}"
|
| 68 |
+
|
| 69 |
+
class Subscription(models.Model):
|
| 70 |
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='subscriptions')
|
| 71 |
+
name = models.CharField(max_length=255)
|
| 72 |
+
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
| 73 |
+
renewal_date = models.DateField(null=True, blank=True)
|
| 74 |
+
usage_level = models.CharField(max_length=50, default='Medium')
|
| 75 |
+
value_score = models.IntegerField(default=3) # 1 to 5
|
| 76 |
+
status = models.CharField(max_length=50, default='active') # active, unused
|
| 77 |
+
last_used_date = models.DateField(null=True, blank=True)
|
| 78 |
+
warning = models.TextField(null=True, blank=True)
|
| 79 |
+
potential_saving = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
| 80 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 81 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 82 |
+
|
| 83 |
+
def __str__(self):
|
| 84 |
+
return f"{self.name} - {self.user.email}"
|
| 85 |
+
|
| 86 |
+
from django.db.models.signals import pre_save, post_save, post_delete
|
| 87 |
+
from django.dispatch import receiver
|
| 88 |
+
from django.db.models import Sum
|
| 89 |
+
from users.models import FinancialProfile
|
| 90 |
+
|
| 91 |
+
@receiver(pre_save, sender=Transaction)
|
| 92 |
+
def store_original_amount(sender, instance, **kwargs):
|
| 93 |
+
if instance.pk:
|
| 94 |
+
try:
|
| 95 |
+
original = Transaction.objects.get(pk=instance.pk)
|
| 96 |
+
instance._original_amount = original.amount
|
| 97 |
+
except Transaction.DoesNotExist:
|
| 98 |
+
instance._original_amount = 0
|
| 99 |
+
else:
|
| 100 |
+
instance._original_amount = 0
|
| 101 |
+
|
| 102 |
+
@receiver(post_save, sender=Transaction)
|
| 103 |
+
def update_profile_balances(sender, instance, created, **kwargs):
|
| 104 |
+
try:
|
| 105 |
+
profile, _ = FinancialProfile.objects.get_or_create(user=instance.user)
|
| 106 |
+
if created:
|
| 107 |
+
profile.cash_available += instance.amount
|
| 108 |
+
profile.net_worth += instance.amount
|
| 109 |
+
else:
|
| 110 |
+
original_amount = getattr(instance, '_original_amount', 0)
|
| 111 |
+
diff = instance.amount - original_amount
|
| 112 |
+
profile.cash_available += diff
|
| 113 |
+
profile.net_worth += diff
|
| 114 |
+
profile.save()
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print("Error in update_profile_balances signal:", e)
|
| 117 |
+
|
| 118 |
+
@receiver(post_delete, sender=Transaction)
|
| 119 |
+
def update_profile_balances_on_delete(sender, instance, **kwargs):
|
| 120 |
+
try:
|
| 121 |
+
profile, _ = FinancialProfile.objects.get_or_create(user=instance.user)
|
| 122 |
+
profile.cash_available -= instance.amount
|
| 123 |
+
profile.net_worth -= instance.amount
|
| 124 |
+
profile.save()
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print("Error in update_profile_balances_on_delete signal:", e)
|
| 127 |
+
|
| 128 |
+
@receiver(post_save, sender=Transaction)
|
| 129 |
+
@receiver(post_delete, sender=Transaction)
|
| 130 |
+
def update_budget_spent(sender, instance, **kwargs):
|
| 131 |
+
try:
|
| 132 |
+
cat_key = instance.category_key
|
| 133 |
+
if cat_key == 'dining' or cat_key == 'food':
|
| 134 |
+
budgets = Budget.objects.filter(user=instance.user, category_key__in=['dining', 'food'])
|
| 135 |
+
else:
|
| 136 |
+
budgets = Budget.objects.filter(user=instance.user, category_key=cat_key)
|
| 137 |
+
|
| 138 |
+
for budget in budgets:
|
| 139 |
+
filter_keys = ['dining', 'food'] if cat_key in ['dining', 'food'] else [cat_key]
|
| 140 |
+
expenses_sum = Transaction.objects.filter(
|
| 141 |
+
user=instance.user,
|
| 142 |
+
category_key__in=filter_keys,
|
| 143 |
+
amount__lt=0
|
| 144 |
+
).aggregate(total=Sum('amount'))['total'] or 0
|
| 145 |
+
|
| 146 |
+
budget.spent_amount = abs(expenses_sum)
|
| 147 |
+
budget.save()
|
| 148 |
+
except Exception as e:
|
| 149 |
+
print("Error in update_budget_spent signal:", e)
|