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.
- django_ledger/__init__.py +1 -1
- django_ledger/context.py +12 -0
- django_ledger/forms/account.py +45 -46
- django_ledger/forms/bill.py +0 -4
- django_ledger/forms/closing_entry.py +13 -1
- django_ledger/forms/data_import.py +182 -63
- django_ledger/forms/estimate.py +3 -6
- django_ledger/forms/invoice.py +3 -7
- django_ledger/forms/item.py +10 -18
- django_ledger/forms/purchase_order.py +2 -4
- django_ledger/io/io_core.py +515 -400
- django_ledger/io/io_generator.py +7 -6
- django_ledger/io/io_library.py +1 -2
- django_ledger/migrations/0025_alter_billmodel_cash_account_and_more.py +70 -0
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/accounts.py +109 -69
- django_ledger/models/bank_account.py +40 -23
- django_ledger/models/bill.py +386 -333
- django_ledger/models/chart_of_accounts.py +173 -105
- django_ledger/models/closing_entry.py +99 -48
- django_ledger/models/customer.py +100 -66
- django_ledger/models/data_import.py +818 -323
- django_ledger/models/deprecations.py +61 -0
- django_ledger/models/entity.py +891 -644
- django_ledger/models/estimate.py +57 -28
- django_ledger/models/invoice.py +46 -26
- django_ledger/models/items.py +503 -142
- django_ledger/models/journal_entry.py +61 -47
- django_ledger/models/ledger.py +106 -42
- django_ledger/models/mixins.py +424 -281
- django_ledger/models/purchase_order.py +39 -17
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +242 -139
- django_ledger/models/unit.py +93 -54
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +121 -70
- django_ledger/report/core.py +2 -14
- django_ledger/settings.py +57 -71
- django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
- django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +25 -0
- django_ledger/static/django_ledger/bundle/styles.bundle.js +1 -1
- django_ledger/static/django_ledger/css/djl_styles.css +273 -0
- django_ledger/templates/django_ledger/bills/includes/card_bill.html +2 -2
- django_ledger/templates/django_ledger/components/menu.html +41 -26
- django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
- django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
- django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
- django_ledger/templates/django_ledger/customer/tags/customer_table.html +8 -6
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
- django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
- django_ledger/templates/django_ledger/entity/includes/card_entity.html +12 -6
- django_ledger/templates/django_ledger/financial_statements/balance_sheet.html +1 -1
- django_ledger/templates/django_ledger/financial_statements/cash_flow.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/income_statement.html +4 -1
- django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +27 -3
- django_ledger/templates/django_ledger/financial_statements/tags/cash_flow_statement.html +16 -4
- django_ledger/templates/django_ledger/financial_statements/tags/income_statement.html +73 -18
- django_ledger/templates/django_ledger/includes/widget_ratios.html +18 -24
- django_ledger/templates/django_ledger/invoice/includes/card_invoice.html +3 -3
- django_ledger/templates/django_ledger/layouts/base.html +7 -2
- django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
- django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
- django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
- django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
- django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +12 -7
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/tests/test_accounts.py +1 -2
- django_ledger/tests/test_io.py +17 -0
- django_ledger/tests/test_purchase_order.py +3 -3
- django_ledger/tests/test_transactions.py +1 -2
- django_ledger/urls/__init__.py +1 -4
- django_ledger/urls/customer.py +3 -0
- django_ledger/urls/data_import.py +3 -0
- django_ledger/urls/receipt.py +102 -0
- django_ledger/urls/vendor.py +1 -0
- django_ledger/views/__init__.py +1 -0
- django_ledger/views/bill.py +8 -11
- django_ledger/views/chart_of_accounts.py +6 -4
- django_ledger/views/closing_entry.py +11 -7
- django_ledger/views/customer.py +68 -30
- django_ledger/views/data_import.py +120 -66
- django_ledger/views/djl_api.py +3 -5
- django_ledger/views/entity.py +2 -4
- django_ledger/views/estimate.py +3 -7
- django_ledger/views/inventory.py +3 -5
- django_ledger/views/invoice.py +4 -6
- django_ledger/views/item.py +7 -11
- django_ledger/views/journal_entry.py +1 -2
- django_ledger/views/mixins.py +125 -93
- django_ledger/views/purchase_order.py +24 -35
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/unit.py +1 -2
- django_ledger/views/vendor.py +54 -16
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/METADATA +43 -75
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/RECORD +104 -122
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +1 -1
- django_ledger-0.8.1.dist-info/top_level.txt +1 -0
- django_ledger/contrib/django_ledger_graphene/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/accounts/schema.py +0 -33
- django_ledger/contrib/django_ledger_graphene/api.py +0 -42
- django_ledger/contrib/django_ledger_graphene/apps.py +0 -6
- django_ledger/contrib/django_ledger_graphene/auth/mutations.py +0 -49
- django_ledger/contrib/django_ledger_graphene/auth/schema.py +0 -6
- django_ledger/contrib/django_ledger_graphene/bank_account/mutations.py +0 -61
- django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/bill/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/bill/schema.py +0 -34
- django_ledger/contrib/django_ledger_graphene/coa/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/coa/schema.py +0 -30
- django_ledger/contrib/django_ledger_graphene/customers/__init__.py +0 -0
- django_ledger/contrib/django_ledger_graphene/customers/mutations.py +0 -71
- django_ledger/contrib/django_ledger_graphene/customers/schema.py +0 -43
- django_ledger/contrib/django_ledger_graphene/data_import/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/data_import/schema.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/entity/schema.py +0 -94
- django_ledger/contrib/django_ledger_graphene/item/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/item/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/journal_entry/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +0 -35
- django_ledger/contrib/django_ledger_graphene/ledger/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/ledger/schema.py +0 -32
- django_ledger/contrib/django_ledger_graphene/purchase_order/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/purchase_order/schema.py +0 -31
- django_ledger/contrib/django_ledger_graphene/transaction/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/transaction/schema.py +0 -36
- django_ledger/contrib/django_ledger_graphene/unit/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/unit/schema.py +0 -27
- django_ledger/contrib/django_ledger_graphene/vendor/mutations.py +0 -0
- django_ledger/contrib/django_ledger_graphene/vendor/schema.py +0 -37
- django_ledger/contrib/django_ledger_graphene/views.py +0 -12
- django_ledger-0.7.11.dist-info/top_level.txt +0 -4
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/AUTHORS.md +0 -0
- {django_ledger-0.7.11.dist-info → django_ledger-0.8.1.dist-info/licenses}/LICENSE +0 -0
django_ledger/models/unit.py
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
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(
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
django_ledger/models/utils.py
CHANGED
|
@@ -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
|
|
django_ledger/models/vendor.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
18
|
-
from django.db.models import
|
|
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.
|
|
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
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
-
|
|
99
|
-
"""
|
|
116
|
+
Manages operations related to VendorModel instances.
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
122
|
+
@deprecated_entity_slug_behavior
|
|
123
|
+
def for_entity(
|
|
124
|
+
self, entity_model: 'EntityModel | str | UUID' = None, **kwargs
|
|
125
|
+
) -> VendorModelQuerySet:
|
|
111
126
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
The entity slug or
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
152
|
+
A queryset filtered for the given entity model.
|
|
132
153
|
"""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
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(
|
|
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)
|
django_ledger/report/core.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|