django-ledger 0.7.11__py3-none-any.whl → 0.8.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ledger might be problematic. Click here for more details.

Files changed (139) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/context.py +12 -0
  3. django_ledger/forms/account.py +45 -46
  4. django_ledger/forms/bill.py +0 -4
  5. django_ledger/forms/closing_entry.py +13 -1
  6. django_ledger/forms/data_import.py +182 -63
  7. django_ledger/forms/estimate.py +3 -6
  8. django_ledger/forms/invoice.py +3 -7
  9. django_ledger/forms/item.py +10 -18
  10. django_ledger/forms/purchase_order.py +2 -4
  11. django_ledger/io/io_core.py +515 -400
  12. django_ledger/io/io_generator.py +7 -6
  13. django_ledger/io/io_library.py +1 -2
  14. django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
  15. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  16. django_ledger/models/__init__.py +2 -1
  17. django_ledger/models/accounts.py +109 -69
  18. django_ledger/models/bank_account.py +40 -23
  19. django_ledger/models/bill.py +386 -333
  20. django_ledger/models/chart_of_accounts.py +173 -105
  21. django_ledger/models/closing_entry.py +99 -48
  22. django_ledger/models/customer.py +100 -66
  23. django_ledger/models/data_import.py +818 -323
  24. django_ledger/models/deprecations.py +61 -0
  25. django_ledger/models/entity.py +891 -644
  26. django_ledger/models/estimate.py +57 -28
  27. django_ledger/models/invoice.py +46 -26
  28. django_ledger/models/items.py +503 -142
  29. django_ledger/models/journal_entry.py +61 -47
  30. django_ledger/models/ledger.py +106 -42
  31. django_ledger/models/mixins.py +424 -281
  32. django_ledger/models/purchase_order.py +39 -17
  33. django_ledger/models/receipt.py +1083 -0
  34. django_ledger/models/transactions.py +242 -139
  35. django_ledger/models/unit.py +93 -54
  36. django_ledger/models/utils.py +12 -2
  37. django_ledger/models/vendor.py +121 -70
  38. django_ledger/report/core.py +2 -14
  39. django_ledger/settings.py +57 -71
  40. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  41. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
  42. django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
  43. django_ledger/static/django_ledger/css/djl_styles.css +273 -0
  44. django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
  45. django_ledger/templates/django_ledger/components/menu.html +41 -26
  46. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  47. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  48. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  49. django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
  50. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  51. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  52. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  53. django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
  54. django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
  55. django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
  56. django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
  57. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
  58. django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
  59. django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
  60. django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
  61. django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
  62. django_ledger/templates/django_ledger/layouts/base.html +7 -2
  63. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  64. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  65. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  66. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  67. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  68. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  69. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
  70. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  71. django_ledger/templatetags/django_ledger.py +338 -191
  72. django_ledger/tests/test_accounts.py +1 -2
  73. django_ledger/tests/test_io.py +17 -0
  74. django_ledger/tests/test_purchase_order.py +3 -3
  75. django_ledger/tests/test_transactions.py +1 -2
  76. django_ledger/urls/__init__.py +1 -4
  77. django_ledger/urls/customer.py +3 -0
  78. django_ledger/urls/data_import.py +3 -0
  79. django_ledger/urls/receipt.py +102 -0
  80. django_ledger/urls/vendor.py +1 -0
  81. django_ledger/views/__init__.py +1 -0
  82. django_ledger/views/bill.py +8 -11
  83. django_ledger/views/chart_of_accounts.py +6 -4
  84. django_ledger/views/closing_entry.py +11 -7
  85. django_ledger/views/customer.py +68 -30
  86. django_ledger/views/data_import.py +120 -66
  87. django_ledger/views/djl_api.py +3 -5
  88. django_ledger/views/entity.py +2 -4
  89. django_ledger/views/estimate.py +3 -7
  90. django_ledger/views/inventory.py +3 -5
  91. django_ledger/views/invoice.py +4 -6
  92. django_ledger/views/item.py +7 -11
  93. django_ledger/views/journal_entry.py +1 -2
  94. django_ledger/views/mixins.py +125 -93
  95. django_ledger/views/purchase_order.py +24 -35
  96. django_ledger/views/receipt.py +294 -0
  97. django_ledger/views/unit.py +1 -2
  98. django_ledger/views/vendor.py +54 -16
  99. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
  100. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
  101. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
  102. django_ledger-0.8.1.dist-info/top_level.txt +1 -0
  103. django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
  104. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
  105. django_ledger/contrib/django_ledger_graphene/api.py +0 -42
  106. django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
  107. django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
  108. django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
  109. django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
  110. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
  111. django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
  112. django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
  113. django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
  114. django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
  115. django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
  116. django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
  117. django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
  118. django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
  119. django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
  120. django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
  121. django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
  122. django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
  123. django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
  124. django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
  125. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
  126. django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
  127. django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
  128. django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
  129. django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
  130. django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
  131. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
  132. django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
  133. django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
  134. django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
  135. django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
  136. django_ledger/contrib/django_ledger_graphene/views.py +0 -12
  137. django_ledger-0.7.11.dist-info/top_level.txt +0 -4
  138. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
  139. {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
@@ -21,14 +21,15 @@ Key advantages of EntityUnits:
21
21
  business units.
22
22
  """
23
23
 
24
+ import warnings
24
25
  from random import choices
25
- from string import ascii_lowercase, digits, ascii_uppercase
26
+ from string import ascii_lowercase, ascii_uppercase, digits
26
27
  from typing import Optional
27
- from uuid import uuid4
28
+ from uuid import UUID, uuid4
28
29
 
29
30
  from django.core.exceptions import ValidationError
30
31
  from django.db import models
31
- from django.db.models import Q, F
32
+ from django.db.models import F, Q
32
33
  from django.urls import reverse
33
34
  from django.utils.text import slugify
34
35
  from django.utils.translation import gettext_lazy as _
@@ -36,7 +37,9 @@ from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
36
37
 
37
38
  from django_ledger.io.io_core import IOMixIn
38
39
  from django_ledger.models import lazy_loader
40
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
39
41
  from django_ledger.models.mixins import CreateUpdateMixIn, SlugNameMixIn
42
+ from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
40
43
 
41
44
  ENTITY_UNIT_RANDOM_SLUG_SUFFIX = ascii_lowercase + digits
42
45
 
@@ -46,13 +49,15 @@ class EntityUnitModelValidationError(ValidationError):
46
49
 
47
50
 
48
51
  class EntityUnitModelQuerySet(MP_NodeQuerySet):
49
- """
50
- A custom defined EntityUnitModel Queryset.
51
- """
52
+ def for_user(self, user_model) -> 'EntityUnitModelQuerySet':
53
+ if user_model.is_superuser:
54
+ return self
55
+ return self.filter(
56
+ Q(entity__admin=user_model) | Q(entity__managers__in=[user_model])
57
+ )
52
58
 
53
59
 
54
60
  class EntityUnitModelManager(MP_NodeManager):
55
-
56
61
  def get_queryset(self):
57
62
  qs = EntityUnitModelQuerySet(self.model, using=self._db)
58
63
  return qs.annotate(
@@ -60,47 +65,64 @@ class EntityUnitModelManager(MP_NodeManager):
60
65
  _entity_name=F('entity__name'),
61
66
  )
62
67
 
63
- def for_user(self, user_model):
64
- qs = self.get_queryset()
65
- if user_model.is_superuser:
66
- return qs
67
- return qs.filter(
68
- Q(entity__admin=user_model) |
69
- Q(entity__managers__in=[user_model])
70
- )
71
-
72
- def for_entity(self, entity_slug: str, user_model):
68
+ @deprecated_entity_slug_behavior
69
+ def for_entity(self, entity_model: 'EntityModel | str | UUID' = None, **kwargs):
73
70
  """
74
- Fetches a QuerySet of EntityUnitModels associated with a specific EntityModel & UserModel.
75
- May pass an instance of EntityModel or a String representing the EntityModel slug.
71
+ Filter the queryset based on the provided entity model, its slug, or its UUID.
72
+
73
+ Provides functionality to filter entities within a queryset by comparing either
74
+ an EntityModel instance, its slug, or its UUID. This method also handles optional
75
+ deprecated behavior for filtering by the 'user_model' for backward compatibility.
76
76
 
77
77
  Parameters
78
78
  ----------
79
- entity_slug: str or EntityModel
80
- The entity slug or EntityModel used for filtering the QuerySet.
81
- user_model
82
- Logged in and authenticated django UserModel instance.
79
+ entity_model : EntityModel | str | UUID
80
+ An entity filter criterion that could be a specific EntityModel instance, a
81
+ string representing the entity slug, or a UUID corresponding to the entity.
82
+
83
+ **kwargs : dict, optional
84
+ Additional keyword arguments. If the 'user_model' parameter is provided,
85
+ a deprecation warning is issued, and the functionality temporarily delegates
86
+ to legacy behavior based on the value of 'DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR'.
83
87
 
84
88
  Returns
85
89
  -------
86
- EntityUnitModelQuerySet
87
- Returns a EntityUnitModelQuerySet with applied filters.
90
+ QuerySet
91
+ A queryset filtered based on the provided entity criteria.
92
+
93
+ Raises
94
+ ------
95
+ EntityUnitModelValidationError
96
+ Raised when the `entity_model` parameter does not match any of the accepted types
97
+ (EntityModel, str, or UUID) and fails validation.
88
98
  """
89
- qs = self.for_user(user_model)
90
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
91
- return qs.filter(
92
- Q(entity=entity_slug)
99
+ EntityModel = lazy_loader.get_entity_model()
93
100
 
101
+ qs = self.get_queryset()
102
+ if 'user_model' in kwargs:
103
+ warnings.warn(
104
+ 'user_model parameter is deprecated and will be removed in a future release. '
105
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
106
+ DeprecationWarning,
107
+ stacklevel=2,
94
108
  )
95
- return qs.filter(
96
- Q(entity__slug__exact=entity_slug)
97
- )
109
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
110
+ qs = qs.for_user(kwargs['user_model'])
111
+
112
+ if isinstance(entity_model, EntityModel):
113
+ qs = qs.filter(entity=entity_model)
114
+ elif isinstance(entity_model, str):
115
+ qs = qs.filter(entity__slug__exact=entity_model)
116
+ elif isinstance(entity_model, UUID):
117
+ qs = qs.filter(entity_id=entity_model)
118
+ else:
119
+ raise EntityUnitModelValidationError(
120
+ message='Must pass EntityModel, slug or UUID'
121
+ )
122
+ return qs
98
123
 
99
124
 
100
- class EntityUnitModelAbstract(MP_Node,
101
- IOMixIn,
102
- SlugNameMixIn,
103
- CreateUpdateMixIn):
125
+ class EntityUnitModelAbstract(MP_Node, IOMixIn, SlugNameMixIn, CreateUpdateMixIn):
104
126
  """
105
127
  Base implementation of the EntityUnitModel.
106
128
 
@@ -125,17 +147,22 @@ class EntityUnitModelAbstract(MP_Node,
125
147
  hidden: bool
126
148
  Hidden Units will not show on drop down menus on the UI. Defaults to False.
127
149
  """
150
+
128
151
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
129
152
  slug = models.SlugField(max_length=50)
130
- entity = models.ForeignKey('django_ledger.EntityModel',
131
- editable=False,
132
- on_delete=models.CASCADE,
133
- verbose_name=_('Unit Entity'))
153
+ entity = models.ForeignKey(
154
+ 'django_ledger.EntityModel',
155
+ editable=False,
156
+ on_delete=models.CASCADE,
157
+ verbose_name=_('Unit Entity'),
158
+ )
134
159
  document_prefix = models.CharField(max_length=3)
135
160
  active = models.BooleanField(default=True, verbose_name=_('Is Active'))
136
161
  hidden = models.BooleanField(default=False, verbose_name=_('Is Hidden'))
137
162
 
138
- objects = EntityUnitModelManager.from_queryset(queryset_class=EntityUnitModelQuerySet)()
163
+ objects = EntityUnitModelManager.from_queryset(
164
+ queryset_class=EntityUnitModelQuerySet
165
+ )()
139
166
 
140
167
  class Meta:
141
168
  abstract = True
@@ -185,19 +212,20 @@ class EntityUnitModelAbstract(MP_Node,
185
212
  str
186
213
  The EntityModelUnit instance dashboard URL.
187
214
  """
188
- return reverse('django_ledger:unit-dashboard',
189
- kwargs={
190
- 'entity_slug': self.slug
191
- })
215
+ return reverse(
216
+ 'django_ledger:unit-dashboard', kwargs={'entity_slug': self.slug}
217
+ )
192
218
 
193
219
  def get_entity_name(self) -> str:
194
220
  return self.entity.name
195
221
 
196
- def create_entity_unit_slug(self,
197
- name: Optional[str] = None,
198
- force: bool = False,
199
- add_suffix: bool = True,
200
- k: int = 5) -> str:
222
+ def create_entity_unit_slug(
223
+ self,
224
+ name: Optional[str] = None,
225
+ force: bool = False,
226
+ add_suffix: bool = True,
227
+ k: int = 5,
228
+ ) -> str:
201
229
  """
202
230
  Automatically generates a EntityUnitModel slug. If slug is present, will not be replaced.
203
231
  Called during the clean() method.
@@ -228,13 +256,24 @@ class EntityUnitModelAbstract(MP_Node,
228
256
  self.slug = unit_slug
229
257
  return self.slug
230
258
 
259
+ def validate_for_entity(self, entity_model: 'EntityModel | str | UUID'):
260
+ EntityModel = lazy_loader.get_entity_model()
261
+
262
+ if isinstance(entity_model, UUID):
263
+ is_valid = self.entity_id == entity_model
264
+ elif isinstance(entity_model, str):
265
+ is_valid = self.entity_slug == entity_model
266
+ elif isinstance(entity_model, EntityModel):
267
+ is_valid = self.entity == entity_model
268
+ if not is_valid:
269
+ raise EntityUnitModelValidationError(
270
+ f'Entity Unit Model {entity_model} is not a valid Entity Model'
271
+ )
272
+
231
273
  def get_absolute_url(self):
232
274
  return reverse(
233
275
  viewname='django_ledger:unit-detail',
234
- kwargs={
235
- 'entity_slug': self.entity.slug,
236
- 'unit_slug': self.slug
237
- }
276
+ kwargs={'entity_slug': self.entity.slug, 'unit_slug': self.slug},
238
277
  )
239
278
 
240
279
 
@@ -75,6 +75,7 @@ class LazyLoader:
75
75
  LEDGER_MODEL = 'ledgermodel'
76
76
  JE_MODEL = 'journalentrymodel'
77
77
  TRANSACTION_MODEL = 'transactionmodel'
78
+ STAGED_TRANSACTION_MODEL = 'stagedtransactionmodel'
78
79
  ACCOUNT_MODEL = 'accountmodel'
79
80
  COA_MODEL = 'chartofaccountmodel'
80
81
 
@@ -88,6 +89,7 @@ class LazyLoader:
88
89
 
89
90
  CUSTOMER_MODEL = 'customermodel'
90
91
  INVOICE_MODEL = 'invoicemodel'
92
+ RECEIPT_MODEL = 'receiptmodel'
91
93
  BILL_MODEL = 'billmodel'
92
94
  UOM_MODEL = 'unitofmeasuremodel'
93
95
  VENDOR_MODEL = 'vendormodel'
@@ -100,8 +102,6 @@ class LazyLoader:
100
102
  INCOME_STATEMENT_REPORT_CLASS = None
101
103
  CASH_FLOW_STATEMENT_REPORT_CLASS = None
102
104
 
103
-
104
-
105
105
  def get_entity_model(self):
106
106
  return self.app_config.get_model(self.ENTITY_MODEL)
107
107
 
@@ -123,6 +123,9 @@ class LazyLoader:
123
123
  def get_txs_model(self):
124
124
  return self.app_config.get_model(self.TRANSACTION_MODEL)
125
125
 
126
+ def get_staged_txs_model(self):
127
+ return self.app_config.get_model(self.STAGED_TRANSACTION_MODEL)
128
+
126
129
  def get_purchase_order_model(self):
127
130
  return self.app_config.get_model(self.PURCHASE_ORDER_MODEL)
128
131
 
@@ -139,6 +142,9 @@ class LazyLoader:
139
142
  def get_item_transaction_model(self):
140
143
  return self.app_config.get_model(self.ITEM_TRANSACTION_MODEL)
141
144
 
145
+ def get_receipt_model(self):
146
+ return self.app_config.get_model(self.RECEIPT_MODEL)
147
+
142
148
  def get_customer_model(self):
143
149
  return self.app_config.get_model(self.CUSTOMER_MODEL)
144
150
 
@@ -166,24 +172,28 @@ class LazyLoader:
166
172
  def get_entity_data_generator(self):
167
173
  if not self.ENTITY_DATA_GENERATOR:
168
174
  from django_ledger.io.io_generator import EntityDataGenerator
175
+
169
176
  self.ENTITY_DATA_GENERATOR = EntityDataGenerator
170
177
  return self.ENTITY_DATA_GENERATOR
171
178
 
172
179
  def get_balance_sheet_report_class(self):
173
180
  if not self.BALANCE_SHEET_REPORT_CLASS:
174
181
  from django_ledger.report.balance_sheet import BalanceSheetReport
182
+
175
183
  self.BALANCE_SHEET_REPORT_CLASS = BalanceSheetReport
176
184
  return self.BALANCE_SHEET_REPORT_CLASS
177
185
 
178
186
  def get_income_statement_report_class(self):
179
187
  if not self.INCOME_STATEMENT_REPORT_CLASS:
180
188
  from django_ledger.report.income_statement import IncomeStatementReport
189
+
181
190
  self.INCOME_STATEMENT_REPORT_CLASS = IncomeStatementReport
182
191
  return self.INCOME_STATEMENT_REPORT_CLASS
183
192
 
184
193
  def get_cash_flow_statement_report_class(self):
185
194
  if not self.CASH_FLOW_STATEMENT_REPORT_CLASS:
186
195
  from django_ledger.report.cash_flow_statement import CashFlowStatementReport
196
+
187
197
  self.CASH_FLOW_STATEMENT_REPORT_CLASS = CashFlowStatementReport
188
198
  return self.CASH_FLOW_STATEMENT_REPORT_CLASS
189
199
 
@@ -10,18 +10,30 @@ Vendors can be flagged as active/inactive or hidden. Vendors who no longer condu
10
10
  whether temporarily or indefinitely may be flagged as inactive (i.e. active is False). Hidden Vendors will not show up
11
11
  as an option in the UI, but can still be used programmatically (via API).
12
12
  """
13
+
13
14
  import os
14
- from uuid import uuid4
15
+ import warnings
16
+ from uuid import UUID, uuid4
15
17
 
16
18
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
17
- from django.db import models, transaction, IntegrityError
18
- from django.db.models import Q, F, QuerySet, Manager
19
+ from django.db import IntegrityError, models, transaction
20
+ from django.db.models import F, Manager, Q, QuerySet
19
21
  from django.utils.text import slugify
20
22
  from django.utils.translation import gettext_lazy as _
21
23
 
22
- from django_ledger.models.mixins import ContactInfoMixIn, CreateUpdateMixIn, FinancialAccountInfoMixin, TaxInfoMixIn
24
+ from django_ledger.models.deprecations import deprecated_entity_slug_behavior
25
+ from django_ledger.models.mixins import (
26
+ ContactInfoMixIn,
27
+ CreateUpdateMixIn,
28
+ FinancialAccountInfoMixin,
29
+ TaxInfoMixIn,
30
+ )
23
31
  from django_ledger.models.utils import lazy_loader
24
- from django_ledger.settings import DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING, DJANGO_LEDGER_VENDOR_NUMBER_PREFIX
32
+ from django_ledger.settings import (
33
+ DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING,
34
+ DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR,
35
+ DJANGO_LEDGER_VENDOR_NUMBER_PREFIX,
36
+ )
25
37
 
26
38
 
27
39
  def vendor_picture_upload_to(instance, filename):
@@ -42,7 +54,15 @@ class VendorModelQuerySet(QuerySet):
42
54
  Custom defined VendorModel QuerySet.
43
55
  """
44
56
 
45
- def active(self) -> QuerySet:
57
+ def for_user(self, user_model) -> 'VendorModelQuerySet':
58
+ if user_model.is_superuser:
59
+ return self
60
+ return self.filter(
61
+ Q(entity_model__admin=user_model)
62
+ | Q(entity_model__managers__in=[user_model])
63
+ )
64
+
65
+ def active(self) -> 'VendorModelQuerySet':
46
66
  """
47
67
  Active vendors can be assigned to new bills and show on dropdown menus and views.
48
68
 
@@ -53,7 +73,7 @@ class VendorModelQuerySet(QuerySet):
53
73
  """
54
74
  return self.filter(active=True)
55
75
 
56
- def inactive(self) -> QuerySet:
76
+ def inactive(self) -> 'VendorModelQuerySet':
57
77
  """
58
78
  Active vendors can be assigned to new bills and show on dropdown menus and views.
59
79
  Marking VendorModels as inactive can help reduce Database load to populate select inputs and also inactivate
@@ -67,7 +87,7 @@ class VendorModelQuerySet(QuerySet):
67
87
  """
68
88
  return self.filter(active=False)
69
89
 
70
- def hidden(self) -> QuerySet:
90
+ def hidden(self) -> 'VendorModelQuerySet':
71
91
  """
72
92
  Hidden vendors do not show on dropdown menus, but may be used via APIs or any other method that does not
73
93
  involve the UI.
@@ -79,7 +99,7 @@ class VendorModelQuerySet(QuerySet):
79
99
  """
80
100
  return self.filter(hidden=True)
81
101
 
82
- def visible(self) -> QuerySet:
102
+ def visible(self) -> 'VendorModelQuerySet':
83
103
  """
84
104
  Visible vendors show on dropdown menus and views. Visible vendors are active and not hidden.
85
105
 
@@ -88,62 +108,79 @@ class VendorModelQuerySet(QuerySet):
88
108
  VendorModelQuerySet
89
109
  A QuerySet of visible Vendors.
90
110
  """
91
- return self.filter(
92
- Q(hidden=False) & Q(active=True)
93
- )
111
+ return self.filter(Q(hidden=False) & Q(active=True))
94
112
 
95
113
 
96
114
  class VendorModelManager(Manager):
97
115
  """
98
- Custom defined VendorModel Manager, which defines many methods for initial query of the Database.
99
- """
116
+ Manages operations related to VendorModel instances.
100
117
 
101
- def for_user(self, user_model):
102
- qs = self.get_queryset()
103
- if user_model.is_superuser:
104
- return qs
105
- return qs.filter(
106
- Q(entity_model__admin=user_model) |
107
- Q(entity_model__managers__in=[user_model])
108
- )
118
+ A specialized manager for handling interactions with VendorModel entities,
119
+ providing additional support for filtering based on associated EntityModel or EntityModel slug.
120
+ """
109
121
 
110
- def for_entity(self, entity_slug, user_model) -> VendorModelQuerySet:
122
+ @deprecated_entity_slug_behavior
123
+ def for_entity(
124
+ self, entity_model: 'EntityModel | str | UUID' = None, **kwargs
125
+ ) -> VendorModelQuerySet:
111
126
  """
112
- Fetches a QuerySet of VendorModel associated with a specific EntityModel & UserModel.
113
- May pass an instance of EntityModel or a String representing the EntityModel slug.
127
+ Filters the queryset for a given entity model.
128
+
129
+ This method modifies the queryset to include only those records
130
+ associated with the specified entity model. The entity model can
131
+ be provided in various formats, such as an instance of `EntityModel`,
132
+ a string representing the entity's slug, or its UUID.
133
+
134
+ If a deprecated parameter `user_model` is provided, it will issue
135
+ a warning and may alter the behavior if the deprecated behavior flag
136
+ `DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR` is set.
114
137
 
115
138
  Parameters
116
139
  ----------
117
- entity_slug: str or EntityModel
118
- The entity slug or EntityModel used for filtering the QuerySet.
119
- user_model
120
- Logged in and authenticated django UserModel instance.
140
+ entity_model : EntityModel | str | UUID
141
+ The entity model or its identifier (slug or UUID) to filter the
142
+ queryset by.
121
143
 
122
- Examples
123
- ________
124
- >>> request_user = request.user
125
- >>> slug = kwargs['entity_slug'] # may come from request kwargs
126
- >>> vendor_model_qs = VendorModel.objects.for_entity(user_model=request_user, entity_slug=slug)
144
+ **kwargs
145
+ Additional parameters for optional functionality. The parameter
146
+ `user_model` is supported for backward compatibility but is
147
+ deprecated and should be avoided.
127
148
 
128
149
  Returns
129
150
  -------
130
151
  VendorModelQuerySet
131
- A filtered VendorModel QuerySet.
152
+ A queryset filtered for the given entity model.
132
153
  """
133
- qs = self.for_user(user_model)
134
- if isinstance(entity_slug, lazy_loader.get_entity_model()):
135
- return qs.filter(
136
- Q(entity_model=entity_slug)
154
+ EntityModel = lazy_loader.get_entity_model()
155
+
156
+ qs = self.get_queryset()
157
+ if 'user_model' in kwargs:
158
+ warnings.warn(
159
+ 'user_model parameter is deprecated and will be removed in a future release. '
160
+ 'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
161
+ DeprecationWarning,
162
+ stacklevel=2,
137
163
  )
138
- return qs.filter(
139
- Q(entity_model__slug__exact=entity_slug)
140
- )
141
164
 
165
+ if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
166
+ qs = qs.for_user(kwargs['user_model'])
167
+
168
+ if isinstance(entity_model, EntityModel):
169
+ qs = qs.filter(entity_model=entity_model)
170
+ elif isinstance(entity_model, str):
171
+ qs = qs.filter(entity_model__slug__exact=entity_model)
172
+ elif isinstance(entity_model, UUID):
173
+ qs = qs.filter(entity_model_id=entity_model)
174
+ else:
175
+ raise VendorModelValidationError(
176
+ 'EntityModel slug must be either a string or an EntityModel instance'
177
+ )
178
+ return qs
142
179
 
143
- class VendorModelAbstract(ContactInfoMixIn,
144
- FinancialAccountInfoMixin,
145
- TaxInfoMixIn,
146
- CreateUpdateMixIn):
180
+
181
+ class VendorModelAbstract(
182
+ ContactInfoMixIn, FinancialAccountInfoMixin, TaxInfoMixIn, CreateUpdateMixIn
183
+ ):
147
184
  """
148
185
  This is the main abstract class which the VendorModel database will inherit from.
149
186
  The VendorModel inherits functionality from the following MixIns:
@@ -182,28 +219,33 @@ class VendorModelAbstract(ContactInfoMixIn,
182
219
 
183
220
 
184
221
  """
222
+
185
223
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
186
224
  vendor_code = models.SlugField(
187
- max_length=50,
225
+ max_length=50, null=True, blank=True, verbose_name='User defined vendor code.'
226
+ )
227
+ vendor_number = models.CharField(
228
+ max_length=30,
188
229
  null=True,
189
230
  blank=True,
190
- verbose_name='User defined vendor code.'
231
+ editable=False,
232
+ verbose_name=_('Vendor Number'),
233
+ help_text='System generated vendor number.',
191
234
  )
192
- vendor_number = models.CharField(max_length=30,
193
- null=True,
194
- blank=True,
195
- editable=False,
196
- verbose_name=_('Vendor Number'), help_text='System generated vendor number.')
197
235
  vendor_name = models.CharField(max_length=100)
198
236
 
199
- entity_model = models.ForeignKey('django_ledger.EntityModel',
200
- on_delete=models.CASCADE,
201
- verbose_name=_('Vendor Entity'),
202
- editable=False)
237
+ entity_model = models.ForeignKey(
238
+ 'django_ledger.EntityModel',
239
+ on_delete=models.CASCADE,
240
+ verbose_name=_('Vendor Entity'),
241
+ editable=False,
242
+ )
203
243
  description = models.TextField()
204
244
  active = models.BooleanField(default=True)
205
245
  hidden = models.BooleanField(default=False)
206
- picture = models.ImageField(upload_to=vendor_picture_upload_to, null=True, blank=True)
246
+ picture = models.ImageField(
247
+ upload_to=vendor_picture_upload_to, null=True, blank=True
248
+ )
207
249
 
208
250
  additional_info = models.JSONField(null=True, blank=True, default=dict)
209
251
 
@@ -219,9 +261,7 @@ class VendorModelAbstract(ContactInfoMixIn,
219
261
  models.Index(fields=['active']),
220
262
  models.Index(fields=['hidden']),
221
263
  ]
222
- unique_together = [
223
- ('entity_model', 'vendor_number')
224
- ]
264
+ unique_together = [('entity_model', 'vendor_number')]
225
265
  abstract = True
226
266
 
227
267
  def __str__(self):
@@ -229,6 +269,20 @@ class VendorModelAbstract(ContactInfoMixIn,
229
269
  f'Unknown Vendor: {self.vendor_name}'
230
270
  return f'{self.vendor_number}: {self.vendor_name}'
231
271
 
272
+ def validate_for_entity(self, entity_model: 'EntityModel | str | UUID'):
273
+ EntityModel = lazy_loader.get_entity_model()
274
+ if isinstance(entity_model, str):
275
+ is_valid = entity_model == self.entity_model.slug
276
+ elif isinstance(entity_model, EntityModel):
277
+ is_valid = entity_model == self.entity_model
278
+ elif isinstance(entity_model, UUID):
279
+ is_valid = entity_model == self.entity_model_id
280
+
281
+ if not is_valid:
282
+ raise VendorModelValidationError(
283
+ 'EntityModel does not belong to this Vendor'
284
+ )
285
+
232
286
  def can_generate_vendor_number(self) -> bool:
233
287
  """
234
288
  Determines if the VendorModel can be issued a Vendor Number.
@@ -239,10 +293,7 @@ class VendorModelAbstract(ContactInfoMixIn,
239
293
  bool
240
294
  True if the vendor number can be generated, else False.
241
295
  """
242
- return all([
243
- self.entity_model_id,
244
- not self.vendor_number
245
- ])
296
+ return all([self.entity_model_id, not self.vendor_number])
246
297
 
247
298
  def _get_next_state_model(self, raise_exception: bool = True):
248
299
  """
@@ -264,10 +315,12 @@ class VendorModelAbstract(ContactInfoMixIn,
264
315
  try:
265
316
  LOOKUP = {
266
317
  'entity_model_id__exact': self.entity_model_id,
267
- 'key__exact': EntityStateModel.KEY_VENDOR
318
+ 'key__exact': EntityStateModel.KEY_VENDOR,
268
319
  }
269
320
 
270
- state_model_qs = EntityStateModel.objects.filter(**LOOKUP).select_for_update()
321
+ state_model_qs = EntityStateModel.objects.filter(
322
+ **LOOKUP
323
+ ).select_for_update()
271
324
  state_model = state_model_qs.get()
272
325
  state_model.sequence = F('sequence') + 1
273
326
  state_model.save()
@@ -275,13 +328,12 @@ class VendorModelAbstract(ContactInfoMixIn,
275
328
 
276
329
  return state_model
277
330
  except ObjectDoesNotExist:
278
-
279
331
  LOOKUP = {
280
332
  'entity_model_id': self.entity_model_id,
281
333
  'entity_unit_id': None,
282
334
  'fiscal_year': None,
283
335
  'key': EntityStateModel.KEY_VENDOR,
284
- 'sequence': 1
336
+ 'sequence': 1,
285
337
  }
286
338
  state_model = EntityStateModel.objects.create(**LOOKUP)
287
339
  return state_model
@@ -306,7 +358,6 @@ class VendorModelAbstract(ContactInfoMixIn,
306
358
  """
307
359
  if self.can_generate_vendor_number():
308
360
  with transaction.atomic(durable=True):
309
-
310
361
  state_model = None
311
362
  while not state_model:
312
363
  state_model = self._get_next_state_model(raise_exception=False)
@@ -6,25 +6,16 @@ from django.core.exceptions import ValidationError
6
6
  from django_ledger.io.io_context import IODigestContextManager
7
7
  from django_ledger.models.ledger import LedgerModel
8
8
  from django_ledger.models.unit import EntityUnitModel
9
- from django_ledger.settings import DJANGO_LEDGER_PDF_SUPPORT_ENABLED
10
9
  from django_ledger.templatetags.django_ledger import currency_symbol, currency_format
11
10
 
12
- if DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
13
- from fpdf import FPDF, XPos, YPos
11
+ from fpdf import FPDF, XPos, YPos
14
12
 
15
13
 
16
14
  class PDFReportValidationError(ValidationError):
17
15
  pass
18
16
 
19
17
 
20
- def load_support():
21
- support = list()
22
- if DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
23
- support.append(FPDF)
24
- return support
25
-
26
-
27
- class BaseReportSupport(*load_support()):
18
+ class BaseReportSupport(FPDF):
28
19
  FOOTER_LOGO_PATH = 'django_ledger/logo/django-ledger-logo-report.png'
29
20
 
30
21
  def __init__(self,
@@ -33,9 +24,6 @@ class BaseReportSupport(*load_support()):
33
24
  report_subtitle: Optional[str] = None,
34
25
  **kwargs):
35
26
 
36
- if not DJANGO_LEDGER_PDF_SUPPORT_ENABLED:
37
- raise NotImplementedError('PDF support not enabled.')
38
-
39
27
  super().__init__(*args, **kwargs)
40
28
  self.REPORT_TYPE: Optional[str] = None
41
29
  self.REPORT_SUBTITLE: Optional[str] = report_subtitle