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.

Files changed (114) 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 +8 -26
  11. django_ledger/io/io_generator.py +7 -6
  12. django_ledger/io/io_library.py +1 -2
  13. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  14. django_ledger/models/accounts.py +109 -69
  15. django_ledger/models/bank_account.py +40 -23
  16. django_ledger/models/bill.py +79 -63
  17. django_ledger/models/chart_of_accounts.py +173 -105
  18. django_ledger/models/closing_entry.py +99 -48
  19. django_ledger/models/customer.py +60 -39
  20. django_ledger/models/data_import.py +55 -41
  21. django_ledger/models/deprecations.py +61 -0
  22. django_ledger/models/entity.py +18 -16
  23. django_ledger/models/estimate.py +57 -28
  24. django_ledger/models/invoice.py +46 -26
  25. django_ledger/models/items.py +503 -142
  26. django_ledger/models/journal_entry.py +61 -47
  27. django_ledger/models/ledger.py +106 -42
  28. django_ledger/models/mixins.py +5 -3
  29. django_ledger/models/purchase_order.py +39 -17
  30. django_ledger/models/transactions.py +152 -113
  31. django_ledger/models/unit.py +57 -30
  32. django_ledger/models/vendor.py +75 -43
  33. django_ledger/report/core.py +2 -14
  34. django_ledger/settings.py +56 -71
  35. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  36. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  37. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  38. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  39. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  40. django_ledger/templates/django_ledger/components/menu.html +41 -26
  41. django_ledger/templates/django_ledger/customer/tags/customer_table.html +5 -5
  42. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  43. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  44. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  45. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  46. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  47. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  48. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  49. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  50. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  51. django_ledger/templates/django_ledger/layouts/base.html +6 -1
  52. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +9 -5
  53. django_ledger/tests/test_accounts.py +1 -2
  54. django_ledger/tests/test_io.py +17 -0
  55. django_ledger/tests/test_purchase_order.py +3 -3
  56. django_ledger/tests/test_transactions.py +1 -2
  57. django_ledger/urls/__init__.py +0 -4
  58. django_ledger/views/bill.py +8 -11
  59. django_ledger/views/chart_of_accounts.py +6 -4
  60. django_ledger/views/closing_entry.py +11 -7
  61. django_ledger/views/customer.py +13 -17
  62. django_ledger/views/data_import.py +7 -6
  63. django_ledger/views/djl_api.py +3 -5
  64. django_ledger/views/entity.py +2 -4
  65. django_ledger/views/estimate.py +3 -7
  66. django_ledger/views/inventory.py +3 -5
  67. django_ledger/views/invoice.py +4 -6
  68. django_ledger/views/item.py +7 -11
  69. django_ledger/views/journal_entry.py +1 -2
  70. django_ledger/views/mixins.py +25 -19
  71. django_ledger/views/purchase_order.py +24 -35
  72. django_ledger/views/unit.py +1 -2
  73. django_ledger/views/vendor.py +1 -2
  74. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/METADATA +43 -75
  75. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/RECORD +79 -108
  76. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info}/WHEEL +1 -1
  77. django_ledger-0.8.0.dist-info/top_level.txt +1 -0
  78. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  79. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  80. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  81. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  82. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  83. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  84. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  85. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  86. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  87. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  88. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  89. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  90. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  91. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  92. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  93. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  94. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  95. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  96. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  97. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  98. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  99. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  100. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  101. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  102. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  103. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  104. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  105. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  106. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  107. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  108. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  109. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  110. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  111. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  112. django_ledger-0.7.11.dist-info/top_level.txt +0 -4
  113. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info/licenses}/AUTHORS.md +0 -0
  114. {django_ledger-0.7.11.dist-info → django_ledger-0.8.0.dist-info/licenses}/LICENSE +0 -0
@@ -25,6 +25,7 @@ The JournalEntryModel is also responsible for validating the Financial Activity
25
25
  business. Whenever an account with ASSET_CA_CASH role is involved in a Journal Entry (see roles for more details), the
26
26
  JE is responsible for programmatically determine the kind of operation for the JE (Operating, Financing, Investing).
