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.

Files changed (56) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/forms/account.py +45 -46
  3. django_ledger/forms/data_import.py +182 -64
  4. django_ledger/io/io_core.py +507 -374
  5. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  6. django_ledger/models/__init__.py +2 -1
  7. django_ledger/models/bill.py +337 -300
  8. django_ledger/models/customer.py +47 -34
  9. django_ledger/models/data_import.py +770 -289
  10. django_ledger/models/entity.py +882 -637
  11. django_ledger/models/mixins.py +421 -282
  12. django_ledger/models/receipt.py +1083 -0
  13. django_ledger/models/transactions.py +105 -41
  14. django_ledger/models/unit.py +42 -30
  15. django_ledger/models/utils.py +12 -2
  16. django_ledger/models/vendor.py +85 -66
  17. django_ledger/settings.py +1 -0
  18. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  19. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +1 -13
  20. django_ledger/templates/django_ledger/bills/bill_update.html +1 -1
  21. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  22. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  23. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  24. django_ledger/templates/django_ledger/customer/tags/customer_table.html +3 -1
  25. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  26. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  27. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  28. django_ledger/templates/django_ledger/invoice/invoice_update.html +1 -1
  29. django_ledger/templates/django_ledger/layouts/base.html +3 -1
  30. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  31. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  32. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  33. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  34. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  35. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  36. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +3 -2
  37. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  38. django_ledger/templatetags/django_ledger.py +338 -191
  39. django_ledger/urls/__init__.py +1 -0
  40. django_ledger/urls/customer.py +3 -0
  41. django_ledger/urls/data_import.py +3 -0
  42. django_ledger/urls/receipt.py +102 -0
  43. django_ledger/urls/vendor.py +1 -0
  44. django_ledger/views/__init__.py +1 -0
  45. django_ledger/views/customer.py +56 -14
  46. django_ledger/views/data_import.py +119 -66
  47. django_ledger/views/mixins.py +112 -86
  48. django_ledger/views/receipt.py +294 -0
  49. django_ledger/views/vendor.py +53 -14
  50. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/METADATA +1 -1
  51. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/RECORD +55 -45
  52. django_ledger/static/django_ledger/bundle/styles.bundle.js +0 -1
  53. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/WHEEL +0 -0
  54. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/AUTHORS.md +0 -0
  55. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/LICENSE +0 -0
  56. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/top_level.txt +0 -0
