django-ledger 0.8.0__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/data_import.py +182 -63
- django_ledger/io/io_core.py +507 -374
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/bill.py +337 -300
- django_ledger/models/customer.py +47 -34
- django_ledger/models/data_import.py +770 -289
- django_ledger/models/entity.py +882 -637
- django_ledger/models/mixins.py +421 -280
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +105 -41
- django_ledger/models/unit.py +42 -30
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +85 -66
- django_ledger/settings.py +1 -0
- 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 +3 -1
- 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/layouts/base.html +1 -1
- 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 +3 -2
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/urls/__init__.py +1 -0
- 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/customer.py +56 -14
- django_ledger/views/data_import.py +119 -66
- django_ledger/views/mixins.py +112 -86
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/vendor.py +53 -14
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/METADATA +1 -1
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/RECORD +51 -40
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/top_level.txt +0 -0
django_ledger/models/bill.py
CHANGED
|
@@ -7,6 +7,7 @@ the Vendor states the amount owed by the recipient for the purposes of supplying
|
|
|
7
7
|
In addition to tracking the bill amount, it tracks the paid and due amount.
|
|
8
8
|
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
import warnings
|
|
11
12
|
from datetime import date, datetime
|
|
12
13
|
from decimal import Decimal
|
|
@@ -26,13 +27,18 @@ from django_ledger.io import ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_P
|
|
|
26
27
|
from django_ledger.io.io_core import get_localtime, get_localdate
|
|
27
28
|
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
28
29
|
from django_ledger.models.entity import EntityModel
|
|
29
|
-
from django_ledger.models.items import
|
|
30
|
+
from django_ledger.models.items import (
|
|
31
|
+
ItemTransactionModelQuerySet,
|
|
32
|
+
ItemTransactionModel,
|
|
33
|
+
ItemModel,
|
|
34
|
+
ItemModelQuerySet,
|
|
35
|
+
)
|
|
30
36
|
from django_ledger.models.mixins import (
|
|
31
37
|
CreateUpdateMixIn,
|
|
32
38
|
AccrualMixIn,
|
|
33
39
|
MarkdownNotesMixIn,
|
|
34
40
|
PaymentTermsMixIn,
|
|
35
|
-
ItemizeMixIn
|
|
41
|
+
ItemizeMixIn,
|
|
36
42
|
)
|
|
37
43
|
from django_ledger.models.signals import (
|
|
38
44
|
bill_status_draft,
|
|
@@ -43,8 +49,11 @@ from django_ledger.models.signals import (
|
|
|
43
49
|
bill_status_void,
|
|
44
50
|
)
|
|
45
51
|
from django_ledger.models.utils import lazy_loader
|
|
46
|
-
from django_ledger.settings import (
|
|
47
|
-
|
|
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
|
+
)
|
|
48
57
|
|
|
49
58
|
UserModel = get_user_model()
|
|
50
59
|
|
|
@@ -83,8 +92,8 @@ class BillModelQuerySet(QuerySet):
|
|
|
83
92
|
if user_model.is_superuser:
|
|
84
93
|
return self
|
|
85
94
|
return self.filter(
|
|
86
|
-
Q(ledger__entity__admin=user_model)
|
|
87
|
-
Q(ledger__entity__managers__in=[user_model])
|
|
95
|
+
Q(ledger__entity__admin=user_model)
|
|
96
|
+
| Q(ledger__entity__managers__in=[user_model])
|
|
88
97
|
)
|
|
89
98
|
|
|
90
99
|
def draft(self):
|
|
@@ -168,8 +177,8 @@ class BillModelQuerySet(QuerySet):
|
|
|
168
177
|
Returns a QuerySet of active bills only.
|
|
169
178
|
"""
|
|
170
179
|
return self.filter(
|
|
171
|
-
Q(bill_status__exact=BillModel.BILL_STATUS_APPROVED)
|
|
172
|
-
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)
|
|
173
182
|
)
|
|
174
183
|
|
|
175
184
|
def overdue(self):
|
|
@@ -205,13 +214,12 @@ class BillModelManager(Manager):
|
|
|
205
214
|
|
|
206
215
|
def get_queryset(self) -> BillModelQuerySet:
|
|
207
216
|
qs = BillModelQuerySet(self.model, using=self._db)
|
|
208
|
-
return qs.select_related(
|
|
209
|
-
'ledger',
|
|
210
|
-
'ledger__entity'
|
|
211
|
-
)
|
|
217
|
+
return qs.select_related('ledger', 'ledger__entity')
|
|
212
218
|
|
|
213
219
|
@deprecated_entity_slug_behavior
|
|
214
|
-
def for_entity(
|
|
220
|
+
def for_entity(
|
|
221
|
+
self, entity_model: EntityModel | str | UUID = None, **kwargs
|
|
222
|
+
) -> BillModelQuerySet:
|
|
215
223
|
"""
|
|
216
224
|
Fetches a QuerySet of BillModels associated with a specific EntityModel & UserModel.
|
|
217
225
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
@@ -232,7 +240,7 @@ class BillModelManager(Manager):
|
|
|
232
240
|
'user_model parameter is deprecated and will be removed in a future release. '
|
|
233
241
|
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
234
242
|
DeprecationWarning,
|
|
235
|
-
stacklevel=2
|
|
243
|
+
stacklevel=2,
|
|
236
244
|
)
|
|
237
245
|
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
238
246
|
qs = qs.for_user(kwargs['user_model'])
|
|
@@ -240,22 +248,16 @@ class BillModelManager(Manager):
|
|
|
240
248
|
if isinstance(entity_model, EntityModel):
|
|
241
249
|
qs = qs.filter(ledger__entity=entity_model)
|
|
242
250
|
elif isinstance(entity_model, str):
|
|
243
|
-
qs =
|
|
251
|
+
qs = qs.filter(ledger__entity__slug__exact=entity_model)
|
|
244
252
|
elif isinstance(entity_model, UUID):
|
|
245
253
|
qs = qs.filter(ledger__entity_id=entity_model)
|
|
246
254
|
else:
|
|
247
|
-
raise BillModelValidationError(
|
|
248
|
-
'Must pass EntityModel, slug or UUID'
|
|
249
|
-
)
|
|
255
|
+
raise BillModelValidationError('Must pass EntityModel, slug or UUID')
|
|
250
256
|
return qs
|
|
251
257
|
|
|
252
258
|
|
|
253
259
|
class BillModelAbstract(
|
|
254
|
-
AccrualMixIn,
|
|
255
|
-
ItemizeMixIn,
|
|
256
|
-
PaymentTermsMixIn,
|
|
257
|
-
MarkdownNotesMixIn,
|
|
258
|
-
CreateUpdateMixIn
|
|
260
|
+
AccrualMixIn, ItemizeMixIn, PaymentTermsMixIn, MarkdownNotesMixIn, CreateUpdateMixIn
|
|
259
261
|
):
|
|
260
262
|
"""
|
|
261
263
|
This is the main abstract class which the BillModel database will inherit from.
|
|
@@ -323,6 +325,7 @@ class BillModelAbstract(
|
|
|
323
325
|
The canceled date represents the date when the BillModel was canceled, if applicable.
|
|
324
326
|
Will be null unless BillModel is canceled. Defaults to :func:`localdate <django.utils.timezone.localdate>`.
|
|
325
327
|
"""
|
|
328
|
+
|
|
326
329
|
REL_NAME_PREFIX = 'bill'
|
|
327
330
|
IS_DEBIT_BALANCE = False
|
|
328
331
|
ALLOW_MIGRATE = True
|
|
@@ -340,7 +343,7 @@ class BillModelAbstract(
|
|
|
340
343
|
(BILL_STATUS_APPROVED, _('Approved')),
|
|
341
344
|
(BILL_STATUS_PAID, _('Paid')),
|
|
342
345
|
(BILL_STATUS_CANCELED, _('Canceled')),
|
|
343
|
-
(BILL_STATUS_VOID, _('Void'))
|
|
346
|
+
(BILL_STATUS_VOID, _('Void')),
|
|
344
347
|
]
|
|
345
348
|
"""
|
|
346
349
|
The different bill status options and their representation in the Database.
|
|
@@ -353,58 +356,79 @@ class BillModelAbstract(
|
|
|
353
356
|
on_delete=models.CASCADE,
|
|
354
357
|
null=True,
|
|
355
358
|
blank=True,
|
|
356
|
-
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'),
|
|
357
418
|
)
|
|
358
|
-
bill_number = models.SlugField(max_length=20, verbose_name=_('Bill Number'), editable=False)
|
|
359
|
-
bill_status = models.CharField(max_length=10,
|
|
360
|
-
choices=BILL_STATUS,
|
|
361
|
-
default=BILL_STATUS[0][0],
|
|
362
|
-
verbose_name=_('Bill Status'))
|
|
363
|
-
xref = models.SlugField(null=True, blank=True, verbose_name=_('External Reference Number'))
|
|
364
|
-
vendor = models.ForeignKey('django_ledger.VendorModel',
|
|
365
|
-
on_delete=models.CASCADE,
|
|
366
|
-
verbose_name=_('Vendor'))
|
|
367
|
-
|
|
368
|
-
cash_account = models.ForeignKey('django_ledger.AccountModel',
|
|
369
|
-
on_delete=models.RESTRICT,
|
|
370
|
-
null=True,
|
|
371
|
-
blank=True,
|
|
372
|
-
verbose_name=_('Cash Account'),
|
|
373
|
-
related_name=f'{REL_NAME_PREFIX}_cash_account')
|
|
374
|
-
prepaid_account = models.ForeignKey('django_ledger.AccountModel',
|
|
375
|
-
on_delete=models.RESTRICT,
|
|
376
|
-
null=True,
|
|
377
|
-
blank=True,
|
|
378
|
-
verbose_name=_('Prepaid Account'),
|
|
379
|
-
related_name=f'{REL_NAME_PREFIX}_prepaid_account')
|
|
380
|
-
unearned_account = models.ForeignKey('django_ledger.AccountModel',
|
|
381
|
-
on_delete=models.RESTRICT,
|
|
382
|
-
null=True,
|
|
383
|
-
blank=True,
|
|
384
|
-
verbose_name=_('Unearned Account'),
|
|
385
|
-
related_name=f'{REL_NAME_PREFIX}_unearned_account')
|
|
386
|
-
|
|
387
|
-
additional_info = models.JSONField(blank=True,
|
|
388
|
-
null=True,
|
|
389
|
-
default=dict,
|
|
390
|
-
verbose_name=_('Bill Additional Info'))
|
|
391
|
-
bill_items = models.ManyToManyField('django_ledger.ItemModel',
|
|
392
|
-
through='django_ledger.ItemTransactionModel',
|
|
393
|
-
through_fields=('bill_model', 'item_model'),
|
|
394
|
-
verbose_name=_('Bill Items'))
|
|
395
|
-
|
|
396
|
-
ce_model = models.ForeignKey('django_ledger.EstimateModel',
|
|
397
|
-
on_delete=models.RESTRICT,
|
|
398
|
-
null=True,
|
|
399
|
-
blank=True,
|
|
400
|
-
verbose_name=_('Associated Customer Job/Estimate'))
|
|
401
419
|
|
|
402
420
|
date_draft = models.DateField(null=True, blank=True, verbose_name=_('Draft Date'))
|
|
403
|
-
date_in_review = models.DateField(
|
|
404
|
-
|
|
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
|
+
)
|
|
405
427
|
date_paid = models.DateField(null=True, blank=True, verbose_name=_('Paid Date'))
|
|
406
428
|
date_void = models.DateField(null=True, blank=True, verbose_name=_('Void Date'))
|
|
407
|
-
date_canceled = models.DateField(
|
|
429
|
+
date_canceled = models.DateField(
|
|
430
|
+
null=True, blank=True, verbose_name=_('Canceled Date')
|
|
431
|
+
)
|
|
408
432
|
|
|
409
433
|
objects = BillModelManager.from_queryset(queryset_class=BillModelQuerySet)()
|
|
410
434
|
|
|
@@ -416,11 +440,9 @@ class BillModelAbstract(
|
|
|
416
440
|
indexes = [
|
|
417
441
|
models.Index(fields=['bill_status']),
|
|
418
442
|
models.Index(fields=['terms']),
|
|
419
|
-
|
|
420
443
|
models.Index(fields=['cash_account']),
|
|
421
444
|
models.Index(fields=['prepaid_account']),
|
|
422
445
|
models.Index(fields=['unearned_account']),
|
|
423
|
-
|
|
424
446
|
models.Index(fields=['date_due']),
|
|
425
447
|
models.Index(fields=['date_draft']),
|
|
426
448
|
models.Index(fields=['date_in_review']),
|
|
@@ -428,7 +450,6 @@ class BillModelAbstract(
|
|
|
428
450
|
models.Index(fields=['date_paid']),
|
|
429
451
|
models.Index(fields=['date_canceled']),
|
|
430
452
|
models.Index(fields=['date_void']),
|
|
431
|
-
|
|
432
453
|
models.Index(fields=['vendor']),
|
|
433
454
|
models.Index(fields=['bill_number']),
|
|
434
455
|
]
|
|
@@ -437,20 +458,19 @@ class BillModelAbstract(
|
|
|
437
458
|
return f'Bill: {self.bill_number} | {self.get_bill_status_display()}'
|
|
438
459
|
|
|
439
460
|
def is_configured(self) -> bool:
|
|
440
|
-
return all([
|
|
441
|
-
super().is_configured(),
|
|
442
|
-
self.bill_status
|
|
443
|
-
])
|
|
461
|
+
return all([super().is_configured(), self.bill_status])
|
|
444
462
|
|
|
445
463
|
# Configuration...
|
|
446
|
-
def configure(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
+
):
|
|
454
474
|
"""
|
|
455
475
|
A configuration hook which executes all initial BillModel setup on to the LedgerModel and all initial
|
|
456
476
|
values of the BillModel. Can only call this method once in the lifetime of a BillModel.
|
|
@@ -483,13 +503,19 @@ class BillModelAbstract(
|
|
|
483
503
|
if not self.is_configured():
|
|
484
504
|
if isinstance(entity_slug, str):
|
|
485
505
|
if not user_model:
|
|
486
|
-
raise BillModelValidationError(
|
|
506
|
+
raise BillModelValidationError(
|
|
507
|
+
_('Must pass user_model when using entity_slug.')
|
|
508
|
+
)
|
|
487
509
|
entity_qs = EntityModel.objects.for_user(user_model=user_model)
|
|
488
|
-
entity_model: EntityModel = get_object_or_404(
|
|
510
|
+
entity_model: EntityModel = get_object_or_404(
|
|
511
|
+
entity_qs, slug__exact=entity_slug
|
|
512
|
+
)
|
|
489
513
|
elif isinstance(entity_slug, EntityModel):
|
|
490
514
|
entity_model = entity_slug
|
|
491
515
|
else:
|
|
492
|
-
raise BillModelValidationError(
|
|
516
|
+
raise BillModelValidationError(
|
|
517
|
+
'entity_slug must be an instance of str or EntityModel'
|
|
518
|
+
)
|
|
493
519
|
|
|
494
520
|
if entity_model.is_accrual_method():
|
|
495
521
|
self.accrue = True
|
|
@@ -504,7 +530,9 @@ class BillModelAbstract(
|
|
|
504
530
|
self.date_draft = get_localdate() if not date_draft else date_draft
|
|
505
531
|
|
|
506
532
|
LedgerModel = lazy_loader.get_ledger_model()
|
|
507
|
-
ledger_model: LedgerModel = LedgerModel(
|
|
533
|
+
ledger_model: LedgerModel = LedgerModel(
|
|
534
|
+
entity=entity_model, posted=ledger_posted
|
|
535
|
+
)
|
|
508
536
|
ledger_name = f'Bill {self.uuid}' if not ledger_name else ledger_name
|
|
509
537
|
ledger_model.name = ledger_name
|
|
510
538
|
ledger_model.configure_for_wrapper_model(model_instance=self)
|
|
@@ -532,24 +560,30 @@ class BillModelAbstract(
|
|
|
532
560
|
return self.is_draft()
|
|
533
561
|
|
|
534
562
|
def migrate_itemtxs(self, itemtxs: Dict, operation: str, commit: bool = False):
|
|
535
|
-
itemtxs_batch = super().migrate_itemtxs(
|
|
563
|
+
itemtxs_batch = super().migrate_itemtxs(
|
|
564
|
+
itemtxs=itemtxs, commit=commit, operation=operation
|
|
565
|
+
)
|
|
536
566
|
self.update_amount_due(itemtxs_qs=itemtxs_batch)
|
|
537
567
|
self.get_state(commit=True)
|
|
538
568
|
|
|
539
569
|
if commit:
|
|
540
|
-
self.save(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
570
|
+
self.save(
|
|
571
|
+
update_fields=[
|
|
572
|
+
'amount_due',
|
|
573
|
+
'amount_receivable',
|
|
574
|
+
'amount_unearned',
|
|
575
|
+
'amount_earned',
|
|
576
|
+
'updated',
|
|
577
|
+
]
|
|
578
|
+
)
|
|
545
579
|
return itemtxs_batch
|
|
546
580
|
|
|
547
581
|
def get_item_model_qs(self) -> ItemModelQuerySet:
|
|
548
|
-
return ItemModel.objects.filter(
|
|
549
|
-
entity_id__exact=self.ledger.entity_id
|
|
550
|
-
).bills()
|
|
582
|
+
return ItemModel.objects.filter(entity_id__exact=self.ledger.entity_id).bills()
|
|
551
583
|
|
|
552
|
-
def validate_itemtxs_qs(
|
|
584
|
+
def validate_itemtxs_qs(
|
|
585
|
+
self, queryset: Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]
|
|
586
|
+
):
|
|
553
587
|
"""
|
|
554
588
|
Validates that the entire ItemTransactionModelQuerySet is bound to the BillModel.
|
|
555
589
|
|
|
@@ -558,16 +592,18 @@ class BillModelAbstract(
|
|
|
558
592
|
queryset: ItemTransactionModelQuerySet or list of ItemTransactionModel.
|
|
559
593
|
ItemTransactionModelQuerySet to validate.
|
|
560
594
|
"""
|
|
561
|
-
valid = all([
|
|
562
|
-
i.bill_model_id == self.uuid for i in queryset
|
|
563
|
-
])
|
|
595
|
+
valid = all([i.bill_model_id == self.uuid for i in queryset])
|
|
564
596
|
if not valid:
|
|
565
|
-
raise BillModelValidationError(
|
|
597
|
+
raise BillModelValidationError(
|
|
598
|
+
f'Invalid queryset. All items must be assigned to Bill {self.uuid}'
|
|
599
|
+
)
|
|
566
600
|
|
|
567
|
-
def get_itemtxs_data(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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]:
|
|
571
607
|
"""
|
|
572
608
|
Fetches the BillModel Items and aggregates the QuerySet.
|
|
573
609
|
|
|
@@ -583,21 +619,18 @@ class BillModelAbstract(
|
|
|
583
619
|
"""
|
|
584
620
|
if not queryset:
|
|
585
621
|
queryset = self.itemtransactionmodel_set.all().select_related(
|
|
586
|
-
'item_model',
|
|
587
|
-
|
|
588
|
-
'po_model',
|
|
589
|
-
'bill_model')
|
|
622
|
+
'item_model', 'entity_unit', 'po_model', 'bill_model'
|
|
623
|
+
)
|
|
590
624
|
else:
|
|
591
625
|
self.validate_itemtxs_qs(queryset)
|
|
592
626
|
|
|
593
627
|
if aggregate_on_db and isinstance(queryset, ItemTransactionModelQuerySet):
|
|
594
628
|
return queryset, queryset.aggregate(
|
|
595
|
-
total_amount__sum=Sum('total_amount'),
|
|
596
|
-
total_items=Count('uuid')
|
|
629
|
+
total_amount__sum=Sum('total_amount'), total_items=Count('uuid')
|
|
597
630
|
)
|
|
598
631
|
return queryset, {
|
|
599
632
|
'total_amount__sum': sum(i.total_amount for i in queryset),
|
|
600
|
-
'total_items': len(queryset)
|
|
633
|
+
'total_items': len(queryset),
|
|
601
634
|
} if not lazy_agg else None
|
|
602
635
|
|
|
603
636
|
# ### ItemizeMixIn implementation END...
|
|
@@ -607,7 +640,9 @@ class BillModelAbstract(
|
|
|
607
640
|
Fetches the TransactionModelQuerySet associated with the BillModel instance.
|
|
608
641
|
"""
|
|
609
642
|
TransactionModel = lazy_loader.get_txs_model()
|
|
610
|
-
transaction_model_qs = TransactionModel.objects.all().for_ledger(
|
|
643
|
+
transaction_model_qs = TransactionModel.objects.all().for_ledger(
|
|
644
|
+
ledger_model=self.ledger_id
|
|
645
|
+
)
|
|
611
646
|
if annotated:
|
|
612
647
|
return transaction_model_qs.with_annotated_details()
|
|
613
648
|
return transaction_model_qs
|
|
@@ -624,8 +659,9 @@ class BillModelAbstract(
|
|
|
624
659
|
"""
|
|
625
660
|
return f'Bill {self.bill_number} account adjustment.'
|
|
626
661
|
|
|
627
|
-
def get_migration_data(
|
|
628
|
-
|
|
662
|
+
def get_migration_data(
|
|
663
|
+
self, queryset: Optional[ItemTransactionModelQuerySet] = None
|
|
664
|
+
) -> ItemTransactionModelQuerySet:
|
|
629
665
|
"""
|
|
630
666
|
Fetches necessary item transaction data to perform a migration into the LedgerModel.
|
|
631
667
|
|
|
@@ -640,21 +676,30 @@ class BillModelAbstract(
|
|
|
640
676
|
else:
|
|
641
677
|
self.validate_itemtxs_qs(queryset)
|
|
642
678
|
|
|
643
|
-
return
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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'))
|
|
654
695
|
)
|
|
655
696
|
|
|
656
|
-
def update_amount_due(
|
|
657
|
-
|
|
697
|
+
def update_amount_due(
|
|
698
|
+
self,
|
|
699
|
+
itemtxs_qs: Optional[
|
|
700
|
+
Union[ItemTransactionModelQuerySet, List[ItemTransactionModel]]
|
|
701
|
+
] = None,
|
|
702
|
+
) -> ItemTransactionModelQuerySet:
|
|
658
703
|
"""
|
|
659
704
|
Updates the BillModel amount due.
|
|
660
705
|
|
|
@@ -737,11 +782,7 @@ class BillModelAbstract(
|
|
|
737
782
|
bool
|
|
738
783
|
True if BillModel is Active, else False.
|
|
739
784
|
"""
|
|
740
|
-
return any([
|
|
741
|
-
self.is_paid(),
|
|
742
|
-
self.is_approved(),
|
|
743
|
-
self.is_void()
|
|
744
|
-
])
|
|
785
|
+
return any([self.is_paid(), self.is_approved(), self.is_void()])
|
|
745
786
|
|
|
746
787
|
def is_void(self) -> bool:
|
|
747
788
|
"""
|
|
@@ -788,10 +829,7 @@ class BillModelAbstract(
|
|
|
788
829
|
bool
|
|
789
830
|
True if BillModel can be marked as in review, else False.
|
|
790
831
|
"""
|
|
791
|
-
return all([
|
|
792
|
-
self.is_configured(),
|
|
793
|
-
self.is_draft()
|
|
794
|
-
])
|
|
832
|
+
return all([self.is_configured(), self.is_draft()])
|
|
795
833
|
|
|
796
834
|
def can_approve(self) -> bool:
|
|
797
835
|
"""
|
|
@@ -827,11 +865,7 @@ class BillModelAbstract(
|
|
|
827
865
|
bool
|
|
828
866
|
True if BillModel can be deleted, else False.
|
|
829
867
|
"""
|
|
830
|
-
return any([
|
|
831
|
-
self.is_review(),
|
|
832
|
-
self.is_draft(),
|
|
833
|
-
not self.ledger.is_locked()
|
|
834
|
-
])
|
|
868
|
+
return any([self.is_review(), self.is_draft(), not self.ledger.is_locked()])
|
|
835
869
|
|
|
836
870
|
def can_void(self) -> bool:
|
|
837
871
|
"""
|
|
@@ -842,10 +876,7 @@ class BillModelAbstract(
|
|
|
842
876
|
bool
|
|
843
877
|
True if BillModel can be marked as void, else False.
|
|
844
878
|
"""
|
|
845
|
-
return all([
|
|
846
|
-
self.is_approved(),
|
|
847
|
-
float(self.amount_paid) == 0.00
|
|
848
|
-
])
|
|
879
|
+
return all([self.is_approved(), float(self.amount_paid) == 0.00])
|
|
849
880
|
|
|
850
881
|
def can_cancel(self) -> bool:
|
|
851
882
|
"""
|
|
@@ -856,10 +887,7 @@ class BillModelAbstract(
|
|
|
856
887
|
bool
|
|
857
888
|
True if BillModel can be marked as canceled, else False.
|
|
858
889
|
"""
|
|
859
|
-
return any([
|
|
860
|
-
self.is_draft(),
|
|
861
|
-
self.is_review()
|
|
862
|
-
])
|
|
890
|
+
return any([self.is_draft(), self.is_review()])
|
|
863
891
|
|
|
864
892
|
def can_edit_items(self) -> bool:
|
|
865
893
|
"""
|
|
@@ -907,16 +935,16 @@ class BillModelAbstract(
|
|
|
907
935
|
"""
|
|
908
936
|
if self.ce_model_id:
|
|
909
937
|
if raise_exception:
|
|
910
|
-
raise BillModelValidationError(
|
|
911
|
-
|
|
938
|
+
raise BillModelValidationError(
|
|
939
|
+
f'Bill {self.bill_number} already bound to '
|
|
940
|
+
f'Estimate {self.ce_model.estimate_number}'
|
|
941
|
+
)
|
|
912
942
|
return False
|
|
913
943
|
|
|
914
944
|
is_approved = estimate_model.is_approved()
|
|
915
945
|
if not is_approved and raise_exception:
|
|
916
|
-
raise BillModelValidationError(
|
|
917
|
-
return all([
|
|
918
|
-
is_approved
|
|
919
|
-
])
|
|
946
|
+
raise BillModelValidationError('Cannot bind estimate that is not approved.')
|
|
947
|
+
return all([is_approved])
|
|
920
948
|
|
|
921
949
|
def can_bind_po(self, po_model, raise_exception: bool = False) -> bool:
|
|
922
950
|
"""
|
|
@@ -939,12 +967,14 @@ class BillModelAbstract(
|
|
|
939
967
|
"""
|
|
940
968
|
if not po_model.is_approved():
|
|
941
969
|
if raise_exception:
|
|
942
|
-
raise BillModelValidationError(
|
|
970
|
+
raise BillModelValidationError('Cannot bind an unapproved PO.')
|
|
943
971
|
return False
|
|
944
972
|
|
|
945
973
|
if po_model.date_approved > self.date_draft:
|
|
946
974
|
if raise_exception:
|
|
947
|
-
raise BillModelValidationError(
|
|
975
|
+
raise BillModelValidationError(
|
|
976
|
+
'Approved PO date cannot be greater than Bill draft date.'
|
|
977
|
+
)
|
|
948
978
|
return False
|
|
949
979
|
|
|
950
980
|
return True
|
|
@@ -959,11 +989,7 @@ class BillModelAbstract(
|
|
|
959
989
|
bool
|
|
960
990
|
True if BillModel can generate its bill_number, else False.
|
|
961
991
|
"""
|
|
962
|
-
return all([
|
|
963
|
-
not self.bill_number,
|
|
964
|
-
self.is_draft(),
|
|
965
|
-
self.is_configured()
|
|
966
|
-
])
|
|
992
|
+
return all([not self.bill_number, self.is_draft(), self.is_configured()])
|
|
967
993
|
|
|
968
994
|
# ACTIONS ---
|
|
969
995
|
|
|
@@ -980,11 +1006,13 @@ class BillModelAbstract(
|
|
|
980
1006
|
"""
|
|
981
1007
|
return self.is_approved()
|
|
982
1008
|
|
|
983
|
-
def make_payment(
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
+
):
|
|
988
1016
|
"""
|
|
989
1017
|
Makes a payment to the BillModel.
|
|
990
1018
|
|
|
@@ -1035,7 +1063,7 @@ class BillModelAbstract(
|
|
|
1035
1063
|
user_model=None,
|
|
1036
1064
|
entity_slug=self.ledger.entity.slug,
|
|
1037
1065
|
je_timestamp=payment_date,
|
|
1038
|
-
raise_exception=True
|
|
1066
|
+
raise_exception=True,
|
|
1039
1067
|
)
|
|
1040
1068
|
self.save(
|
|
1041
1069
|
update_fields=[
|
|
@@ -1043,10 +1071,13 @@ class BillModelAbstract(
|
|
|
1043
1071
|
'amount_earned',
|
|
1044
1072
|
'amount_unearned',
|
|
1045
1073
|
'amount_receivable',
|
|
1046
|
-
'updated'
|
|
1047
|
-
]
|
|
1074
|
+
'updated',
|
|
1075
|
+
]
|
|
1076
|
+
)
|
|
1048
1077
|
|
|
1049
|
-
def bind_estimate(
|
|
1078
|
+
def bind_estimate(
|
|
1079
|
+
self, estimate_model, commit: bool = False, raise_exception: bool = True
|
|
1080
|
+
):
|
|
1050
1081
|
"""
|
|
1051
1082
|
Binds BillModel to a given EstimateModel. Raises ValueError if EstimateModel cannot be bound.
|
|
1052
1083
|
|
|
@@ -1070,12 +1101,11 @@ class BillModelAbstract(
|
|
|
1070
1101
|
self.ce_model = estimate_model
|
|
1071
1102
|
self.clean()
|
|
1072
1103
|
if commit:
|
|
1073
|
-
self.save(update_fields=[
|
|
1074
|
-
'ce_model',
|
|
1075
|
-
'updated'
|
|
1076
|
-
])
|
|
1104
|
+
self.save(update_fields=['ce_model', 'updated'])
|
|
1077
1105
|
|
|
1078
|
-
def mark_as_draft(
|
|
1106
|
+
def mark_as_draft(
|
|
1107
|
+
self, date_draft: Optional[date] = None, commit: bool = False, **kwargs
|
|
1108
|
+
):
|
|
1079
1109
|
"""
|
|
1080
1110
|
Marks BillModel as Draft.
|
|
1081
1111
|
|
|
@@ -1104,16 +1134,10 @@ class BillModelAbstract(
|
|
|
1104
1134
|
|
|
1105
1135
|
self.clean()
|
|
1106
1136
|
if commit:
|
|
1107
|
-
self.save(
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
'updated'
|
|
1112
|
-
]
|
|
1113
|
-
)
|
|
1114
|
-
bill_status_draft.send_robust(sender=self.__class__,
|
|
1115
|
-
instance=self,
|
|
1116
|
-
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
|
+
)
|
|
1117
1141
|
|
|
1118
1142
|
def get_mark_as_draft_html_id(self) -> str:
|
|
1119
1143
|
"""
|
|
@@ -1143,11 +1167,10 @@ class BillModelAbstract(
|
|
|
1143
1167
|
"""
|
|
1144
1168
|
if not entity_slug:
|
|
1145
1169
|
entity_slug = self.ledger.entity.slug
|
|
1146
|
-
return reverse(
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
})
|
|
1170
|
+
return reverse(
|
|
1171
|
+
'django_ledger:bill-action-mark-as-draft',
|
|
1172
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1173
|
+
)
|
|
1151
1174
|
|
|
1152
1175
|
def get_mark_as_draft_message(self) -> str:
|
|
1153
1176
|
"""
|
|
@@ -1161,12 +1184,14 @@ class BillModelAbstract(
|
|
|
1161
1184
|
return _('Do you want to mark Bill %s as Draft?') % self.bill_number
|
|
1162
1185
|
|
|
1163
1186
|
# IN REVIEW ACTIONS....
|
|
1164
|
-
def mark_as_review(
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
+
):
|
|
1170
1195
|
"""
|
|
1171
1196
|
Marks BillModel as In Review.
|
|
1172
1197
|
|
|
@@ -1194,7 +1219,9 @@ class BillModelAbstract(
|
|
|
1194
1219
|
self.validate_itemtxs_qs(queryset=itemtxs_qs)
|
|
1195
1220
|
|
|
1196
1221
|
if not itemtxs_qs.count():
|
|
1197
|
-
raise BillModelValidationError(
|
|
1222
|
+
raise BillModelValidationError(
|
|
1223
|
+
message=f'Cannot review a {self.__class__.__name__} without items...'
|
|
1224
|
+
)
|
|
1198
1225
|
|
|
1199
1226
|
if not self.amount_due:
|
|
1200
1227
|
raise BillModelValidationError(
|
|
@@ -1213,16 +1240,10 @@ class BillModelAbstract(
|
|
|
1213
1240
|
|
|
1214
1241
|
self.clean()
|
|
1215
1242
|
if commit:
|
|
1216
|
-
self.save(
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
'updated'
|
|
1221
|
-
]
|
|
1222
|
-
)
|
|
1223
|
-
bill_status_in_review.send_robust(sender=self.__class__,
|
|
1224
|
-
instance=self,
|
|
1225
|
-
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
|
+
)
|
|
1226
1247
|
|
|
1227
1248
|
def get_mark_as_review_html_id(self) -> str:
|
|
1228
1249
|
"""
|
|
@@ -1252,11 +1273,10 @@ class BillModelAbstract(
|
|
|
1252
1273
|
"""
|
|
1253
1274
|
if not entity_slug:
|
|
1254
1275
|
entity_slug = self.ledger.entity.slug
|
|
1255
|
-
return reverse(
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
})
|
|
1276
|
+
return reverse(
|
|
1277
|
+
'django_ledger:bill-action-mark-as-review',
|
|
1278
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1279
|
+
)
|
|
1260
1280
|
|
|
1261
1281
|
def get_mark_as_review_message(self) -> str:
|
|
1262
1282
|
"""
|
|
@@ -1270,14 +1290,16 @@ class BillModelAbstract(
|
|
|
1270
1290
|
return _('Do you want to mark Bill %s as In Review?') % self.bill_number
|
|
1271
1291
|
|
|
1272
1292
|
# APPROVED ACTIONS....
|
|
1273
|
-
def mark_as_approved(
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
+
):
|
|
1281
1303
|
"""
|
|
1282
1304
|
Marks BillModel as Approved.
|
|
1283
1305
|
|
|
@@ -1327,12 +1349,12 @@ class BillModelAbstract(
|
|
|
1327
1349
|
entity_slug=entity_slug,
|
|
1328
1350
|
user_model=user_model,
|
|
1329
1351
|
je_timestamp=date_approved,
|
|
1330
|
-
force_migrate=self.accrue
|
|
1352
|
+
force_migrate=self.accrue,
|
|
1331
1353
|
)
|
|
1332
1354
|
self.ledger.post(commit=commit, raise_exception=raise_exception)
|
|
1333
|
-
bill_status_approved.send_robust(
|
|
1334
|
-
|
|
1335
|
-
|
|
1355
|
+
bill_status_approved.send_robust(
|
|
1356
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1357
|
+
)
|
|
1336
1358
|
|
|
1337
1359
|
def get_mark_as_approved_html_id(self) -> str:
|
|
1338
1360
|
"""
|
|
@@ -1362,11 +1384,10 @@ class BillModelAbstract(
|
|
|
1362
1384
|
"""
|
|
1363
1385
|
if not entity_slug:
|
|
1364
1386
|
entity_slug = self.ledger.entity.slug
|
|
1365
|
-
return reverse(
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
})
|
|
1387
|
+
return reverse(
|
|
1388
|
+
'django_ledger:bill-action-mark-as-approved',
|
|
1389
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1390
|
+
)
|
|
1370
1391
|
|
|
1371
1392
|
def get_mark_as_approved_message(self) -> str:
|
|
1372
1393
|
"""
|
|
@@ -1380,14 +1401,15 @@ class BillModelAbstract(
|
|
|
1380
1401
|
return _('Do you want to mark Bill %s as Approved?') % self.bill_number
|
|
1381
1402
|
|
|
1382
1403
|
# PAY ACTIONS....
|
|
1383
|
-
def mark_as_paid(
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
+
):
|
|
1391
1413
|
"""
|
|
1392
1414
|
Marks BillModel as Paid.
|
|
1393
1415
|
|
|
@@ -1410,7 +1432,9 @@ class BillModelAbstract(
|
|
|
1410
1432
|
Commits transaction into the Database. Defaults to False.
|
|
1411
1433
|
"""
|
|
1412
1434
|
if not self.can_pay():
|
|
1413
|
-
raise BillModelValidationError(
|
|
1435
|
+
raise BillModelValidationError(
|
|
1436
|
+
f'Cannot mark Bill {self.bill_number} as paid...'
|
|
1437
|
+
)
|
|
1414
1438
|
|
|
1415
1439
|
if date_paid:
|
|
1416
1440
|
if isinstance(date_paid, datetime):
|
|
@@ -1424,10 +1448,13 @@ class BillModelAbstract(
|
|
|
1424
1448
|
self.amount_paid = self.amount_due
|
|
1425
1449
|
|
|
1426
1450
|
if self.date_paid > get_localdate():
|
|
1427
|
-
raise BillModelValidationError(
|
|
1451
|
+
raise BillModelValidationError(
|
|
1452
|
+
f'Cannot pay {self.__class__.__name__} in the future.'
|
|
1453
|
+
)
|
|
1428
1454
|
if self.date_paid < self.date_approved:
|
|
1429
1455
|
raise BillModelValidationError(
|
|
1430
|
-
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
|
+
)
|
|
1431
1458
|
|
|
1432
1459
|
self.bill_status = self.BILL_STATUS_PAID
|
|
1433
1460
|
self.get_state(commit=True)
|
|
@@ -1441,9 +1468,9 @@ class BillModelAbstract(
|
|
|
1441
1468
|
if commit:
|
|
1442
1469
|
self.save()
|
|
1443
1470
|
ItemTransactionModel = lazy_loader.get_item_transaction_model()
|
|
1444
|
-
itemtxs_qs.filter(
|
|
1445
|
-
|
|
1446
|
-
)
|
|
1471
|
+
itemtxs_qs.filter(po_model_id__isnull=False).update(
|
|
1472
|
+
po_item_status=ItemTransactionModel.STATUS_ORDERED
|
|
1473
|
+
)
|
|
1447
1474
|
|
|
1448
1475
|
if not entity_slug:
|
|
1449
1476
|
entity_slug = self.ledger.entity.slug
|
|
@@ -1453,12 +1480,12 @@ class BillModelAbstract(
|
|
|
1453
1480
|
entity_slug=entity_slug,
|
|
1454
1481
|
itemtxs_qs=itemtxs_qs,
|
|
1455
1482
|
je_timestamp=date_paid,
|
|
1456
|
-
force_migrate=True
|
|
1483
|
+
force_migrate=True,
|
|
1457
1484
|
)
|
|
1458
1485
|
self.lock_ledger(commit=True)
|
|
1459
|
-
bill_status_paid.send_robust(
|
|
1460
|
-
|
|
1461
|
-
|
|
1486
|
+
bill_status_paid.send_robust(
|
|
1487
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1488
|
+
)
|
|
1462
1489
|
|
|
1463
1490
|
def get_mark_as_paid_html_id(self) -> str:
|
|
1464
1491
|
"""
|
|
@@ -1488,11 +1515,10 @@ class BillModelAbstract(
|
|
|
1488
1515
|
"""
|
|
1489
1516
|
if not entity_slug:
|
|
1490
1517
|
entity_slug = self.ledger.entity.slug
|
|
1491
|
-
return reverse(
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
})
|
|
1518
|
+
return reverse(
|
|
1519
|
+
'django_ledger:bill-action-mark-as-paid',
|
|
1520
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1521
|
+
)
|
|
1496
1522
|
|
|
1497
1523
|
def get_mark_as_paid_message(self) -> str:
|
|
1498
1524
|
"""
|
|
@@ -1506,12 +1532,14 @@ class BillModelAbstract(
|
|
|
1506
1532
|
return _('Do you want to mark Bill %s as Paid?') % self.bill_number
|
|
1507
1533
|
|
|
1508
1534
|
# VOID Actions...
|
|
1509
|
-
def mark_as_void(
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
+
):
|
|
1515
1543
|
"""
|
|
1516
1544
|
Marks BillModel as Void.
|
|
1517
1545
|
When mark as void, all transactions associated with BillModel are reversed as of the void date.
|
|
@@ -1532,7 +1560,9 @@ class BillModelAbstract(
|
|
|
1532
1560
|
Commits transaction into DB. Defaults to False.
|
|
1533
1561
|
"""
|
|
1534
1562
|
if not self.can_void():
|
|
1535
|
-
raise BillModelValidationError(
|
|
1563
|
+
raise BillModelValidationError(
|
|
1564
|
+
f'Bill {self.bill_number} cannot be voided. Must be approved.'
|
|
1565
|
+
)
|
|
1536
1566
|
|
|
1537
1567
|
if date_void:
|
|
1538
1568
|
if isinstance(date_void, datetime):
|
|
@@ -1557,12 +1587,13 @@ class BillModelAbstract(
|
|
|
1557
1587
|
void=True,
|
|
1558
1588
|
void_date=self.date_void,
|
|
1559
1589
|
raise_exception=False,
|
|
1560
|
-
force_migrate=True
|
|
1590
|
+
force_migrate=True,
|
|
1591
|
+
)
|
|
1561
1592
|
self.save()
|
|
1562
1593
|
self.lock_ledger(commit=False, raise_exception=False)
|
|
1563
|
-
bill_status_void.send_robust(
|
|
1564
|
-
|
|
1565
|
-
|
|
1594
|
+
bill_status_void.send_robust(
|
|
1595
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1596
|
+
)
|
|
1566
1597
|
|
|
1567
1598
|
def get_mark_as_void_html_id(self) -> str:
|
|
1568
1599
|
"""
|
|
@@ -1591,11 +1622,10 @@ class BillModelAbstract(
|
|
|
1591
1622
|
"""
|
|
1592
1623
|
if not entity_slug:
|
|
1593
1624
|
entity_slug = self.ledger.entity.slug
|
|
1594
|
-
return reverse(
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
})
|
|
1625
|
+
return reverse(
|
|
1626
|
+
'django_ledger:bill-action-mark-as-void',
|
|
1627
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1628
|
+
)
|
|
1599
1629
|
|
|
1600
1630
|
def get_mark_as_void_message(self) -> str:
|
|
1601
1631
|
"""
|
|
@@ -1609,7 +1639,9 @@ class BillModelAbstract(
|
|
|
1609
1639
|
return _('Do you want to void Bill %s?') % self.bill_number
|
|
1610
1640
|
|
|
1611
1641
|
# Cancel Actions...
|
|
1612
|
-
def mark_as_canceled(
|
|
1642
|
+
def mark_as_canceled(
|
|
1643
|
+
self, date_canceled: Optional[date] = None, commit: bool = False, **kwargs
|
|
1644
|
+
):
|
|
1613
1645
|
"""
|
|
1614
1646
|
Mark BillModel as Canceled.
|
|
1615
1647
|
|
|
@@ -1623,16 +1655,18 @@ class BillModelAbstract(
|
|
|
1623
1655
|
Commits transaction into the Database. Defaults to False.
|
|
1624
1656
|
"""
|
|
1625
1657
|
if not self.can_cancel():
|
|
1626
|
-
raise BillModelValidationError(
|
|
1658
|
+
raise BillModelValidationError(
|
|
1659
|
+
f'Bill {self.bill_number} cannot be canceled. Must be draft or in review.'
|
|
1660
|
+
)
|
|
1627
1661
|
|
|
1628
1662
|
self.date_canceled = get_localdate() if not date_canceled else date_canceled
|
|
1629
1663
|
self.bill_status = self.BILL_STATUS_CANCELED
|
|
1630
1664
|
self.clean()
|
|
1631
1665
|
if commit:
|
|
1632
1666
|
self.save()
|
|
1633
|
-
bill_status_canceled.send_robust(
|
|
1634
|
-
|
|
1635
|
-
|
|
1667
|
+
bill_status_canceled.send_robust(
|
|
1668
|
+
sender=self.__class__, instance=self, commited=commit, **kwargs
|
|
1669
|
+
)
|
|
1636
1670
|
|
|
1637
1671
|
def get_mark_as_canceled_html_id(self) -> str:
|
|
1638
1672
|
"""
|
|
@@ -1664,11 +1698,10 @@ class BillModelAbstract(
|
|
|
1664
1698
|
if not entity_slug:
|
|
1665
1699
|
entity_slug = self.ledger.entity.slug
|
|
1666
1700
|
|
|
1667
|
-
return reverse(
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
})
|
|
1701
|
+
return reverse(
|
|
1702
|
+
'django_ledger:bill-action-mark-as-canceled',
|
|
1703
|
+
kwargs={'entity_slug': entity_slug, 'bill_pk': self.uuid},
|
|
1704
|
+
)
|
|
1672
1705
|
|
|
1673
1706
|
def get_mark_as_canceled_message(self) -> str:
|
|
1674
1707
|
"""
|
|
@@ -1846,28 +1879,28 @@ class BillModelAbstract(
|
|
|
1846
1879
|
'entity_model_id__exact': self.ledger.entity_id,
|
|
1847
1880
|
'entity_unit_id__exact': None,
|
|
1848
1881
|
'fiscal_year': fy_key,
|
|
1849
|
-
'key__exact': EntityStateModel.KEY_BILL
|
|
1882
|
+
'key__exact': EntityStateModel.KEY_BILL,
|
|
1850
1883
|
}
|
|
1851
1884
|
|
|
1852
|
-
state_model_qs =
|
|
1853
|
-
|
|
1885
|
+
state_model_qs = (
|
|
1886
|
+
EntityStateModel.objects.filter(**LOOKUP)
|
|
1887
|
+
.select_related('entity_model')
|
|
1888
|
+
.select_for_update()
|
|
1889
|
+
)
|
|
1854
1890
|
state_model = state_model_qs.get()
|
|
1855
1891
|
state_model.sequence = F('sequence') + 1
|
|
1856
1892
|
state_model.save(update_fields=['sequence'])
|
|
1857
1893
|
state_model.refresh_from_db()
|
|
1858
1894
|
return state_model
|
|
1859
1895
|
except ObjectDoesNotExist:
|
|
1860
|
-
EntityModel = lazy_loader.get_entity_model()
|
|
1861
|
-
entity_model = EntityModel.objects.get(uuid__exact=self.ledger.entity_id)
|
|
1862
|
-
fy_key = entity_model.get_fy_for_date(dt=self.date_draft)
|
|
1863
|
-
|
|
1864
1896
|
LOOKUP = {
|
|
1865
1897
|
'entity_model_id': entity_model.uuid,
|
|
1866
1898
|
'entity_unit_id': None,
|
|
1867
1899
|
'fiscal_year': fy_key,
|
|
1868
1900
|
'key': EntityStateModel.KEY_BILL,
|
|
1869
|
-
'sequence': 1
|
|
1901
|
+
'sequence': 1,
|
|
1870
1902
|
}
|
|
1903
|
+
|
|
1871
1904
|
state_model = EntityStateModel.objects.create(**LOOKUP)
|
|
1872
1905
|
return state_model
|
|
1873
1906
|
except IntegrityError as e:
|
|
@@ -1892,12 +1925,13 @@ class BillModelAbstract(
|
|
|
1892
1925
|
"""
|
|
1893
1926
|
if self.can_generate_bill_number():
|
|
1894
1927
|
with transaction.atomic(durable=True):
|
|
1895
|
-
|
|
1896
1928
|
state_model = None
|
|
1897
1929
|
while not state_model:
|
|
1898
1930
|
state_model = self._get_next_state_model(raise_exception=False)
|
|
1899
1931
|
|
|
1900
|
-
seq = str(state_model.sequence).zfill(
|
|
1932
|
+
seq = str(state_model.sequence).zfill(
|
|
1933
|
+
DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING
|
|
1934
|
+
)
|
|
1901
1935
|
self.bill_number = f'{DJANGO_LEDGER_BILL_NUMBER_PREFIX}-{state_model.fiscal_year}-{seq}'
|
|
1902
1936
|
|
|
1903
1937
|
if commit:
|
|
@@ -1910,11 +1944,10 @@ class BillModelAbstract(
|
|
|
1910
1944
|
|
|
1911
1945
|
# --> URLs <---
|
|
1912
1946
|
def get_absolute_url(self):
|
|
1913
|
-
return reverse(
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
})
|
|
1947
|
+
return reverse(
|
|
1948
|
+
'django_ledger:bill-detail',
|
|
1949
|
+
kwargs={'entity_slug': self.ledger.entity.slug, 'bill_pk': self.uuid},
|
|
1950
|
+
)
|
|
1918
1951
|
|
|
1919
1952
|
def clean(self, commit: bool = True):
|
|
1920
1953
|
"""
|
|
@@ -1932,9 +1965,13 @@ class BillModelAbstract(
|
|
|
1932
1965
|
if self.cash_account.role != ASSET_CA_CASH:
|
|
1933
1966
|
raise ValidationError(f'Cash account must be of role {ASSET_CA_CASH}.')
|
|
1934
1967
|
if self.prepaid_account.role != ASSET_CA_PREPAID:
|
|
1935
|
-
raise ValidationError(
|
|
1968
|
+
raise ValidationError(
|
|
1969
|
+
f'Prepaid account must be of role {ASSET_CA_PREPAID}.'
|
|
1970
|
+
)
|
|
1936
1971
|
if self.unearned_account.role != LIABILITY_CL_ACC_PAYABLE:
|
|
1937
|
-
raise ValidationError(
|
|
1972
|
+
raise ValidationError(
|
|
1973
|
+
f'Unearned account must be of role {LIABILITY_CL_ACC_PAYABLE}.'
|
|
1974
|
+
)
|
|
1938
1975
|
|
|
1939
1976
|
|
|
1940
1977
|
class BillModel(BillModelAbstract):
|