27
27
  """
28
+ import warnings
28
29
  from datetime import date, datetime
29
30
  from decimal import Decimal
30
31
  from enum import Enum
@@ -41,6 +42,7 @@ from django.urls import reverse
41
42
  from django.utils.timezone import localtime
42
43
  from django.utils.translation import gettext_lazy as _
43
44
 
45
+ from django_ledger.io import roles
44
46
  from django_ledger.io.io_core import get_localtime
45
47
  from django_ledger.io.roles import (
46
48
  ASSET_CA_CASH, GROUP_CFS_FIN_DIVIDENDS, GROUP_CFS_FIN_ISSUING_EQUITY,
@@ -50,6 +52,7 @@ from django_ledger.io.roles import (
50
52
  validate_roles
51
53
  )
52
54
  from django_ledger.models.accounts import CREDIT, DEBIT
55
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
53
56
  from django_ledger.models.entity import EntityStateModel, EntityModel
54
57
  from django_ledger.models.ledger import LedgerModel
55
58
  from django_ledger.models.mixins import CreateUpdateMixIn
@@ -59,13 +62,12 @@ from django_ledger.models.signals import (
59
62
  journal_entry_posted,
60
63
  journal_entry_unposted
61
64
  )
62
- from django_ledger.models.transactions import TransactionModelQuerySet, TransactionModel
65
+ from django_ledger.models.transactions import TransactionModelQuerySet
63
66
  from django_ledger.settings import (
64
67
  DJANGO_LEDGER_JE_NUMBER_PREFIX,
65
68
  DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING,
66
- DJANGO_LEDGER_JE_NUMBER_NO_UNIT_PREFIX
69
+ DJANGO_LEDGER_JE_NUMBER_NO_UNIT_PREFIX, DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
67
70
  )
68
- from django_ledger.io import roles
69
71
 
70
72
 
71
73
  class JournalEntryValidationError(ValidationError):
@@ -82,7 +84,33 @@ class JournalEntryModelQuerySet(QuerySet):
82
84
  locked entries, and querying entries associated with specific ledgers.
83
85
  """
84
86
 
85
- def create(self, verify_on_save: bool = False, force_create: bool = False, **kwargs):
87
+ def for_user(self, user_model) -> 'JournalEntryModelQuerySet':
88
+ """
89
+ Filters the JournalEntryModel queryset for the given user.
90
+
91
+ - Superusers will have access to all journal entries.
92
+ - Other authenticated users will only see entries for entities where
93
+ they are admins or managers.
94
+
95
+ Parameters
96
+ ----------
97
+ user_model : UserModel
98
+ An authenticated Django user object.
99
+
100
+ Returns
101
+ -------
102
+ JournalEntryModelQuerySet
103
+ A filtered queryset restricted by the user's entity relationships.
104
+ """
105
+ if user_model.is_superuser:
106
+ return self
107
+
108
+ return self.filter(
109
+ Q(ledger__entity__admin=user_model) | # Entries for entities where the user is admin
110
+ Q(ledger__entity__managers__in=[user_model]) # Entries for entities where the user is a manager
111
+ )
112
+
113
+ def create(self, verify_on_save: bool = False, force_create: bool = False, **kwargs) -> 'JournalEntryModelQuerySet':
86
114
  """
87
115
  Creates a new Journal Entry while enforcing business logic validations.
88
116
 
@@ -124,7 +152,7 @@ class JournalEntryModelQuerySet(QuerySet):
124
152
  obj.save(force_insert=True, using=self.db, verify=verify_on_save)
125
153
  return obj
126
154
 
127
- def posted(self):
155
+ def posted(self) -> 'JournalEntryModelQuerySet':
128
156
  """
129
157
  Filters the QuerySet to include only "posted" Journal Entries.
130
158
 
@@ -135,7 +163,7 @@ class JournalEntryModelQuerySet(QuerySet):
135
163
  """
136
164
  return self.filter(posted=True)
137
165
 
138
- def unposted(self):
166
+ def unposted(self) -> 'JournalEntryModelQuerySet':
139
167
  """
140
168
  Filters the QuerySet to include only "unposted" Journal Entries.
141
169
 