@@ -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 timedelta, date, datetime
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, Dict
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 MinValueValidator, MaxValueValidator, MinLengthValidator
19
- from django.core.validators import int_list_validator
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, LIABILITY_CL_ST_NOTES_PAYABLE, LIABILITY_LTL_MORTGAGE_PAYABLE,
28
- LIABILITY_CL_ACC_PAYABLE, LIABILITY_CL_OTHER, LIABILITY_LTL_NOTES_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
- slug = models.SlugField(max_length=50,
48
- editable=False,
49
- unique=True,
50
- validators=[
51
- MinLengthValidator(limit_value=10,
52
- message=_('Slug field must contain at least 10 characters.'))
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
- address_1 = models.CharField(max_length=70, verbose_name=_('Address Line 1'), null=True, blank=True)
110
- address_2 = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('Address Line 2'))
111
- city = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('City'))
112
- state = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('State/Province'))
113
- zip_code = models.CharField(null=True, blank=True, max_length=20, verbose_name=_('Zip Code'))
114
- country = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('Country'))
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(max_length=30, null=True, blank=True, verbose_name=_('Phone Number'))
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
- self.city,
125
- self.state,
126
- self.zip_code,
127
- self.country,
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
- {'address_1': _('Address line 1 is required if address_2 is provided.')},
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(default=0,
183
- max_digits=20,
184
- decimal_places=2,
185
- verbose_name=_('Amount Due'))
186
-
187
- amount_paid = models.DecimalField(default=0,
188
- max_digits=20,
189
- decimal_places=2,
190
- verbose_name=_('Amount Paid'),
191
- validators=[MinValueValidator(limit_value=0)])
192
-
193
- amount_receivable = models.DecimalField(default=0,
194
- max_digits=20,
195
- decimal_places=2,
196
- verbose_name=_('Amount Receivable'),
197
- validators=[MinValueValidator(limit_value=0)])
198
- amount_unearned = models.DecimalField(default=0,
199
- max_digits=20,
200
- decimal_places=2,
201
- verbose_name=_('Amount Unearned'),
202
- validators=[MinValueValidator(limit_value=0)])
203
- amount_earned = models.DecimalField(default=0,
204
- max_digits=20,
205
- decimal_places=2,
206
- verbose_name=_('Amount Earned'),
207
- validators=[MinValueValidator(limit_value=0)])
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(default=0,
213
- verbose_name=_('Progress Amount'),
214
- decimal_places=2,
215
- max_digits=3,
216
- validators=[
217
- MinValueValidator(limit_value=0),
218
- MaxValueValidator(limit_value=1)
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('django_ledger.LedgerModel',
223
- editable=False,
224
- verbose_name=_('Ledger'),
225
- on_delete=models.CASCADE)
226
- cash_account = models.ForeignKey('django_ledger.AccountModel',
227
- on_delete=models.RESTRICT,
228
- blank=True,
229
- null=True,
230
- verbose_name=_('Cash Account'),
231
- related_name=f'{REL_NAME_PREFIX}_cash_account')
232
- prepaid_account = models.ForeignKey('django_ledger.AccountModel',
233
- on_delete=models.RESTRICT,
234
- blank=True,
235
- null=True,
236
- verbose_name=_('Prepaid Account'),
237
- related_name=f'{REL_NAME_PREFIX}_prepaid_account')
238
- unearned_account = models.ForeignKey('django_ledger.AccountModel',
239
- on_delete=models.RESTRICT,
240
- blank=True,
241
- null=True,
242
- verbose_name=_('Unearned Account'),
243
- related_name=f'{REL_NAME_PREFIX}_unearned_account')
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
- self.ledger_id is not None,
260
- self.cash_account_id is not None,
261
- self.unearned_account_id is not None,
262
- self.prepaid_account_id is not None
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(cls, amount: Union[Decimal, float],
442
- unit_split: Dict,
443
- account_uuid: UUID,
444
- account_balance_type: str) -> Dict:
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)] = amount - running_alloc
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(f'Bill ledger {ledger_model.name} is already locked...')
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(self, commit: bool = False, raise_exception: bool = True, **kwargs):
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(f'Bill ledger {ledger_model.name} is already unlocked...')
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(f'Bill ledger {ledger_model.name} is already posted...')
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(self, commit: bool = False, raise_exception: bool = True, **kwargs):
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(f'Bill ledger {ledger_model.name} is not posted...')
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(self,
552
- # todo: remove usermodel param...?
553
- user_model,
554
- entity_slug: str,
555
- itemtxs_qs: Optional[QuerySet] = None,
556
- force_migrate: bool = False,
557
- commit: bool = True,
558
- void: bool = False,
559
- je_timestamp: Optional[Union[str, date, datetime]] = None,
560
- raise_exception: bool = True,
561
- **kwargs):
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('item_model__inventory_account__uuid')
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('item_model__expense_account__balance_type')
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('item_model__inventory_account__balance_type')
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
- account_uuid_earnings = item.get('item_model__earnings_account__uuid')
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('item_model__inventory_account__uuid')
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('item_model__earnings_account__balance_type')
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
- account_uuid_cogs,
666
- item.get('entity_unit__uuid'),
667
- item.get('item_model__cogs_account__balance_type')
668
- )] += tot_amt * progress
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
- account_uuid_inventory,
673
- item.get('entity_unit__uuid'),
674
- item.get('item_model__inventory_account__balance_type')
675
- )] -= tot_amt * progress
676
-
677
- item_data_gb = groupby(item_data,
678
- key=lambda a: (a['account_uuid'],
679
- a['entity_unit__uuid'],
680
- a['account_balance_type']))
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) for idx, ad in item_data_gb
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') for k, v in unit_amounts.items()
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')) - current_ledger_state.get(k, Decimal('0.00')) for k in
743
- idx_keys
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
- ) for u in unit_uuids
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
- (unit_uuid, TransactionModel(
776
- journal_entry=je_list.get(unit_uuid),
777
- amount=abs(round(amt, 2)),
778
- tx_type=self.get_tx_type(acc_bal_type=bal_type, adjustment_amount=amt),
779
- account_id=acc_uuid,
780
- description=self.get_migrate_state_desc()
781
- )) for (acc_uuid, unit_uuid, bal_type), amt in diff_idx.items() if amt
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(tx_data=[tx for ui, tx in txs_list if uid == ui], perform_correction=True)
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(commit=False, verify=False, raise_exception=True)
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
- else:
812
- if raise_exception:
813
- raise ValidationError(f'{self.REL_NAME_PREFIX.upper()} state migration not allowed')
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(f'Accrued {self.__class__.__name__} must define a Prepaid Expense account.')
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(f'Accrued {self.__class__.__name__} must define an Unearned Income account.')
894
-
895
- if any([
896
- self.cash_account_id is not None,
897
- self.prepaid_account_id is not None,
898
- self.unearned_account_id is not None
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
- raise ValidationError('Must provide all accounts Cash, Prepaid, UnEarned.')
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(f'Amount paid {self.amount_paid} cannot exceed amount due {self.amount_due}')
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(f'Cannot pay {self.__class__.__name__} in the future.')
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
- self.amount_paid,
931
- self.amount_earned,
932
- self.amount_unearned,
933
- self.amount_receivable
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(max_length=10,
978
- default='on_receipt',
979
- choices=TERM_CHOICES,
980
- verbose_name=_('Terms'))
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
- markdown_notes = models.TextField(blank=True, null=True, verbose_name=_('Markdown Notes'))
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(max_length=30,
1222
- null=True,
1223
- blank=True,
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
- sales_tax_rate = models.FloatField(default=0.00000,
1244
- verbose_name=_('Sales Tax Rate'),
1245
- null=True,
1246
- blank=True,
1247
- validators=[
1248
- MinValueValidator(limit_value=0.00000),
1249
- MaxValueValidator(limit_value=1.00000)
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(f'{self.__class__.__name__} must define LOGGER_NAME_ATTRIBUTE of implement '
1270
- 'get_logger_name() function.')
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(self, queryset=None, aggregate_on_db: bool = False, lazy_agg: bool = False):
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
- all([
1337
- isinstance(d, dict),
1338
- 'unit_cost' in d,
1339
- 'quantity' in d,
1340
- 'total_amount' in d
1341
- ]) for i, d in itemtxs.items()
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(message=f'Got items {itemtxs.keys()}, but only {item_model_qs_map.keys()} exists.')
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
- ) for item_number, i in itemtxs.items()
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
- ) for item_number, i in itemtxs.items()
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
- ) for item_number, i in itemtxs.items()
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: