django-ledger 0.7.11__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 (139) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/context.py +12 -0
  3. django_ledger/forms/account.py +45 -46
  4. django_ledger/forms/bill.py +0 -4
  5. django_ledger/forms/closing_entry.py +13 -1
  6. django_ledger/forms/data_import.py +182 -63
  7. django_ledger/forms/estimate.py +3 -6
  8. django_ledger/forms/invoice.py +3 -7
  9. django_ledger/forms/item.py +10 -18
  10. django_ledger/forms/purchase_order.py +2 -4
  11. django_ledger/io/io_core.py +515 -400
  12. django_ledger/io/io_generator.py +7 -6
  13. django_ledger/io/io_library.py +1 -2
  14. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  15. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  16. django_ledger/models/__init__.py +2 -1
  17. django_ledger/models/accounts.py +109 -69
  18. django_ledger/models/bank_account.py +40 -23
  19. django_ledger/models/bill.py +386 -333
  20. django_ledger/models/chart_of_accounts.py +173 -105
  21. django_ledger/models/closing_entry.py +99 -48
  22. django_ledger/models/customer.py +100 -66
  23. django_ledger/models/data_import.py +818 -323
  24. django_ledger/models/deprecations.py +61 -0
  25. django_ledger/models/entity.py +891 -644
  26. django_ledger/models/estimate.py +57 -28
  27. django_ledger/models/invoice.py +46 -26
  28. django_ledger/models/items.py +503 -142
  29. django_ledger/models/journal_entry.py +61 -47
  30. django_ledger/models/ledger.py +106 -42
  31. django_ledger/models/mixins.py +424 -281
  32. django_ledger/models/purchase_order.py +39 -17
  33. django_ledger/models/receipt.py +1083 -0
  34. django_ledger/models/transactions.py +242 -139
  35. django_ledger/models/unit.py +93 -54
  36. django_ledger/models/utils.py +12 -2
  37. django_ledger/models/vendor.py +121 -70
  38. django_ledger/report/core.py +2 -14
  39. django_ledger/settings.py +57 -71
  40. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  41. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  42. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  43. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  44. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  45. django_ledger/templates/django_ledger/components/menu.html +41 -26
  46. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  47. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  48. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  49. django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
  50. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  51. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  52. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  53. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  54. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  55. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  56. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  57. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  58. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  59. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  60. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  61. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  62. django_ledger/templates/django_ledger/layouts/base.html +7 -2
  63. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  64. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  65. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  66. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  67. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  68. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  69. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
  70. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  71. django_ledger/templatetags/django_ledger.py +338 -191
  72. django_ledger/tests/test_accounts.py +1 -2
  73. django_ledger/tests/test_io.py +17 -0
  74. django_ledger/tests/test_purchase_order.py +3 -3
  75. django_ledger/tests/test_transactions.py +1 -2
  76. django_ledger/urls/__init__.py +1 -4
  77. django_ledger/urls/customer.py +3 -0
  78. django_ledger/urls/data_import.py +3 -0
  79. django_ledger/urls/receipt.py +102 -0
  80. django_ledger/urls/vendor.py +1 -0
  81. django_ledger/views/__init__.py +1 -0
  82. django_ledger/views/bill.py +8 -11
  83. django_ledger/views/chart_of_accounts.py +6 -4
  84. django_ledger/views/closing_entry.py +11 -7
  85. django_ledger/views/customer.py +68 -30
  86. django_ledger/views/data_import.py +120 -66
  87. django_ledger/views/djl_api.py +3 -5
  88. django_ledger/views/entity.py +2 -4
  89. django_ledger/views/estimate.py +3 -7
  90. django_ledger/views/inventory.py +3 -5
  91. django_ledger/views/invoice.py +4 -6
  92. django_ledger/views/item.py +7 -11
  93. django_ledger/views/journal_entry.py +1 -2
  94. django_ledger/views/mixins.py +125 -93
  95. django_ledger/views/purchase_order.py +24 -35
  96. django_ledger/views/receipt.py +294 -0
  97. django_ledger/views/unit.py +1 -2
  98. django_ledger/views/vendor.py +54 -16
  99. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
  100. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
  101. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
  102. django_ledger-0.8.1.dist-info/top_level.txt +1 -0
  103. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  104. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  105. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  106. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  107. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  108. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  109. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  110. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  111. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  112. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  113. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  114. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  115. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  116. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  117. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  118. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  119. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  120. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  121. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  122. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  123. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  124. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  125. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  126. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  127. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  128. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  129. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  130. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  131. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  132. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  133. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  134. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  135. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  136. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  137. django_ledger-0.7.11.dist-info/top_level.txt +0 -4
  138. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
  139. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +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,29 +126,54 @@ 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'))
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):
169
+ if self.address_2 and not self.address_1:
170
+ raise ValidationError(
171
+ {
172
+ 'address_1': _(
173
+ 'Address line 1 is required if address_2 is provided.'
174
+ )
175
+ },
176
+ )
132
177
  super().clean()