@@ -146,7 +174,7 @@ class JournalEntryModelQuerySet(QuerySet):
146
174
  """
147
175
  return self.filter(posted=False)
148
176
 
149
- def locked(self):
177
+ def locked(self) -> 'JournalEntryModelQuerySet':
150
178
  """
151
179
  Filters the QuerySet to include only "locked" Journal Entries.
152
180
 
@@ -157,7 +185,7 @@ class JournalEntryModelQuerySet(QuerySet):
157
185
  """
158
186
  return self.filter(locked=True)
159
187
 
160
- def unlocked(self):
188
+ def unlocked(self) -> 'JournalEntryModelQuerySet':
161
189
  """
162
190
  Filters the QuerySet to include only "unlocked" Journal Entries.
163
191
 
@@ -168,7 +196,7 @@ class JournalEntryModelQuerySet(QuerySet):
168
196
  """
169
197
  return self.filter(locked=False)
170
198
 
171
- def for_ledger(self, ledger_pk: Union[str, UUID, LedgerModel]):
199
+ def for_ledger(self, ledger_pk: Union[str, UUID, LedgerModel]) -> 'JournalEntryModelQuerySet':
172
200
  """
173
201
  Filters the QuerySet to include Journal Entries associated with a specific Ledger.
174
202
 
@@ -223,34 +251,8 @@ class JournalEntryModelManager(Manager):
223
251
  txs_count=Count('transactionmodel') # Annotates the count of transactions
224
252
  )
225
253
 
226
- def for_user(self, user_model) -> JournalEntryModelQuerySet:
227
- """
228
- Filters the JournalEntryModel queryset for the given user.
229
-
230
- - Superusers will have access to all journal entries.
231
- - Other authenticated users will only see entries for entities where
232
- they are admins or managers.
233
-
234
- Parameters
235
- ----------
236
- user_model : UserModel
237
- An authenticated Django user object.
238
-
239
- Returns
240
- -------
241
- JournalEntryModelQuerySet
242
- A filtered queryset restricted by the user's entity relationships.
243
- """
244
- qs = self.get_queryset()
245
- if user_model.is_superuser:
246
- return qs
247
-
248
- return qs.filter(
249
- Q(ledger__entity__admin=user_model) | # Entries for entities where the user is admin
250
- Q(ledger__entity__managers__in=[user_model]) # Entries for entities where the user is a manager
251
- )
252
-
253
- def for_entity(self, entity_slug: Union[str, EntityModel], user_model) -> JournalEntryModelQuerySet:
254
+ @deprecated_entity_slug_behavior
255
+ def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> JournalEntryModelQuerySet:
254
256
  """
255
257
  Filters the JournalEntryModel queryset for a specific entity and user.
256
258
 
@@ -260,24 +262,36 @@ class JournalEntryModelManager(Manager):
260
262
 
261
263
  Parameters
262
264
  ----------
263
- entity_slug : str or EntityModel
265
+ entity_model : str or EntityModel
264
266
  The slug of the entity (or an instance of `EntityModel`) used for filtering.
265
- user_model : UserModel
266
- An authenticated Django user object.
267
-
268
267
  Returns
269
268
  -------
270
269
  JournalEntryModelQuerySet
271
270
  A customized queryset containing journal entries associated with the
272
271
  given entity and restricted by the user's access permissions.
273
272
  """
274
- qs = self.for_user(user_model)
275
-
276
- # Handle the `entity_slug` as either a string or an EntityModel instance
277
- if isinstance(entity_slug, EntityModel):
278
- return qs.filter(ledger__entity=entity_slug)
273
+ qs = self.get_queryset()
274
+ if 'user_model' in kwargs:
275
+ warnings.warn(
276
+ 'user_model parameter is deprecated and will be removed in a future release. '
277
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
278
+ DeprecationWarning,
279
+ stacklevel=2
280
+ )
281
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
282
+ qs = qs.for_user(kwargs['user_model'])
279
283
 
