Ticket #26189: models__piece.py

File models__piece.py, 7.7 KB (added by Leonard Kioi kinyanjui, 9 years ago)
Line 
1class Transaction(models.Model):
2 """
3 Financial tracking entry. Either expense, income or transfer between two
4 accounts.
5
6 user - owner of the transaction
7 date - date when transaction occurred
8 type - one of 3: 'EXPENSES', 'INCOME' or 'TRANSFER'
9 amount - amount of money in transaction
10 currency - currency that was used for the transaction
11 amount_primary - amount in user's primary currency
12 category and sub_category - ids for category and sub_category if the are
13 present
14 account - account that was used in transaction
15 source_account - if transfer, money go from it to `account`
16 tags - another way to categorize transactions. Transaction may only have
17 one category but as many tags as needed
18 project - yet another way to categorize transactions and share them (TODO)
19 note - optional user's note
20 recurrence - repeat config for repeating transactions
21 recurrence_parent - link to original transaction for repeating
22 transactions
23 exchange_rate - json data
24 updated_at - when transaction was edited last time
25 created_at - when transaction was created
26 """
27
28 EXPENSES, INCOME, TRANSFER = 'EXPENSES', 'INCOME', 'TRANSFER'
29 TRANSACTION_TYPE_CHOICES = (
30 (EXPENSES, EXPENSES),
31 (INCOME, INCOME),
32 (TRANSFER, TRANSFER),
33 )
34
35 user = models.ForeignKey(User)
36 date = models.DateTimeField(db_index=True)
37 type = models.CharField(
38 choices=TRANSACTION_TYPE_CHOICES,
39 default=EXPENSES,
40 max_length=10,
41 db_index=True
42 )
43 amount = models.DecimalField(max_digits=12, decimal_places=2)
44 currency = models.ForeignKey(Currency)
45 amount_primary = models.DecimalField(max_digits=12, decimal_places=2,
46 null=True, blank=True)
47 category = models.ForeignKey(
48 Category,
49 null=True,
50 blank=True,
51 on_delete=models.SET_NULL
52 )
53 sub_category = models.ForeignKey(SubCategory, null=True, blank=True,
54 on_delete=models.SET_NULL)
55 source_account = models.ForeignKey(
56 Account,
57 null=True,
58 blank=True,
59 related_name='source_transactions',
60 on_delete=models.SET_NULL
61 )
62 account = models.ForeignKey(Account, related_name='account_transactions')
63 tags = models.ManyToManyField('Tag', null=True, blank=True,
64 related_name='transactions')
65 project = models.ForeignKey(Project, blank=True, null=True)
66 note = models.TextField(null=True, blank=True)
67 recurrence = models.ForeignKey('Recurrence', null=True, blank=True,
68 on_delete=models.SET_NULL)
69 recurrence_parent = models.ForeignKey(
70 'Transaction',
71 null=True,
72 blank=True,
73 on_delete=models.SET_NULL
74 )
75 exchange_rate = models.ForeignKey('ExchangeRate')
76 updated_at = models.DateTimeField(
77 auto_now=True,
78 null=True,
79 blank=True,
80 db_index=True
81 )
82 created_at = models.DateTimeField(
83 auto_now_add=True,
84 blank=True,
85 null=True,
86 db_index=True
87 )
88
89 _original_amount = None
90 _original_currency = None
91
92 class Meta:
93 ordering = ('-date', '-id')
94
95 def __init__(self, *args, **kwargs):
96 super().__init__(*args, **kwargs)
97 self._original_amount = self.amount
98 self._original_currency = getattr(self, 'currency', None)
99
100 def save(self, *args, **kwargs):
101 """
102 Check if amount or currency have changed and recalculate
103 primary_amount if that's the case.
104
105 Check if sub_category and category fields don't match and
106 fix it (with priority to sub_category)
107 """
108 if (
109 self.amount_primary is None or
110 self.amount != self._original_amount or
111 self.currency != self._original_currency
112 ):
113 self.amount_primary = self.primary_amount()
114
115 if self.sub_category is not None:
116 if self.category != self.sub_category.category:
117 self.category = self.sub_category.category
118
119 super().save(*args, **kwargs)
120 self._original_amount = self.amount
121 self._original_currency = self.currency
122
123 def clean(self):
124 """
125 Override Transaction's clean method to do validation regarding
126 source_account
127 """
128 super(Transaction, self).clean()
129
130 if self.type == Transaction.TRANSFER and self.source_account is None:
131 raise exceptions.ValidationError({
132 'source_account': ['This has to be set when type == ' +
133 str(self.TRANSFER)]
134 })
135 elif self.source_account == self.account:
136 raise exceptions.ValidationError({
137 'source_account': ['This cannot be the same as account']
138 })
139
140 # Check that all items associated with the transaction belong to the
141 # user associated aith the transaction
142 if self.sub_category and self.sub_category.user != self.user:
143 raise exceptions.ValidationError({
144 'sub_category': ['belongs to different user']
145 })
146
147 if self.source_account and self.source_account.user != self.user:
148 raise exceptions.ValidationError({
149 'source_account': ['belongs to different user']
150 })
151
152 if self.account and self.account.user != self.user:
153 raise exceptions.ValidationError({
154 'account': ['belongs to different user']
155 })
156
157 if self.project and self.project.user != self.user:
158 raise exceptions.ValidationError({
159 'project': ['belongs to different user']
160 })
161
162 if self.category and self.category.user != self.user:
163 raise exceptions.ValidationError({
164 'category': ['belongs to different user']
165 })
166
167 # Set the project to standard project if
168 # user does not explicitly set it
169 if not self.project:
170 project = Project.objects.filter(user=self.user,
171 default=True).first()
172 if not project:
173 raise exceptions.ValidationError('Missing default project')
174
175 self.project = project
176
177 def primary_amount(self):
178 """
179 Return the user's amount in his/her primary currency
180 """
181 if self.user.preferences.primary_currency is None:
182 return decimal.Decimal(0)
183
184 if self.currency.code == self.user.preferences.primary_currency.code:
185 return decimal.Decimal(self.amount)
186
187 current_rate = decimal.Decimal(
188 self.exchange_rate.rates[self.currency.code]
189 )
190
191 amount = decimal.Decimal(self.amount)
192
193 current_usd_amount = amount/current_rate
194
195 currency_code = self.user.preferences.primary_currency.code
196
197 return current_usd_amount * decimal.Decimal(
198 self.exchange_rate.rates[currency_code]
199 )
200
201 def primary_currency(self):
202 """
203 Return the user's primary currency
204 """
205 return self.user.preferences.primary_currency
206
207 def __str__(self):
208 return self.user.get_full_name() + ' : ' + str(self.amount)
209
210 @property
211 def is_income(self, ):
212 """
213 Is this transaction an income?
214 """
215 return self.type == self.INCOME
216
217 @property
218 def is_expense(self, ):
219 """
220 Is this an expense?
221 """
222 return self.type == self.EXPENSES
223
224 @property
225 def is_transfer(self, ):
226 """
227 Is this transaction a transfer?
228 """
229 return self.type == self.TRANSFER
Back to Top