133
178
 
134
179
 
@@ -165,6 +210,7 @@ class AccrualMixIn(models.Model):
165
210
  The AccountModel used to track receivables to the financial instrument. Must be of role
166
211
  LIABILITY_CL_DEFERRED_REVENUE.
167
212
  """
213
+
168
214
  IS_DEBIT_BALANCE = None
169
215
  REL_NAME_PREFIX = None
170
216
  ALLOW_MIGRATE = True
@@ -175,70 +221,82 @@ class AccrualMixIn(models.Model):
175
221
  'di': 'debit',
176
222
  }
177
223
 
178
- amount_due = models.DecimalField(default=0,
179
- max_digits=20,
180
- decimal_places=2,
181
- verbose_name=_('Amount Due'))
182
-
183
- amount_paid = models.DecimalField(default=0,
184
- max_digits=20,
185
- decimal_places=2,
186
- verbose_name=_('Amount Paid'),
187
- validators=[MinValueValidator(limit_value=0)])
188
-
189
- amount_receivable = models.DecimalField(default=0,
190
- max_digits=20,
191
- decimal_places=2,
192
- verbose_name=_('Amount Receivable'),
193
- validators=[MinValueValidator(limit_value=0)])
194
- amount_unearned = models.DecimalField(default=0,
195
- max_digits=20,
196
- decimal_places=2,
197
- verbose_name=_('Amount Unearned'),
198
- validators=[MinValueValidator(limit_value=0)])
199
- amount_earned = models.DecimalField(default=0,
200
- max_digits=20,
201
- decimal_places=2,
202
- verbose_name=_('Amount Earned'),
203
- 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
+ )
204
257
 
205
258
  accrue = models.BooleanField(default=False, verbose_name=_('Accrue'))
206
259
 
207
260
  # todo: change progress method from percent to currency amount and FloatField??...
208
- progress = models.DecimalField(default=0,
209
- verbose_name=_('Progress Amount'),
210
- decimal_places=2,
211
- max_digits=3,
212
- validators=[
213
- MinValueValidator(limit_value=0),
214
- MaxValueValidator(limit_value=1)
215
- ])
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
+ )
216
268
 
217
269
  # todo: rename to ledger_model...
218
- ledger = models.OneToOneField('django_ledger.LedgerModel',
219
- editable=False,
220
- verbose_name=_('Ledger'),
221
- on_delete=models.CASCADE)
222
- cash_account = models.ForeignKey('django_ledger.AccountModel',
223
- on_delete=models.RESTRICT,
224
- blank=True,
225
- null=True,
226
- verbose_name=_('Cash Account'),
227
- related_name=f'{REL_NAME_PREFIX}_cash_account')
228
- prepaid_account = models.ForeignKey('django_ledger.AccountModel',
229
- on_delete=models.RESTRICT,
230
- blank=True,
231
- null=True,
232
- verbose_name=_('Prepaid Account'),
233
- related_name=f'{REL_NAME_PREFIX}_prepaid_account')
234
-
235
- # todo: rename to payable account...
236
- unearned_account = models.ForeignKey('django_ledger.AccountModel',
237
- on_delete=models.RESTRICT,
238
- blank=True,
239
- null=True,
240
- verbose_name=_('Unearned Account'),
241
- 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
+ )
242
300
 
243
301
  class Meta:
244
302
  abstract = True
@@ -253,12 +311,14 @@ class AccrualMixIn(models.Model):
253
311
  bool
254
312
  True if configured, else False.
255
313
  """
256
- return all([
257
- self.ledger_id is not None,
258
- self.cash_account_id is not None,
259
- self.unearned_account_id is not None,
260
- self.prepaid_account_id is not None
261
- ])
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
+ )
262
322
 
263
323
  def is_posted(self):