280
- return qs.filter(ledger__entity__slug__iexact=entity_slug) # Case-insensitive slug match
284
+ if isinstance(entity_model, EntityModel):
285
+ qs = qs.filter(ledger__entity=entity_model)
286
+ elif isinstance(entity_model, str):
287
+ qs = qs.filter(ledger__entity__slug__exact=entity_model)
288
+ elif isinstance(entity_model, UUID):
289
+ qs = qs.filter(ledger__entity_id=entity_model)
290
+ else:
291
+ raise JournalEntryValidationError(
292
+ message='Must provide EntityModel, slug or UUID',
293
+ )
294
+ return qs
281
295
 
282
296
 
283
297
  class ActivityEnum(Enum):
@@ -23,20 +23,22 @@ layer as possible in order to minimize the amount of data being pulled for analy
23
23
  The Django Ledger core model follows the following structure:
24
24
  EntityModel -< LedgerModel -< JournalEntryModel -< TransactionModel
25
25
  """
26
+ import warnings
26
27
  from datetime import date
27
28
  from string import ascii_lowercase, digits
28
29
  from typing import Optional
29
- from uuid import uuid4
30
+ from uuid import uuid4, UUID
30
31
 
31
32
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
32
33
  from django.core.serializers.json import DjangoJSONEncoder
33
34
  from django.db import models
34
- from django.db.models import Q, Min, F, Count
35
+ from django.db.models import Q, Min, F, Count, Manager, QuerySet
35
36
  from django.urls import reverse
36
37
  from django.utils.translation import gettext_lazy as _
37
38
 
38
39
  from django_ledger.io.io_core import IOMixIn
39
40
  from django_ledger.models import lazy_loader
41
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
40
42
  from django_ledger.models.mixins import CreateUpdateMixIn
41
43
  from django_ledger.models.signals import (
42
44
  ledger_posted,
@@ -46,6 +48,7 @@ from django_ledger.models.signals import (
46
48
  ledger_hidden,
47
49
  ledger_unhidden
48
50
  )
51
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
49
52
 
50
53
  LEDGER_ID_CHARS = ascii_lowercase + digits
51
54
 
@@ -54,75 +57,130 @@ class LedgerModelValidationError(ValidationError):
54
57
  pass
55
58
 
56
59
 
57
- class LedgerModelQuerySet(models.QuerySet):
60
+ class LedgerModelQuerySet(QuerySet):
58
61
  """
59
- Custom defined LedgerModel QuerySet.
62
+ Custom QuerySet for filtering LedgerModel instances based on specific fields.
63
+
64
+ Provides predefined filtering methods to simplify working with ledger data.
65
+ These filters allow querying for entries that are locked, unlocked, posted,
66
+ unposted, hidden, visible, or current.
67
+
68
+ Methods
69
+ -------
70
+ locked()
71
+ Filters instances based on the 'locked' attribute set to `True`.
72
+ unlocked()
73
+ Filters instances based on the 'locked' attribute set to `False`.
74
+ posted()
75
+ Filters a queryset to include only posted entries with 'posted' set to `True`.
76
+ unposted()
77
+ Filters a queryset to include only unposted entries with 'posted' set to `False`.
78
+ hidden()
79
+ Filters a queryset to include only items marked as hidden.
80
+ visible()
81
+ Filters out hidden items from the queryset.
82
+ current()
83
+ Filters the queryset to include items where the earliest timestamp
84
+ comes after the entity's last closing date or is null.
60
85
  """
61
86
 
62
- def locked(self):
87
+ def for_user(self, user_model) -> 'LedgerModelQuerySet':
88
+ if user_model.is_superuser:
89
+ return self
90
+ return self.filter(
91
+ Q(entity__admin=user_model) |
92
+ Q(entity__managers__in=[user_model])
93
+ )
94
+
95
+ def locked(self) -> 'LedgerModelQuerySet':
63
96
  """
64
- Filters the QuerySet to only locked LedgerModel.
97
+ Filters instances based on the 'locked' attribute.
98
+
99
+ This method returns a new instance where the 'locked' attribute is set to `True`.
65
100
 
66
101
  Returns
67
102
  -------
68
103
  LedgerModelQuerySet
69
- A QuerySet with applied filters.
104
+ A new instance filtered with the attribute 'locked' set to `True`.
70
105
  """
71
106
  return self.filter(locked=True)
72
107
 
73
- def unlocked(self):
108
+ def unlocked(self) -> 'LedgerModelQuerySet':
74
109
  """
