django-ledger 0.7.11__py3-none-any.whl → 0.8.0__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/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +1 -1
- 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 +8 -26
- 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/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +79 -63
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +60 -39
- django_ledger/models/data_import.py +55 -41
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +18 -16
- 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 +5 -3
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/transactions.py +152 -113
- django_ledger/models/unit.py +57 -30
- django_ledger/models/vendor.py +75 -43
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +56 -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/customer/tags/customer_table.html +5 -5
- 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 +6 -1
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +9 -5
- 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 +0 -4
- 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 +13 -17
- django_ledger/views/data_import.py +7 -6
- 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 +25 -19
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +1 -2
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/RECORD +79 -108
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/WHEEL +1 -1
- django_ledger-0.8.0.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.0.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
|
|
3
3
|
Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
|
-
from datetime import datetime, time
|
|
5
|
+
import warnings
|
|
6
|
+
from datetime import datetime, time, date
|
|
7
7
|
from decimal import Decimal
|
|
8
8
|
from itertools import groupby, chain
|
|
9
9
|
from typing import Optional
|
|
@@ -12,58 +12,75 @@ from uuid import uuid4, UUID
|
|
|
12
12
|
from django.core.exceptions import ValidationError
|
|
13
13
|
from django.core.validators import MinValueValidator
|
|
14
14
|
from django.db import models
|
|
15
|
-
from django.db.models import Q, Count
|
|
15
|
+
from django.db.models import Q, Manager, QuerySet, Count
|
|
16
16
|
from django.db.models.signals import pre_save
|
|
17
17
|
from django.urls import reverse
|
|
18
18
|
from django.utils.timezone import make_aware
|
|
19
19
|
from django.utils.translation import gettext_lazy as _
|
|
20
20
|
|
|
21
|
+
from django_ledger.models import EntityModel, deprecated_entity_slug_behavior, lazy_loader
|
|
21
22
|
from django_ledger.models.journal_entry import JournalEntryModel
|
|
22
23
|
from django_ledger.models.ledger import LedgerModel
|
|
23
24
|
from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn
|
|
24
25
|
from django_ledger.models.transactions import TransactionModel
|
|
25
|
-
from django_ledger.
|
|
26
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class ClosingEntryValidationError(ValidationError):
|
|
29
30
|
pass
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
class ClosingEntryModelQuerySet(
|
|
33
|
+
class ClosingEntryModelQuerySet(QuerySet):
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
+
def for_user(self, user_model):
|
|
36
|
+
if user_model.is_superuser:
|
|
37
|
+
return self
|
|
38
|
+
return self.filter(
|
|
39
|
+
Q(entity_model__admin=user_model) |
|
|
40
|
+
Q(entity_model__managers__in=[user_model])
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def posted(self) -> 'ClosingEntryModelQuerySet':
|
|
35
44
|
return self.filter(posted=True)
|
|
36
45
|
|
|
37
|
-
def not_posted(self):
|
|
46
|
+
def not_posted(self) -> 'ClosingEntryModelQuerySet':
|
|
38
47
|
return self.filter(posted=False)
|
|
39
48
|
|
|
40
49
|
|
|
41
|
-
class ClosingEntryModelManager(
|
|
50
|
+
class ClosingEntryModelManager(Manager):
|
|
42
51
|
|
|
43
|
-
def get_queryset(self):
|
|
52
|
+
def get_queryset(self) -> ClosingEntryModelQuerySet:
|
|
44
53
|
qs = ClosingEntryModelQuerySet(self.model, using=self._db)
|
|
45
54
|
return qs.annotate(
|
|
46
55
|
ce_txs_count=Count('closingentrytransactionmodel')
|
|
47
56
|
)
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
@deprecated_entity_slug_behavior
|
|
59
|
+
def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> ClosingEntryModelQuerySet:
|
|
60
|
+
|
|
50
61
|
qs = self.get_queryset()
|
|
51
|
-
if user_model.is_superuser:
|
|
52
|
-
return qs
|
|
53
|
-
return qs.filter(
|
|
54
|
-
Q(entity_model__admin=user_model) |
|
|
55
|
-
Q(entity_model__managers__in=[user_model])
|
|
56
|
-
)
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
if 'user_model' in kwargs:
|
|
64
|
+
warnings.warn(
|
|
65
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
66
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
67
|
+
DeprecationWarning,
|
|
68
|
+
stacklevel=2
|
|
63
69
|
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
71
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
72
|
+
|
|
73
|
+
if isinstance(entity_model, EntityModel):
|
|
74
|
+
qs = qs.filter(entity_model=entity_model)
|
|
75
|
+
elif isinstance(entity_model, str):
|
|
76
|
+
qs = qs.filter(entity_model__slug__exact=entity_model)
|
|
77
|
+
elif isinstance(entity_model, UUID):
|
|
78
|
+
qs = qs.filter(entity_model_id=entity_model)
|
|
79
|
+
else:
|
|
80
|
+
raise ClosingEntryValidationError(
|
|
81
|
+
message='Must pass EntityModel, slug or UUID'
|
|
82
|
+
)
|
|
83
|
+
return qs
|
|
67
84
|
|
|
68
85
|
|
|
69
86
|
class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
@@ -247,7 +264,7 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
|
247
264
|
self.posted = False
|
|
248
265
|
|
|
249
266
|
TransactionModel.objects.for_entity(
|
|
250
|
-
|
|
267
|
+
entity_model=self.entity_model_id
|
|
251
268
|
).for_ledger(ledger_model=self.ledger_model).delete()
|
|
252
269
|
|
|
253
270
|
self.ledger_model.journal_entries.all().delete()
|
|
@@ -321,7 +338,7 @@ class ClosingEntryModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn):
|
|
|
321
338
|
self.ledger_model.unpost(commit=True, raise_exception=True)
|
|
322
339
|
|
|
323
340
|
TransactionModel.objects.for_entity(
|
|
324
|
-
|
|
341
|
+
entity_model=self.entity_model_id
|
|
325
342
|
).for_ledger(ledger_model=self.ledger_model).delete()
|
|
326
343
|
|
|
327
344
|
return self.ledger_model.delete()
|
|
@@ -361,19 +378,62 @@ class ClosingEntryModel(ClosingEntryModelAbstract):
|
|
|
361
378
|
abstract = False
|
|
362
379
|
|
|
363
380
|
|
|
364
|
-
|
|
365
|
-
|
|
381
|
+
def closingentrymodel_presave(instance: ClosingEntryModel, **kwargs):
|
|
382
|
+
instance.create_entry_ledger(commit=False)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
pre_save.connect(closingentrymodel_presave, sender=ClosingEntryModel)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class ClosingEntryTransactionModelQuerySet(QuerySet):
|
|
366
389
|
|
|
390
|
+
def for_user(self, user_model) -> 'ClosingEntryTransactionModelQuerySet':
|
|
391
|
+
if user_model.is_superuser:
|
|
392
|
+
return self
|
|
393
|
+
return self.filter(
|
|
394
|
+
Q(closing_entry_model__entity_model__admin=user_model) |
|
|
395
|
+
Q(closing_entry_model__entity_model__managers__in=[user_model])
|
|
396
|
+
)
|
|
367
397
|
|
|
368
|
-
|
|
398
|
+
def for_closing_date(self, closing_date: date) -> 'ClosingEntryTransactionModelQuerySet':
|
|
399
|
+
return self.filter(
|
|
400
|
+
closing_entry_model__closing_date__exact=closing_date
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class ClosingEntryTransactionModelManager(Manager):
|
|
405
|
+
|
|
406
|
+
def get_queryset(self) -> ClosingEntryTransactionModelQuerySet:
|
|
407
|
+
return ClosingEntryTransactionModelQuerySet(self.model, using=self._db)
|
|
408
|
+
|
|
409
|
+
@deprecated_entity_slug_behavior
|
|
410
|
+
def for_entity(self, entity_model: EntityModel | str | UUID = None,
|
|
411
|
+
**kwargs) -> ClosingEntryTransactionModelQuerySet:
|
|
412
|
+
EntityModel = lazy_loader.get_entity(entity_model)
|
|
369
413
|
|
|
370
|
-
def for_entity(self, entity_slug):
|
|
371
414
|
qs = self.get_queryset()
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
415
|
+
|
|
416
|
+
if 'user_model' in kwargs:
|
|
417
|
+
warnings.warn(
|
|
418
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
419
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
420
|
+
DeprecationWarning,
|
|
421
|
+
stacklevel=2
|
|
422
|
+
)
|
|
423
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
424
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
425
|
+
|
|
426
|
+
if isinstance(entity_model, EntityModel):
|
|
427
|
+
qs = qs.filter(closing_entry_model__entity_model=entity_model)
|
|
428
|
+
elif isinstance(entity_model, UUID):
|
|
429
|
+
qs = qs.filter(closing_entry_model__entity_model_id=entity_model)
|
|
430
|
+
elif isinstance(entity_model, str):
|
|
431
|
+
qs = qs.filter(closing_entry_model__slug__exact=entity_model)
|
|
432
|
+
else:
|
|
433
|
+
raise ClosingEntryValidationError(
|
|
434
|
+
message=_('Must pass EntityModel or UUID or str')
|
|
435
|
+
)
|
|
436
|
+
return qs
|
|
377
437
|
|
|
378
438
|
|
|
379
439
|
class ClosingEntryTransactionModelAbstract(CreateUpdateMixIn):
|
|
@@ -453,13 +513,11 @@ class ClosingEntryTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
453
513
|
def __str__(self):
|
|
454
514
|
return f'{self.__class__.__name__}: {self.closing_entry_model.closing_date.strftime("%D")} | {self.balance}'
|
|
455
515
|
|
|
456
|
-
def is_debit(self) ->
|
|
457
|
-
|
|
458
|
-
return self.tx_type == TransactionModel.DEBIT
|
|
516
|
+
def is_debit(self) -> bool:
|
|
517
|
+
return self.tx_type == TransactionModel.DEBIT
|
|
459
518
|
|
|
460
|
-
def is_credit(self) ->
|
|
461
|
-
|
|
462
|
-
return self.tx_type == TransactionModel.CREDIT
|
|
519
|
+
def is_credit(self) -> bool:
|
|
520
|
+
return self.tx_type == TransactionModel.CREDIT
|
|
463
521
|
|
|
464
522
|
def adjust_tx_type_for_negative_balance(self):
|
|
465
523
|
if self.balance < Decimal('0.00'):
|
|
@@ -485,13 +543,6 @@ class ClosingEntryTransactionModel(ClosingEntryTransactionModelAbstract):
|
|
|
485
543
|
abstract = False
|
|
486
544
|
|
|
487
545
|
|
|
488
|
-
def closingentrymodel_presave(instance: ClosingEntryModel, **kwargs):
|
|
489
|
-
instance.create_entry_ledger(commit=False)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
pre_save.connect(closingentrymodel_presave, sender=ClosingEntryModel)
|
|
493
|
-
|
|
494
|
-
|
|
495
546
|
def closingentrytransactionmodel_presave(instance: ClosingEntryTransactionModel, **kwargs):
|
|
496
547
|
instance.adjust_tx_type_for_negative_balance()
|
|
497
548
|
|
django_ledger/models/customer.py
CHANGED
|
@@ -7,17 +7,23 @@ created before it can be assigned to the InvoiceModel. Only customers who are ac
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
-
|
|
10
|
+
import warnings
|
|
11
|
+
from uuid import uuid4, UUID
|
|
11
12
|
|
|
12
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
13
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
13
14
|
from django.db import models, transaction, IntegrityError
|
|
14
15
|
from django.db.models import Q, F, QuerySet, Manager
|
|
15
16
|
from django.utils.text import slugify
|
|
16
17
|
from django.utils.translation import gettext_lazy as _
|
|
17
18
|
|
|
19
|
+
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
18
20
|
from django_ledger.models.mixins import ContactInfoMixIn, CreateUpdateMixIn, TaxCollectionMixIn
|
|
19
21
|
from django_ledger.models.utils import lazy_loader
|
|
20
|
-
from django_ledger.settings import
|
|
22
|
+
from django_ledger.settings import (
|
|
23
|
+
DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING,
|
|
24
|
+
DJANGO_LEDGER_CUSTOMER_NUMBER_PREFIX,
|
|
25
|
+
DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
26
|
+
)
|
|
21
27
|
|
|
22
28
|
|
|
23
29
|
def customer_picture_upload_to(instance, filename):
|
|
@@ -32,6 +38,10 @@ def customer_picture_upload_to(instance, filename):
|
|
|
32
38
|
return f'customer_pictures/{customer_number}/{safe_name}{ext.lower()}'
|
|
33
39
|
|
|
34
40
|
|
|
41
|
+
class CustomerModelValidationError(ValidationError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
35
45
|
class CustomerModelQueryset(QuerySet):
|
|
36
46
|
"""
|
|
37
47
|
A custom defined QuerySet for the CustomerModel. This implements multiple methods or queries needed to get a
|
|
@@ -40,6 +50,27 @@ class CustomerModelQueryset(QuerySet):
|
|
|
40
50
|
reports.
|
|
41
51
|
"""
|
|
42
52
|
|
|
53
|
+
def for_user(self, user_model):
|
|
54
|
+
"""
|
|
55
|
+
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
56
|
+
May include BillModels from multiple Entities.
|
|
57
|
+
|
|
58
|
+
The user has access to bills if:
|
|
59
|
+
1. Is listed as Manager of Entity.
|
|
60
|
+
2. Is the Admin of the Entity.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
__________
|
|
64
|
+
user_model
|
|
65
|
+
Logged in and authenticated django UserModel instance.
|
|
66
|
+
"""
|
|
67
|
+
if user_model.is_superuser:
|
|
68
|
+
return self
|
|
69
|
+
return self.filter(
|
|
70
|
+
Q(entity_model__admin=user_model) |
|
|
71
|
+
Q(entity_model__managers__in=[user_model])
|
|
72
|
+
)
|
|
73
|
+
|
|
43
74
|
def active(self) -> QuerySet:
|
|
44
75
|
"""
|
|
45
76
|
Active customers can be assigned to new Invoices and show on dropdown menus and views.
|
|
@@ -93,59 +124,49 @@ class CustomerModelQueryset(QuerySet):
|
|
|
93
124
|
|
|
94
125
|
class CustomerModelManager(Manager):
|
|
95
126
|
"""
|
|
96
|
-
A custom
|
|
127
|
+
A custom-defined CustomerModelManager that will act as an interface to handling the DB queries to the
|
|
97
128
|
CustomerModel.
|
|
98
129
|
"""
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Fetches a QuerySet of BillModels that the UserModel as access to.
|
|
103
|
-
May include BillModels from multiple Entities.
|
|
104
|
-
|
|
105
|
-
The user has access to bills if:
|
|
106
|
-
1. Is listed as Manager of Entity.
|
|
107
|
-
2. Is the Admin of the Entity.
|
|
108
|
-
|
|
109
|
-
Parameters
|
|
110
|
-
__________
|
|
111
|
-
user_model
|
|
112
|
-
Logged in and authenticated django UserModel instance.
|
|
113
|
-
"""
|
|
114
|
-
qs = self.get_queryset()
|
|
115
|
-
if user_model.is_superuser:
|
|
116
|
-
return qs
|
|
117
|
-
return qs.filter(
|
|
118
|
-
Q(entity_model__admin=user_model) |
|
|
119
|
-
Q(entity_model__managers__in=[user_model])
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
def for_entity(self, entity_slug, user_model) -> CustomerModelQueryset:
|
|
131
|
+
@deprecated_entity_slug_behavior
|
|
132
|
+
def for_entity(self, entity_model: 'EntityModel | str | UUID' = None, **kwargs) -> CustomerModelQueryset:
|
|
123
133
|
"""
|
|
124
134
|
Fetches a QuerySet of CustomerModel associated with a specific EntityModel & UserModel.
|
|
125
135
|
May pass an instance of EntityModel or a String representing the EntityModel slug.
|
|
126
136
|
|
|
127
137
|
Parameters
|
|
128
138
|
__________
|
|
129
|
-
|
|
139
|
+
entity_model: str or EntityModel
|
|
130
140
|
The entity slug or EntityModel used for filtering the QuerySet.
|
|
131
|
-
user_model
|
|
132
|
-
Logged in and authenticated django UserModel instance.
|
|
133
|
-
|
|
134
141
|
|
|
135
142
|
Returns
|
|
136
143
|
-------
|
|
137
144
|
CustomerModelQueryset
|
|
138
145
|
A filtered CustomerModel QuerySet.
|
|
139
146
|
"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
EntityModel = lazy_loader.get_entity_model()
|
|
148
|
+
qs = self.get_queryset()
|
|
149
|
+
if 'user_model' in kwargs:
|
|
150
|
+
warnings.warn(
|
|
151
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
152
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
153
|
+
DeprecationWarning,
|
|
154
|
+
stacklevel=2
|
|
145
155
|
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
157
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
158
|
+
|
|
159
|
+
if isinstance(entity_model, EntityModel):
|
|
160
|
+
qs = qs.filter(entity_model=entity_model)
|
|
161
|
+
elif isinstance(entity_model, str):
|
|
162
|
+
qs = qs.filter(entity_model__slug__exact=entity_model)
|
|
163
|
+
elif isinstance(entity_model, UUID):
|
|
164
|
+
qs = qs.filter(entity_model_id=entity_model)
|
|
165
|
+
else:
|
|
166
|
+
raise CustomerModelValidationError(
|
|
167
|
+
message='Must pass EntityModel, slug or EntityModel UUID',
|
|
168
|
+
)
|
|
169
|
+
return qs
|
|
149
170
|
|
|
150
171
|
|
|
151
172
|
class CustomerModelAbstract(ContactInfoMixIn, TaxCollectionMixIn, CreateUpdateMixIn):
|
|
@@ -10,7 +10,7 @@ application. It introduces two primary models to facilitate the import and proce
|
|
|
10
10
|
or further processing.
|
|
11
11
|
|
|
12
12
|
"""
|
|
13
|
-
|
|
13
|
+
import warnings
|
|
14
14
|
from decimal import Decimal
|
|
15
15
|
from typing import Optional, Set, Dict, List, Union
|
|
16
16
|
from uuid import uuid4, UUID
|
|
@@ -23,10 +23,11 @@ from django.db.models.signals import pre_save
|
|
|
23
23
|
from django.utils.translation import gettext_lazy as _
|
|
24
24
|
|
|
25
25
|
from django_ledger.io import ASSET_CA_CASH, CREDIT, DEBIT
|
|
26
|
-
from django_ledger.models import JournalEntryModel
|
|
26
|
+
from django_ledger.models import JournalEntryModel, deprecated_entity_slug_behavior
|
|
27
27
|
from django_ledger.models.entity import EntityModel
|
|
28
28
|
from django_ledger.models.mixins import CreateUpdateMixIn
|
|
29
29
|
from django_ledger.models.utils import lazy_loader
|
|
30
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class ImportJobModelValidationError(ValidationError):
|
|
@@ -34,7 +35,34 @@ class ImportJobModelValidationError(ValidationError):
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class ImportJobModelQuerySet(QuerySet):
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
def for_user(self, user_model) -> 'ImportJobModelQuerySet':
|
|
40
|
+
"""
|
|
41
|
+
Filters the queryset based on the user's permissions for accessing the data
|
|
42
|
+
related to bank accounts and entities they manage or administer.
|
|
43
|
+
|
|
44
|
+
This method first retrieves the default queryset. If the user is a superuser,
|
|
45
|
+
the query will return the full queryset without any filters. Otherwise, the
|
|
46
|
+
query will be limited to the entities that the user either administers or is
|
|
47
|
+
listed as a manager for.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
user_model : User
|
|
52
|
+
The user model instance whose permissions determine the filtering of the queryset.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
QuerySet
|
|
57
|
+
A filtered queryset based on the user's role and associated permissions.
|
|
58
|
+
"""
|
|
59
|
+
if user_model.is_superuser:
|
|
60
|
+
return self
|
|
61
|
+
return self.filter(
|
|
62
|
+
Q(bank_account_model__entity_model__admin=user_model) |
|
|
63
|
+
Q(bank_account_model__entity_model__managers__in=[user_model])
|
|
64
|
+
|
|
65
|
+
)
|
|
38
66
|
|
|
39
67
|
|
|
40
68
|
class ImportJobModelManager(Manager):
|
|
@@ -49,7 +77,7 @@ class ImportJobModelManager(Manager):
|
|
|
49
77
|
|
|
50
78
|
"""
|
|
51
79
|
|
|
52
|
-
def get_queryset(self):
|
|
80
|
+
def get_queryset(self) -> ImportJobModelQuerySet:
|
|
53
81
|
"""
|
|
54
82
|
Generates a QuerySet with annotated data for ImportJobModel.
|
|
55
83
|
|
|
@@ -100,44 +128,30 @@ class ImportJobModelManager(Manager):
|
|
|
100
128
|
'ledger_model'
|
|
101
129
|
)
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Filters the queryset based on the user's permissions for accessing the data
|
|
106
|
-
related to bank accounts and entities they manage or administer.
|
|
107
|
-
|
|
108
|
-
This method first retrieves the default queryset. If the user is a superuser,
|
|
109
|
-
the query will return the full queryset without any filters. Otherwise, the
|
|
110
|
-
query will be limited to the entities that the user either administers or is
|
|
111
|
-
listed as a manager for.
|
|
112
|
-
|
|
113
|
-
Parameters
|
|
114
|
-
----------
|
|
115
|
-
user_model : User
|
|
116
|
-
The user model instance whose permissions determine the filtering of the queryset.
|
|
117
|
-
|
|
118
|
-
Returns
|
|
119
|
-
-------
|
|
120
|
-
QuerySet
|
|
121
|
-
A filtered queryset based on the user's role and associated permissions.
|
|
122
|
-
"""
|
|
131
|
+
@deprecated_entity_slug_behavior
|
|
132
|
+
def for_entity(self, entity_model: Union[EntityModel, str, UUID] = None, **kwargs) -> ImportJobModelQuerySet:
|
|
123
133
|
qs = self.get_queryset()
|
|
124
|
-
if user_model
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if isinstance(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
if 'user_model' in kwargs:
|
|
135
|
+
warnings.warn(
|
|
136
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
137
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
138
|
+
DeprecationWarning,
|
|
139
|
+
stacklevel=2
|
|
140
|
+
)
|
|
141
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
142
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
143
|
+
|
|
144
|
+
if isinstance(entity_model, EntityModel):
|
|
145
|
+
qs = qs.filter(bank_account_model__entity_model=entity_model)
|
|
146
|
+
elif isinstance(entity_model, UUID):
|
|
147
|
+
qs = qs.filter(bank_account_model__entity_model_id=entity_model)
|
|
148
|
+
elif isinstance(entity_model, str):
|
|
149
|
+
qs = qs.filter(bank_account_model__slug__exact=entity_model)
|
|
150
|
+
else:
|
|
151
|
+
raise ImportJobModelValidationError(
|
|
152
|
+
message=_('Must pass EntityModel, slug or UUID'),
|
|
153
|
+
)
|
|
154
|
+
return qs
|
|
141
155
|
|
|
142
156
|
|
|
143
157
|
class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
5
|
+
|
|
6
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
7
|
+
warnings.warn(
|
|
8
|
+
message=(
|
|
9
|
+
'You are using the deprecated behavior of django_ledger. '
|
|
10
|
+
'Set DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR = False to transition to new API.'
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def deprecated_entity_slug_behavior(func=None, *, message=None):
|
|
16
|
+
"""
|
|
17
|
+
Decorator for for_entity(...) methods to warn about the deprecated `entity_slug` argument
|
|
18
|
+
and optionally map it to `entity_model` for backward compatibility.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
@deprecated_for_entity_behavior
|
|
22
|
+
def for_entity(self, entity_model=None, **kwargs):
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
or with custom message:
|
|
26
|
+
@deprecated_for_entity_behavior(message="custom deprecation message")
|
|
27
|
+
def for_entity(...):
|
|
28
|
+
...
|
|
29
|
+
"""
|
|
30
|
+
default_message = (
|
|
31
|
+
'entity_slug parameter is deprecated and will be removed in a future release. '
|
|
32
|
+
'Use entity_model instead (accepts EntityModel instance, UUID, or slug string).'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if func is None:
|
|
36
|
+
# Called as @deprecated_for_entity_behavior(...)
|
|
37
|
+
return lambda f: deprecated_entity_slug_behavior(f, message=message)
|
|
38
|
+
|
|
39
|
+
@wraps(func)
|
|
40
|
+
def wrapper(*args, **kwargs):
|
|
41
|
+
if 'entity_slug' in kwargs and kwargs.get('entity_slug') is not None:
|
|
42
|
+
warnings.warn(message or default_message, DeprecationWarning, stacklevel=2)
|
|
43
|
+
|
|
44
|
+
# If both are provided, fail fast to avoid ambiguity
|
|
45
|
+
if kwargs.get('entity_model') is not None:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
'Cannot specify both `entity_model` and `entity_slug`. '
|
|
48
|
+
'entity_slug is deprecated; pass only entity_model.'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Maintain legacy behavior when enabled by settings toggle
|
|
52
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
53
|
+
kwargs['entity_model'] = kwargs.pop('entity_slug')
|
|
54
|
+
else:
|
|
55
|
+
# If deprecated behavior is disabled, drop the deprecated param
|
|
56
|
+
# and rely on the method's own validation (likely to fail fast).
|
|
57
|
+
kwargs.pop('entity_slug', None)
|
|
58
|
+
|
|
59
|
+
return func(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
return wrapper
|
django_ledger/models/entity.py
CHANGED
|
@@ -24,7 +24,7 @@ from decimal import Decimal
|
|
|
24
24
|
from itertools import zip_longest
|
|
25
25
|
from random import choices
|
|
26
26
|
from string import ascii_lowercase, digits
|
|
27
|
-
from typing import Tuple, Union, Optional, List, Dict, Set
|
|
27
|
+
from typing import Tuple, Union, Optional, List, Dict, Set, Self
|
|
28
28
|
from uuid import uuid4, UUID
|
|
29
29
|
|
|
30
30
|
from django.contrib.auth import get_user_model
|
|
@@ -71,7 +71,7 @@ class EntityModelQuerySet(MP_NodeQuerySet):
|
|
|
71
71
|
Inherits from the Materialized Path Node QuerySet Class from Django Treebeard.
|
|
72
72
|
"""
|
|
73
73
|
|
|
74
|
-
def hidden(self):
|
|
74
|
+
def hidden(self) -> 'EntityModelQuerySet':
|
|
75
75
|
"""
|
|
76
76
|
A QuerySet of all hidden EntityModel.
|
|
77
77
|
|
|
@@ -82,7 +82,7 @@ class EntityModelQuerySet(MP_NodeQuerySet):
|
|
|
82
82
|
"""
|
|
83
83
|
return self.filter(hidden=True)
|
|
84
84
|
|
|
85
|
-
def visible(self):
|
|
85
|
+
def visible(self) -> 'EntityModelQuerySet':
|
|
86
86
|
"""
|
|
87
87
|
A Queryset of all visible EntityModel.
|
|
88
88
|
|
|
@@ -108,7 +108,7 @@ class EntityModelManager(MP_NodeManager):
|
|
|
108
108
|
|
|
109
109
|
"""
|
|
110
110
|
|
|
111
|
-
def get_queryset(self):
|
|
111
|
+
def get_queryset(self) -> EntityModelQuerySet:
|
|
112
112
|
"""Sets the custom queryset as the default."""
|
|
113
113
|
qs = EntityModelQuerySet(
|
|
114
114
|
self.model,
|
|
@@ -441,12 +441,13 @@ class EntityModelClosingEntryMixIn:
|
|
|
441
441
|
)
|
|
442
442
|
|
|
443
443
|
# ---> Closing Entry IO Digest <---
|
|
444
|
-
def get_closing_entry_digest(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
444
|
+
def get_closing_entry_digest(
|
|
445
|
+
self,
|
|
446
|
+
to_date: date,
|
|
447
|
+
from_date: Optional[date] = None,
|
|
448
|
+
user_model: Optional[UserModel] = None,
|
|
449
|
+
closing_entry_model=None,
|
|
450
|
+
**kwargs: Dict) -> Tuple:
|
|
450
451
|
ClosingEntryModel = lazy_loader.get_closing_entry_model()
|
|
451
452
|
ClosingEntryTransactionModel = lazy_loader.get_closing_entry_transaction_model()
|
|
452
453
|
|
|
@@ -512,8 +513,8 @@ class EntityModelClosingEntryMixIn:
|
|
|
512
513
|
def get_closing_entry_queryset_for_date(self, closing_date: date):
|
|
513
514
|
ClosingEntryTransactionModel = lazy_loader.get_closing_entry_transaction_model()
|
|
514
515
|
return ClosingEntryTransactionModel.objects.for_entity(
|
|
515
|
-
|
|
516
|
-
).
|
|
516
|
+
entity_model=self,
|
|
517
|
+
).for_closing_date(closing_date)
|
|
517
518
|
|
|
518
519
|
def get_closing_entry_queryset_for_month(self, year: int, month: int):
|
|
519
520
|
_, end_day = monthrange(year, month)
|
|
@@ -538,7 +539,7 @@ class EntityModelClosingEntryMixIn:
|
|
|
538
539
|
message=_(f'Cannot create closing entry with a future date {closing_date}.')
|
|
539
540
|
)
|
|
540
541
|
|
|
541
|
-
if closing_entry_model is None
|
|
542
|
+
if closing_entry_model is None:
|
|
542
543
|
self.closingentrymodel_set.filter(closing_date__exact=closing_date).delete()
|
|
543
544
|
else:
|
|
544
545
|
closing_entry_model.closingentrytransactionmodel_set.all().delete()
|
|
@@ -787,7 +788,6 @@ class EntityModelAbstract(MP_Node,
|
|
|
787
788
|
meta = models.JSONField(default=dict, null=True, blank=True)
|
|
788
789
|
objects = EntityModelManager.from_queryset(queryset_class=EntityModelQuerySet)()
|
|
789
790
|
|
|
790
|
-
|
|
791
791
|
class Meta:
|
|
792
792
|
abstract = True
|
|
793
793
|
ordering = ['-created']
|
|
@@ -1547,7 +1547,7 @@ class EntityModelAbstract(MP_Node,
|
|
|
1547
1547
|
return qs
|
|
1548
1548
|
|
|
1549
1549
|
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
1550
|
-
qs = JournalEntryModel.objects.for_entity(
|
|
1550
|
+
qs = JournalEntryModel.objects.for_entity(entity_model=self)
|
|
1551
1551
|
if posted:
|
|
1552
1552
|
return qs.posted()
|
|
1553
1553
|
return qs
|
|
@@ -2603,7 +2603,9 @@ class EntityModelAbstract(MP_Node,
|
|
|
2603
2603
|
ItemTransactionModel = lazy_loader.get_item_transaction_model()
|
|
2604
2604
|
ItemModel = lazy_loader.get_item_model()
|
|
2605
2605
|
|
|
2606
|
-
counted_qs: ItemTransactionModelQuerySet = ItemTransactionModel.objects.inventory_count(
|
|
2606
|
+
counted_qs: ItemTransactionModelQuerySet = ItemTransactionModel.objects.inventory_count(
|
|
2607
|
+
entity_model=self.slug
|
|
2608
|
+
)
|
|
2607
2609
|
recorded_qs: ItemModelQuerySet = self.recorded_inventory(as_values=False)
|
|
2608
2610
|
recorded_qs_values = self.recorded_inventory(item_qs=recorded_qs, as_values=True)
|
|
2609
2611
|
|