264
324
  """
@@ -341,15 +401,9 @@ class AccrualMixIn(models.Model):
341
401
  payments = self.amount_paid or Decimal.from_float(0.00)
342
402
  if self.accrue:
343
403
  amt_earned = self.get_amount_earned()
344
- if all([
345
- self.IS_DEBIT_BALANCE,
346
- amt_earned >= payments
347
- ]):
404
+ if all([self.IS_DEBIT_BALANCE, amt_earned >= payments]):
348
405
  return self.get_amount_earned() - payments
349
- elif all([
350
- not self.IS_DEBIT_BALANCE,
351
- amt_earned <= payments
352
- ]):
406
+ elif all([not self.IS_DEBIT_BALANCE, amt_earned <= payments]):
353
407
  return payments - self.get_amount_earned()
354
408
  return Decimal.from_float(0.00)
355
409
 
@@ -364,15 +418,9 @@ class AccrualMixIn(models.Model):
364
418
  """
365
419
  if self.accrue:
366
420
  amt_earned = self.get_amount_earned()
367
- if all([
368
- self.IS_DEBIT_BALANCE,
369
- amt_earned <= self.amount_paid
370
- ]):
421
+ if all([self.IS_DEBIT_BALANCE, amt_earned <= self.amount_paid]):
371
422
  return self.amount_paid - amt_earned
372
- elif all([
373
- not self.IS_DEBIT_BALANCE,
374
- amt_earned >= self.amount_paid
375
- ]):
423
+ elif all([not self.IS_DEBIT_BALANCE, amt_earned >= self.amount_paid]):
376
424
  return amt_earned - self.amount_paid
377
425
  return Decimal.from_float(0.00)
378
426
 
@@ -412,9 +460,7 @@ class AccrualMixIn(models.Model):
412
460
  return False
413
461
  return not self.ledger.is_locked()
414
462
 
415
- def get_tx_type(self,
416
- acc_bal_type: dict,
417
- adjustment_amount: Decimal):
463
+ def get_tx_type(self, acc_bal_type: dict, adjustment_amount: Decimal):
418
464
  """
419
465
  Determines the transaction type associated with an increase/decrease of an account balance of the financial
420
466
  instrument.
@@ -436,10 +482,13 @@ class AccrualMixIn(models.Model):
436
482
  return self.TX_TYPE_MAPPING[acc_bal_type + d_or_i]
437
483
 
438
484
  @classmethod
439
- def split_amount(cls, amount: Union[Decimal, float],
440
- unit_split: Dict,
441
- account_uuid: UUID,
442
- 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:
443
492
  """
444
493
  Splits an amount into different proportions representing the unit splits.
445
494
  Makes sure that 100% of the amount is numerically allocated taking into consideration decimal points.
@@ -465,7 +514,9 @@ class AccrualMixIn(models.Model):
465
514
  split_results = dict()
466
515
  for i, (u, p) in enumerate(unit_split.items()):
467
516
  if i == SPLIT_LEN:
468
- 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
+ )
469
520
  else:
470
521
  alloc = round(p * amount, 2)
471
522
  split_results[(account_uuid, u, account_balance_type)] = alloc
@@ -487,11 +538,15 @@ class AccrualMixIn(models.Model):
487
538
  ledger_model = self.ledger
488
539
  if ledger_model.locked:
489
540
  if raise_exception:
490
- 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
+ )
491
544
  return
492
545
  ledger_model.lock(commit, raise_exception=raise_exception)
493
546
 
494
- 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
+ ):
495
550
  """
496
551
  Convenience method to un-lock the LedgerModel associated with the Accruable financial instrument.
497
552
 
@@ -505,7 +560,9 @@ class AccrualMixIn(models.Model):
505
560
  ledger_model = self.ledger
506
561
  if not ledger_model.is_locked():
507
562
  if raise_exception:
508
- 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
+ )
509
566
  return
510
567
  ledger_model.unlock(commit, raise_exception=raise_exception)
511
568
 
@@ -524,11 +581,15 @@ class AccrualMixIn(models.Model):
524
581
  ledger_model = self.ledger
525
582
  if ledger_model.posted:
526
583
  if raise_exception:
527
- 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
+ )
528
587
  return
529
588
  ledger_model.post(commit, raise_exception=raise_exception)
530
589
 
531
- 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
+ ):
532
593
  """
533
594
  Convenience method to un-lock the LedgerModel associated with the Accruable financial instrument.
534
595
 
@@ -542,22 +603,25 @@ class AccrualMixIn(models.Model):
542
603
  ledger_model = self.ledger
543
604
  if not ledger_model.is_posted():
544
605
  if raise_exception:
545
- 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
+ )
546
609
  return
547
610
  ledger_model.post(commit, raise_exception=raise_exception)
548
611
 
