django-ledger 0.8.0__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/data_import.py +182 -63
- django_ledger/io/io_core.py +507 -374
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/bill.py +337 -300
- django_ledger/models/customer.py +47 -34
- django_ledger/models/data_import.py +770 -289
- django_ledger/models/entity.py +882 -637
- django_ledger/models/mixins.py +421 -280
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +105 -41
- django_ledger/models/unit.py +42 -30
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +85 -66
- django_ledger/settings.py +1 -0
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +3 -1
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/layouts/base.html +1 -1
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +3 -2
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/urls/__init__.py +1 -0
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/customer.py +56 -14
- django_ledger/views/data_import.py +119 -66
- django_ledger/views/mixins.py +112 -86
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/vendor.py +53 -14
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/METADATA +1 -1
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/RECORD +51 -40
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/top_level.txt +0 -0
django_ledger/models/mixins.py
CHANGED
|
@@ -5,18 +5,23 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
|
|
|
5
5
|
This module implements the different model MixIns used on different Django Ledger Models to implement common
|
|
6
6
|
functionality.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
import logging
|
|
9
10
|
from collections import defaultdict
|
|
10
|
-
from datetime import
|
|
11
|
+
from datetime import date, datetime, timedelta
|
|
11
12
|
from decimal import Decimal
|
|
12
13
|
from itertools import groupby
|
|
13
|
-
from typing import Optional, Union
|
|
14
|
+
from typing import Dict, Optional, Union
|
|
14
15
|
from uuid import UUID
|
|
15
16
|
|
|
16
17
|
from django.conf import settings
|
|
17
18
|
from django.core.exceptions import ValidationError
|
|
18
|
-
from django.core.validators import
|
|
19
|
-
|
|
19
|
+
from django.core.validators import (
|
|
20
|
+
MaxValueValidator,
|
|
21
|
+
MinLengthValidator,
|
|
22
|
+
MinValueValidator,
|
|
23
|
+
int_list_validator,
|
|
24
|
+
)
|
|
20
25
|
from django.db import models
|
|
21
26
|
from django.db.models import QuerySet
|
|
22
27
|
from django.utils.encoding import force_str
|
|
@@ -24,10 +29,19 @@ from django.utils.translation import gettext_lazy as _
|
|
|
24
29
|
from markdown import markdown
|
|
25
30
|
|
|
26
31
|
from django_ledger.io import (
|
|
27
|
-
ASSET_CA_CASH,
|
|
28
|
-
LIABILITY_CL_ACC_PAYABLE,
|
|
32
|
+
ASSET_CA_CASH,
|
|
33
|
+
LIABILITY_CL_ACC_PAYABLE,
|
|
34
|
+
LIABILITY_CL_OTHER,
|
|
35
|
+
LIABILITY_CL_ST_NOTES_PAYABLE,
|
|
36
|
+
LIABILITY_LTL_MORTGAGE_PAYABLE,
|
|
37
|
+
LIABILITY_LTL_NOTES_PAYABLE,
|
|
38
|
+
)
|
|
39
|
+
from django_ledger.io.io_core import (
|
|
40
|
+
check_tx_balance,
|
|
41
|
+
get_localdate,
|
|
42
|
+
get_localtime,
|
|
43
|
+
validate_io_timestamp,
|
|
29
44
|
)
|
|
30
|
-
from django_ledger.io.io_core import validate_io_timestamp, check_tx_balance, get_localtime, get_localdate
|
|
31
45
|
from django_ledger.models.utils import lazy_loader
|
|
32
46
|
|
|
33
47
|
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
|
|
@@ -44,13 +58,18 @@ class SlugNameMixIn(models.Model):
|
|
|
44
58
|
name: str
|
|
45
59
|
A human-readable name for display purposes. Maximum 150 characters.
|
|
46
60
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
|
|
62
|
+
slug = models.SlugField(
|
|
63
|
+
max_length=50,
|
|
64
|
+
editable=False,
|
|
65
|
+
unique=True,
|
|
66
|
+
validators=[
|
|
67
|
+
MinLengthValidator(
|
|
68
|
+
limit_value=10,
|
|
69
|
+
message=_('Slug field must contain at least 10 characters.'),
|
|
70
|
+
)
|
|
71
|
+
],
|
|
72
|
+
)
|
|
54
73
|
name = models.CharField(max_length=150, null=True, blank=True)
|
|
55
74
|
|
|
56
75
|
class Meta:
|
|
@@ -71,6 +90,7 @@ class CreateUpdateMixIn(models.Model):
|
|
|
71
90
|
updated: str
|
|
72
91
|
An updated timestamp used to identify when models are updated.
|
|
73
92
|
"""
|
|
93
|
+
|
|
74
94
|
created = models.DateTimeField(auto_now_add=True)
|
|
75
95
|
updated = models.DateTimeField(auto_now=True, null=True, blank=True)
|
|
76
96
|
|
|
@@ -106,32 +126,53 @@ class ContactInfoMixIn(models.Model):
|
|
|
106
126
|
phone: str
|
|
107
127
|
A string used to document the contact phone.
|
|
108
128
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
|
|
130
|
+
address_1 = models.CharField(
|
|
131
|
+
max_length=70, verbose_name=_('Address Line 1'), null=True, blank=True
|
|
132
|
+
)
|
|
133
|
+
address_2 = models.CharField(
|
|
134
|
+
null=True, blank=True, max_length=70, verbose_name=_('Address Line 2')
|
|
135
|
+
)
|
|
136
|
+
city = models.CharField(
|
|
137
|
+
null=True, blank=True, max_length=70, verbose_name=_('City')
|
|
138
|
+
)
|
|
139
|
+
state = models.CharField(
|
|
140
|
+
null=True, blank=True, max_length=70, verbose_name=_('State/Province')
|
|
141
|
+
)
|
|
142
|
+
zip_code = models.CharField(
|
|
143
|
+
null=True, blank=True, max_length=20, verbose_name=_('Zip Code')
|
|
144
|
+
)
|
|
145
|
+
country = models.CharField(
|
|
146
|
+
null=True, blank=True, max_length=70, verbose_name=_('Country')
|
|
147
|
+
)
|
|
115
148
|
email = models.EmailField(null=True, blank=True, verbose_name=_('Email'))
|
|
116
149
|
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
|
117
|
-
phone = models.CharField(
|
|
150
|
+
phone = models.CharField(
|
|
151
|
+
max_length=30, null=True, blank=True, verbose_name=_('Phone Number')
|
|
152
|
+
)
|
|
118
153
|
|
|
119
154
|
class Meta:
|
|
120
155
|
abstract = True
|
|
121
156
|
|
|
122
157
|
def get_cszc(self):
|
|
123
|
-
if all(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
if all(
|
|
159
|
+
[
|
|
160
|
+
self.city,
|
|
161
|
+
self.state,
|
|
162
|
+
self.zip_code,
|
|
163
|
+
self.country,
|
|
164
|
+
]
|
|
165
|
+
):
|
|
129
166
|
return f'{self.city}, {self.state}. {self.zip_code}. {self.country}'
|
|
130
167
|
|
|
131
168
|
def clean(self):
|
|
132
169
|
if self.address_2 and not self.address_1:
|
|
133
170
|
raise ValidationError(
|
|
134
|
-
{
|
|
171
|
+
{
|
|
172
|
+
'address_1': _(
|
|
173
|
+
'Address line 1 is required if address_2 is provided.'
|
|
174
|
+
)
|
|
175
|
+
},
|
|
135
176
|
)
|
|
136
177
|
super().clean()
|
|
137
178
|
|
|
@@ -169,6 +210,7 @@ class AccrualMixIn(models.Model):
|
|
|
169
210
|
The AccountModel used to track receivables to the financial instrument. Must be of role
|
|
170
211
|
LIABILITY_CL_DEFERRED_REVENUE.
|
|
171
212
|
"""
|
|
213
|
+
|
|
172
214
|
IS_DEBIT_BALANCE = None
|
|
173
215
|
REL_NAME_PREFIX = None
|
|
174
216
|
ALLOW_MIGRATE = True
|
|
@@ -179,68 +221,82 @@ class AccrualMixIn(models.Model):
|
|
|
179
221
|
'di': 'debit',
|
|
180
222
|
}
|
|
181
223
|
|
|
182
|
-
amount_due = models.DecimalField(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
224
|
+
amount_due = models.DecimalField(
|
|
225
|
+
default=0, max_digits=20, decimal_places=2, verbose_name=_('Amount Due')
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
amount_paid = models.DecimalField(
|
|
229
|
+
default=0,
|
|
230
|
+
max_digits=20,
|
|
231
|
+
decimal_places=2,
|
|
232
|
+
verbose_name=_('Amount Paid'),
|
|
233
|
+
validators=[MinValueValidator(limit_value=0)],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
amount_receivable = models.DecimalField(
|
|
237
|
+
default=0,
|
|
238
|
+
max_digits=20,
|
|
239
|
+
decimal_places=2,
|
|
240
|
+
verbose_name=_('Amount Receivable'),
|
|
241
|
+
validators=[MinValueValidator(limit_value=0)],
|
|
242
|
+
)
|
|
243
|
+
amount_unearned = models.DecimalField(
|
|
244
|
+
default=0,
|
|
245
|
+
max_digits=20,
|
|
246
|
+
decimal_places=2,
|
|
247
|
+
verbose_name=_('Amount Unearned'),
|
|
248
|
+
validators=[MinValueValidator(limit_value=0)],
|
|
249
|
+
)
|
|
250
|
+
amount_earned = models.DecimalField(
|
|
251
|
+
default=0,
|
|
252
|
+
max_digits=20,
|
|
253
|
+
decimal_places=2,
|
|
254
|
+
verbose_name=_('Amount Earned'),
|
|
255
|
+
validators=[MinValueValidator(limit_value=0)],
|
|
256
|
+
)
|
|
208
257
|
|
|
209
258
|
accrue = models.BooleanField(default=False, verbose_name=_('Accrue'))
|
|
210
259
|
|
|
211
260
|
# todo: change progress method from percent to currency amount and FloatField??...
|
|
212
|
-
progress = models.DecimalField(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
])
|
|
261
|
+
progress = models.DecimalField(
|
|
262
|
+
default=0,
|
|
263
|
+
verbose_name=_('Progress Amount'),
|
|
264
|
+
decimal_places=2,
|
|
265
|
+
max_digits=3,
|
|
266
|
+
validators=[MinValueValidator(limit_value=0), MaxValueValidator(limit_value=1)],
|
|
267
|
+
)
|
|
220
268
|
|
|
221
269
|
# todo: rename to ledger_model...
|
|
222
|
-
ledger = models.OneToOneField(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
270
|
+
ledger = models.OneToOneField(
|
|
271
|
+
'django_ledger.LedgerModel',
|
|
272
|
+
editable=False,
|
|
273
|
+
verbose_name=_('Ledger'),
|
|
274
|
+
on_delete=models.CASCADE,
|
|
275
|
+
)
|
|
276
|
+
cash_account = models.ForeignKey(
|
|
277
|
+
'django_ledger.AccountModel',
|
|
278
|
+
on_delete=models.RESTRICT,
|
|
279
|
+
blank=True,
|
|
280
|
+
null=True,
|
|
281
|
+
verbose_name=_('Cash Account'),
|
|
282
|
+
related_name=f'{REL_NAME_PREFIX}_cash_account',
|
|
283
|
+
)
|
|
284
|
+
prepaid_account = models.ForeignKey(
|
|
285
|
+
'django_ledger.AccountModel',
|
|
286
|
+
on_delete=models.RESTRICT,
|
|
287
|
+
blank=True,
|
|
288
|
+
null=True,
|
|
289
|
+
verbose_name=_('Prepaid Account'),
|
|
290
|
+
related_name=f'{REL_NAME_PREFIX}_prepaid_account',
|
|
291
|
+
)
|
|
292
|
+
unearned_account = models.ForeignKey(
|
|
293
|
+
'django_ledger.AccountModel',
|
|
294
|
+
on_delete=models.RESTRICT,
|
|
295
|
+
blank=True,
|
|
296
|
+
null=True,
|
|
297
|
+
verbose_name=_('Unearned Account'),
|
|
298
|
+
related_name=f'{REL_NAME_PREFIX}_unearned_account',
|
|
299
|
+
)
|
|
244
300
|
|
|
245
301
|
class Meta:
|
|
246
302
|
abstract = True
|
|
@@ -255,12 +311,14 @@ class AccrualMixIn(models.Model):
|
|
|
255
311
|
bool
|
|
256
312
|
True if configured, else False.
|
|
257
313
|
"""
|
|
258
|
-
return all(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
314
|
+
return all(
|
|
315
|
+
[
|
|
316
|
+
self.ledger_id is not None,
|
|
317
|
+
self.cash_account_id is not None,
|
|
318
|
+
self.unearned_account_id is not None,
|
|
319
|
+
self.prepaid_account_id is not None,
|
|
320
|
+
]
|
|
321
|
+
)
|
|
264
322
|
|
|
265
323
|
def is_posted(self):
|
|
266
324
|
"""
|
|
@@ -343,15 +401,9 @@ class AccrualMixIn(models.Model):
|
|
|
343
401
|
payments = self.amount_paid or Decimal.from_float(0.00)
|
|
344
402
|
if self.accrue:
|
|
345
403
|
amt_earned = self.get_amount_earned()
|
|
346
|
-
if all([
|
|
347
|
-
self.IS_DEBIT_BALANCE,
|
|
348
|
-
amt_earned >= payments
|
|
349
|
-
]):
|
|
404
|
+
if all([self.IS_DEBIT_BALANCE, amt_earned >= payments]):
|
|
350
405
|
return self.get_amount_earned() - payments
|
|
351
|
-
elif all([
|
|
352
|
-
not self.IS_DEBIT_BALANCE,
|
|
353
|
-
amt_earned <= payments
|
|
354
|
-
]):
|
|
406
|
+
elif all([not self.IS_DEBIT_BALANCE, amt_earned <= payments]):
|
|
355
407
|
return payments - self.get_amount_earned()
|
|
356
408
|
return Decimal.from_float(0.00)
|
|
357
409
|
|
|
@@ -366,15 +418,9 @@ class AccrualMixIn(models.Model):
|
|
|
366
418
|
"""
|
|
367
419
|
if self.accrue:
|
|
368
420
|
amt_earned = self.get_amount_earned()
|
|
369
|
-
if all([
|
|
370
|
-
self.IS_DEBIT_BALANCE,
|
|
371
|
-
amt_earned <= self.amount_paid
|
|
372
|
-
]):
|
|
421
|
+
if all([self.IS_DEBIT_BALANCE, amt_earned <= self.amount_paid]):
|
|
373
422
|
return self.amount_paid - amt_earned
|
|
374
|
-
elif all([
|
|
375
|
-
not self.IS_DEBIT_BALANCE,
|
|
376
|
-
amt_earned >= self.amount_paid
|
|
377
|
-
]):
|
|
423
|
+
elif all([not self.IS_DEBIT_BALANCE, amt_earned >= self.amount_paid]):
|
|
378
424
|
return amt_earned - self.amount_paid
|
|
379
425
|
return Decimal.from_float(0.00)
|
|
380
426
|
|
|
@@ -414,9 +460,7 @@ class AccrualMixIn(models.Model):
|
|
|
414
460
|
return False
|
|
415
461
|
return not self.ledger.is_locked()
|
|
416
462
|
|
|
417
|
-
def get_tx_type(self,
|
|
418
|
-
acc_bal_type: dict,
|
|
419
|
-
adjustment_amount: Decimal):
|
|
463
|
+
def get_tx_type(self, acc_bal_type: dict, adjustment_amount: Decimal):
|
|
420
464
|
"""
|
|
421
465
|
Determines the transaction type associated with an increase/decrease of an account balance of the financial
|
|
422
466
|
instrument.
|
|
@@ -438,10 +482,13 @@ class AccrualMixIn(models.Model):
|
|
|
438
482
|
return self.TX_TYPE_MAPPING[acc_bal_type + d_or_i]
|
|
439
483
|
|
|
440
484
|
@classmethod
|
|
441
|
-
def split_amount(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
485
|
+
def split_amount(
|
|
486
|
+
cls,
|
|
487
|
+
amount: Union[Decimal, float],
|
|
488
|
+
unit_split: Dict,
|
|
489
|
+
account_uuid: UUID,
|
|
490
|
+
account_balance_type: str,
|
|
491
|
+
) -> Dict:
|
|
445
492
|
"""
|
|
446
493
|
Splits an amount into different proportions representing the unit splits.
|
|
447
494
|
Makes sure that 100% of the amount is numerically allocated taking into consideration decimal points.
|
|
@@ -467,7 +514,9 @@ class AccrualMixIn(models.Model):
|
|
|
467
514
|
split_results = dict()
|
|
468
515
|
for i, (u, p) in enumerate(unit_split.items()):
|
|
469
516
|
if i == SPLIT_LEN:
|
|
470
|
-
split_results[(account_uuid, u, account_balance_type)] =
|
|
517
|
+
split_results[(account_uuid, u, account_balance_type)] = (
|
|
518
|
+
amount - running_alloc
|
|
519
|
+
)
|
|
471
520
|
else:
|
|
472
521
|
alloc = round(p * amount, 2)
|
|
473
522
|
split_results[(account_uuid, u, account_balance_type)] = alloc
|
|
@@ -489,11 +538,15 @@ class AccrualMixIn(models.Model):
|
|
|
489
538
|
ledger_model = self.ledger
|
|
490
539
|
if ledger_model.locked:
|
|
491
540
|
if raise_exception:
|
|
492
|
-
raise ValidationError(
|
|
541
|
+
raise ValidationError(
|
|
542
|
+
f'Bill ledger {ledger_model.name} is already locked...'
|
|
543
|
+
)
|
|
493
544
|
return
|
|
494
545
|
ledger_model.lock(commit, raise_exception=raise_exception)
|
|
495
546
|
|
|
496
|
-
def unlock_ledger(
|
|
547
|
+
def unlock_ledger(
|
|
548
|
+
self, commit: bool = False, raise_exception: bool = True, **kwargs
|
|
549
|
+
):
|
|
497
550
|
"""
|
|
498
551
|
Convenience method to un-lock the LedgerModel associated with the Accruable financial instrument.
|
|
499
552
|
|
|
@@ -507,7 +560,9 @@ class AccrualMixIn(models.Model):
|
|
|
507
560
|
ledger_model = self.ledger
|
|
508
561
|
if not ledger_model.is_locked():
|
|
509
562
|
if raise_exception:
|
|
510
|
-
raise ValidationError(
|
|
563
|
+
raise ValidationError(
|
|
564
|
+
f'Bill ledger {ledger_model.name} is already unlocked...'
|
|
565
|
+
)
|
|
511
566
|
return
|
|
512
567
|
ledger_model.unlock(commit, raise_exception=raise_exception)
|
|
513
568
|
|
|
@@ -526,11 +581,15 @@ class AccrualMixIn(models.Model):
|
|
|
526
581
|
ledger_model = self.ledger
|
|
527
582
|
if ledger_model.posted:
|
|
528
583
|
if raise_exception:
|
|
529
|
-
raise ValidationError(
|
|
584
|
+
raise ValidationError(
|
|
585
|
+
f'Bill ledger {ledger_model.name} is already posted...'
|
|
586
|
+
)
|
|
530
587
|
return
|
|
531
588
|
ledger_model.post(commit, raise_exception=raise_exception)
|
|
532
589
|
|
|
533
|
-
def unpost_ledger(
|
|
590
|
+
def unpost_ledger(
|
|
591
|
+
self, commit: bool = False, raise_exception: bool = True, **kwargs
|
|
592
|
+
):
|
|
534
593
|
"""
|
|
535
594
|
Convenience method to un-lock the LedgerModel associated with the Accruable financial instrument.
|
|
536
595
|
|
|
@@ -544,22 +603,25 @@ class AccrualMixIn(models.Model):
|
|
|
544
603
|
ledger_model = self.ledger
|
|
545
604
|
if not ledger_model.is_posted():
|
|
546
605
|
if raise_exception:
|
|
547
|
-
raise ValidationError(
|
|
606
|
+
raise ValidationError(
|
|
607
|
+
f'Bill ledger {ledger_model.name} is not posted...'
|
|
608
|
+
)
|
|
548
609
|
return
|
|
549
610
|
ledger_model.post(commit, raise_exception=raise_exception)
|
|
550
611
|
|
|
551
|
-
def migrate_state(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
612
|
+
def migrate_state(
|
|
613
|
+
self,
|
|
614
|
+
# todo: remove usermodel param...?
|
|
615
|
+
user_model,
|
|
616
|
+
entity_slug: str,
|
|
617
|
+
itemtxs_qs: Optional[QuerySet] = None,
|
|
618
|
+
force_migrate: bool = False,
|
|
619
|
+
commit: bool = True,
|
|
620
|
+
void: bool = False,
|
|
621
|
+
je_timestamp: Optional[Union[str, date, datetime]] = None,
|
|
622
|
+
raise_exception: bool = True,
|
|
623
|
+
**kwargs,
|
|
624
|
+
):
|
|
563
625
|
"""
|
|
564
626
|
Migrates the current Accruable financial instrument into the books. The main objective of the migrate_state
|
|
565
627
|
method is to determine the JournalEntry and TransactionModels necessary to accurately reflect the financial
|
|
@@ -573,7 +635,7 @@ class AccrualMixIn(models.Model):
|
|
|
573
635
|
The EntityModel slug.
|
|
574
636
|
itemtxs_qs: ItemTransactionModelQuerySet
|
|
575
637
|
The pre-fetched ItemTransactionModelQuerySet containing the item information associated with the financial
|
|
576
|
-
element migration. If provided, will avoid additional database query.
|
|
638
|
+
element migration. If provided, it will avoid an additional database query.
|
|
577
639
|
force_migrate: bool
|
|
578
640
|
Forces migration of the financial instrument bypassing the can_migrate() check.
|
|
579
641
|
commit: bool
|
|
@@ -592,60 +654,45 @@ class AccrualMixIn(models.Model):
|
|
|
592
654
|
"""
|
|
593
655
|
|
|
594
656
|
if self.can_migrate() or force_migrate:
|
|
595
|
-
|
|
596
|
-
# getting current ledger state
|
|
597
|
-
# todo: validate itemtxs_qs...?
|
|
598
|
-
io_digest = self.ledger.digest(
|
|
599
|
-
user_model=user_model,
|
|
600
|
-
entity_slug=entity_slug,
|
|
601
|
-
process_groups=True,
|
|
602
|
-
process_roles=False,
|
|
603
|
-
process_ratios=False,
|
|
604
|
-
signs=False,
|
|
605
|
-
by_unit=True
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
io_data = io_digest.get_io_data()
|
|
609
|
-
|
|
610
|
-
accounts_data = io_data['accounts']
|
|
611
|
-
|
|
612
|
-
# Index (account_uuid, unit_uuid, balance_type, role)
|
|
613
|
-
current_ledger_state = {
|
|
614
|
-
(a['account_uuid'], a['unit_uuid'], a['balance_type']): a['balance'] for a in accounts_data
|
|
615
|
-
# (a['account_uuid'], a['unit_uuid'], a['balance_type'], a['role']): a['balance'] for a in digest_data
|
|
616
|
-
}
|
|
617
|
-
|
|
618
657
|
item_data = list(self.get_migration_data(queryset=itemtxs_qs))
|
|
619
658
|
cogs_adjustment = defaultdict(lambda: Decimal('0.00'))
|
|
620
659
|
inventory_adjustment = defaultdict(lambda: Decimal('0.00'))
|
|
621
660
|
progress = self.get_progress()
|
|
622
661
|
|
|
623
662
|
if isinstance(self, lazy_loader.get_bill_model()):
|
|
624
|
-
|
|
625
663
|
for item in item_data:
|
|
626
664
|
account_uuid_expense = item.get('item_model__expense_account__uuid')
|
|
627
|
-
account_uuid_inventory = item.get(
|
|
665
|
+
account_uuid_inventory = item.get(
|
|
666
|
+
'item_model__inventory_account__uuid'
|
|
667
|
+
)
|
|
628
668
|
if account_uuid_expense:
|
|
629
669
|
item['account_uuid'] = account_uuid_expense
|
|
630
|
-
item['account_balance_type'] = item.get(
|
|
670
|
+
item['account_balance_type'] = item.get(
|
|
671
|
+
'item_model__expense_account__balance_type'
|
|
672
|
+
)
|
|
631
673
|
elif account_uuid_inventory:
|
|
632
674
|
item['account_uuid'] = account_uuid_inventory
|
|
633
|
-
item['account_balance_type'] = item.get(
|
|
675
|
+
item['account_balance_type'] = item.get(
|
|
676
|
+
'item_model__inventory_account__balance_type'
|
|
677
|
+
)
|
|
634
678
|
|
|
635
679
|
elif isinstance(self, lazy_loader.get_invoice_model()):
|
|
636
|
-
|
|
637
680
|
for item in item_data:
|
|
638
|
-
|
|
639
|
-
|
|
681
|
+
account_uuid_earnings = item.get(
|
|
682
|
+
'item_model__earnings_account__uuid'
|
|
683
|
+
)
|
|
640
684
|
account_uuid_cogs = item.get('item_model__cogs_account__uuid')
|
|
641
|
-
account_uuid_inventory = item.get(
|
|
685
|
+
account_uuid_inventory = item.get(
|
|
686
|
+
'item_model__inventory_account__uuid'
|
|
687
|
+
)
|
|
642
688
|
|
|
643
689
|
if account_uuid_earnings:
|
|
644
690
|
item['account_uuid'] = account_uuid_earnings
|
|
645
|
-
item['account_balance_type'] = item.get(
|
|
691
|
+
item['account_balance_type'] = item.get(
|
|
692
|
+
'item_model__earnings_account__balance_type'
|
|
693
|
+
)
|
|
646
694
|
|
|
647
695
|
if account_uuid_cogs and account_uuid_inventory:
|
|
648
|
-
|
|
649
696
|
try:
|
|
650
697
|
irq = item.get('item_model__inventory_received')
|
|
651
698
|
irv = item.get('item_model__inventory_received_value')
|
|
@@ -661,27 +708,38 @@ class AccrualMixIn(models.Model):
|
|
|
661
708
|
|
|
662
709
|
if tot_amt != 0:
|
|
663
710
|
# keeps track of necessary transactions to increase COGS account...
|
|
664
|
-
cogs_adjustment[
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
711
|
+
cogs_adjustment[
|
|
712
|
+
(
|
|
713
|
+
account_uuid_cogs,
|
|
714
|
+
item.get('entity_unit__uuid'),
|
|
715
|
+
item.get('item_model__cogs_account__balance_type'),
|
|
716
|
+
)
|
|
717
|
+
] += tot_amt * progress
|
|
669
718
|
|
|
670
719
|
# keeps track of necessary transactions to reduce inventory account...
|
|
671
|
-
inventory_adjustment[
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
720
|
+
inventory_adjustment[
|
|
721
|
+
(
|
|
722
|
+
account_uuid_inventory,
|
|
723
|
+
item.get('entity_unit__uuid'),
|
|
724
|
+
item.get(
|
|
725
|
+
'item_model__inventory_account__balance_type'
|
|
726
|
+
),
|
|
727
|
+
)
|
|
728
|
+
] -= tot_amt * progress
|
|
729
|
+
|
|
730
|
+
item_data_gb = groupby(
|
|
731
|
+
item_data,
|
|
732
|
+
key=lambda a: (
|
|
733
|
+
a['account_uuid'],
|
|
734
|
+
a['entity_unit__uuid'],
|
|
735
|
+
a['account_balance_type'],
|
|
736
|
+
),
|
|
737
|
+
)
|
|
681
738
|
|
|
682
739
|
# scaling down item amount based on progress...
|
|
683
740
|
progress_item_idx = {
|
|
684
|
-
idx: round(sum(a['account_unit_total'] for a in ad) * progress, 2)
|
|
741
|
+
idx: round(sum(a['account_unit_total'] for a in ad) * progress, 2)
|
|
742
|
+
for idx, ad in item_data_gb
|
|
685
743
|
}
|
|
686
744
|
|
|
687
745
|
# tuple ( unit_uuid, total_amount ) sorted by uuid...
|
|
@@ -696,7 +754,8 @@ class AccrualMixIn(models.Model):
|
|
|
696
754
|
|
|
697
755
|
# { unit_uuid: float (percent) }
|
|
698
756
|
unit_percents = {
|
|
699
|
-
k: (v / total_amount) if progress and total_amount else Decimal('0.00')
|
|
757
|
+
k: (v / total_amount) if progress and total_amount else Decimal('0.00')
|
|
758
|
+
for k, v in unit_amounts.items()
|
|
700
759
|
}
|
|
701
760
|
|
|
702
761
|
if not void:
|
|
@@ -708,19 +767,19 @@ class AccrualMixIn(models.Model):
|
|
|
708
767
|
amount=new_state['amount_paid'],
|
|
709
768
|
unit_split=unit_percents,
|
|
710
769
|
account_uuid=self.cash_account_id,
|
|
711
|
-
account_balance_type='debit'
|
|
770
|
+
account_balance_type='debit',
|
|
712
771
|
)
|
|
713
772
|
amount_prepaid_split = self.split_amount(
|
|
714
773
|
amount=new_state['amount_receivable'],
|
|
715
774
|
unit_split=unit_percents,
|
|
716
775
|
account_uuid=self.prepaid_account_id,
|
|
717
|
-
account_balance_type='debit'
|
|
776
|
+
account_balance_type='debit',
|
|
718
777
|
)
|
|
719
778
|
amount_unearned_split = self.split_amount(
|
|
720
779
|
amount=new_state['amount_unearned'],
|
|
721
780
|
unit_split=unit_percents,
|
|
722
781
|
account_uuid=self.unearned_account_id,
|
|
723
|
-
account_balance_type='credit'
|
|
782
|
+
account_balance_type='credit',
|
|
724
783
|
)
|
|
725
784
|
|
|
726
785
|
new_ledger_state = dict()
|
|
@@ -734,19 +793,40 @@ class AccrualMixIn(models.Model):
|
|
|
734
793
|
|
|
735
794
|
new_ledger_state.update(progress_item_idx)
|
|
736
795
|
|
|
796
|
+
# getting current ledger state
|
|
797
|
+
# todo: validate itemtxs_qs...?
|
|
798
|
+
io_digest = self.ledger.digest(
|
|
799
|
+
user_model=user_model,
|
|
800
|
+
entity_slug=entity_slug,
|
|
801
|
+
process_groups=True,
|
|
802
|
+
process_roles=False,
|
|
803
|
+
process_ratios=False,
|
|
804
|
+
signs=False,
|
|
805
|
+
by_unit=True,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
io_data = io_digest.get_io_data()
|
|
809
|
+
accounts_data = io_data['accounts']
|
|
810
|
+
|
|
811
|
+
# Index (account_uuid, unit_uuid, balance_type, role)
|
|
812
|
+
current_ledger_state = {
|
|
813
|
+
(a['account_uuid'], a['unit_uuid'], a['balance_type']): a['balance']
|
|
814
|
+
for a in accounts_data
|
|
815
|
+
# (a['account_uuid'], a['unit_uuid'], a['balance_type'], a['role']): a['balance'] for a in digest_data
|
|
816
|
+
}
|
|
817
|
+
|
|
737
818
|
# list of all keys involved
|
|
738
819
|
idx_keys = set(list(current_ledger_state) + list(new_ledger_state))
|
|
739
820
|
|
|
740
821
|
# difference between new vs current
|
|
741
822
|
diff_idx = {
|
|
742
|
-
k: new_ledger_state.get(k, Decimal('0.00'))
|
|
743
|
-
|
|
823
|
+
k: new_ledger_state.get(k, Decimal('0.00'))
|
|
824
|
+
- current_ledger_state.get(k, Decimal('0.00'))
|
|
825
|
+
for k in idx_keys
|
|
744
826
|
}
|
|
745
827
|
|
|
746
828
|
# eliminates transactions with no amount...
|
|
747
|
-
diff_idx = {
|
|
748
|
-
k: v for k, v in diff_idx.items() if v
|
|
749
|
-
}
|
|
829
|
+
diff_idx = {k: v for k, v in diff_idx.items() if v}
|
|
750
830
|
|
|
751
831
|
if commit:
|
|
752
832
|
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
@@ -764,21 +844,29 @@ class AccrualMixIn(models.Model):
|
|
|
764
844
|
timestamp=now_timestamp,
|
|
765
845
|
description=self.get_migrate_state_desc(),
|
|
766
846
|
origin='migration',
|
|
767
|
-
ledger_id=self.ledger_id
|
|
768
|
-
)
|
|
847
|
+
ledger_id=self.ledger_id,
|
|
848
|
+
)
|
|
849
|
+
for u in unit_uuids
|
|
769
850
|
}
|
|
770
851
|
|
|
771
852
|
for u, je in je_list.items():
|
|
772
853
|
je.clean(verify=False)
|
|
773
854
|
|
|
774
855
|
txs_list = [
|
|
775
|
-
(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
856
|
+
(
|
|
857
|
+
unit_uuid,
|
|
858
|
+
TransactionModel(
|
|
859
|
+
journal_entry=je_list.get(unit_uuid),
|
|
860
|
+
amount=abs(round(amt, 2)),
|
|
861
|
+
tx_type=self.get_tx_type(
|
|
862
|
+
acc_bal_type=bal_type, adjustment_amount=amt
|
|
863
|
+
),
|
|
864
|
+
account_id=acc_uuid,
|
|
865
|
+
description=self.get_migrate_state_desc(),
|
|
866
|
+
),
|
|
867
|
+
)
|
|
868
|
+
for (acc_uuid, unit_uuid, bal_type), amt in diff_idx.items()
|
|
869
|
+
if amt
|
|
782
870
|
]
|
|
783
871
|
|
|
784
872
|
for unit_uuid, tx in txs_list:
|
|
@@ -786,7 +874,10 @@ class AccrualMixIn(models.Model):
|
|
|
786
874
|
|
|
787
875
|
for uid in unit_uuids:
|
|
788
876
|
# validates each unit txs independently...
|
|
789
|
-
check_tx_balance(
|
|
877
|
+
check_tx_balance(
|
|
878
|
+
tx_data=[tx for ui, tx in txs_list if uid == ui],
|
|
879
|
+
perform_correction=True,
|
|
880
|
+
)
|
|
790
881
|
|
|
791
882
|
# validates all txs as a whole (for safety)...
|
|
792
883
|
txs = [tx for _, tx in txs_list]
|
|
@@ -798,19 +889,24 @@ class AccrualMixIn(models.Model):
|
|
|
798
889
|
je.clean(verify=True)
|
|
799
890
|
if je.is_verified():
|
|
800
891
|
je.mark_as_locked(commit=False, raise_exception=True)
|
|
801
|
-
je.mark_as_posted(
|
|
892
|
+
je.mark_as_posted(
|
|
893
|
+
commit=False, verify=False, raise_exception=True
|
|
894
|
+
)
|
|
802
895
|
|
|
803
896
|
if all([je.is_verified() for _, je in je_list.items()]):
|
|
804
897
|
# only if all JEs have been verified will be posted and locked...
|
|
805
898
|
JournalEntryModel.objects.bulk_update(
|
|
806
899
|
objs=[je for _, je in je_list.items()],
|
|
807
|
-
fields=['posted', 'locked', 'activity']
|
|
900
|
+
fields=['posted', 'locked', 'activity'],
|
|
808
901
|
)
|
|
809
902
|
|
|
810
903
|
return item_data, io_data
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
904
|
+
|
|
905
|
+
if not raise_exception:
|
|
906
|
+
return None
|
|
907
|
+
raise ValidationError(
|
|
908
|
+
f'{self.REL_NAME_PREFIX.upper()} state migration not allowed'
|
|
909
|
+
)
|
|
814
910
|
|
|
815
911
|
def void_state(self, commit: bool = False) -> Dict:
|
|
816
912
|
"""
|
|
@@ -854,7 +950,7 @@ class AccrualMixIn(models.Model):
|
|
|
854
950
|
'amount_paid': self.get_amount_cash(),
|
|
855
951
|
'amount_receivable': self.get_amount_prepaid(),
|
|
856
952
|
'amount_unearned': self.get_amount_unearned(),
|
|
857
|
-
'amount_earned': self.get_amount_earned()
|
|
953
|
+
'amount_earned': self.get_amount_earned(),
|
|
858
954
|
}
|
|
859
955
|
if commit:
|
|
860
956
|
self.update_state(new_state)
|
|
@@ -877,7 +973,6 @@ class AccrualMixIn(models.Model):
|
|
|
877
973
|
self.amount_earned = state['amount_earned']
|
|
878
974
|
|
|
879
975
|
def clean(self):
|
|
880
|
-
|
|
881
976
|
super().clean()
|
|
882
977
|
|
|
883
978
|
if not self.amount_due:
|
|
@@ -888,21 +983,31 @@ class AccrualMixIn(models.Model):
|
|
|
888
983
|
|
|
889
984
|
if self.accrue:
|
|
890
985
|
if not self.prepaid_account_id:
|
|
891
|
-
raise ValidationError(
|
|
986
|
+
raise ValidationError(
|
|
987
|
+
f'Accrued {self.__class__.__name__} must define a Prepaid Expense account.'
|
|
988
|
+
)
|
|
892
989
|
if not self.unearned_account_id:
|
|
893
|
-
raise ValidationError(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
]):
|
|
900
|
-
if not all([
|
|
990
|
+
raise ValidationError(
|
|
991
|
+
f'Accrued {self.__class__.__name__} must define an Unearned Income account.'
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
if any(
|
|
995
|
+
[
|
|
901
996
|
self.cash_account_id is not None,
|
|
902
997
|
self.prepaid_account_id is not None,
|
|
903
|
-
self.unearned_account_id is not None
|
|
904
|
-
]
|
|
905
|
-
|
|
998
|
+
self.unearned_account_id is not None,
|
|
999
|
+
]
|
|
1000
|
+
):
|
|
1001
|
+
if not all(
|
|
1002
|
+
[
|
|
1003
|
+
self.cash_account_id is not None,
|
|
1004
|
+
self.prepaid_account_id is not None,
|
|
1005
|
+
self.unearned_account_id is not None,
|
|
1006
|
+
]
|
|
1007
|
+
):
|
|
1008
|
+
raise ValidationError(
|
|
1009
|
+
'Must provide all accounts Cash, Prepaid, UnEarned.'
|
|
1010
|
+
)
|
|
906
1011
|
|
|
907
1012
|
# if self.accrue:
|
|
908
1013
|
# if self.is_approved():
|
|
@@ -911,7 +1016,9 @@ class AccrualMixIn(models.Model):
|
|
|
911
1016
|
# self.progress = Decimal.from_float(0.00)
|
|
912
1017
|
|
|
913
1018
|
if self.amount_paid > self.amount_due:
|
|
914
|
-
raise ValidationError(
|
|
1019
|
+
raise ValidationError(
|
|
1020
|
+
f'Amount paid {self.amount_paid} cannot exceed amount due {self.amount_due}'
|
|
1021
|
+
)
|
|
915
1022
|
|
|
916
1023
|
if self.is_paid():
|
|
917
1024
|
self.progress = Decimal.from_float(1.0)
|
|
@@ -921,17 +1028,21 @@ class AccrualMixIn(models.Model):
|
|
|
921
1028
|
if not self.date_paid:
|
|
922
1029
|
self.date_paid = today
|
|
923
1030
|
if self.date_paid > today:
|
|
924
|
-
raise ValidationError(
|
|
1031
|
+
raise ValidationError(
|
|
1032
|
+
f'Cannot pay {self.__class__.__name__} in the future.'
|
|
1033
|
+
)
|
|
925
1034
|
else:
|
|
926
1035
|
self.date_paid = None
|
|
927
1036
|
|
|
928
1037
|
if self.is_void():
|
|
929
|
-
if any(
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1038
|
+
if any(
|
|
1039
|
+
[
|
|
1040
|
+
self.amount_paid,
|
|
1041
|
+
self.amount_earned,
|
|
1042
|
+
self.amount_unearned,
|
|
1043
|
+
self.amount_receivable,
|
|
1044
|
+
]
|
|
1045
|
+
):
|
|
935
1046
|
raise ValidationError('Voided element cannot have any balance.')
|
|
936
1047
|
|
|
937
1048
|
self.progress = Decimal.from_float(0.00)
|
|
@@ -952,6 +1063,7 @@ class PaymentTermsMixIn(models.Model):
|
|
|
952
1063
|
A choice of TERM_CHOICES that determines the payment terms.
|
|
953
1064
|
|
|
954
1065
|
"""
|
|
1066
|
+
|
|
955
1067
|
TERMS_ON_RECEIPT = 'on_receipt'
|
|
956
1068
|
TERMS_NET_30 = 'net_30'
|
|
957
1069
|
TERMS_NET_60 = 'net_60'
|
|
@@ -971,13 +1083,15 @@ class PaymentTermsMixIn(models.Model):
|
|
|
971
1083
|
TERMS_NET_30: 30,
|
|
972
1084
|
TERMS_NET_60: 60,
|
|
973
1085
|
TERMS_NET_90: 90,
|
|
974
|
-
TERMS_NET_90_PLUS: 120
|
|
1086
|
+
TERMS_NET_90_PLUS: 120,
|
|
975
1087
|
}
|
|
976
1088
|
|
|
977
|
-
terms = models.CharField(
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1089
|
+
terms = models.CharField(
|
|
1090
|
+
max_length=10,
|
|
1091
|
+
default='on_receipt',
|
|
1092
|
+
choices=TERM_CHOICES,
|
|
1093
|
+
verbose_name=_('Terms'),
|
|
1094
|
+
)
|
|
981
1095
|
date_due = models.DateField(verbose_name=_('Due Date'), null=True, blank=True)
|
|
982
1096
|
|
|
983
1097
|
class Meta:
|
|
@@ -1090,7 +1204,10 @@ class MarkdownNotesMixIn(models.Model):
|
|
|
1090
1204
|
markdown_notes: str
|
|
1091
1205
|
A string of text representing the mark-down document.
|
|
1092
1206
|
"""
|
|
1093
|
-
|
|
1207
|
+
|
|
1208
|
+
markdown_notes = models.TextField(
|
|
1209
|
+
blank=True, null=True, verbose_name=_('Markdown Notes')
|
|
1210
|
+
)
|
|
1094
1211
|
|
|
1095
1212
|
class Meta:
|
|
1096
1213
|
abstract = True
|
|
@@ -1149,7 +1266,7 @@ class FinancialAccountInfoMixin(models.Model):
|
|
|
1149
1266
|
ACCOUNT_ST_LOAN: LIABILITY_CL_ST_NOTES_PAYABLE,
|
|
1150
1267
|
ACCOUNT_LT_LOAN: LIABILITY_LTL_NOTES_PAYABLE,
|
|
1151
1268
|
ACCOUNT_MORTGAGE: LIABILITY_LTL_MORTGAGE_PAYABLE,
|
|
1152
|
-
ACCOUNT_OTHER: LIABILITY_CL_OTHER
|
|
1269
|
+
ACCOUNT_OTHER: LIABILITY_CL_OTHER,
|
|
1153
1270
|
}
|
|
1154
1271
|
|
|
1155
1272
|
ACCOUNT_TYPE_CHOICES = [
|
|
@@ -1169,7 +1286,7 @@ class FinancialAccountInfoMixin(models.Model):
|
|
|
1169
1286
|
'SAVINGS': ACCOUNT_SAVINGS,
|
|
1170
1287
|
'MONEYMRKT': ACCOUNT_MONEY_MKT,
|
|
1171
1288
|
'CREDITLINE': ACCOUNT_CREDIT_CARD,
|
|
1172
|
-
'CD': ACCOUNT_CERT_DEPOSIT
|
|
1289
|
+
'CD': ACCOUNT_CERT_DEPOSIT,
|
|
1173
1290
|
}
|
|
1174
1291
|
|
|
1175
1292
|
VALID_ACCOUNT_TYPES = tuple(atc[0] for atc in ACCOUNT_TYPE_CHOICES)
|
|
@@ -1179,23 +1296,33 @@ class FinancialAccountInfoMixin(models.Model):
|
|
|
1179
1296
|
blank=True,
|
|
1180
1297
|
null=True,
|
|
1181
1298
|
verbose_name=_('Financial Institution'),
|
|
1182
|
-
help_text=_('Name of the financial institution (i.e. Bank Name).')
|
|
1299
|
+
help_text=_('Name of the financial institution (i.e. Bank Name).'),
|
|
1300
|
+
)
|
|
1301
|
+
account_number = models.CharField(
|
|
1302
|
+
max_length=30,
|
|
1303
|
+
null=True,
|
|
1304
|
+
blank=True,
|
|
1305
|
+
validators=[int_list_validator(sep='', message=_('Only digits allowed'))],
|
|
1306
|
+
verbose_name=_('Account Number'),
|
|
1307
|
+
)
|
|
1308
|
+
routing_number = models.CharField(
|
|
1309
|
+
max_length=30,
|
|
1310
|
+
null=True,
|
|
1311
|
+
blank=True,
|
|
1312
|
+
validators=[int_list_validator(sep='', message=_('Only digits allowed'))],
|
|
1313
|
+
verbose_name=_('Routing Number'),
|
|
1314
|
+
)
|
|
1315
|
+
aba_number = models.CharField(
|
|
1316
|
+
max_length=30, null=True, blank=True, verbose_name=_('ABA Number')
|
|
1317
|
+
)
|
|
1318
|
+
swift_number = models.CharField(
|
|
1319
|
+
max_length=30, null=True, blank=True, verbose_name=_('SWIFT Number')
|
|
1183
1320
|
)
|
|
1184
|
-
account_number = models.CharField(max_length=30, null=True, blank=True,
|
|
1185
|
-
validators=[
|
|
1186
|
-
int_list_validator(sep='', message=_('Only digits allowed'))
|
|
1187
|
-
], verbose_name=_('Account Number'))
|
|
1188
|
-
routing_number = models.CharField(max_length=30, null=True, blank=True,
|
|
1189
|
-
validators=[
|
|
1190
|
-
int_list_validator(sep='', message=_('Only digits allowed'))
|
|
1191
|
-
], verbose_name=_('Routing Number'))
|
|
1192
|
-
aba_number = models.CharField(max_length=30, null=True, blank=True, verbose_name=_('ABA Number'))
|
|
1193
|
-
swift_number = models.CharField(max_length=30, null=True, blank=True, verbose_name=_('SWIFT Number'))
|
|
1194
1321
|
account_type = models.CharField(
|
|
1195
1322
|
choices=ACCOUNT_TYPE_CHOICES,
|
|
1196
1323
|
max_length=20,
|
|
1197
1324
|
default=ACCOUNT_CHECKING,
|
|
1198
|
-
verbose_name=_('Account Type')
|
|
1325
|
+
verbose_name=_('Account Type'),
|
|
1199
1326
|
)
|
|
1200
1327
|
|
|
1201
1328
|
class Meta:
|
|
@@ -1212,16 +1339,13 @@ class FinancialAccountInfoMixin(models.Model):
|
|
|
1212
1339
|
return f'*{self.routing_number[-n:]}'
|
|
1213
1340
|
|
|
1214
1341
|
def get_account_type_from_ofx(self, ofx_type):
|
|
1215
|
-
return self.ACCOUNT_TYPE_OFX_MAPPING.get(
|
|
1216
|
-
ofx_type, self.ACCOUNT_OTHER
|
|
1217
|
-
)
|
|
1342
|
+
return self.ACCOUNT_TYPE_OFX_MAPPING.get(ofx_type, self.ACCOUNT_OTHER)
|
|
1218
1343
|
|
|
1219
1344
|
|
|
1220
1345
|
class TaxInfoMixIn(models.Model):
|
|
1221
|
-
tax_id_number = models.CharField(
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
verbose_name=_('Tax Registration Number'))
|
|
1346
|
+
tax_id_number = models.CharField(
|
|
1347
|
+
max_length=30, null=True, blank=True, verbose_name=_('Tax Registration Number')
|
|
1348
|
+
)
|
|
1225
1349
|
|
|
1226
1350
|
class Meta:
|
|
1227
1351
|
abstract = True
|
|
@@ -1240,14 +1364,17 @@ class TaxCollectionMixIn(models.Model):
|
|
|
1240
1364
|
sales_tax_rate: float
|
|
1241
1365
|
The tax rate as a float. A Number between 0.00 and 1.00.
|
|
1242
1366
|
"""
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1367
|
+
|
|
1368
|
+
sales_tax_rate = models.FloatField(
|
|
1369
|
+
default=0.00000,
|
|
1370
|
+
verbose_name=_('Sales Tax Rate'),
|
|
1371
|
+
null=True,
|
|
1372
|
+
blank=True,
|
|
1373
|
+
validators=[
|
|
1374
|
+
MinValueValidator(limit_value=0.00000),
|
|
1375
|
+
MaxValueValidator(limit_value=1.00000),
|
|
1376
|
+
],
|
|
1377
|
+
)
|
|
1251
1378
|
|
|
1252
1379
|
class Meta:
|
|
1253
1380
|
abstract = True
|
|
@@ -1261,13 +1388,16 @@ class LoggingMixIn:
|
|
|
1261
1388
|
Implements functionality used to add logging capabilities to any python class.
|
|
1262
1389
|
Useful for production and or testing environments.
|
|
1263
1390
|
"""
|
|
1391
|
+
|
|
1264
1392
|
LOGGER_NAME_ATTRIBUTE = None
|
|
1265
1393
|
LOGGER_BYPASS_DEBUG = False
|
|
1266
1394
|
|
|
1267
1395
|
def get_logger_name(self):
|
|
1268
1396
|
if self.LOGGER_NAME_ATTRIBUTE is None:
|
|
1269
|
-
raise NotImplementedError(
|
|
1270
|
-
|
|
1397
|
+
raise NotImplementedError(
|
|
1398
|
+
f'{self.__class__.__name__} must define LOGGER_NAME_ATTRIBUTE of implement '
|
|
1399
|
+
'get_logger_name() function.'
|
|
1400
|
+
)
|
|
1271
1401
|
return getattr(self, self.LOGGER_NAME_ATTRIBUTE)
|
|
1272
1402
|
|
|
1273
1403
|
def get_logger(self) -> logging.Logger:
|
|
@@ -1302,7 +1432,9 @@ class ItemizeMixIn(models.Model):
|
|
|
1302
1432
|
"""
|
|
1303
1433
|
raise NotImplementedError()
|
|
1304
1434
|
|
|
1305
|
-
def get_itemtxs_data(
|
|
1435
|
+
def get_itemtxs_data(
|
|
1436
|
+
self, queryset=None, aggregate_on_db: bool = False, lazy_agg: bool = False
|
|
1437
|
+
):
|
|
1306
1438
|
"""
|
|
1307
1439
|
Fetches the ItemTransactionModelQuerySet associated with the model.
|
|
1308
1440
|
|
|
@@ -1332,14 +1464,19 @@ class ItemizeMixIn(models.Model):
|
|
|
1332
1464
|
Item transaction list to replace/aggregate.
|
|
1333
1465
|
"""
|
|
1334
1466
|
if isinstance(itemtxs, dict):
|
|
1335
|
-
if all(
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1467
|
+
if all(
|
|
1468
|
+
[
|
|
1469
|
+
all(
|
|
1470
|
+
[
|
|
1471
|
+
isinstance(d, dict),
|
|
1472
|
+
'unit_cost' in d,
|
|
1473
|
+
'quantity' in d,
|
|
1474
|
+
'total_amount' in d,
|
|
1475
|
+
]
|
|
1476
|
+
)
|
|
1477
|
+
for i, d in itemtxs.items()
|
|
1478
|
+
]
|
|
1479
|
+
):
|
|
1343
1480
|
return
|
|
1344
1481
|
raise ItemizeError('itemtxs must be an instance of dict.')
|
|
1345
1482
|
|
|
@@ -1363,7 +1500,9 @@ class ItemizeMixIn(models.Model):
|
|
|
1363
1500
|
item_model_qs_map = {i.item_number: i for i in item_model_qs}
|
|
1364
1501
|
|
|
1365
1502
|
if itemtxs.keys() != item_model_qs_map.keys():
|
|
1366
|
-
raise ItemizeError(
|
|
1503
|
+
raise ItemizeError(
|
|
1504
|
+
message=f'Got items {itemtxs.keys()}, but only {item_model_qs_map.keys()} exists.'
|
|
1505
|
+
)
|
|
1367
1506
|
|
|
1368
1507
|
if isinstance(self, EstimateModel):
|
|
1369
1508
|
return [
|
|
@@ -1373,7 +1512,8 @@ class ItemizeMixIn(models.Model):
|
|
|
1373
1512
|
ce_quantity=i['quantity'],
|
|
1374
1513
|
ce_unit_cost_estimate=i['unit_cost'],
|
|
1375
1514
|
ce_unit_revenue_estimate=i['unit_revenue'],
|
|
1376
|
-
)
|
|
1515
|
+
)
|
|
1516
|
+
for item_number, i in itemtxs.items()
|
|
1377
1517
|
]
|
|
1378
1518
|
|
|
1379
1519
|
if isinstance(self, PurchaseOrder):
|
|
@@ -1383,7 +1523,8 @@ class ItemizeMixIn(models.Model):
|
|
|
1383
1523
|
item_model=item_model_qs_map[item_number],
|
|
1384
1524
|
po_quantity=i['quantity'],
|
|
1385
1525
|
po_unit_cost=i['unit_cost'],
|
|
1386
|
-
)
|
|
1526
|
+
)
|
|
1527
|
+
for item_number, i in itemtxs.items()
|
|
1387
1528
|
]
|
|
1388
1529
|
|
|
1389
1530
|
BillModel = lazy_loader.get_bill_model()
|
|
@@ -1395,8 +1536,9 @@ class ItemizeMixIn(models.Model):
|
|
|
1395
1536
|
invoice_model=self if isinstance(self, InvoiceModel) else None,
|
|
1396
1537
|
item_model=item_model_qs_map[item_number],
|
|
1397
1538
|
quantity=i['quantity'],
|
|
1398
|
-
unit_cost=i['unit_cost']
|
|
1399
|
-
)
|
|
1539
|
+
unit_cost=i['unit_cost'],
|
|
1540
|
+
)
|
|
1541
|
+
for item_number, i in itemtxs.items()
|
|
1400
1542
|
]
|
|
1401
1543
|
|
|
1402
1544
|
def migrate_itemtxs(self, itemtxs: Dict, operation: str, commit: bool = False):
|
|
@@ -1430,7 +1572,6 @@ class ItemizeMixIn(models.Model):
|
|
|
1430
1572
|
itx.clean()
|
|
1431
1573
|
|
|
1432
1574
|
if commit:
|
|
1433
|
-
|
|
1434
1575
|
ItemTransactionModel = lazy_loader.get_item_transaction_model()
|
|
1435
1576
|
|
|
1436
1577
|
if operation == self.ITEMIZE_APPEND:
|