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/bill.py
CHANGED
|
@@ -6,19 +6,13 @@ This module implements the BillModel, which represents an Invoice received from
|
|
|
6
6
|
the Vendor states the amount owed by the recipient for the purposes of supplying goods and/or services.
|
|
7
7
|
In addition to tracking the bill amount, it tracks the paid and due amount.
|
|
8
8
|
|
|
9
|
-
Examples
|
|
10
|
-
________
|
|
11
|
-
>>> user_model = request.user # django UserModel
|
|
12
|
-
>>> entity_slug = kwargs['entity_slug'] # may come from view kwargs
|
|
13
|
-
>>> bill_model = BillModel()
|
|
14
|
-
>>> ledger_model, bill_model = bill_model.configure(entity_slug=entity_slug, user_model=user_model)
|
|
15
|
-
>>> bill_model.save()
|
|
16
9
|
"""
|
|
17
10
|
|
|
11
|
+
import warnings
|
|
18
12
|
from datetime import date, datetime
|
|
19
13
|
from decimal import Decimal
|
|
20
14
|
from typing import Union, Optional, Tuple, Dict, List
|
|
21
|
-
from uuid import uuid4
|
|
15
|
+
from uuid import uuid4, UUID
|
|
22
16
|
|
|
23
17
|
from django.contrib.auth import get_user_model
|
|
24
18
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|
@@ -31,14 +25,20 @@ from django.utils.translation import gettext_lazy as _
|
|
|
31
25
|
|
|
32
26
|
from django_ledger.io import ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_PAYABLE
|
|
33
27
|
from django_ledger.io.io_core import get_localtime, get_localdate
|
|
28
|
+
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
34
29
|
from django_ledger.models.entity import EntityModel
|
|
35
|
-
from django_ledger.models.items import
|
|
30
|
+
from django_ledger.models.items import (
|
|
31
|
+
ItemTransactionModelQuerySet,
|
|
32
|
+
ItemTransactionModel,
|
|
33
|
+
ItemModel,
|
|
34
|
+
ItemModelQuerySet,
|
|
35
|
+
)
|
|
36
36
|
from django_ledger.models.mixins import (
|
|
37
37
|
CreateUpdateMixIn,
|
|
38
38
|
AccrualMixIn,
|
|
39
39
|
MarkdownNotesMixIn,
|
|
40
40
|
PaymentTermsMixIn,
|
|
41
|
-
ItemizeMixIn
|
|
41
|
+
ItemizeMixIn,
|
|
42
42
|
)
|
|
43
43
|
from django_ledger.models.signals import (
|
|
44
44
|
bill_status_draft,
|
|
@@ -49,7 +49,11 @@ from django_ledger.models.signals import (
|
|
|
49
49
|
bill_status_void,
|
|
50
50
|
)
|
|
51
51
|
from django_ledger.models.utils import lazy_loader
|
|
52
|
-
from django_ledger.settings import (
|
|
52
|
+
from django_ledger.settings import (
|
|
53
|
+
DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING,
|
|
54
|
+
DJANGO_LEDGER_BILL_NUMBER_PREFIX,
|
|
55
|
+
DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR,
|
|
56
|
+
)
|
|
53
57
|
|
|
54
58
|
UserModel = get_user_model()
|
|
55
59
|
|
|
@@ -66,6 +70,32 @@ class BillModelQuerySet(QuerySet):
|
|
|
66
70
|
building customized reports.
|
|
67
71
|
"""
|
|
68
72
|
|
|
73
|
+
def for_user(self, user_model) -> 'BillModelQuerySet':
|
|
74
|
+
"""
|
|
75
|
+
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
76
|
+
May include BillModels from multiple Entities.
|
|
77
|
+
|
|
78
|
+
The user has access to bills if:
|
|
79
|
+
1. Is listed as Manager of Entity.
|
|
80
|
+
2. Is the Admin of the Entity.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
__________
|
|
84
|
+
user_model
|
|
85
|
+
Logged in and authenticated django UserModel instance.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
_______
|
|
89
|
+
BillModelQuerySet
|
|
90
|
+
Returns a BillModelQuerySet with applied filters.
|
|
91
|
+
"""
|
|
92
|
+
if user_model.is_superuser:
|
|
93
|
+
return self
|
|
94
|
+
return self.filter(
|
|
95
|
+
Q(ledger__entity__admin=user_model)
|
|
96
|
+
| Q(ledger__entity__managers__in=[user_model])
|
|
97
|
+
)
|
|
98
|
+
|
|
69
99
|
def draft(self):
|
|
70
100
|
"""
|
|
71
101
|
Default status of any bill that is created.
|
|
@@ -147,8 +177,8 @@ class BillModelQuerySet(QuerySet):
|
|
|
147
177
|
Returns a QuerySet of active bills only.
|
|
148
178
|
"""
|
|
149
179
|
return self.filter(
|
|
150
|
-
Q(bill_status__exact=BillModel.BILL_STATUS_APPROVED)
|
|
151
|
-
Q(bill_status__exact=BillModel.BILL_STATUS_PAID)
|
|
180
|
+
Q(bill_status__exact=BillModel.BILL_STATUS_APPROVED)
|
|
181
|
+
| Q(bill_status__exact=BillModel.BILL_STATUS_PAID)
|
|
152
182
|
)
|
|
153
183
|
|
|
154
184
|
def overdue(self):
|
|
@@ -162,7 +192,7 @@ class BillModelQuerySet(QuerySet):
|
|
|
162
192
|
"""
|
|
163
193
|
return self.filter(date_due__lt=get_localdate())
|
|
164
194
|
|
|
165
|
-
def unpaid(self):
|
|
195
|
+
def unpaid(self) -> 'BillModelQuerySet':
|
|
166
196
|
"""
|
|
167
197
|
Unpaid bills are those that are approved but have not received 100% of the amount due.
|
|
168
198
|
Equivalent to approved().
|
|
@@ -177,90 +207,57 @@ class BillModelQuerySet(QuerySet):
|
|
|
177
207
|
|
|
178
208
|
class BillModelManager(Manager):
|
|
179
209
|
"""
|
|
180
|
-
A custom
|
|
210
|
+
A custom-defined BillModelManager that will act as an interface to handling the initial DB queries
|
|
181
211
|
to the BillModel. The default "get_queryset" has been overridden to refer the custom defined
|
|
182
212
|
"BillModelQuerySet".
|
|
183
213
|
"""
|
|
184
214
|
|
|
185
|
-
def get_queryset(self):
|
|
186
|
-
qs =
|
|
187
|
-
return qs.select_related(
|
|
188
|
-
'ledger',
|
|
189
|
-
'ledger__entity'
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
def for_user(self, user_model) -> BillModelQuerySet:
|
|
193
|
-
"""
|
|
194
|
-
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
195
|
-
May include BillModels from multiple Entities.
|
|
196
|
-
|
|
197
|
-
The user has access to bills if:
|
|
198
|
-
1. Is listed as Manager of Entity.
|
|
199
|
-
2. Is the Admin of the Entity.
|
|
200
|
-
|
|
201
|
-
Parameters
|
|
202
|
-
__________
|
|
203
|
-
user_model
|
|
204
|
-
Logged in and authenticated django UserModel instance.
|
|
205
|
-
|
|
206
|
-
Examples
|
|
207
|
-
________
|
|
208
|
-
>>> request_user = request.user
|
|
209
|
-
>>> bill_model_qs = BillModel.objects.for_user(user_model=request_user)
|
|
210
|
-
|
|
211
|
-
Returns
|
|
212
|
-
_______
|
|
213
|
-
BillModelQuerySet
|
|
214
|
-
Returns a BillModelQuerySet with applied filters.
|
|
215
|
-
"""
|
|
216
|
-
qs = self.get_queryset()
|
|
217
|
-
if user_model.is_superuser:
|
|
218
|
-
return qs
|
|
219
|
-
return qs.filter(
|
|
220
|
-
Q(ledger__entity__admin=user_model) |
|
|
221
|
-
Q(ledger__entity__managers__in=[user_model])
|
|
222
|
-
)
|
|
215
|
+
def get_queryset(self) -> BillModelQuerySet:
|
|
216
|
+
qs = BillModelQuerySet(self.model, using=self._db)
|
|
217
|
+
return qs.select_related('ledger', 'ledger__entity')
|
|
223
218
|
|
|
224
|
-
|
|
219
|
+
@deprecated_entity_slug_behavior
|
|
220
|
+
def for_entity(
|
|
221
|
+
self, entity_model: EntityModel | str | UUID = None, **kwargs
|
|
222
|
+
) -> BillModelQuerySet:
|
|
225
223
|
"""
|
|
226
224
|
Fetches a QuerySet of BillModels associated with a specific EntityModel & UserModel.
|
|
227
225
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
228
226
|
|
|
229
227
|
Parameters
|
|
230
228
|
__________
|
|
231
|
-
|
|
229
|
+
entity_model: str or EntityModel
|
|
232
230
|
The entity slug or EntityModel used for filtering the QuerySet.
|
|
233
|
-
user_model
|
|
234
|
-
Logged in and authenticated django UserModel instance.
|
|
235
|
-
|
|
236
|
-
Examples
|
|
237
|
-
________
|
|
238
|
-
>>> request_user = request.user
|
|
239
|
-
>>> slug = kwargs['entity_slug'] # may come from request kwargs
|
|
240
|
-
>>> bill_model_qs = BillModel.objects.for_entity(user_model=request_user, entity_slug=slug)
|
|
241
231
|
|
|
242
232
|
Returns
|
|
243
233
|
_______
|
|
244
234
|
BillModelQuerySet
|
|
245
235
|
Returns a BillModelQuerySet with applied filters.
|
|
246
236
|
"""
|
|
247
|
-
qs = self.
|
|
248
|
-
if
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Q(ledger__entity__slug__exact=entity_slug)
|
|
237
|
+
qs = self.get_queryset()
|
|
238
|
+
if 'user_model' in kwargs:
|
|
239
|
+
warnings.warn(
|
|
240
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
241
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
242
|
+
DeprecationWarning,
|
|
243
|
+
stacklevel=2,
|
|
255
244
|
)
|
|
245
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
246
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
247
|
+
|
|
248
|
+
if isinstance(entity_model, EntityModel):
|
|
249
|
+
qs = qs.filter(ledger__entity=entity_model)
|
|
250
|
+
elif isinstance(entity_model, str):
|
|
251
|
+
qs = qs.filter(ledger__entity__slug__exact=entity_model)
|
|
252
|
+
elif isinstance(entity_model, UUID):
|
|
253
|
+
qs = qs.filter(ledger__entity_id=entity_model)
|
|
254
|
+
else:
|
|
255
|
+
raise BillModelValidationError('Must pass EntityModel, slug or UUID')
|
|
256
|
+
return qs
|
|
256
257
|
|
|
257
258
|
|
|
258
259
|
class BillModelAbstract(
|
|
259
|
-
AccrualMixIn,
|
|
260
|
-
ItemizeMixIn,
|
|
261
|
-
PaymentTermsMixIn,
|
|
262
|
-
MarkdownNotesMixIn,
|
|
263
|
-
CreateUpdateMixIn
|
|
260
|
+
AccrualMixIn, ItemizeMixIn, PaymentTermsMixIn, MarkdownNotesMixIn, CreateUpdateMixIn
|
|
264
261
|
):
|
|
265
262
|
"""
|
|
266
263
|
This is the main abstract class which the BillModel database will inherit from.
|
|
@@ -328,6 +325,7 @@ class BillModelAbstract(
|
|
|
328
325
|
The canceled date represents the date when the BillModel was canceled, if applicable.
|
|
329
326
|
Will be null unless BillModel is canceled. Defaults to :func:`localdate <django.utils.timezone.localdate>`.
|
|
330
327
|
"""
|
|
328
|
+
|
|
331
329
|
REL_NAME_PREFIX = 'bill'
|
|
332
330
|
IS_DEBIT_BALANCE = False
|
|
333
331
|
ALLOW_MIGRATE = True
|
|
@@ -345,7 +343,7 @@ class BillModelAbstract(
|
|
|
345
343
|
(BILL_STATUS_APPROVED, _('Approved')),
|
|
346
344
|
(BILL_STATUS_PAID, _('Paid')),
|
|
347
345
|
(BILL_STATUS_CANCELED, _('Canceled')),
|
|
348
|
-
(BILL_STATUS_VOID, _('Void'))
|
|
346
|
+
(BILL_STATUS_VOID, _('Void')),
|
|
349
347
|
]
|
|
350
348
|
"""
|
|
351
349
|
The different bill status options and their representation in the Database.
|
|
@@ -358,38 +356,79 @@ class BillModelAbstract(
|
|
|
358
356
|
on_delete=models.CASCADE,
|
|
359
357
|
null=True,
|
|
360
358
|
blank=True,
|
|
361
|
-
editable=False
|
|
359
|
+
editable=False,
|
|
360
|
+
)
|
|
361
|
+
bill_number = models.SlugField(
|
|
362
|
+
max_length=20, verbose_name=_('Bill Number'), editable=False
|
|
363
|
+
)
|
|
364
|
+
bill_status = models.CharField(
|
|
365
|
+
max_length=10,
|
|
366
|
+
choices=BILL_STATUS,
|
|
367
|
+
default=BILL_STATUS[0][0],
|
|
368
|
+
verbose_name=_('Bill Status'),
|
|
369
|
+
)
|
|
370
|
+
xref = models.SlugField(
|
|
371
|
+
null=True, blank=True, verbose_name=_('External Reference Number')
|
|
372
|
+
)
|
|
373
|
+
vendor = models.ForeignKey(
|
|
374
|
+
'django_ledger.VendorModel', on_delete=models.CASCADE, verbose_name=_('Vendor')
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
cash_account = models.ForeignKey(
|
|
378
|
+
'django_ledger.AccountModel',
|
|
379
|
+
on_delete=models.RESTRICT,
|
|
380
|
+
null=True,
|
|
381
|
+
blank=True,
|
|
382
|
+
verbose_name=_('Cash Account'),
|
|
383
|
+
related_name=f'{REL_NAME_PREFIX}_cash_account',
|
|
384
|
+
)
|
|
385
|
+
prepaid_account = models.ForeignKey(
|
|
386
|
+
'django_ledger.AccountModel',
|
|
387
|
+
on_delete=models.RESTRICT,
|
|
388
|
+
null=True,
|
|
389
|
+
blank=True,
|
|
390
|
+
verbose_name=_('Prepaid Account'),
|
|
391
|
+
related_name=f'{REL_NAME_PREFIX}_prepaid_account',
|
|
392
|
+
)
|
|
393
|
+
unearned_account = models.ForeignKey(
|
|
394
|
+
'django_ledger.AccountModel',
|
|
395
|
+
on_delete=models.RESTRICT,
|
|
396
|
+
null=True,
|
|
397
|
+
blank=True,
|
|
398
|
+
verbose_name=_('Unearned Account'),
|
|
399
|
+
related_name=f'{REL_NAME_PREFIX}_unearned_account',
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
additional_info = models.JSONField(
|
|
403
|
+
blank=True, null=True, default=dict, verbose_name=_('Bill Additional Info')
|
|
404
|
+
)
|
|
405
|
+
bill_items = models.ManyToManyField(
|
|
406
|
+
'django_ledger.ItemModel',
|
|
407
|
+
through='django_ledger.ItemTransactionModel',
|
|
408
|
+
through_fields=('bill_model', 'item_model'),
|
|
409
|
+
verbose_name=_('Bill Items'),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
ce_model = models.ForeignKey(
|
|
413
|
+
'django_ledger.EstimateModel',
|
|
414
|
+
on_delete=models.RESTRICT,
|
|
415
|
+
null=True,
|
|
416
|
+
blank=True,
|
|
417
|
+
verbose_name=_('Associated Customer Job/Estimate'),
|
|
362
418
|
)
|
|
363
|
-
bill_number = models.SlugField(max_length=20, verbose_name=_('Bill Number'), editable=False)
|
|
364
|
-
bill_status = models.CharField(max_length=10,
|
|
365
|
-
choices=BILL_STATUS,
|
|
366
|
-
default=BILL_STATUS[0][0],
|
|
367
|
-
verbose_name=_('Bill Status'))
|
|
368
|
-
xref = models.SlugField(null=True, blank=True, verbose_name=_('External Reference Number'))
|
|
369
|
-
vendor = models.ForeignKey('django_ledger.VendorModel',
|
|
370
|
-
on_delete=models.CASCADE,
|
|
371
|
-
verbose_name=_('Vendor'))
|
|
372
|
-
additional_info = models.JSONField(blank=True,
|
|
373
|
-
null=True,
|
|
374
|
-
default=dict,
|
|
375
|
-
verbose_name=_('Bill Additional Info'))
|
|
376
|
-
bill_items = models.ManyToManyField('django_ledger.ItemModel',
|
|
377
|
-
through='django_ledger.ItemTransactionModel',
|
|
378
|
-
through_fields=('bill_model', 'item_model'),
|
|
379
|
-
verbose_name=_('Bill Items'))
|
|
380
|
-
|
|
381
|
-
ce_model = models.ForeignKey('django_ledger.EstimateModel',
|
|
382
|
-
on_delete=models.RESTRICT,
|
|
383
|
-
null=True,
|
|
384
|
-
blank=True,
|
|
385
|
-
verbose_name=_('Associated Customer Job/Estimate'))
|
|
386
419
|
|
|
387
420
|
date_draft = models.DateField(null=True, blank=True, verbose_name=_('Draft Date'))
|
|
388
|
-
date_in_review = models.DateField(
|
|
389
|
-
|
|
421
|
+
date_in_review = models.DateField(
|
|
422
|
+
null=True, blank=True, verbose_name=_('In Review Date')
|
|
423
|
+
)
|
|
424
|
+
date_approved = models.DateField(
|
|
425
|
+
null=True, blank=True, verbose_name=_('Approved Date')
|
|
426
|
+
)
|
|
390
427
|
date_paid = models.DateField(null=True, blank=True, verbose_name=_('Paid Date'))
|
|
391
428
|
date_void = models.DateField(null=True, blank=True, verbose_name=_('Void Date'))
|
|
392
|
-
date_canceled = models.DateField(
|
|
429
|
+
date_canceled = models.DateField(
|
|
430
|
+
null=True, blank=True, verbose_name=_('Canceled Date')
|
|
431
|
+
)
|
|
393
432
|
|
|
394
433
|
objects = BillModelManager.from_queryset(queryset_class=BillModelQuerySet)()
|
|
395
434
|
|
|
@@ -401,11 +440,9 @@ class BillModelAbstract(
|
|
|
401
440
|
indexes = [
|
|
402
441
|
models.Index(fields=['bill_status']),
|
|
403
442
|
models.Index(fields=['terms']),
|
|
404
|
-
|
|
405
443
|
models.Index(fields=['cash_account']),
|
|
406
444
|
models.Index(fields=['prepaid_account']),
|
|
407
445
|
models.Index(fields=['unearned_account']),
|
|
408
|
-
|
|
409
446
|
models.Index(fields=['date_due']),
|
|
410
447
|
models.Index(fields=['date_draft']),
|
|
411
448
|
models.Index(fields=['date_in_review']),
|
|
@@ -413,7 +450,6 @@ class BillModelAbstract(
|
|
|
413
450
|
models.Index(fields=['date_paid']),
|
|
414
451
|
models.Index(fields=['date_canceled']),
|
|
415
452
|
models.Index(fields=['date_void']),
|
|
416
|
-
|
|
417
453
|
models.Index(fields=['vendor']),
|
|
418
454
|
models.Index(fields=['bill_number']),
|
|
419
455
|
]
|
|
@@ -422,20 +458,19 @@ class BillModelAbstract(
|
|
|
422
458
|
return f'Bill: {self.bill_number} | {self.get_bill_status_display()}'
|
|
423
459
|
|
|
424
460
|
def is_configured(self) -> bool:
|
|
425
|
-
return all([
|
|
426
|
-
super().is_configured(),
|
|
427
|
-
self.bill_status
|
|
428
|
-
])
|
|
461
|
+
return all([super().is_configured(), self.bill_status])
|
|
429
462
|
|
|
430
463
|
# Configuration...
|
|
431
|
-
def configure(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
464
|
+
def configure(
|
|
465
|
+
self,
|
|
466
|
+
entity_slug: Union[str, EntityModel],
|
|
467
|
+
user_model: Optional[UserModel] = None,
|
|
468
|
+
date_draft: Optional[Union[date, datetime]] = None,
|
|
469
|
+
ledger_posted: bool = False,
|
|
470
|
+
ledger_name: str = None,
|
|
471
|
+
commit: bool = False,
|
|
472
|
+
commit_ledger: bool = False,
|
|
473
|
+
):
|
|
439
474
|
"""
|
|
440
475
|
A configuration hook which executes all initial BillModel setup on to the LedgerModel and all initial
|
|
441
476
|
values of the BillModel. Can only call this method once in the lifetime of a BillModel.
|
|
@@ -468,13 +503,19 @@ class BillModelAbstract(
|
|
|
468
503
|
if not self.is_configured():
|
|
469
504
|
if isinstance(entity_slug, str):
|
|
470
505
|
if not user_model:
|
|
471
|
-
raise BillModelValidationError(
|
|
506
|
+
raise BillModelValidationError(
|
|
507
|
+
_('Must pass user_model when using entity_slug.')
|
|
508
|
+
)
|
|
472
509
|
entity_qs = EntityModel.objects.for_user(user_model=user_model)
|
|
473
|
-
entity_model: EntityModel = get_object_or_404(
|
|
510
|
+
entity_model: EntityModel = get_object_or_404(
|
|
511
|
+
entity_qs, slug__exact=entity_slug
|
|
512
|
+
)
|
|
474
513
|
elif isinstance(entity_slug, EntityModel):
|
|
475
514
|
entity_model = entity_slug
|
|
476
515
|
else:
|
|
477
|
-
raise BillModelValidationError(
|
|
516
|
+
raise BillModelValidationError(
|
|
517
|
+
'entity_slug must be an instance of str or EntityModel'
|
|
518
|
+
)
|
|
478
519
|
|
|
479
520
|
if entity_model.is_accrual_method():
|
|
480
521
|
self.accrue = True
|
|
@@ -489,7 +530,9 @@ class BillModelAbstract(
|
|
|
489
530
|
self.date_draft = get_localdate() if not date_draft else date_draft
|
|
490
531
|
|
|
491
532
|
LedgerModel = lazy_loader.get_ledger_model()
|
|
492
|
-
ledger_model: LedgerModel = LedgerModel(
|
|
533
|
+
ledger_model: LedgerModel = LedgerModel(
|
|
534
|
+
entity=entity_model, posted=ledger_posted
|
|
535
|
+
)
|
|
493
536
|
ledger_name = f'Bill {self.uuid}' if not ledger_name else ledger_name
|
|
494
537
|
ledger_model.name = ledger_name
|
|
495
538
|
ledger_model.configure_for_wrapper_model(model_instance=self)
|
|
@@ -517,24 +560,30 @@ class BillModelAbstract(
|
|
|
517
560
|
return self.is_draft()
|
|
518
561
|
|
|
519
562
|
def migrate_itemtxs(self, itemtxs: Dict, operation: str, commit: bool = False):
|
|
520
|
-
itemtxs_batch = super().migrate_itemtxs(
|
|
563
|
+
itemtxs_batch = super().migrate_itemtxs(
|
|
564
|
+
itemtxs=itemtxs, commit=commit, operation=operation
|
|
565
|
+
)
|
|
521
566
|
self.update_amount_due(itemtxs_qs=itemtxs_batch)
|
|
522
567
|
self.get_state(commit=True)
|
|
523
568
|
|
|
524
569
|
if commit:
|
|
525
|
-
self.save(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
570
|
+
self.save(
|
|
571
|
+
update_fields=[
|
|
572
|
+
'amount_due',
|
|
573
|
+
'amount_receivable',
|
|
574
|
+
'amount_unearned',
|
|
575
|
+
'amount_earned',
|
|
576
|
+
'updated',
|
|
577
|
+
]
|
|
578
|
+
)
|
|
530
579
|
return itemtxs_batch
|
|
531
580
|
|
|
532
581
|
def get_item_model_qs(self) -> ItemModelQuerySet:
|
|
533
|
-
return ItemModel.objects.filter(
|
|
534
|
-
entity_id__exact=self.ledger.entity_id
|
|
535
|
-
).bills()
|
|
582
|
+
return ItemModel.objects.filter(entity_id__exact=self.ledger.entity_id).bills()
|
|
536
583
|
|
|
537
|
-
def validate_itemtxs_qs(
|
|
584
|
+
def validate_itemtxs_qs(
|
|
585
|
+
self, queryset: Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]
|
|
586
|
+
):
|
|
538
587
|
"""
|
|
539
588
|
Validates that the entire ItemTransactionModelQuerySet is bound to the BillModel.
|
|
540
589
|
|
|
@@ -543,16 +592,18 @@ class BillModelAbstract(
|
|
|
543
592
|
queryset: ItemTransactionModelQuerySet or list of ItemTransactionModel.
|
|
544
593
|
ItemTransactionModelQuerySet to validate.
|
|
545
594
|
"""
|
|
546
|
-
valid = all([
|
|
547
|
-
i.bill_model_id == self.uuid for i in queryset
|
|
548
|
-
])
|
|
595
|
+
valid = all([i.bill_model_id == self.uuid for i in queryset])
|
|
549
596
|
if not valid:
|
|
550
|
-
raise BillModelValidationError(
|
|
597
|
+
raise BillModelValidationError(
|
|
598
|
+
f'Invalid queryset. All items must be assigned to Bill {self.uuid}'
|
|
599
|
+
)
|
|
551
600
|
|
|
552
|
-
def get_itemtxs_data(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
601
|
+
def get_itemtxs_data(
|
|
602
|
+
self,
|
|
603
|
+
queryset: Optional[ItemTransactionModelQuerySet] = None,
|
|
604
|
+
aggregate_on_db: bool = False,
|
|
605
|
+
lazy_agg: bool = False,
|
|
606
|
+
) -> Tuple[ItemTransactionModelQuerySet, Dict]:
|
|
556
607
|
"""
|
|
557
608
|
Fetches the BillModel Items and aggregates the QuerySet.
|
|
558
609
|
|
|
@@ -568,21 +619,18 @@ class BillModelAbstract(
|
|
|
568
619
|
"""
|
|
569
620
|
if not queryset:
|
|
570
621
|
queryset = self.itemtransactionmodel_set.all().select_related(
|
|
571
|
-
'item_model',
|
|
572
|
-
|
|
573
|
-
'po_model',
|
|
574
|
-
'bill_model')
|
|
622
|
+
'item_model', 'entity_unit', 'po_model', 'bill_model'
|
|
623
|
+
)
|
|
575
624
|
else:
|
|
576
625
|
self.validate_itemtxs_qs(queryset)
|
|
577
626
|
|
|
578
627
|
if aggregate_on_db and isinstance(queryset, ItemTransactionModelQuerySet):
|
|
579
628
|
return queryset, queryset.aggregate(
|
|
580
|
-
total_amount__sum=Sum('total_amount'),
|
|
581
|
-
total_items=Count('uuid')
|
|
629
|
+
total_amount__sum=Sum('total_amount'), total_items=Count('uuid')
|
|
582
630
|
)
|
|
583
631
|
return queryset, {
|
|
584
632
|
'total_amount__sum': sum(i.total_amount for i in queryset),
|
|
585
|
-
'total_items': len(queryset)
|
|
633
|
+
'total_items': len(queryset),
|
|
586
634
|
} if not lazy_agg else None
|
|
587
635
|
|
|
588
636
|
# ### ItemizeMixIn implementation END...
|
|
@@ -592,7 +640,9 @@ class BillModelAbstract(
|
|
|
592
640
|
Fetches the TransactionModelQuerySet associated with the BillModel instance.
|
|
593
641
|
"""
|
|
594
642
|
TransactionModel = lazy_loader.get_txs_model()
|
|
595
|
-
transaction_model_qs = TransactionModel.objects.all().for_ledger(
|
|
643
|
+
transaction_model_qs = TransactionModel.objects.all().for_ledger(
|
|
644
|
+
ledger_model=self.ledger_id
|
|
645
|
+
)
|
|
596
646
|
if annotated:
|
|
597
647
|
return transaction_model_qs.with_annotated_details()
|
|
598
648
|
return transaction_model_qs
|
|
@@ -609,8 +659,9 @@ class BillModelAbstract(
|
|
|
609
659
|
"""
|
|
610
660
|
return f'Bill {self.bill_number} account adjustment.'
|
|
611
661
|
|
|
612
|
-
def get_migration_data(
|
|
613
|
-
|
|
662
|
+
def get_migration_data(
|
|
663
|
+
self, queryset: Optional[ItemTransactionModelQuerySet] = None
|
|
664
|
+
) -> ItemTransactionModelQuerySet:
|
|
614
665
|
"""
|
|
615
666
|
Fetches necessary item transaction data to perform a migration into the LedgerModel.
|
|
616
667
|
|
|
@@ -625,21 +676,30 @@ class BillModelAbstract(
|
|
|
625
676
|
else:
|
|
626
677
|
self.validate_itemtxs_qs(queryset)
|
|
627
678
|
|
|
628
|
-
return
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
679
|
+
return (
|
|
680
|
+
queryset.order_by(
|
|
681
|
+
'item_model__expense_account__uuid',
|
|
682
|
+
'entity_unit__uuid',
|
|
683
|
+
'item_model__expense_account__balance_type',
|
|
684
|
+
)
|
|
685
|
+
.values(
|
|
686
|
+
'item_model__expense_account__uuid',
|
|
687
|
+
'item_model__inventory_account__uuid',
|
|
688
|
+
'item_model__expense_account__balance_type',
|
|
689
|
+
'item_model__inventory_account__balance_type',
|
|
690
|
+
'entity_unit__slug',
|
|
691
|
+
'entity_unit__uuid',
|
|
692
|
+
'total_amount',
|
|
693
|
+
)
|
|
694
|
+
.annotate(account_unit_total=Sum('total_amount'))
|
|
639
695
|
)
|
|
640
696
|
|
|
641
|
-
def update_amount_due(
|
|
642
|
-
|
|
697
|
+
def update_amount_due(
|
|
698
|
+
self,
|
|
699
|
+
itemtxs_qs: Optional[
|
|
700
|
+
Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]
|
|
701
|
+
] = None,
|
|
702
|
+
) -> ItemTransactionModelQuerySet:
|
|
643
703
|
"""
|
|
644
704
|
Updates the BillModel amount due.
|
|
645
705
|
|
|
@@ -722,11 +782,7 @@ class BillModelAbstract(
|
|
|
722
782
|
bool
|
|
723
783
|
True if BillModel is Active, else False.
|
|
724
784
|
"""
|
|
725
|
-
return any([
|
|
726
|
-
self.is_paid(),
|
|
727
|
-
self.is_approved(),
|
|
728
|
-
self.is_void()
|
|
729
|
-
])
|
|
785
|
+
return any([self.is_paid(), self.is_approved(), self.is_void()])
|
|
730
786
|
|
|
731
787
|
def is_void(self) -> bool:
|
|
732
788
|
"""
|
|
@@ -773,10 +829,7 @@ class BillModelAbstract(
|
|
|
773
829
|
bool
|
|
774
830
|
True if BillModel can be marked as in review, else False.
|
|
775
831
|
"""
|
|
776
|
-
return all([
|
|
777
|
-
self.is_configured(),
|
|
778
|
-
self.is_draft()
|
|
779
|
-
])
|
|
832
|
+
return all([self.is_configured(), self.is_draft()])
|
|
780
833
|
|
|
781
834
|
def can_approve(self) -> bool:
|
|
782
835
|
"""
|
|
@@ -812,11 +865,7 @@ class BillModelAbstract(
|
|
|
812
865
|
bool
|
|
813
866
|
True if BillModel can be deleted, else False.
|
|
814
867
|
"""
|
|
815
|
-
return any([
|
|
816
|
-
self.is_review(),
|
|
817
|
-
self.is_draft(),
|
|
818
|
-
not self.ledger.is_locked()
|
|
819
|
-
])
|
|
868
|
+
return any([self.is_review(), self.is_draft(), not self.ledger.is_locked()])
|
|
820
869
|
|
|
821
870
|
def can_void(self) -> bool:
|
|
822
871
|
"""
|
|
@@ -827,10 +876,7 @@ class BillModelAbstract(
|
|
|
827
876
|
bool
|
|
828
877
|
True if BillModel can be marked as void, else False.
|
|
829
878
|
"""
|
|
830
|
-
return all([
|
|
831
|
-
self.is_approved(),
|
|
832
|
-
float(self.amount_paid) == 0.00
|
|
833
|
-
])
|
|
879
|
+
return all([self.is_approved(), float(self.amount_paid) == 0.00])
|
|
834
880
|
|
|
835
881
|
def can_cancel(self) -> bool:
|
|
836
882
|
"""
|
|
@@ -841,10 +887,7 @@ class BillModelAbstract(
|
|
|
841
887
|
bool
|
|
842
888
|
True if BillModel can be marked as canceled, else False.
|
|
843
889
|
"""
|
|
844
|
-
return any([
|
|
845
|
-
self.is_draft(),
|
|
846
|
-
self.is_review()
|
|
847
|
-
])
|
|
890
|
+
return any([self.is_draft(), self.is_review()])
|
|
848
891
|
|
|
849
892
|
def can_edit_items(self) -> bool:
|
|
850
893
|
"""
|
|
@@ -892,16 +935,16 @@ class BillModelAbstract(
|
|
|
892
935
|
"""
|
|
893
936
|
if self.ce_model_id:
|
|
894
937
|
if raise_exception:
|
|
895
|
-
raise BillModelValidationError(
|
|
896
|
-
|
|
938
|
+
raise BillModelValidationError(
|
|
939
|
+
f'Bill {self.bill_number} already bound to '
|
|
940
|
+
f'Estimate {self.ce_model.estimate_number}'
|
|
941
|
+
)
|
|
897
942
|
return False
|
|
898
943
|
|
|
899
944
|
is_approved = estimate_model.is_approved()
|
|
900
945
|
if not is_approved and raise_exception:
|
|
901
|
-
raise BillModelValidationError(
|
|
902
|
-
return all([
|
|
903
|
-
is_approved
|
|
904
|
-
])
|
|
946
|
+
raise BillModelValidationError('Cannot bind estimate that is not approved.')
|
|
947
|
+
return all([is_approved])
|
|
905
948
|
|
|
906
949
|
def can_bind_po(self, po_model, raise_exception: bool = False) -> bool:
|
|
907
950
|
"""
|
|
@@ -924,12 +967,14 @@ class BillModelAbstract(
|
|
|
924
967
|
"""
|
|
925
968
|
if not po_model.is_approved():
|
|
926
969
|
if raise_exception:
|
|
927
|
-
raise BillModelValidationError(
|
|
970
|
+
raise BillModelValidationError('Cannot bind an unapproved PO.')
|
|
928
971
|
return False
|
|
929
972
|
|
|
930
973
|
if po_model.date_approved > self.date_draft:
|
|
931
974
|
if raise_exception:
|
|
932
|
-
raise BillModelValidationError(
|
|
975
|
+
raise BillModelValidationError(
|
|
976
|
+
'Approved PO date cannot be greater than Bill draft date.'
|
|
977
|
+
)
|
|
933
978
|
return False
|
|
934
979
|
|
|
935
980
|
return True
|
|
@@ -944,11 +989,7 @@ class BillModelAbstract(
|
|
|
944
989
|
bool
|
|
945
990
|
True if BillModel can generate its bill_number, else False.
|
|
946
991
|
"""
|
|
947
|
-
return all([
|
|
948
|
-
not self.bill_number,
|
|
949
|
-
self.is_draft(),
|
|
950
|
-
self.is_configured()
|
|
951
|
-
])
|
|
992
|
+
return all([not self.bill_number, self.is_draft(), self.is_configured()])
|
|
952
993
|
|
|
953
994
|
# ACTIONS ---
|
|
954
995
|
|
|
@@ -965,11 +1006,13 @@ class BillModelAbstract(
|
|
|
965
1006
|
"""
|
|
966
1007
|
return self.is_approved()
|
|
967
1008
|
|
|
968
|
-
def make_payment(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1009
|
+
def make_payment(
|
|
1010
|
+
self,
|
|
1011
|
+
payment_amount: Union[Decimal, float, int],
|
|
1012
|
+
payment_date: Optional[Union[datetime, date]] = None,
|
|
1013
|
+
commit: bool = False,
|
|
1014
|
+
raise_exception: bool = True,
|
|
1015
|
+
):
|
|
973
1016
|
"""
|
|
974
1017
|
Makes a payment to the BillModel.
|
|
975
1018
|
|
|
@@ -1020,7 +1063,7 @@ class BillModelAbstract(
|
|
|
1020
1063
|
user_model=None,
|
|
1021
1064
|
entity_slug=self.ledger.entity.slug,
|
|
1022
1065
|
je_timestamp=payment_date,
|
|
1023
|
-
raise_exception=True
|
|
1066
|
+
raise_exception=True,
|
|
1024
1067
|
)
|
|
1025
1068
|
self.save(
|
|
1026
1069
|
update_fields=[
|
|
@@ -1028,10 +1071,13 @@ class BillModelAbstract(
|
|
|
1028
1071
|
'amount_earned',
|
|
1029
1072
|
'amount_unearned',
|
|
1030
1073
|
'amount_receivable',
|
|
1031
|
-
'updated'
|
|
1032
|
-
]
|
|
1074
|
+
'updated',
|
|
1075
|
+
]
|
|
1076
|
+
)
|
|
1033
1077
|
|
|
1034
|
-
def bind_estimate(
|
|
1078
|
+
def bind_estimate(
|
|
1079
|
+
self, estimate_model, commit: bool = False, raise_exception: bool = True
|
|
1080
|
+
):
|
|
1035
1081
|
"""
|
|
1036
1082
|
Binds BillModel to a given EstimateModel. Raises ValueError if EstimateModel cannot be bound.
|
|
1037
1083
|
|
|
@@ -1055,12 +1101,11 @@ class BillModelAbstract(
|
|
|
1055
1101
|
self.ce_model = estimate_model
|
|
1056
1102
|
self.clean()
|
|
1057
1103
|
if commit:
|
|
1058
|
-
self.save(update_fields=[
|
|
1059
|
-
'ce_model',
|
|
1060
|
-
'updated'
|
|
1061
|
-
])
|
|
1104
|
+
self.save(update_fields=['ce_model', 'updated'])
|
|
1062
1105
|
|
|
1063
|
-
def mark_as_draft(
|
|
1106
|
+
def mark_as_draft(
|
|
1107
|
+
self, date_draft: Optional[date] = None, commit: bool = False, **kwargs
|
|
1108
|
+
):
|
|
1064
1109
|
"""
|
|
1065
1110
|
Marks BillModel as Draft.
|
|
1066
1111
|
|
|
@@ -1089,16 +1134,10 @@ class BillModelAbstract(
|
|
|
1089
1134
|
|
|
1090
1135
|
self.clean()
|
|
1091
1136
|
if commit:
|
|
1092
|
-
self.save(
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
'updated'
|
|
1097
|
-
]
|
|
1098
|
-
)
|
|
1099
|
-
bill_status_draft.send_robust(sender=self.__class__,
|
|
1100
|
-
instance=self,
|
|
1101
|
-
commited=commit, **kwargs)
|
|
1137
|
+
self.save(update_fields=['bill_status', 'date_draft', 'updated'])
|
|
1138
|
+
bill_status_draft.send_robust(
|
|
1139
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1140
|
+
)
|
|
1102
1141
|
|
|
1103
1142
|
def get_mark_as_draft_html_id(self) -> str:
|
|
1104
1143
|
"""
|
|
@@ -1128,11 +1167,10 @@ class BillModelAbstract(
|
|
|
1128
1167
|
"""
|
|
1129
1168
|
if not entity_slug:
|
|
1130
1169
|
entity_slug = self.ledger.entity.slug
|
|
1131
|
-
return reverse(
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
})
|
|
1170
|
+
return reverse(
|
|
1171
|
+
'django_ledger:bill-action-mark-as-draft',
|
|
1172
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1173
|
+
)
|
|
1136
1174
|
|
|
1137
1175
|
def get_mark_as_draft_message(self) -> str:
|
|
1138
1176
|
"""
|
|
@@ -1146,12 +1184,14 @@ class BillModelAbstract(
|
|
|
1146
1184
|
return _('Do you want to mark Bill %s as Draft?') % self.bill_number
|
|
1147
1185
|
|
|
1148
1186
|
# IN REVIEW ACTIONS....
|
|
1149
|
-
def mark_as_review(
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1187
|
+
def mark_as_review(
|
|
1188
|
+
self,
|
|
1189
|
+
commit: bool = False,
|
|
1190
|
+
itemtxs_qs: ItemTransactionModelQuerySet = None,
|
|
1191
|
+
date_in_review: Optional[date] = None,
|
|
1192
|
+
raise_exception: bool = True,
|
|
1193
|
+
**kwargs,
|
|
1194
|
+
):
|
|
1155
1195
|
"""
|
|
1156
1196
|
Marks BillModel as In Review.
|
|
1157
1197
|
|
|
@@ -1179,7 +1219,10 @@ class BillModelAbstract(
|
|
|
1179
1219
|
self.validate_itemtxs_qs(queryset=itemtxs_qs)
|
|
1180
1220
|
|
|
1181
1221
|
if not itemtxs_qs.count():
|
|
1182
|
-
raise BillModelValidationError(
|
|
1222
|
+
raise BillModelValidationError(
|
|
1223
|
+
message=f'Cannot review a {self.__class__.__name__} without items...'
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1183
1226
|
if not self.amount_due:
|
|
1184
1227
|
raise BillModelValidationError(
|
|
1185
1228
|
f'Bill {self.bill_number} cannot be marked as in review. Amount due must be greater than 0.'
|
|
@@ -1197,16 +1240,10 @@ class BillModelAbstract(
|
|
|
1197
1240
|
|
|
1198
1241
|
self.clean()
|
|
1199
1242
|
if commit:
|
|
1200
|
-
self.save(
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
'updated'
|
|
1205
|
-
]
|
|
1206
|
-
)
|
|
1207
|
-
bill_status_in_review.send_robust(sender=self.__class__,
|
|
1208
|
-
instance=self,
|
|
1209
|
-
commited=commit, **kwargs)
|
|
1243
|
+
self.save(update_fields=['date_in_review', 'bill_status', 'updated'])
|
|
1244
|
+
bill_status_in_review.send_robust(
|
|
1245
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1246
|
+
)
|
|
1210
1247
|
|
|
1211
1248
|
def get_mark_as_review_html_id(self) -> str:
|
|
1212
1249
|
"""
|
|
@@ -1236,11 +1273,10 @@ class BillModelAbstract(
|
|
|
1236
1273
|
"""
|
|
1237
1274
|
if not entity_slug:
|
|
1238
1275
|
entity_slug = self.ledger.entity.slug
|
|
1239
|
-
return reverse(
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
})
|
|
1276
|
+
return reverse(
|
|
1277
|
+
'django_ledger:bill-action-mark-as-review',
|
|
1278
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1279
|
+
)
|
|
1244
1280
|
|
|
1245
1281
|
def get_mark_as_review_message(self) -> str:
|
|
1246
1282
|
"""
|
|
@@ -1254,14 +1290,16 @@ class BillModelAbstract(
|
|
|
1254
1290
|
return _('Do you want to mark Bill %s as In Review?') % self.bill_number
|
|
1255
1291
|
|
|
1256
1292
|
# APPROVED ACTIONS....
|
|
1257
|
-
def mark_as_approved(
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1293
|
+
def mark_as_approved(
|
|
1294
|
+
self,
|
|
1295
|
+
user_model,
|
|
1296
|
+
entity_slug: Optional[str] = None,
|
|
1297
|
+
date_approved: Optional[Union[date, datetime]] = None,
|
|
1298
|
+
commit: bool = False,
|
|
1299
|
+
force_migrate: bool = False,
|
|
1300
|
+
raise_exception: bool = True,
|
|
1301
|
+
**kwargs,
|
|
1302
|
+
):
|
|
1265
1303
|
"""
|
|
1266
1304
|
Marks BillModel as Approved.
|
|
1267
1305
|
|
|
@@ -1311,12 +1349,12 @@ class BillModelAbstract(
|
|
|
1311
1349
|
entity_slug=entity_slug,
|
|
1312
1350
|
user_model=user_model,
|
|
1313
1351
|
je_timestamp=date_approved,
|
|
1314
|
-
force_migrate=self.accrue
|
|
1352
|
+
force_migrate=self.accrue,
|
|
1315
1353
|
)
|
|
1316
1354
|
self.ledger.post(commit=commit, raise_exception=raise_exception)
|
|
1317
|
-
bill_status_approved.send_robust(
|
|
1318
|
-
|
|
1319
|
-
|
|
1355
|
+
bill_status_approved.send_robust(
|
|
1356
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1357
|
+
)
|
|
1320
1358
|
|
|
1321
1359
|
def get_mark_as_approved_html_id(self) -> str:
|
|
1322
1360
|
"""
|
|
@@ -1346,11 +1384,10 @@ class BillModelAbstract(
|
|
|
1346
1384
|
"""
|
|
1347
1385
|
if not entity_slug:
|
|
1348
1386
|
entity_slug = self.ledger.entity.slug
|
|
1349
|
-
return reverse(
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
})
|
|
1387
|
+
return reverse(
|
|
1388
|
+
'django_ledger:bill-action-mark-as-approved',
|
|
1389
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1390
|
+
)
|
|
1354
1391
|
|
|
1355
1392
|
def get_mark_as_approved_message(self) -> str:
|
|
1356
1393
|
"""
|
|
@@ -1364,14 +1401,15 @@ class BillModelAbstract(
|
|
|
1364
1401
|
return _('Do you want to mark Bill %s as Approved?') % self.bill_number
|
|
1365
1402
|
|
|
1366
1403
|
# PAY ACTIONS....
|
|
1367
|
-
def mark_as_paid(
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1404
|
+
def mark_as_paid(
|
|
1405
|
+
self,
|
|
1406
|
+
user_model,
|
|
1407
|
+
entity_slug: Optional[str] = None,
|
|
1408
|
+
date_paid: Optional[Union[date, datetime]] = None,
|
|
1409
|
+
itemtxs_qs: Optional[ItemTransactionModelQuerySet] = None,
|
|
1410
|
+
commit: bool = False,
|
|
1411
|
+
**kwargs,
|
|
1412
|
+
):
|
|
1375
1413
|
"""
|
|
1376
1414
|
Marks BillModel as Paid.
|
|
1377
1415
|
|
|
@@ -1394,7 +1432,9 @@ class BillModelAbstract(
|
|
|
1394
1432
|
Commits transaction into the Database. Defaults to False.
|
|
1395
1433
|
"""
|
|
1396
1434
|
if not self.can_pay():
|
|
1397
|
-
raise BillModelValidationError(
|
|
1435
|
+
raise BillModelValidationError(
|
|
1436
|
+
f'Cannot mark Bill {self.bill_number} as paid...'
|
|
1437
|
+
)
|
|
1398
1438
|
|
|
1399
1439
|
if date_paid:
|
|
1400
1440
|
if isinstance(date_paid, datetime):
|
|
@@ -1408,10 +1448,13 @@ class BillModelAbstract(
|
|
|
1408
1448
|
self.amount_paid = self.amount_due
|
|
1409
1449
|
|
|
1410
1450
|
if self.date_paid > get_localdate():
|
|
1411
|
-
raise BillModelValidationError(
|
|
1451
|
+
raise BillModelValidationError(
|
|
1452
|
+
f'Cannot pay {self.__class__.__name__} in the future.'
|
|
1453
|
+
)
|
|
1412
1454
|
if self.date_paid < self.date_approved:
|
|
1413
1455
|
raise BillModelValidationError(
|
|
1414
|
-
f'Cannot pay {self.__class__.__name__} before approved date {self.date_approved}.'
|
|
1456
|
+
f'Cannot pay {self.__class__.__name__} before approved date {self.date_approved}.'
|
|
1457
|
+
)
|
|
1415
1458
|
|
|
1416
1459
|
self.bill_status = self.BILL_STATUS_PAID
|
|
1417
1460
|
self.get_state(commit=True)
|
|
@@ -1425,9 +1468,9 @@ class BillModelAbstract(
|
|
|
1425
1468
|
if commit:
|
|
1426
1469
|
self.save()
|
|
1427
1470
|
ItemTransactionModel = lazy_loader.get_item_transaction_model()
|
|
1428
|
-
itemtxs_qs.filter(
|
|
1429
|
-
|
|
1430
|
-
)
|
|
1471
|
+
itemtxs_qs.filter(po_model_id__isnull=False).update(
|
|
1472
|
+
po_item_status=ItemTransactionModel.STATUS_ORDERED
|
|
1473
|
+
)
|
|
1431
1474
|
|
|
1432
1475
|
if not entity_slug:
|
|
1433
1476
|
entity_slug = self.ledger.entity.slug
|
|
@@ -1437,12 +1480,12 @@ class BillModelAbstract(
|
|
|
1437
1480
|
entity_slug=entity_slug,
|
|
1438
1481
|
itemtxs_qs=itemtxs_qs,
|
|
1439
1482
|
je_timestamp=date_paid,
|
|
1440
|
-
force_migrate=True
|
|
1483
|
+
force_migrate=True,
|
|
1441
1484
|
)
|
|
1442
1485
|
self.lock_ledger(commit=True)
|
|
1443
|
-
bill_status_paid.send_robust(
|
|
1444
|
-
|
|
1445
|
-
|
|
1486
|
+
bill_status_paid.send_robust(
|
|
1487
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1488
|
+
)
|
|
1446
1489
|
|
|
1447
1490
|
def get_mark_as_paid_html_id(self) -> str:
|
|
1448
1491
|
"""
|
|
@@ -1472,11 +1515,10 @@ class BillModelAbstract(
|
|
|
1472
1515
|
"""
|
|
1473
1516
|
if not entity_slug:
|
|
1474
1517
|
entity_slug = self.ledger.entity.slug
|
|
1475
|
-
return reverse(
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
})
|
|
1518
|
+
return reverse(
|
|
1519
|
+
'django_ledger:bill-action-mark-as-paid',
|
|
1520
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1521
|
+
)
|
|
1480
1522
|
|
|
1481
1523
|
def get_mark_as_paid_message(self) -> str:
|
|
1482
1524
|
"""
|
|
@@ -1490,12 +1532,14 @@ class BillModelAbstract(
|
|
|
1490
1532
|
return _('Do you want to mark Bill %s as Paid?') % self.bill_number
|
|
1491
1533
|
|
|
1492
1534
|
# VOID Actions...
|
|
1493
|
-
def mark_as_void(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1535
|
+
def mark_as_void(
|
|
1536
|
+
self,
|
|
1537
|
+
user_model,
|
|
1538
|
+
entity_slug: Optional[str] = None,
|
|
1539
|
+
date_void: Optional[date] = None,
|
|
1540
|
+
commit: bool = False,
|
|
1541
|
+
**kwargs,
|
|
1542
|
+
):
|
|
1499
1543
|
"""
|
|
1500
1544
|
Marks BillModel as Void.
|
|
1501
1545
|
When mark as void, all transactions associated with BillModel are reversed as of the void date.
|
|
@@ -1516,7 +1560,9 @@ class BillModelAbstract(
|
|
|
1516
1560
|
Commits transaction into DB. Defaults to False.
|
|
1517
1561
|
"""
|
|
1518
1562
|
if not self.can_void():
|
|
1519
|
-
raise BillModelValidationError(
|
|
1563
|
+
raise BillModelValidationError(
|
|
1564
|
+
f'Bill {self.bill_number} cannot be voided. Must be approved.'
|
|
1565
|
+
)
|
|
1520
1566
|
|
|
1521
1567
|
if date_void:
|
|
1522
1568
|
if isinstance(date_void, datetime):
|
|
@@ -1541,12 +1587,13 @@ class BillModelAbstract(
|
|
|
1541
1587
|
void=True,
|
|
1542
1588
|
void_date=self.date_void,
|
|
1543
1589
|
raise_exception=False,
|
|
1544
|
-
force_migrate=True
|
|
1590
|
+
force_migrate=True,
|
|
1591
|
+
)
|
|
1545
1592
|
self.save()
|
|
1546
1593
|
self.lock_ledger(commit=False, raise_exception=False)
|
|
1547
|
-
bill_status_void.send_robust(
|
|
1548
|
-
|
|
1549
|
-
|
|
1594
|
+
bill_status_void.send_robust(
|
|
1595
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1596
|
+
)
|
|
1550
1597
|
|
|
1551
1598
|
def get_mark_as_void_html_id(self) -> str:
|
|
1552
1599
|
"""
|
|
@@ -1575,11 +1622,10 @@ class BillModelAbstract(
|
|
|
1575
1622
|
"""
|
|
1576
1623
|
if not entity_slug:
|
|
1577
1624
|
entity_slug = self.ledger.entity.slug
|
|
1578
|
-
return reverse(
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
})
|
|
1625
|
+
return reverse(
|
|
1626
|
+
'django_ledger:bill-action-mark-as-void',
|
|
1627
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1628
|
+
)
|
|
1583
1629
|
|
|
1584
1630
|
def get_mark_as_void_message(self) -> str:
|
|
1585
1631
|
"""
|
|
@@ -1593,7 +1639,9 @@ class BillModelAbstract(
|
|
|
1593
1639
|
return _('Do you want to void Bill %s?') % self.bill_number
|
|
1594
1640
|
|
|
1595
1641
|
# Cancel Actions...
|
|
1596
|
-
def mark_as_canceled(
|
|
1642
|
+
def mark_as_canceled(
|
|
1643
|
+
self, date_canceled: Optional[date] = None, commit: bool = False, **kwargs
|
|
1644
|
+
):
|
|
1597
1645
|
"""
|
|
1598
1646
|
Mark BillModel as Canceled.
|
|
1599
1647
|
|
|
@@ -1607,16 +1655,18 @@ class BillModelAbstract(
|
|
|
1607
1655
|
Commits transaction into the Database. Defaults to False.
|
|
1608
1656
|
"""
|
|
1609
1657
|
if not self.can_cancel():
|
|
1610
|
-
raise BillModelValidationError(
|
|
1658
|
+
raise BillModelValidationError(
|
|
1659
|
+
f'Bill {self.bill_number} cannot be canceled. Must be draft or in review.'
|
|
1660
|
+
)
|
|
1611
1661
|
|
|
1612
1662
|
self.date_canceled = get_localdate() if not date_canceled else date_canceled
|
|
1613
1663
|
self.bill_status = self.BILL_STATUS_CANCELED
|
|
1614
1664
|
self.clean()
|
|
1615
1665
|
if commit:
|
|
1616
1666
|
self.save()
|
|
1617
|
-
bill_status_canceled.send_robust(
|
|
1618
|
-
|
|
1619
|
-
|
|
1667
|
+
bill_status_canceled.send_robust(
|
|
1668
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1669
|
+
)
|
|
1620
1670
|
|
|
1621
1671
|
def get_mark_as_canceled_html_id(self) -> str:
|
|
1622
1672
|
"""
|
|
@@ -1648,11 +1698,10 @@ class BillModelAbstract(
|
|
|
1648
1698
|
if not entity_slug:
|
|
1649
1699
|
entity_slug = self.ledger.entity.slug
|
|
1650
1700
|
|
|
1651
|
-
return reverse(
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
})
|
|
1701
|
+
return reverse(
|
|
1702
|
+
'django_ledger:bill-action-mark-as-canceled',
|
|
1703
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1704
|
+
)
|
|
1656
1705
|
|
|
1657
1706
|
def get_mark_as_canceled_message(self) -> str:
|
|
1658
1707
|
"""
|
|
@@ -1830,28 +1879,28 @@ class BillModelAbstract(
|
|
|
1830
1879
|
'entity_model_id__exact': self.ledger.entity_id,
|
|
1831
1880
|
'entity_unit_id__exact': None,
|
|
1832
1881
|
'fiscal_year': fy_key,
|
|
1833
|
-
'key__exact': EntityStateModel.KEY_BILL
|
|
1882
|
+
'key__exact': EntityStateModel.KEY_BILL,
|
|
1834
1883
|
}
|
|
1835
1884
|
|
|
1836
|
-
state_model_qs =
|
|
1837
|
-
|
|
1885
|
+
state_model_qs = (
|
|
1886
|
+
EntityStateModel.objects.filter(**LOOKUP)
|
|
1887
|
+
.select_related('entity_model')
|
|
1888
|
+
.select_for_update()
|
|
1889
|
+
)
|
|
1838
1890
|
state_model = state_model_qs.get()
|
|
1839
1891
|
state_model.sequence = F('sequence') + 1
|
|
1840
1892
|
state_model.save(update_fields=['sequence'])
|
|
1841
1893
|
state_model.refresh_from_db()
|
|
1842
1894
|
return state_model
|
|
1843
1895
|
except ObjectDoesNotExist:
|
|
1844
|
-
EntityModel = lazy_loader.get_entity_model()
|
|
1845
|
-
entity_model = EntityModel.objects.get(uuid__exact=self.ledger.entity_id)
|
|
1846
|
-
fy_key = entity_model.get_fy_for_date(dt=self.date_draft)
|
|
1847
|
-
|
|
1848
1896
|
LOOKUP = {
|
|
1849
1897
|
'entity_model_id': entity_model.uuid,
|
|
1850
1898
|
'entity_unit_id': None,
|
|
1851
1899
|
'fiscal_year': fy_key,
|
|
1852
1900
|
'key': EntityStateModel.KEY_BILL,
|
|
1853
|
-
'sequence': 1
|
|
1901
|
+
'sequence': 1,
|
|
1854
1902
|
}
|
|
1903
|
+
|
|
1855
1904
|
state_model = EntityStateModel.objects.create(**LOOKUP)
|
|
1856
1905
|
return state_model
|
|
1857
1906
|
except IntegrityError as e:
|
|
@@ -1876,12 +1925,13 @@ class BillModelAbstract(
|
|
|
1876
1925
|
"""
|
|
1877
1926
|
if self.can_generate_bill_number():
|
|
1878
1927
|
with transaction.atomic(durable=True):
|
|
1879
|
-
|
|
1880
1928
|
state_model = None
|
|
1881
1929
|
while not state_model:
|
|
1882
1930
|
state_model = self._get_next_state_model(raise_exception=False)
|
|
1883
1931
|
|
|
1884
|
-
seq = str(state_model.sequence).zfill(
|
|
1932
|
+
seq = str(state_model.sequence).zfill(
|
|
1933
|
+
DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING
|
|
1934
|
+
)
|
|
1885
1935
|
self.bill_number = f'{DJANGO_LEDGER_BILL_NUMBER_PREFIX}-{state_model.fiscal_year}-{seq}'
|
|
1886
1936
|
|
|
1887
1937
|
if commit:
|
|
@@ -1894,11 +1944,10 @@ class BillModelAbstract(
|
|
|
1894
1944
|
|
|
1895
1945
|
# --> URLs <---
|
|
1896
1946
|
def get_absolute_url(self):
|
|
1897
|
-
return reverse(
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
})
|
|
1947
|
+
return reverse(
|
|
1948
|
+
'django_ledger:bill-detail',
|
|
1949
|
+
kwargs={'entity_slug': self.ledger.entity.slug, 'bill_pk': self.uuid},
|
|
1950
|
+
)
|
|
1902
1951
|
|
|
1903
1952
|
def clean(self, commit: bool = True):
|
|
1904
1953
|
"""
|
|
@@ -1916,9 +1965,13 @@ class BillModelAbstract(
|
|
|
1916
1965
|
if self.cash_account.role != ASSET_CA_CASH:
|
|
1917
1966
|
raise ValidationError(f'Cash account must be of role {ASSET_CA_CASH}.')
|
|
1918
1967
|
if self.prepaid_account.role != ASSET_CA_PREPAID:
|
|
1919
|
-
raise ValidationError(
|
|
1968
|
+
raise ValidationError(
|
|
1969
|
+
f'Prepaid account must be of role {ASSET_CA_PREPAID}.'
|
|
1970
|
+
)
|
|
1920
1971
|
if self.unearned_account.role != LIABILITY_CL_ACC_PAYABLE:
|
|
1921
|
-
raise ValidationError(
|
|
1972
|
+
raise ValidationError(
|
|
1973
|
+
f'Unearned account must be of role {LIABILITY_CL_ACC_PAYABLE}.'
|
|
1974
|
+
)
|
|
1922
1975
|
|
|
1923
1976
|
|
|
1924
1977
|
class BillModel(BillModelAbstract):
|