549
- def migrate_state(self,
550
- # todo: remove usermodel param...?
551
- user_model,
552
- entity_slug: str,
553
- itemtxs_qs: Optional[QuerySet] = None,
554
- force_migrate: bool = False,
555
- commit: bool = True,
556
- void: bool = False,
557
- je_timestamp: Optional[Union[str, date, datetime]] = None,
558
- raise_exception: bool = True,
559
- **kwargs):
560
-
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
+ ):
561
625
  """
562
626
  Migrates the current Accruable financial instrument into the books. The main objective of the migrate_state
563
627
  method is to determine the JournalEntry and TransactionModels necessary to accurately reflect the financial
@@ -571,7 +635,7 @@ class AccrualMixIn(models.Model):
571
635
  The EntityModel slug.
572
636
  itemtxs_qs: ItemTransactionModelQuerySet
573
637
  The pre-fetched ItemTransactionModelQuerySet containing the item information associated with the financial
574
- element migration. If provided, will avoid additional database query.
638
+ element migration. If provided, it will avoid an additional database query.
575
639
  force_migrate: bool
576
640
  Forces migration of the financial instrument bypassing the can_migrate() check.
577
641
  commit: bool
@@ -590,60 +654,45 @@ class AccrualMixIn(models.Model):
590
654
  """
591
655
 
592
656
  if self.can_migrate() or force_migrate:
593
-
594
- # getting current ledger state
595
- # todo: validate itemtxs_qs...?
596
- io_digest = self.ledger.digest(
597
- user_model=user_model,
598
- entity_slug=entity_slug,
599
- process_groups=True,
600
- process_roles=False,
601
- process_ratios=False,
602
- signs=False,
603
- by_unit=True
604
- )
605
-
606
- io_data = io_digest.get_io_data()
607
-
608
- accounts_data = io_data['accounts']
609
-
610
- # Index (account_uuid, unit_uuid, balance_type, role)
611
- current_ledger_state = {
612
- (a['account_uuid'], a['unit_uuid'], a['balance_type']): a['balance'] for a in accounts_data
613
- # (a['account_uuid'], a['unit_uuid'], a['balance_type'], a['role']): a['balance'] for a in digest_data
614
- }
615
-
616
657
  item_data = list(self.get_migration_data(queryset=itemtxs_qs))
617
658
  cogs_adjustment = defaultdict(lambda: Decimal('0.00'))
618
659
  inventory_adjustment = defaultdict(lambda: Decimal('0.00'))
619
660
  progress = self.get_progress()
620
661
 
621
662
  if isinstance(self, lazy_loader.get_bill_model()):
622
-
623
663
  for item in item_data:
624
664
  account_uuid_expense = item.get('item_model__expense_account__uuid')
625
- account_uuid_inventory = item.get('item_model__inventory_account__uuid')
665
+ account_uuid_inventory = item.get(
666
+ 'item_model__inventory_account__uuid'
667
+ )
626
668
  if account_uuid_expense:
627
669
  item['account_uuid'] = account_uuid_expense
628
- 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
+ )
629
673
  elif account_uuid_inventory:
630
674
  item['account_uuid'] = account_uuid_inventory
631
- 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
+ )
632
678
 
633
679
  elif isinstance(self, lazy_loader.get_invoice_model()):
634
-
635
680
  for item in item_data:
636
-
637
- account_uuid_earnings = item.get('item_model__earnings_account__uuid')
681
+ account_uuid_earnings = item.get(
682
+ 'item_model__earnings_account__uuid'
683
+ )
638
684
  account_uuid_cogs = item.get('item_model__cogs_account__uuid')
639
- account_uuid_inventory = item.get('item_model__inventory_account__uuid')
685
+ account_uuid_inventory = item.get(
686
+ 'item_model__inventory_account__uuid'
687
+ )
640
688
 
641
689
  if account_uuid_earnings:
642
690
  item['account_uuid'] = account_uuid_earnings
643
- 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
+ )
644
694
 
645
695
  if account_uuid_cogs and account_uuid_inventory:
646
-
647
696
  try:
648
697
  irq = item.get('item_model__inventory_received')
649
698
  irv = item.get('item_model__inventory_received_value')
@@ -659,27 +708,38 @@ class AccrualMixIn(models.Model):
659
708
 
660
709
  if tot_amt != 0:
661
710
  # keeps track of necessary transactions to increase COGS account...
662
- cogs_adjustment[(
663
- account_uuid_cogs,
664
- item.get('entity_unit__uuid'),
665
- item.get('item_model__cogs_account__balance_type')
666
- )] += 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
667
718
 
668
719
  # keeps track of necessary transactions to reduce inventory account...
