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.

Files changed (51) 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 -63
  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 -280
  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/templates/django_ledger/components/period_navigator.html +5 -3
  19. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  20. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  21. django_ledger/templates/django_ledger/customer/tags/customer_table.html +3 -1
  22. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  23. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  24. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  25. django_ledger/templates/django_ledger/layouts/base.html +1 -1
  26. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  27. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  28. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  29. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  30. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  31. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  32. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +3 -2
  33. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  34. django_ledger/templatetags/django_ledger.py +338 -191
  35. django_ledger/urls/__init__.py +1 -0
  36. django_ledger/urls/customer.py +3 -0
  37. django_ledger/urls/data_import.py +3 -0
  38. django_ledger/urls/receipt.py +102 -0
  39. django_ledger/urls/vendor.py +1 -0
  40. django_ledger/views/__init__.py +1 -0
  41. django_ledger/views/customer.py +56 -14
  42. django_ledger/views/data_import.py +119 -66
  43. django_ledger/views/mixins.py +112 -86
  44. django_ledger/views/receipt.py +294 -0
  45. django_ledger/views/vendor.py +53 -14
  46. {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/METADATA +1 -1
  47. {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/RECORD +51 -40
  48. {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +0 -0
  49. {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/AUTHORS.md +0 -0
  50. {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/LICENSE +0 -0
  51. {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.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,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, 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
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
- 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
- ])
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
- 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'))
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(max_length=30, null=True, blank=True, verbose_name=_('Phone Number'))
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
- self.city,
125
- self.state,
126
- self.zip_code,
127
- self.country,
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
- {'address_1': _('Address line 1 is required if address_2 is provided.')},
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(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)])
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(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
- ])
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('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')
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
- 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
- ])
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(cls, amount: Union[Decimal, float],
442
- unit_split: Dict,
443
- account_uuid: UUID,
444
- account_balance_type: str) -> Dict:
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)] = amount - running_alloc
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(f'Bill ledger {ledger_model.name} is already locked...')
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(self, commit: bool = False, raise_exception: bool = True, **kwargs):
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(f'Bill ledger {ledger_model.name} is already unlocked...')
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(f'Bill ledger {ledger_model.name} is already posted...')
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(self, commit: bool = False, raise_exception: bool = True, **kwargs):
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(f'Bill ledger {ledger_model.name} is not posted...')
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(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
-
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('item_model__inventory_account__uuid')
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('item_model__expense_account__balance_type')
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('item_model__inventory_account__balance_type')
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
- account_uuid_earnings = item.get('item_model__earnings_account__uuid')
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('item_model__inventory_account__uuid')
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('item_model__earnings_account__balance_type')
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
- account_uuid_cogs,
666
- item.get('entity_unit__uuid'),
667
- item.get('item_model__cogs_account__balance_type')
668
- )] += tot_amt * progress
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
- 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']))
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) for idx, ad in item_data_gb
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') for k, v in unit_amounts.items()
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')) - current_ledger_state.get(k, Decimal('0.00')) for k in
743
- idx_keys
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
- ) for u in unit_uuids
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
- (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
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(tx_data=[tx for ui, tx in txs_list if uid == ui], perform_correction=True)
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(commit=False, verify=False, raise_exception=True)
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
- else:
812
- if raise_exception:
813
- raise ValidationError(f'{self.REL_NAME_PREFIX.upper()} state migration not allowed')
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(f'Accrued {self.__class__.__name__} must define a Prepaid Expense account.')
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(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([
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
- raise ValidationError('Must provide all accounts Cash, Prepaid, UnEarned.')
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(f'Amount paid {self.amount_paid} cannot exceed amount due {self.amount_due}')
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(f'Cannot pay {self.__class__.__name__} in the future.')
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
- self.amount_paid,
931
- self.amount_earned,
932
- self.amount_unearned,
933
- self.amount_receivable
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(max_length=10,
978
- default='on_receipt',
979
- choices=TERM_CHOICES,
980
- verbose_name=_('Terms'))
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
- markdown_notes = models.TextField(blank=True, null=True, verbose_name=_('Markdown Notes'))
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(max_length=30,
1222
- null=True,
1223
- blank=True,
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
- 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
- ])
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(f'{self.__class__.__name__} must define LOGGER_NAME_ATTRIBUTE of implement '
1270
- 'get_logger_name() function.')
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(self, queryset=None, aggregate_on_db: bool = False, lazy_agg: bool = False):
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
- 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
- ]):
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(message=f'Got items {itemtxs.keys()}, but only {item_model_qs_map.keys()} exists.')
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
- ) for item_number, i in itemtxs.items()
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
- ) for item_number, i in itemtxs.items()
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
- ) for item_number, i in itemtxs.items()
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: