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.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +182 -63
- django_ledger/forms/estimate.py +3 -6
- django_ledger/forms/invoice.py +3 -7
- django_ledger/forms/item.py +10 -18
- django_ledger/forms/purchase_order.py +2 -4
- django_ledger/io/io_core.py +515 -400
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +386 -333
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +100 -66
- django_ledger/models/data_import.py +818 -323
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +891 -644
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +46 -26
- django_ledger/models/items.py +503 -142
- django_ledger/models/journal_entry.py +61 -47
- django_ledger/models/ledger.py +106 -42
- django_ledger/models/mixins.py +424 -281
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +242 -139
- django_ledger/models/unit.py +93 -54
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +121 -70
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +57 -71
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/static/django_ledger/css/djl_styles.css +273 -0
- django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
- django_ledger/templates/django_ledger/components/menu.html +41 -26
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
- django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
- django_ledger/templates/django_ledger/layouts/base.html +7 -2
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/tests/test_accounts.py +1 -2
- django_ledger/tests/test_io.py +17 -0
- django_ledger/tests/test_purchase_order.py +3 -3
- django_ledger/tests/test_transactions.py +1 -2
- django_ledger/urls/__init__.py +1 -4
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/bill.py +8 -11
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +68 -30
- django_ledger/views/data_import.py +120 -66
- django_ledger/views/djl_api.py +3 -5
- django_ledger/views/entity.py +2 -4
- django_ledger/views/estimate.py +3 -7
- django_ledger/views/inventory.py +3 -5
- django_ledger/views/invoice.py +4 -6
- django_ledger/views/item.py +7 -11
- django_ledger/views/journal_entry.py +1 -2
- django_ledger/views/mixins.py +125 -93
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +54 -16
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
- django_ledger-0.8.1.dist-info/top_level.txt +1 -0
- django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
- django_ledger/contrib/django_ledger_graphene/api.py +0 -42
- django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
- django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
- django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
- django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
- django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
- django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
- django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
- django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
- django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
- django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
- django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
- django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
- django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
- django_ledger/contrib/django_ledger_graphene/views.py +0 -12
- django_ledger-0.7.11.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
django_ledger/models/mixins.py
CHANGED
|
@@ -5,18 +5,23 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
|
|
|
5
5
|
This module implements the different model MixIns used on different Django Ledger Models to implement common
|
|
6
6
|
functionality.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
import logging
|
|
9
10
|
from collections import defaultdict
|
|
10
|
-
from datetime import
|
|
11
|
+
from datetime import date, datetime, timedelta
|
|
11
12
|
from decimal import Decimal
|
|
12
13
|
from itertools import groupby
|
|
13
|
-
from typing import Optional, Union
|
|
14
|
+
from typing import Dict, Optional, Union
|
|
14
15
|
from uuid import UUID
|
|
15
16
|
|
|
16
17
|
from django.conf import settings
|
|
17
18
|
from django.core.exceptions import ValidationError
|
|
18
|
-
from django.core.validators import
|
|
19
|
-
|
|
19
|
+
from django.core.validators import (
|
|
20
|
+
MaxValueValidator,
|
|
21
|
+
MinLengthValidator,
|
|
22
|
+
MinValueValidator,
|
|
23
|
+
int_list_validator,
|
|
24
|
+
)
|
|
20
25
|
from django.db import models
|
|
21
26
|
from django.db.models import QuerySet
|
|
22
27
|
from django.utils.encoding import force_str
|
|
@@ -24,10 +29,19 @@ from django.utils.translation import gettext_lazy as _
|
|
|
24
29
|
from markdown import markdown
|
|
25
30
|
|
|
26
31
|
from django_ledger.io import (
|
|
27
|
-
ASSET_CA_CASH,
|
|
28
|
-
LIABILITY_CL_ACC_PAYABLE,
|
|
32
|
+
ASSET_CA_CASH,
|
|
33
|
+
LIABILITY_CL_ACC_PAYABLE,
|
|
34
|
+
LIABILITY_CL_OTHER,
|
|
35
|
+
LIABILITY_CL_ST_NOTES_PAYABLE,
|
|
36
|
+
LIABILITY_LTL_MORTGAGE_PAYABLE,
|
|
37
|
+
LIABILITY_LTL_NOTES_PAYABLE,
|
|
38
|
+
)
|
|
39
|
+
from django_ledger.io.io_core import (
|
|
40
|
+
check_tx_balance,
|
|
41
|
+
get_localdate,
|
|
42
|
+
get_localtime,
|
|
43
|
+
validate_io_timestamp,
|
|
29
44
|
)
|
|
30
|
-
from django_ledger.io.io_core import validate_io_timestamp, check_tx_balance, get_localtime, get_localdate
|
|
31
45
|
from django_ledger.models.utils import lazy_loader
|
|
32
46
|
|
|
33
47
|
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
|
|
@@ -44,13 +58,18 @@ class SlugNameMixIn(models.Model):
|
|
|
44
58
|
name: str
|
|
45
59
|
A human-readable name for display purposes. Maximum 150 characters.
|
|
46
60
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
|
|
62
|
+
slug = models.SlugField(
|
|
63
|
+
max_length=50,
|
|
64
|
+
editable=False,
|
|
65
|
+
unique=True,
|
|
66
|
+
validators=[
|
|
67
|
+
MinLengthValidator(
|
|
68
|
+
limit_value=10,
|
|
69
|
+
message=_('Slug field must contain at least 10 characters.'),
|
|
70
|
+
)
|
|
71
|
+
],
|
|
72
|
+
)
|
|
54
73
|
name = models.CharField(max_length=150, null=True, blank=True)
|
|
55
74
|
|
|
56
75
|
class Meta:
|
|
@@ -71,6 +90,7 @@ class CreateUpdateMixIn(models.Model):
|
|
|
71
90
|
updated: str
|
|
72
91
|
An updated timestamp used to identify when models are updated.
|
|
73
92
|
"""
|
|
93
|
+
|
|
74
94
|
created = models.DateTimeField(auto_now_add=True)
|
|
75
95
|
updated = models.DateTimeField(auto_now=True, null=True, blank=True)
|
|
76
96
|
|
|
@@ -106,29 +126,54 @@ class ContactInfoMixIn(models.Model):
|
|
|
106
126
|
phone: str
|
|
107
127
|
A string used to document the contact phone.
|
|
108
128
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
|
|
130
|
+
address_1 = models.CharField(
|
|
131
|
+
max_length=70, verbose_name=_('Address Line 1'), null=True, blank=True
|
|
132
|
+
)
|
|
133
|
+
address_2 = models.CharField(
|
|
134
|
+
null=True, blank=True, max_length=70, verbose_name=_('Address Line 2')
|
|
135
|
+
)
|
|
136
|
+
city = models.CharField(
|
|
137
|
+
null=True, blank=True, max_length=70, verbose_name=_('City')
|
|
138
|
+
)
|
|
139
|
+
state = models.CharField(
|
|
140
|
+
null=True, blank=True, max_length=70, verbose_name=_('State/Province')
|
|
141
|
+
)
|
|
142
|
+
zip_code = models.CharField(
|
|
143
|
+
null=True, blank=True, max_length=20, verbose_name=_('Zip Code')
|
|
144
|
+
)
|
|
145
|
+
country = models.CharField(
|
|
146
|
+
null=True, blank=True, max_length=70, verbose_name=_('Country')
|
|
147
|
+
)
|
|
115
148
|
email = models.EmailField(null=True, blank=True, verbose_name=_('Email'))
|
|
116
149
|
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
|
117
|
-
phone = models.CharField(
|
|
150
|
+
phone = models.CharField(
|
|
151
|
+
max_length=30, null=True, blank=True, verbose_name=_('Phone Number')
|
|
152
|
+
)
|
|
118
153
|
|
|
119
154
|
class Meta:
|
|
120
155
|
abstract = True
|
|
121
156
|
|
|
122
157
|
def get_cszc(self):
|
|
123
|
-
if all(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
if all(
|
|
159
|
+
[
|
|
160
|
+
self.city,
|
|
161
|
+
self.state,
|
|
162
|
+
self.zip_code,
|
|
163
|
+
self.country,
|
|
164
|
+
]
|
|
165
|
+
):
|
|
129
166
|
return f'{self.city}, {self.state}. {self.zip_code}. {self.country}'
|
|
130
167
|
|
|
131
168
|
def clean(self):
|
|
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(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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)] =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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)
|
|
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')
|
|
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'))
|
|
741
|
-
|
|
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
|
-
)
|
|
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
|
-
(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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(
|
|
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(
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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(
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
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(
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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(
|
|
1268
|
-
|
|
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(
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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(
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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:
|