669
- inventory_adjustment[(
670
- account_uuid_inventory,
671
- item.get('entity_unit__uuid'),
672
- item.get('item_model__inventory_account__balance_type')
673
- )] -= tot_amt * progress
674
-
675
- item_data_gb = groupby(item_data,
676
- key=lambda a: (a['account_uuid'],
677
- a['entity_unit__uuid'],
678
- 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
+ )
679
738
 
680
739
  # scaling down item amount based on progress...
681
740
  progress_item_idx = {
682
- 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
683
743
  }
684
744
 
685
745
  # tuple ( unit_uuid, total_amount ) sorted by uuid...
@@ -694,7 +754,8 @@ class AccrualMixIn(models.Model):
694
754
 
695
755
  # { unit_uuid: float (percent) }
696
756
  unit_percents = {
697
- 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()
698
759
  }
699
760
 
700
761
  if not void:
@@ -706,19 +767,19 @@ class AccrualMixIn(models.Model):
706
767
  amount=new_state['amount_paid'],
707
768
  unit_split=unit_percents,
708
769
  account_uuid=self.cash_account_id,
709
- account_balance_type='debit'
770
+ account_balance_type='debit',
710
771
  )
711
772
  amount_prepaid_split = self.split_amount(
712
773
  amount=new_state['amount_receivable'],
713
774
  unit_split=unit_percents,
714
775
  account_uuid=self.prepaid_account_id,
715
- account_balance_type='debit'
776
+ account_balance_type='debit',
716
777
  )
717
778
  amount_unearned_split = self.split_amount(
718
779
  amount=new_state['amount_unearned'],
719
780
  unit_split=unit_percents,
720
781
  account_uuid=self.unearned_account_id,
721
- account_balance_type='credit'
782
+ account_balance_type='credit',
722
783
  )
723
784
 
724
785
  new_ledger_state = dict()
@@ -732,19 +793,40 @@ class AccrualMixIn(models.Model):
732
793
 
733
794
  new_ledger_state.update(progress_item_idx)
734
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
+
735
818
  # list of all keys involved
736
819
  idx_keys = set(list(current_ledger_state) + list(new_ledger_state))
737
820
 
738
821
  # difference between new vs current
739
822
  diff_idx = {
740
- k: new_ledger_state.get(k, Decimal('0.00')) - current_ledger_state.get(k, Decimal('0.00')) for k in
741
- 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
742
826
  }
743
827
 
744
828
  # eliminates transactions with no amount...
745
- diff_idx = {
746
- k: v for k, v in diff_idx.items() if v
747
- }
829
+ diff_idx = {k: v for k, v in diff_idx.items() if v}
748
830
 
749
831
  if commit:
750
832
  JournalEntryModel = lazy_loader.get_journal_entry_model()
@@ -762,21 +844,29 @@ class AccrualMixIn(models.Model):
762
844
  timestamp=now_timestamp,
763
845
  description=self.get_migrate_state_desc(),
764
846
  origin='migration',
765
- ledger_id=self.ledger_id
766
- ) for u in unit_uuids
847
+ ledger_id=self.ledger_id,
848
+ )
849
+ for u in unit_uuids
767
850
  }
768
851
 
769
852
  for u, je in je_list.items():
770
853
  je.clean(verify=False)
771
854
 
772
855
  txs_list = [
773
- (unit_uuid, TransactionModel(
774
- journal_entry=je_list.get(unit_uuid),
775
- amount=abs(round(amt, 2)),
776
- tx_type=self.get_tx_type(acc_bal_type=bal_type, adjustment_amount=amt),
777
- account_id=acc_uuid,
778
- description=self.get_migrate_state_desc()
779
- )) 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
780
870
  ]
781
871
 
782
872
  for unit_uuid, tx in txs_list:
@@ -784,7 +874,10 @@ class AccrualMixIn(models.Model):
784
874
 
785
875
  for uid in unit_uuids:
786
876
  # validates each unit txs independently...
787
- 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
+ )
788
881
 
789
882
  # validates all txs as a whole (for safety)...
790
883
  txs = [tx for _, tx in txs_list]
@@ -796,19 +889,24 @@ class AccrualMixIn(models.Model):
796
889
  je.clean(verify=True)
797
890
  if je.is_verified():
798
891
  je.mark_as_locked(commit=False, raise_exception=True)
799
- 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
+ )
800
895
 
801
896
  if all([je.is_verified() for _, je in je_list.items()]):
802
897
  # only if all JEs have been verified will be posted and locked...
803
898
  JournalEntryModel.objects.bulk_update(
804
899
  objs=[je for _, je in je_list.items()],
805
- fields=['posted', 'locked', 'activity']
900
+ fields=['posted', 'locked', 'activity'],
806
901
  )
807
902
 
808
903
  return item_data, io_data
809
- else:
810
- if raise_exception:
811
- 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
+ )
812
910
 
813
911
  def void_state(self, commit: bool = False) -> Dict:
814
912
  """
@@ -852,7 +950,7 @@ class AccrualMixIn(models.Model):
852
950
  'amount_paid': self.get_amount_cash(),
853
951
  'amount_receivable': self.get_amount_prepaid(),
854
952
  'amount_unearned': self.get_amount_unearned(),
855
- 'amount_earned': self.get_amount_earned()
953
+ 'amount_earned': self.get_amount_earned(),
856
954
  }
857
955
  if commit:
858
956
  self.update_state(new_state)
@@ -875,7 +973,6 @@ class AccrualMixIn(models.Model):
875
973
  self.amount_earned = state['amount_earned']
876
974
 
877
975
  def clean(self):
878
-
879
976
  super().clean()
880
977
 
881
978
  if not self.amount_due:
@@ -886,21 +983,31 @@ class AccrualMixIn(models.Model):
886
983
 
887
984
  if self.accrue:
888
985
  if not self.prepaid_account_id:
889
- 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
+ )
890
989
  if not self.unearned_account_id:
891
- raise ValidationError(f'Accrued {self.__class__.__name__} must define an Unearned Income account.')
892
-
893
- if any([
894
- self.cash_account_id is not None,
895
- self.prepaid_account_id is not None,
896
- self.unearned_account_id is not None
897
- ]):
898
- if not all([
990
+ raise ValidationError(
991
+ f'Accrued {self.__class__.__name__} must define an Unearned Income account.'
992
+ )
993
+
994
+ if any(
995
+ [
899
996
  self.cash_account_id is not None,
900
997
  self.prepaid_account_id is not None,
901
- self.unearned_account_id is not None
902
- ]):
903
- 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
+ )
904
1011
 
905
1012
  # if self.accrue:
906
1013
  # if self.is_approved():
@@ -909,7 +1016,9 @@ class AccrualMixIn(models.Model):
909
1016
  # self.progress = Decimal.from_float(0.00)
910
1017
 
911
1018
  if self.amount_paid > self.amount_due:
912
- 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
+ )
913
1022
 
914
1023
  if self.is_paid():
915
1024
  self.progress = Decimal.from_float(1.0)
@@ -919,17 +1028,21 @@ class AccrualMixIn(models.Model):
919
1028
  if not self.date_paid:
920
1029
  self.date_paid = today
921
1030
  if self.date_paid > today:
922
- 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
+ )
923
1034
  else:
924
1035
  self.date_paid = None
925
1036
 
926
1037
  if self.is_void():
927
- if any([
928
- self.amount_paid,
929
- self.amount_earned,
930
- self.amount_unearned,
931
- self.amount_receivable
932
- ]):
1038
+ if any(
1039
+ [
1040
+ self.amount_paid,
1041
+ self.amount_earned,
1042
+ self.amount_unearned,
1043
+ self.amount_receivable,
1044
+ ]
1045
+ ):
933
1046
  raise ValidationError('Voided element cannot have any balance.')
934
1047
 
935
1048
  self.progress = Decimal.from_float(0.00)
@@ -950,6 +1063,7 @@ class PaymentTermsMixIn(models.Model):
950
1063
  A choice of TERM_CHOICES that determines the payment terms.
951
1064
 
952
1065
  """
1066
+
953
1067
  TERMS_ON_RECEIPT = 'on_receipt'
954
1068
  TERMS_NET_30 = 'net_30'
955
1069
  TERMS_NET_60 = 'net_60'
@@ -969,13 +1083,15 @@ class PaymentTermsMixIn(models.Model):
969
1083
  TERMS_NET_30: 30,
970
1084
  TERMS_NET_60: 60,
971
1085
  TERMS_NET_90: 90,
972
- TERMS_NET_90_PLUS: 120
1086
+ TERMS_NET_90_PLUS: 120,
973
1087
  }
974
1088
 
975
- terms = models.CharField(max_length=10,
976
- default='on_receipt',
977
- choices=TERM_CHOICES,
978
- verbose_name=_('Terms'))
1089
+ terms = models.CharField(
1090
+ max_length=10,
1091
+ default='on_receipt',
1092
+ choices=TERM_CHOICES,
1093
+ verbose_name=_('Terms'),
1094
+ )
979
1095
  date_due = models.DateField(verbose_name=_('Due Date'), null=True, blank=True)
