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