75
- Filters the QuerySet to only un-locked LedgerModel.
110
+ Filters and returns an instance where the `locked` attribute is set to `False`.
76
111
 
77
112
  Returns
78
113
  -------
79
114
  LedgerModelQuerySet
80
- A QuerySet with applied filters.
115
+ A new or modified instance of the object with the filter applied.
81
116
  """
82
117
  return self.filter(locked=False)
83
118
 
84
- def posted(self):
119
+ def posted(self) -> 'LedgerModelQuerySet':
85
120
  """
86
- Filters the QuerySet to only posted LedgerModel.
121
+ Filters a queryset to include only posted entries.
87
122
 
88
123
  Returns
89
124
  -------
90
125
  LedgerModelQuerySet
91
- A QuerySet with applied filters.
126
+ A new queryset instance containing only objects with
127
+ the `posted` field set to True.
92
128
  """
93
129
  return self.filter(posted=True)
94
130
 
95
- def unposted(self):
131
+ def unposted(self) -> 'LedgerModelQuerySet':
96
132
  """
97
- Filters the QuerySet to only un-posted LedgerModel.
133
+ Filters a queryset or a similar iterable-like object to include only items that
134
+ have the attribute `posted` set to `True`. This method returns a new instance
135
+ containing the filtered results.
98
136
 
99
137
  Returns
100
138
  -------
101
139
  LedgerModelQuerySet
102
- A QuerySet with applied filters.
140
+ A new instance containing only the filtered results with `posted` set to True.
103
141
  """
104
142
  return self.filter(posted=True)
105
143
 
106
- def hidden(self):
144
+ def hidden(self) -> 'LedgerModelQuerySet':
145
+ """
146
+ Filters the queryset to include only objects marked as hidden.
147
+
148
+ Returns
149
+ -------
150
+ LedgerModelQuerySet
151
+ A filtered queryset containing only objects with the `hidden` attribute
152
+ set to True.
153
+ """
107
154
  return self.filter(hidden=True)
108
155
 
109
- def visible(self):
156
+ def visible(self) -> 'LedgerModelQuerySet':
157
+ """
158
+ Filters out hidden items from the queryset.
159
+
160
+ Ensures that only items with the `hidden` attribute set to `False`
161
+ are included in the returned queryset.
162
+
163
+ Returns
164
+ -------
165
+ LedgerModelQuerySet
166
+ A queryset containing only visible items.
167
+ """
110
168
  return self.filter(hidden=False)
111
169
 
112
- def current(self):
170
+ def current(self) -> 'LedgerModelQuerySet':
113
171
  return self.filter(
114
172
  Q(earliest_timestamp__date__gt=F('entity__last_closing_date'))
115
173
  | Q(earliest_timestamp__isnull=True)
116
174
  )
117
175
 
118
176
 
119
- class LedgerModelManager(models.Manager):
177
+ class LedgerModelManager(Manager):
120
178
  """
121
179
  A custom-defined LedgerModelManager that implements custom QuerySet methods related to the LedgerModel.
122
180
  """
123
181
 
124
- def get_queryset(self):
125
- qs = super().get_queryset()
182
+ def get_queryset(self) -> LedgerModelQuerySet:
183
+ qs = LedgerModelQuerySet(self.model, using=self._db)
126
184
  return qs.select_related('entity').annotate(
127
185
  Count('journal_entries'),
128
186
  _entity_slug=F('entity__slug'),
@@ -130,40 +188,46 @@ class LedgerModelManager(models.Manager):
130
188
  filter=Q(journal_entries__posted=True)),
131
189
  )
132
190
 
133
- def for_user(self, user_model):
134
- qs = self.get_queryset()
135
- if user_model.is_superuser:
136
- return qs
137
- return qs.filter(
138
- Q(entity__admin=user_model) |
139
- Q(entity__managers__in=[user_model])
140
- )
141
-
142
- def for_entity(self, entity_slug, user_model):
191
+ @deprecated_entity_slug_behavior
192
+ def for_entity(self, entity_model: 'EntityModel | str | UUID' = None, **kwargs) -> LedgerModelQuerySet:
143
193
  """