980
1096
 
981
1097
  class Meta:
@@ -1088,7 +1204,10 @@ class MarkdownNotesMixIn(models.Model):
1088
1204
  markdown_notes: str
1089
1205
  A string of text representing the mark-down document.
1090
1206
  """
1091
- 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
+ )
1092
1211
 
1093
1212
  class Meta:
1094
1213
  abstract = True
@@ -1147,7 +1266,7 @@ class FinancialAccountInfoMixin(models.Model):
1147
1266
  ACCOUNT_ST_LOAN: LIABILITY_CL_ST_NOTES_PAYABLE,
1148
1267
  ACCOUNT_LT_LOAN: LIABILITY_LTL_NOTES_PAYABLE,
1149
1268
  ACCOUNT_MORTGAGE: LIABILITY_LTL_MORTGAGE_PAYABLE,
1150
- ACCOUNT_OTHER: LIABILITY_CL_OTHER
1269
+ ACCOUNT_OTHER: LIABILITY_CL_OTHER,
1151
1270
  }
1152
1271
 
1153
1272
  ACCOUNT_TYPE_CHOICES = [
@@ -1167,7 +1286,7 @@ class FinancialAccountInfoMixin(models.Model):
1167
1286
  'SAVINGS': ACCOUNT_SAVINGS,
1168
1287
  'MONEYMRKT': ACCOUNT_MONEY_MKT,
1169
1288
  'CREDITLINE': ACCOUNT_CREDIT_CARD,
1170
- 'CD': ACCOUNT_CERT_DEPOSIT
1289
+ 'CD': ACCOUNT_CERT_DEPOSIT,
1171
1290
  }
1172
1291
 
1173
1292
  VALID_ACCOUNT_TYPES = tuple(atc[0] for atc in ACCOUNT_TYPE_CHOICES)
@@ -1177,23 +1296,33 @@ class FinancialAccountInfoMixin(models.Model):
1177
1296
  blank=True,
1178
1297
  null=True,
1179
1298
  verbose_name=_('Financial Institution'),
1180
- 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')
1181
1320
  )
1182
- account_number = models.CharField(max_length=30, null=True, blank=True,
1183
- validators=[
1184
- int_list_validator(sep='', message=_('Only digits allowed'))
1185
- ], verbose_name=_('Account Number'))
1186
- routing_number = models.CharField(max_length=30, null=True, blank=True,
1187
- validators=[
1188
- int_list_validator(sep='', message=_('Only digits allowed'))
1189
- ], verbose_name=_('Routing Number'))
1190
- aba_number = models.CharField(max_length=30, null=True, blank=True, verbose_name=_('ABA Number'))
1191
- swift_number = models.CharField(max_length=30, null=True, blank=True, verbose_name=_('SWIFT Number'))
1192
1321
  account_type = models.CharField(
1193
1322
  choices=ACCOUNT_TYPE_CHOICES,
1194
1323
  max_length=20,
1195
1324
  default=ACCOUNT_CHECKING,
1196
- verbose_name=_('Account Type')
1325
+ verbose_name=_('Account Type'),
1197
1326
  )
1198
1327
 
1199
1328
  class Meta:
@@ -1210,16 +1339,13 @@ class FinancialAccountInfoMixin(models.Model):
1210
1339
  return f'*{self.routing_number[-n:]}'
1211
1340
 
1212
1341
  def get_account_type_from_ofx(self, ofx_type):
1213
- return self.ACCOUNT_TYPE_OFX_MAPPING.get(
1214
- ofx_type, self.ACCOUNT_OTHER
1215
- )
1342
+ return self.ACCOUNT_TYPE_OFX_MAPPING.get(ofx_type, self.ACCOUNT_OTHER)
1216
1343
 
1217
1344
 
1218
1345
  class TaxInfoMixIn(models.Model):
1219
- tax_id_number = models.CharField(max_length=30,
1220
- null=True,
1221
- blank=True,
1222
- 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
+ )
1223
1349
 
1224
1350
  class Meta:
1225
1351
  abstract = True
@@ -1238,14 +1364,17 @@ class TaxCollectionMixIn(models.Model):
1238
1364
  sales_tax_rate: float
1239
1365
  The tax rate as a float. A Number between 0.00 and 1.00.
1240
1366
  """
