django-ledger 0.7.10__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.

Files changed (115) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/context.py +12 -0
  3. django_ledger/forms/bill.py +0 -4
  4. django_ledger/forms/closing_entry.py +13 -1
  5. django_ledger/forms/data_import.py +1 -1
  6. django_ledger/forms/estimate.py +3 -6
  7. django_ledger/forms/invoice.py +3 -7
  8. django_ledger/forms/item.py +10 -18
  9. django_ledger/forms/purchase_order.py +2 -4
  10. django_ledger/io/io_core.py +25 -32
  11. django_ledger/io/io_generator.py +7 -6
  12. django_ledger/io/io_library.py +1 -2
  13. django_ledger/migrations/0024_billmodel_entity_model_invoicemodel_entity_model.py +24 -0
  14. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  15. django_ledger/models/accounts.py +109 -69
  16. django_ledger/models/bank_account.py +40 -23
  17. django_ledger/models/bill.py +89 -63
  18. django_ledger/models/chart_of_accounts.py +173 -105
  19. django_ledger/models/closing_entry.py +99 -48
  20. django_ledger/models/customer.py +60 -39
  21. django_ledger/models/data_import.py +55 -41
  22. django_ledger/models/deprecations.py +61 -0
  23. django_ledger/models/entity.py +18 -16
  24. django_ledger/models/estimate.py +57 -28
  25. django_ledger/models/invoice.py +58 -28
  26. django_ledger/models/items.py +503 -142
  27. django_ledger/models/journal_entry.py +61 -47
  28. django_ledger/models/ledger.py +106 -42
  29. django_ledger/models/mixins.py +16 -10
  30. django_ledger/models/purchase_order.py +39 -17
  31. django_ledger/models/transactions.py +152 -113
  32. django_ledger/models/unit.py +57 -30
  33. django_ledger/models/vendor.py +75 -43
  34. django_ledger/report/core.py +2 -14
  35. django_ledger/settings.py +56 -71
  36. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  37. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  38. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  39. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  40. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  41. django_ledger/templates/django_ledger/components/menu.html +41 -26
  42. django_ledger/templates/django_ledger/customer/tags/customer_table.html +5 -5
  43. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  44. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  45. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  46. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  47. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  48. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  49. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  50. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  51. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  52. django_ledger/templates/django_ledger/layouts/base.html +6 -1
  53. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +9 -5
  54. django_ledger/tests/test_accounts.py +1 -2
  55. django_ledger/tests/test_io.py +17 -0
  56. django_ledger/tests/test_purchase_order.py +3 -3
  57. django_ledger/tests/test_transactions.py +1 -2
  58. django_ledger/urls/__init__.py +0 -4
  59. django_ledger/views/bill.py +8 -13
  60. django_ledger/views/chart_of_accounts.py +6 -4
  61. django_ledger/views/closing_entry.py +11 -7
  62. django_ledger/views/customer.py +13 -17
  63. django_ledger/views/data_import.py +7 -6
  64. django_ledger/views/djl_api.py +3 -5
  65. django_ledger/views/entity.py +2 -4
  66. django_ledger/views/estimate.py +3 -7
  67. django_ledger/views/inventory.py +3 -5
  68. django_ledger/views/invoice.py +4 -6
  69. django_ledger/views/item.py +7 -11
  70. django_ledger/views/journal_entry.py +1 -2
  71. django_ledger/views/mixins.py +25 -19
  72. django_ledger/views/purchase_order.py +24 -35
  73. django_ledger/views/unit.py +1 -2
  74. django_ledger/views/vendor.py +1 -2
  75. {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info}/METADATA +43 -75
  76. {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info}/RECORD +80 -108
  77. {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info}/WHEEL +1 -1
  78. django_ledger-0.8.0.dist-info/top_level.txt +1 -0
  79. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  80. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  81. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  82. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  83. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  84. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  85. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  86. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  87. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  88. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  89. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  90. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  91. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  92. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  93. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  94. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  95. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  96. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  97. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  98. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  99. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  100. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  101. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  102. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  103. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  104. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  105. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  106. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  107. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  108. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  109. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  110. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  111. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  112. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  113. django_ledger-0.7.10.dist-info/top_level.txt +0 -4
  114. {django_ledger-0.7.10.dist-info → django_ledger-0.8.0.dist-info/licenses}/AUTHORS.md +0 -0
  115. {django_ledger-0.7.10.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.models.utils import lazy_loader
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(models.QuerySet):
33
+ class ClosingEntryModelQuerySet(QuerySet):
33
34
 
34
- def posted(self):
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(models.Manager):
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
- def for_user(self, user_model):
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
- def for_entity(self, entity_slug, user_model):
59
- qs = self.for_user(user_model)
60
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
61
- return qs.filter(
62
- Q(entity_model=entity_slug)
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
- return qs.filter(
65
- Q(entity_model__slug__exact=entity_slug)
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
- entity_slug=self.entity_model_id
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
- entity_slug=self.entity_model_id
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
- class ClosingEntryTransactionModelQuerySet(models.QuerySet):
365
- pass
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
- class ClosingEntryTransactionModelManager(models.Manager):
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
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
373
- return qs.filter(closing_entry_model__entity_model=entity_slug)
374
- elif isinstance(entity_slug, UUID):
375
- return qs.filter(closing_entry_model__entity_model__uuid__exact=entity_slug)
376
- return qs.filter(closing_entry_model__entity_model__slug__exact=entity_slug)
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) -> Optional[bool]:
457
- if self.tx_type is not None:
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) -> Optional[bool]:
461
- if self.tx_type is not None:
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
 
@@ -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
- from uuid import uuid4
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 DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_CUSTOMER_NUMBER_PREFIX
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 defined CustomerModelManager that will act as an interface to handling the DB queries to the
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
- def for_user(self, user_model):
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
- entity_slug: str or EntityModel
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
- qs = self.for_user(user_model)
141
-
142
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
143
- return qs.filter(
144
- Q(entity_model=entity_slug)
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
- return qs.filter(
147
- Q(entity_model__slug__exact=entity_slug)
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
- pass
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
- def for_user(self, user_model):
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.is_superuser:
125
- return qs
126
- return qs.filter(
127
- Q(bank_account_model__entity_model__admin=user_model) |
128
- Q(bank_account_model__entity_model__managers__in=[user_model])
129
-
130
- )
131
-
132
- def for_entity(self, entity_slug: Union[EntityModel, str], user_model):
133
- qs = self.for_user(user_model)
134
- if isinstance(entity_slug, EntityModel):
135
- return qs.filter(
136
- Q(bank_account_model__entity_model=entity_slug)
137
- )
138
- return qs.filter(
139
- Q(bank_account_model__entity_model__slug__exact=entity_slug)
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
@@ -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(self,
445
- to_date: date,
446
- from_date: Optional[date] = None,
447
- user_model: Optional[UserModel] = None,
448
- closing_entry_model=None,
449
- **kwargs: Dict) -> Tuple:
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
- entity_slug=self,
516
- ).filter(closing_entry_model__closing_date__exact=closing_date)
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 or closing_entry_exists:
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(entity_slug=self)
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(entity_slug=self.slug)
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