144
194
  Returns a QuerySet of LedgerModels associated with a specific EntityModel & UserModel.
145
195
  May pass an instance of EntityModel or a String representing the EntityModel slug.
146
196
 
147
197
  Parameters
148
198
  ----------
149
- entity_slug: str or EntityModel
199
+ entity_model: str or EntityModel
150
200
  The entity slug or EntityModel used for filtering the QuerySet.
151
- user_model
152
- The request UserModel to check for privileges.
153
201
 
154
202
  Returns
155
203
  -------
156
204
  LedgerModelQuerySet
157
205
  A Filtered LedgerModelQuerySet.
158
206
  """
159
- qs = self.for_user(user_model)
160
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
161
- return qs.filter(
162
- Q(entity=entity_slug)
207
+ EntityModel = lazy_loader.get_entity_model()
208
+
209
+ qs = self.get_queryset()
210
+ if 'user_model' in kwargs:
211
+ warnings.warn(
212
+ 'user_model parameter is deprecated and will be removed in a future release. '
213
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
214
+ DeprecationWarning,
215
+ stacklevel=2
163
216
  )
164
- return qs.filter(
165
- Q(entity__slug__exact=entity_slug)
166
- )
217
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
218
+ qs = qs.for_user(kwargs['user_model'])
219
+
220
+ if isinstance(entity_model, EntityModel):
221
+ qs = qs.filter(entity=entity_model)
222
+ elif isinstance(entity_model, str):
223
+ qs = qs.filter(entity__slug__exact=entity_model)
224
+ elif isinstance(entity_model, UUID):
225
+ qs = qs.filter(entity_id=entity_model)
226
+ else:
227
+ raise LedgerModelValidationError(
228
+ message='Must provide entity slug, EntityModel, or UUID parameter'
229
+ )
230
+ return qs
167
231
 
168
232
 
169
233
  class LedgerModelAbstract(CreateUpdateMixIn, IOMixIn):
@@ -106,7 +106,7 @@ class ContactInfoMixIn(models.Model):
106
106
  phone: str
107
107
  A string used to document the contact phone.
108
108
  """
109
- address_1 = models.CharField(max_length=70, verbose_name=_('Address Line 1'))
109
+ address_1 = models.CharField(max_length=70, verbose_name=_('Address Line 1'), null=True, blank=True)
110
110
  address_2 = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('Address Line 2'))
111
111
  city = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('City'))
112
112
  state = models.CharField(null=True, blank=True, max_length=70, verbose_name=_('State/Province'))
@@ -129,6 +129,10 @@ class ContactInfoMixIn(models.Model):
129
129
  return f'{self.city}, {self.state}. {self.zip_code}. {self.country}'
130
130
 
131
131
  def clean(self):
132
+ if self.address_2 and not self.address_1:
133
+ raise ValidationError(
134
+ {'address_1': _('Address line 1 is required if address_2 is provided.')},
135
+ )
132
136
  super().clean()
133
137
 
134
138
 
@@ -231,8 +235,6 @@ class AccrualMixIn(models.Model):
231
235
  null=True,
232
236
  verbose_name=_('Prepaid Account'),
233
237
  related_name=f'{REL_NAME_PREFIX}_prepaid_account')
234
-
235
- # todo: rename to payable account...
236
238
  unearned_account = models.ForeignKey('django_ledger.AccountModel',
237
239
  on_delete=models.RESTRICT,
238
240
  blank=True,
@@ -12,10 +12,11 @@ starts in draft model by default and goes through different states including InR
12
12
  Void. The PurchaseOrderModel also keeps track of when these states take place.
13
13
 
14
14
  """
15
+ import warnings
15
16
  from datetime import date
16
17
  from string import ascii_uppercase, digits
17
18
  from typing import Tuple, List, Union, Optional, Dict
18
- from uuid import uuid4
19
+ from uuid import uuid4, UUID
19
20
 
20
21
  from django.contrib.auth import get_user_model
21
22
  from django.core.exceptions import ValidationError, ObjectDoesNotExist
@@ -30,6 +31,7 @@ from django.utils.translation import gettext_lazy as _
30
31
 
31
32
  from django_ledger.io.io_core import get_localdate
32
33
  from django_ledger.models.bill import BillModel, BillModelQuerySet
34
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
33
35
  from django_ledger.models.entity import EntityModel
34
36
  from django_ledger.models.items import ItemTransactionModel, ItemTransactionModelQuerySet, ItemModelQuerySet, ItemModel
35
37
  from django_ledger.models.mixins import CreateUpdateMixIn, MarkdownNotesMixIn, ItemizeMixIn
@@ -42,7 +44,8 @@ from django_ledger.models.signals import (
42
44
  po_status_in_review
43
45
  )
44
46
  from django_ledger.models.utils import lazy_loader
45
- from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_PO_NUMBER_PREFIX
47
+ from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_PO_NUMBER_PREFIX, \
48
+ DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
46
49
 
47
50
  PO_NUMBER_CHARS = ascii_uppercase + digits
48
51
 
@@ -55,9 +58,17 @@ class PurchaseOrderModelValidationError(ValidationError):
55
58
 
56
59
  class PurchaseOrderModelQuerySet(QuerySet):
57
60
  """
58
- A custom defined PurchaseOrderModel QuerySet.
61
+ A custom-defined PurchaseOrderModel QuerySet.
59
62
  """
60
63
 
64
+ def for_user(self, user_model):
65
+ if user_model.is_superuser:
66
+ return self
67
+ return self.filter(
68
+ Q(entity__admin=user_model) |
69
+ Q(entity__managers__in=[user_model])
70
+ )
71
+
61
72
  def approved(self):
62
73
  """
63
74
  Filters the QuerySet to include Approved PurchaseOrderModels only.
@@ -105,16 +116,8 @@ class PurchaseOrderModelManager(Manager):
105
116
  A custom defined PurchaseOrderModel Manager.
106
117
  """
107
118
 
108
- def for_user(self, user_model):
109
- qs = self.get_queryset()
110
- if user_model.is_superuser:
111
- return qs
112
- return qs.filter(
113
- Q(entity__admin=user_model) |
114
- Q(entity__managers__in=[user_model])
115
- )
116
-
117
- def for_entity(self, entity_slug, user_model) -> PurchaseOrderModelQuerySet:
119
+ @deprecated_entity_slug_behavior
120
+ def for_entity(self, entity_model: EntityModel | str | UUID = None, **kwargs) -> PurchaseOrderModelQuerySet:
118
121
  """
119
122
  Fetches a QuerySet of PurchaseOrderModel associated with a specific EntityModel & UserModel.
120
123
  May pass an instance of EntityModel or a String representing the EntityModel slug.
@@ -124,10 +127,29 @@ class PurchaseOrderModelManager(Manager):
124
127
  PurchaseOrderModelQuerySet
125
128
  A PurchaseOrderModelQuerySet with applied filters.
126
129
  """
127
- qs = self.for_user(user_model)
128
- if isinstance(entity_slug, EntityModel):
129
- qs = qs.filter(entity=entity_slug)
130
- return qs.filter(entity__slug__exact=entity_slug)
130
+
131
+ qs = self.get_queryset()
132
+ if 'user_model' in kwargs:
133
+ warnings.warn(
134
+ 'user_model parameter is deprecated and will be removed in a future release. '
135
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
136
+ DeprecationWarning,
137
+ stacklevel=2
138
+ )
139
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
140
+ qs = qs.for_user(kwargs['user_model'])
141
+
142
+ if isinstance(entity_model, EntityModel):
143
+ qs = qs.filter(entity=entity_model)
144
+ elif isinstance(entity_model, str):
145
+ qs = qs.filter(entity__slug__exact=entity_model)
146
+ elif isinstance(entity_model, UUID):
147
+ qs = qs.filter(entity_id=entity_model)
148
+ else:
149
+ raise PurchaseOrderModelValidationError(
150
+ message='Entity slug must be either an EntityModel or a String representing the EntityModel slug',
151
+ )
152
+ return qs
131
153
 
132
154
 
133
155
  class PurchaseOrderModelAbstract(CreateUpdateMixIn,