1241
- sales_tax_rate = models.FloatField(default=0.00000,
1242
- verbose_name=_('Sales Tax Rate'),
1243
- null=True,
1244
- blank=True,
1245
- validators=[
1246
- MinValueValidator(limit_value=0.00000),
1247
- MaxValueValidator(limit_value=1.00000)
1248
- ])
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
+ )
1249
1378
 
1250
1379
  class Meta:
1251
1380
  abstract = True
@@ -1259,13 +1388,16 @@ class LoggingMixIn:
1259
1388
  Implements functionality used to add logging capabilities to any python class.
1260
1389
  Useful for production and or testing environments.
1261
1390
  """
1391
+
1262
1392
  LOGGER_NAME_ATTRIBUTE = None
1263
1393
  LOGGER_BYPASS_DEBUG = False
1264
1394
 
1265
1395
  def get_logger_name(self):
1266
1396
  if self.LOGGER_NAME_ATTRIBUTE is None:
1267
- raise NotImplementedError(f'{self.__class__.__name__} must define LOGGER_NAME_ATTRIBUTE of implement '
1268
- '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
+ )
1269
1401
  return getattr(self, self.LOGGER_NAME_ATTRIBUTE)
1270
1402
 
1271
1403
  def get_logger(self) -> logging.Logger:
@@ -1300,7 +1432,9 @@ class ItemizeMixIn(models.Model):
1300
1432
  """
1301
1433
  raise NotImplementedError()
1302
1434
 
1303
- 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
+ ):
1304
1438
  """
1305
1439
  Fetches the ItemTransactionModelQuerySet associated with the model.
1306
1440
 
@@ -1330,14 +1464,19 @@ class ItemizeMixIn(models.Model):
1330
1464
  Item transaction list to replace/aggregate.
1331
1465
  """
1332
1466
  if isinstance(itemtxs, dict):
1333
- if all([
1334
- all([
1335
- isinstance(d, dict),
1336
- 'unit_cost' in d,
1337
- 'quantity' in d,
1338
- 'total_amount' in d
1339
- ]) for i, d in itemtxs.items()
1340
- ]):
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
+ ):
1341
1480
  return
1342
1481
  raise ItemizeError('itemtxs must be an instance of dict.')
1343
1482
 
@@ -1361,7 +1500,9 @@ class ItemizeMixIn(models.Model):
1361
1500
  item_model_qs_map = {i.item_number: i for i in item_model_qs}
1362
1501
 
1363
1502
  if itemtxs.keys() != item_model_qs_map.keys():
1364
- 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
+ )
1365
1506
 
1366
1507
  if isinstance(self, EstimateModel):
1367
1508
  return [
@@ -1371,7 +1512,8 @@ class ItemizeMixIn(models.Model):
1371
1512
  ce_quantity=i['quantity'],
1372
1513
  ce_unit_cost_estimate=i['unit_cost'],
1373
1514
  ce_unit_revenue_estimate=i['unit_revenue'],
1374
- ) for item_number, i in itemtxs.items()
1515
+ )
1516
+ for item_number, i in itemtxs.items()
1375
1517
  ]
1376
1518
 
1377
1519
  if isinstance(self, PurchaseOrder):
@@ -1381,7 +1523,8 @@ class ItemizeMixIn(models.Model):
1381
1523
  item_model=item_model_qs_map[item_number],
1382
1524
  po_quantity=i['quantity'],
1383
1525
  po_unit_cost=i['unit_cost'],
1384
- ) for item_number, i in itemtxs.items()
1526
+ )
1527
+ for item_number, i in itemtxs.items()
1385
1528
  ]
1386
1529
 
1387
1530
  BillModel = lazy_loader.get_bill_model()
@@ -1393,8 +1536,9 @@ class ItemizeMixIn(models.Model):
1393
1536
  invoice_model=self if isinstance(self, InvoiceModel) else None,
1394
1537
  item_model=item_model_qs_map[item_number],
1395
1538
  quantity=i['quantity'],
1396
- unit_cost=i['unit_cost']
1397
- ) for item_number, i in itemtxs.items()
1539
+ unit_cost=i['unit_cost'],
1540
+ )
1541
+ for item_number, i in itemtxs.items()
1398
1542
  ]
1399
1543
 
1400
1544
  def migrate_itemtxs(self, itemtxs: Dict, operation: str, commit: bool = False):
@@ -1428,7 +1572,6 @@ class ItemizeMixIn(models.Model):
1428
1572
  itx.clean()
1429
1573
 
1430
1574
  if commit:
1431
-
1432
1575
  ItemTransactionModel = lazy_loader.get_item_transaction_model()
1433
1576
 
1434
1577
  if operation == self.ITEMIZE_APPEND: