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
|
@@ -8,48 +8,102 @@ application. It introduces two primary models to facilitate the import and proce
|
|
|
8
8
|
1. `ImportJobModel` - Represents jobs that handle financial data import tasks.
|
|
9
9
|
2. `StagedTransactionModel` - Represents individual transactions, including those that are staged for review, mapping,
|
|
10
10
|
or further processing.
|
|
11
|
-
|
|
12
11
|
"""
|
|
13
12
|
|
|
13
|
+
import warnings
|
|
14
|
+
from datetime import date, datetime
|
|
14
15
|
from decimal import Decimal
|
|
15
|
-
from typing import
|
|
16
|
-
from uuid import
|
|
17
|
-
|
|
18
|
-
from django.core.exceptions import ValidationError
|
|
19
|
-
from django.db import models
|
|
20
|
-
from django.db.models import
|
|
16
|
+
from typing import Dict, List, Optional, Set, Union
|
|
17
|
+
from uuid import UUID, uuid4
|
|
18
|
+
|
|
19
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
20
|
+
from django.db import models, transaction
|
|
21
|
+
from django.db.models import (
|
|
22
|
+
BooleanField,
|
|
23
|
+
Case,
|
|
24
|
+
Count,
|
|
25
|
+
DecimalField,
|
|
26
|
+
F,
|
|
27
|
+
Manager,
|
|
28
|
+
Q,
|
|
29
|
+
QuerySet,
|
|
30
|
+
Sum,
|
|
31
|
+
Value,
|
|
32
|
+
When,
|
|
33
|
+
)
|
|
21
34
|
from django.db.models.functions import Coalesce
|
|
22
35
|
from django.db.models.signals import pre_save
|
|
23
36
|
from django.utils.translation import gettext_lazy as _
|
|
24
37
|
|
|
25
38
|
from django_ledger.io import ASSET_CA_CASH, CREDIT, DEBIT
|
|
26
|
-
from django_ledger.models import
|
|
39
|
+
from django_ledger.models import AccountModel
|
|
40
|
+
from django_ledger.models.deprecations import deprecated_entity_slug_behavior
|
|
27
41
|
from django_ledger.models.entity import EntityModel
|
|
42
|
+
from django_ledger.models.journal_entry import JournalEntryModel
|
|
28
43
|
from django_ledger.models.mixins import CreateUpdateMixIn
|
|
29
|
-
from django_ledger.models.
|
|
44
|
+
from django_ledger.models.receipt import ReceiptModel
|
|
45
|
+
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
30
46
|
|
|
31
47
|
|
|
32
48
|
class ImportJobModelValidationError(ValidationError):
|
|
49
|
+
"""
|
|
50
|
+
Represents an error that occurs during the validation of an import job model.
|
|
51
|
+
|
|
52
|
+
This class is a specific type of `ValidationError` raised when validation
|
|
53
|
+
of an import job model fails due to incorrect or invalid data. It serves
|
|
54
|
+
as a means to categorize and identify errors related to the import job
|
|
55
|
+
model validation process. This class does not redefine or add functionality
|
|
56
|
+
but exists to provide semantic clarity when handling this specific type
|
|
57
|
+
of validation failure.
|
|
58
|
+
"""
|
|
59
|
+
|
|
33
60
|
pass
|
|
34
61
|
|
|
35
62
|
|
|
36
63
|
class ImportJobModelQuerySet(QuerySet):
|
|
37
|
-
|
|
64
|
+
""" """
|
|
38
65
|
|
|
66
|
+
def for_user(self, user_model) -> 'ImportJobModelQuerySet':
|
|
67
|
+
"""
|
|
68
|
+
Filters the queryset based on the user's permissions for accessing the data
|
|
69
|
+
related to bank accounts and entities they manage or administer.
|
|
39
70
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
This method first retrieves the default queryset. If the user is a superuser,
|
|
72
|
+
the query will return the full queryset without any filters. Otherwise, the
|
|
73
|
+
query will be limited to the entities that the user either administers or is
|
|
74
|
+
listed as a manager for.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
user_model : User
|
|
79
|
+
The user model instance whose permissions determine the filtering of the queryset.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
QuerySet
|
|
84
|
+
A filtered queryset based on the user's role and associated permissions.
|
|
85
|
+
"""
|
|
86
|
+
if user_model.is_superuser:
|
|
87
|
+
return self
|
|
88
|
+
return self.filter(
|
|
89
|
+
Q(bank_account_model__entity_model__admin=user_model)
|
|
90
|
+
| Q(bank_account_model__entity_model__managers__in=[user_model])
|
|
91
|
+
)
|
|
43
92
|
|
|
44
|
-
This manager provides custom queryset handling for import job models, including
|
|
45
|
-
annotations for custom fields like transaction counts, user-specific filters,
|
|
46
|
-
and entity-specific filters. It is integrated with the ImportJobModel, designed
|
|
47
|
-
to support complex query requirements with field annotations and related object
|
|
48
|
-
optimizations for performance efficiency.
|
|
49
93
|
|
|
94
|
+
class ImportJobModelManager(Manager):
|
|
95
|
+
"""
|
|
96
|
+
Manager class for handling ImportJobModel queries.
|
|
97
|
+
|
|
98
|
+
This class provides custom query methods for the ImportJobModel, allowing
|
|
99
|
+
efficient querying and annotation of related fields. It is tailored to
|
|
100
|
+
facilitate operations involving entities, accounts, and transactions with
|
|
101
|
+
various computed properties including counts, pending transactions, and
|
|
102
|
+
completion status. It also supports entity-specific filtering and deprecated
|
|
103
|
+
behavior for backward compatibility.
|
|
50
104
|
"""
|
|
51
105
|
|
|
52
|
-
def get_queryset(self):
|
|
106
|
+
def get_queryset(self) -> ImportJobModelQuerySet:
|
|
53
107
|
"""
|
|
54
108
|
Generates a QuerySet with annotated data for ImportJobModel.
|
|
55
109
|
|
|
@@ -74,109 +128,115 @@ class ImportJobModelManager(Manager):
|
|
|
74
128
|
(no pending transactions or total count is zero).
|
|
75
129
|
"""
|
|
76
130
|
qs = ImportJobModelQuerySet(self.model, using=self._db)
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
131
|
+
return (
|
|
132
|
+
qs.annotate(
|
|
133
|
+
_entity_uuid=F('ledger_model__entity__uuid'),
|
|
134
|
+
_entity_slug=F('ledger_model__entity__slug'),
|
|
135
|
+
txs_count=Count(
|
|
136
|
+
'stagedtransactionmodel',
|
|
137
|
+
filter=Q(stagedtransactionmodel__parent__isnull=False),
|
|
138
|
+
),
|
|
139
|
+
txs_mapped_count=Count(
|
|
140
|
+
'stagedtransactionmodel__account_model_id',
|
|
141
|
+
filter=Q(stagedtransactionmodel__parent__isnull=False)
|
|
142
|
+
| Q(stagedtransactionmodel__parent__parent__isnull=False),
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
.annotate(txs_pending=F('txs_count') - F('txs_mapped_count'))
|
|
146
|
+
.annotate(
|
|
147
|
+
is_complete=Case(
|
|
148
|
+
When(txs_count__exact=0, then=False),
|
|
149
|
+
When(txs_pending__exact=0, then=True),
|
|
150
|
+
default=False,
|
|
151
|
+
output_field=BooleanField(),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
.select_related(
|
|
155
|
+
'bank_account_model',
|
|
156
|
+
'bank_account_model__account_model',
|
|
157
|
+
'ledger_model',
|
|
158
|
+
)
|
|
101
159
|
)
|
|
102
160
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
This method first retrieves the default queryset. If the user is a superuser,
|
|
109
|
-
the query will return the full queryset without any filters. Otherwise, the
|
|
110
|
-
query will be limited to the entities that the user either administers or is
|
|
111
|
-
listed as a manager for.
|
|
112
|
-
|
|
113
|
-
Parameters
|
|
114
|
-
----------
|
|
115
|
-
user_model : User
|
|
116
|
-
The user model instance whose permissions determine the filtering of the queryset.
|
|
117
|
-
|
|
118
|
-
Returns
|
|
119
|
-
-------
|
|
120
|
-
QuerySet
|
|
121
|
-
A filtered queryset based on the user's role and associated permissions.
|
|
122
|
-
"""
|
|
161
|
+
@deprecated_entity_slug_behavior
|
|
162
|
+
def for_entity(
|
|
163
|
+
self, entity_model: Union[EntityModel, str, UUID] = None, **kwargs
|
|
164
|
+
) -> ImportJobModelQuerySet:
|
|
123
165
|
qs = self.get_queryset()
|
|
124
|
-
if user_model
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if isinstance(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
if 'user_model' in kwargs:
|
|
167
|
+
warnings.warn(
|
|
168
|
+
'user_model parameter is deprecated and will be removed in a future release. '
|
|
169
|
+
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
170
|
+
DeprecationWarning,
|
|
171
|
+
stacklevel=2,
|
|
172
|
+
)
|
|
173
|
+
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
174
|
+
qs = qs.for_user(kwargs['user_model'])
|
|
175
|
+
|
|
176
|
+
if isinstance(entity_model, EntityModel):
|
|
177
|
+
qs = qs.filter(bank_account_model__entity_model=entity_model)
|
|
178
|
+
elif isinstance(entity_model, UUID):
|
|
179
|
+
qs = qs.filter(bank_account_model__entity_model_id=entity_model)
|
|
180
|
+
elif isinstance(entity_model, str):
|
|
181
|
+
qs = qs.filter(bank_account_model__slug__exact=entity_model)
|
|
182
|
+
else:
|
|
183
|
+
raise ImportJobModelValidationError(
|
|
184
|
+
message=_('Must pass EntityModel, slug or UUID'),
|
|
185
|
+
)
|
|
186
|
+
return qs
|
|
141
187
|
|
|
142
188
|
|
|
143
189
|
class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
144
190
|
"""
|
|
145
|
-
|
|
191
|
+
Represents an abstract model for managing import jobs.
|
|
146
192
|
|
|
147
|
-
This
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
193
|
+
This class provides attributes and methods to facilitate the creation,
|
|
194
|
+
configuration, and management of import jobs. It is designed to work
|
|
195
|
+
with ledger and bank account models, enabling tight integration with
|
|
196
|
+
ledger-based systems. The model is marked as abstract and is intended
|
|
197
|
+
to be extended by other concrete models.
|
|
152
198
|
|
|
153
199
|
Attributes
|
|
154
200
|
----------
|
|
155
201
|
uuid : UUID
|
|
156
|
-
|
|
202
|
+
The universally unique identifier for the import job.
|
|
157
203
|
description : str
|
|
158
|
-
|
|
159
|
-
bank_account_model : BankAccountModel
|
|
160
|
-
|
|
161
|
-
ledger_model : LedgerModel
|
|
162
|
-
|
|
204
|
+
A brief description of the import job.
|
|
205
|
+
bank_account_model : django_ledger.BankAccountModel
|
|
206
|
+
The foreign key relating the import job to a specific bank account model.
|
|
207
|
+
ledger_model : django_ledger.LedgerModel
|
|
208
|
+
A one-to-one relation to the ledger model associated with the import job.
|
|
209
|
+
This field may be null or blank.
|
|
163
210
|
completed : bool
|
|
164
211
|
Indicates whether the import job has been completed.
|
|
165
212
|
objects : ImportJobModelManager
|
|
166
|
-
|
|
213
|
+
The default manager for the model.
|
|
214
|
+
|
|
215
|
+
Meta
|
|
216
|
+
----
|
|
217
|
+
This class is abstract and serves as a base for other models.
|
|
218
|
+
It includes additional metadata such as field verbose names
|
|
219
|
+
and database indexing.
|
|
167
220
|
"""
|
|
221
|
+
|
|
168
222
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
169
223
|
description = models.CharField(max_length=200, verbose_name=_('Description'))
|
|
170
|
-
bank_account_model = models.ForeignKey(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
224
|
+
bank_account_model = models.ForeignKey(
|
|
225
|
+
'django_ledger.BankAccountModel',
|
|
226
|
+
on_delete=models.CASCADE,
|
|
227
|
+
verbose_name=_('Associated Bank Account Model'),
|
|
228
|
+
)
|
|
229
|
+
ledger_model = models.OneToOneField(
|
|
230
|
+
'django_ledger.LedgerModel',
|
|
231
|
+
editable=False,
|
|
232
|
+
on_delete=models.CASCADE,
|
|
233
|
+
verbose_name=_('Ledger Model'),
|
|
234
|
+
null=True,
|
|
235
|
+
blank=True,
|
|
236
|
+
)
|
|
237
|
+
completed = models.BooleanField(
|
|
238
|
+
default=False, verbose_name=_('Import Job Completed')
|
|
239
|
+
)
|
|
180
240
|
objects = ImportJobModelManager()
|
|
181
241
|
|
|
182
242
|
class Meta:
|
|
@@ -244,10 +304,9 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
244
304
|
True if both `ledger_model_id` and `bank_account_model_id` attributes
|
|
245
305
|
are set (not None), otherwise False.
|
|
246
306
|
"""
|
|
247
|
-
return all(
|
|
248
|
-
self.ledger_model_id is not None,
|
|
249
|
-
|
|
250
|
-
])
|
|
307
|
+
return all(
|
|
308
|
+
[self.ledger_model_id is not None, self.bank_account_model_id is not None]
|
|
309
|
+
)
|
|
251
310
|
|
|
252
311
|
def configure(self, commit: bool = True):
|
|
253
312
|
"""
|
|
@@ -268,15 +327,25 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
268
327
|
name=self.description
|
|
269
328
|
)
|
|
270
329
|
if commit:
|
|
271
|
-
self.save(
|
|
272
|
-
update_fields=[
|
|
273
|
-
'ledger_model'
|
|
274
|
-
])
|
|
330
|
+
self.save(update_fields=['ledger_model'])
|
|
275
331
|
|
|
276
332
|
def get_delete_message(self) -> str:
|
|
277
333
|
return _(f'Are you sure you want to delete Import Job {self.description}?')
|
|
278
334
|
|
|
279
335
|
|
|
336
|
+
class StagedTransactionModelValidationError(ValidationError):
|
|
337
|
+
"""
|
|
338
|
+
A custom exception class that represents errors during staged model validation.
|
|
339
|
+
|
|
340
|
+
This exception is a specialized type of ValidationError that can be raised
|
|
341
|
+
during the validation process of staged models. It is intended to provide
|
|
342
|
+
an explicit representation of validation failures specifically designed for
|
|
343
|
+
use cases involving staged models in the application.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
|
|
280
349
|
class StagedTransactionModelQuerySet(QuerySet):
|
|
281
350
|
"""
|
|
282
351
|
Represents a custom QuerySet for handling staged transaction models.
|
|
@@ -353,6 +422,21 @@ class StagedTransactionModelQuerySet(QuerySet):
|
|
|
353
422
|
|
|
354
423
|
|
|
355
424
|
class StagedTransactionModelManager(Manager):
|
|
425
|
+
"""
|
|
426
|
+
Manager for staged transaction models to provide custom querysets.
|
|
427
|
+
|
|
428
|
+
This manager is customized to enhance query access for staged transaction models.
|
|
429
|
+
The main functionality includes fetching related fields, adding annotations to
|
|
430
|
+
facilitate business logic computations, and sorting the resulting queryset. It
|
|
431
|
+
incorporates annotations to compute field values like entity slug, child transaction
|
|
432
|
+
mappings, grouping IDs, readiness for import, and eligibility for splitting into
|
|
433
|
+
journal entries. The manager simplifies accessing such precomputed fields.
|
|
434
|
+
|
|
435
|
+
Methods
|
|
436
|
+
-------
|
|
437
|
+
get_queryset():
|
|
438
|
+
Fetch and annotate the queryset with related fields and calculated annotations.
|
|
439
|
+
"""
|
|
356
440
|
|
|
357
441
|
def get_queryset(self):
|
|
358
442
|
"""
|
|
@@ -372,165 +456,280 @@ class StagedTransactionModelManager(Manager):
|
|
|
372
456
|
for staged transaction models.
|
|
373
457
|
"""
|
|
374
458
|
qs = StagedTransactionModelQuerySet(self.model, using=self._db)
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
459
|
+
return (
|
|
460
|
+
qs.select_related(
|
|
461
|
+
'account_model',
|
|
462
|
+
'unit_model',
|
|
463
|
+
'vendor_model',
|
|
464
|
+
'customer_model',
|
|
465
|
+
'transaction_model',
|
|
466
|
+
'transaction_model__journal_entry',
|
|
467
|
+
'transaction_model__account',
|
|
468
|
+
'import_job',
|
|
469
|
+
'import_job__bank_account_model__account_model',
|
|
470
|
+
# selecting parent data....
|
|
471
|
+
'parent',
|
|
472
|
+
'parent__account_model',
|
|
473
|
+
'parent__unit_model',
|
|
474
|
+
)
|
|
475
|
+
.annotate(
|
|
476
|
+
entity_slug=F('import_job__bank_account_model__entity_model__slug'),
|
|
477
|
+
entity_unit=F('transaction_model__journal_entry__entity_unit__name'),
|
|
478
|
+
_receipt_uuid=F('receiptmodel__uuid'),
|
|
479
|
+
children_count=Count('split_transaction_set'),
|
|
480
|
+
children_mapped_count=Count(
|
|
481
|
+
'split_transaction_set__account_model__uuid'
|
|
482
|
+
),
|
|
483
|
+
total_amount_split=Coalesce(
|
|
484
|
+
Sum('split_transaction_set__amount_split'),
|
|
485
|
+
Value(value=0.00, output_field=DecimalField()),
|
|
486
|
+
),
|
|
487
|
+
group_uuid=Case(
|
|
488
|
+
When(parent_id__isnull=True, then=F('uuid')),
|
|
489
|
+
When(parent_id__isnull=False, then=F('parent_id')),
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
.annotate(
|
|
493
|
+
children_mapping_pending_count=F('children_count')
|
|
494
|
+
- F('children_mapped_count'),
|
|
495
|
+
)
|
|
496
|
+
.annotate(
|
|
497
|
+
children_mapping_done=Case(
|
|
498
|
+
When(children_mapping_pending_count=0, then=True),
|
|
499
|
+
default=False,
|
|
500
|
+
output_field=BooleanField(),
|
|
413
501
|
),
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
Q(
|
|
419
|
-
Q(
|
|
420
|
-
Q(parent__isnull=True)
|
|
421
|
-
Q(transaction_model__isnull=True)
|
|
502
|
+
ready_to_import=Case(
|
|
503
|
+
# single transaction...
|
|
504
|
+
When(
|
|
505
|
+
condition=(
|
|
506
|
+
Q(children_count__exact=0)
|
|
507
|
+
& Q(account_model__isnull=False)
|
|
508
|
+
& Q(parent__isnull=True)
|
|
509
|
+
& Q(transaction_model__isnull=True)
|
|
510
|
+
& (
|
|
511
|
+
(
|
|
512
|
+
# transactions with no receipt...
|
|
513
|
+
Q(receipt_type__isnull=True)
|
|
514
|
+
& Q(vendor_model__isnull=True)
|
|
515
|
+
& Q(customer_model__isnull=True)
|
|
516
|
+
)
|
|
517
|
+
| (
|
|
518
|
+
# transaction with receipt...
|
|
519
|
+
Q(receipt_type__isnull=False)
|
|
520
|
+
& (
|
|
521
|
+
(
|
|
522
|
+
Q(vendor_model__isnull=False)
|
|
523
|
+
& Q(customer_model__isnull=True)
|
|
524
|
+
)
|
|
525
|
+
| (
|
|
526
|
+
Q(vendor_model__isnull=True)
|
|
527
|
+
& Q(customer_model__isnull=False)
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
),
|
|
533
|
+
then=True,
|
|
422
534
|
),
|
|
423
|
-
|
|
535
|
+
# is children, mapped and all parent amount is split...
|
|
536
|
+
When(
|
|
537
|
+
condition=(
|
|
538
|
+
# no receipt type selected...
|
|
539
|
+
# will import the transaction as is...
|
|
540
|
+
(
|
|
541
|
+
Q(children_count__gt=0)
|
|
542
|
+
& Q(receipt_type__isnull=True)
|
|
543
|
+
& Q(children_count=F('children_mapped_count'))
|
|
544
|
+
& Q(total_amount_split__exact=F('amount'))
|
|
545
|
+
& Q(parent__isnull=True)
|
|
546
|
+
& Q(transaction_model__isnull=True)
|
|
547
|
+
)
|
|
548
|
+
# receipt type is assigned... at least a customer or vendor is selected...
|
|
549
|
+
| (
|
|
550
|
+
Q(children_count__gt=0)
|
|
551
|
+
& Q(receipt_type__isnull=False)
|
|
552
|
+
& (
|
|
553
|
+
(
|
|
554
|
+
Q(vendor_model__isnull=False)
|
|
555
|
+
& Q(customer_model__isnull=True)
|
|
556
|
+
)
|
|
557
|
+
| (
|
|
558
|
+
Q(vendor_model__isnull=True)
|
|
559
|
+
& Q(customer_model__isnull=False)
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
& Q(children_count=F('children_mapped_count'))
|
|
563
|
+
& Q(total_amount_split__exact=F('amount'))
|
|
564
|
+
& Q(parent__isnull=True)
|
|
565
|
+
& Q(transaction_model__isnull=True)
|
|
566
|
+
)
|
|
567
|
+
),
|
|
568
|
+
then=True,
|
|
569
|
+
),
|
|
570
|
+
default=False,
|
|
571
|
+
output_field=BooleanField(),
|
|
424
572
|
),
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
Q(
|
|
432
|
-
Q(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
Q(transaction_model__isnull=True)
|
|
573
|
+
can_split_into_je=Case(
|
|
574
|
+
When(
|
|
575
|
+
condition=(
|
|
576
|
+
Q(children_count__gt=0)
|
|
577
|
+
& Q(children_count=F('children_mapped_count'))
|
|
578
|
+
& Q(total_amount_split__exact=F('amount'))
|
|
579
|
+
& Q(parent__isnull=True)
|
|
580
|
+
& Q(transaction_model__isnull=True)
|
|
581
|
+
),
|
|
582
|
+
then=True,
|
|
436
583
|
),
|
|
437
|
-
|
|
584
|
+
default=False,
|
|
585
|
+
output_field=BooleanField(),
|
|
438
586
|
),
|
|
439
|
-
default=False,
|
|
440
|
-
output_field=BooleanField()
|
|
441
587
|
)
|
|
442
|
-
|
|
443
|
-
'date_posted',
|
|
444
|
-
'group_uuid',
|
|
445
|
-
'-children_count'
|
|
588
|
+
.order_by('date_posted', 'group_uuid', '-children_count')
|
|
446
589
|
)
|
|
447
590
|
|
|
448
591
|
|
|
449
592
|
class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
450
593
|
"""
|
|
451
|
-
|
|
594
|
+
Abstract model representing a staged transaction within the application.
|
|
452
595
|
|
|
453
|
-
This
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
role mapping, unit handling, and other important functionalities required for staged
|
|
458
|
-
transactions.
|
|
596
|
+
This class defines the structure, behavior, and relationships for staged transactions.
|
|
597
|
+
It helps manage various aspects of financial transactions such as splitting, associating
|
|
598
|
+
with accounts, vendors, or customers, and bundling transactions. The model is abstract
|
|
599
|
+
and serves as a basis for actual concrete models in the application.
|
|
459
600
|
|
|
460
601
|
Attributes
|
|
461
602
|
----------
|
|
462
603
|
uuid : UUIDField
|
|
463
|
-
The unique identifier for the transaction.
|
|
604
|
+
The unique identifier for the staged transaction.
|
|
464
605
|
parent : ForeignKey
|
|
465
|
-
|
|
606
|
+
The parent transaction associated with this transaction in case of split transactions.
|
|
466
607
|
import_job : ForeignKey
|
|
467
|
-
Reference to the
|
|
608
|
+
Reference to the import job this transaction belongs to.
|
|
468
609
|
fit_id : CharField
|
|
469
|
-
|
|
610
|
+
A unique identifier for the financial institution's transaction ID.
|
|
470
611
|
date_posted : DateField
|
|
471
612
|
The date on which the transaction was posted.
|
|
472
613
|
bundle_split : BooleanField
|
|
473
|
-
Indicates whether
|
|
474
|
-
activity : CharField
|
|
475
|
-
|
|
476
|
-
amount : DecimalField
|
|
477
|
-
The transaction amount
|
|
478
|
-
amount_split : DecimalField
|
|
479
|
-
The
|
|
480
|
-
name : CharField
|
|
481
|
-
The name or
|
|
482
|
-
memo : CharField
|
|
483
|
-
|
|
484
|
-
account_model : ForeignKey
|
|
485
|
-
The
|
|
486
|
-
unit_model : ForeignKey
|
|
487
|
-
The unit model
|
|
488
|
-
transaction_model : OneToOneField
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
614
|
+
Indicates whether related split transactions should be bundled.
|
|
615
|
+
activity : CharField, optional
|
|
616
|
+
The proposed activity type for the transaction.
|
|
617
|
+
amount : DecimalField, optional
|
|
618
|
+
The primary transaction amount (non-editable).
|
|
619
|
+
amount_split : DecimalField, optional
|
|
620
|
+
The amount for split transactions.
|
|
621
|
+
name : CharField, optional
|
|
622
|
+
The name or short description of the transaction.
|
|
623
|
+
memo : CharField, optional
|
|
624
|
+
A memo or additional note related to the transaction.
|
|
625
|
+
account_model : ForeignKey, optional
|
|
626
|
+
The associated account model for the transaction.
|
|
627
|
+
unit_model : ForeignKey, optional
|
|
628
|
+
The entity unit model associated with the transaction.
|
|
629
|
+
transaction_model : OneToOneField, optional
|
|
630
|
+
Reference to a specific transaction model.
|
|
631
|
+
receipt_type : CharField, optional
|
|
632
|
+
Type of receipt associated with the transaction.
|
|
633
|
+
vendor_model : ForeignKey, optional
|
|
634
|
+
The vendor associated with the transaction.
|
|
635
|
+
customer_model : ForeignKey, optional
|
|
636
|
+
The customer associated with the transaction.
|
|
637
|
+
|
|
638
|
+
Meta
|
|
639
|
+
----
|
|
640
|
+
abstract : bool
|
|
641
|
+
Indicates this is an abstract model.
|
|
642
|
+
verbose_name : str
|
|
643
|
+
The human-readable name for this model.
|
|
644
|
+
indexes : list
|
|
645
|
+
Indexes for optimizing database queries on certain fields.
|
|
646
|
+
|
|
647
|
+
Methods
|
|
648
|
+
-------
|
|
649
|
+
from_commit_dict(split_amount: Optional[Decimal]) -> List[Dict]
|
|
650
|
+
Converts a commit dictionary to a list of dictionaries containing transactional data.
|
|
651
|
+
to_commit_dict() -> List[Dict]
|
|
652
|
+
Converts the current transaction or its children into a list of commit dictionaries.
|
|
653
|
+
commit_dict(split_txs: bool) -> list
|
|
654
|
+
Generates a list of commit dictionaries or splits commit dictionaries based on staged amounts.
|
|
492
655
|
"""
|
|
656
|
+
|
|
493
657
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
494
|
-
parent = models.ForeignKey(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
658
|
+
parent = models.ForeignKey(
|
|
659
|
+
'self',
|
|
660
|
+
null=True,
|
|
661
|
+
blank=True,
|
|
662
|
+
editable=False,
|
|
663
|
+
on_delete=models.CASCADE,
|
|
664
|
+
related_name='split_transaction_set',
|
|
665
|
+
verbose_name=_('Parent Transaction'),
|
|
666
|
+
)
|
|
667
|
+
import_job = models.ForeignKey(
|
|
668
|
+
'django_ledger.ImportJobModel', on_delete=models.CASCADE
|
|
669
|
+
)
|
|
502
670
|
fit_id = models.CharField(max_length=100)
|
|
503
671
|
date_posted = models.DateField(verbose_name=_('Date Posted'))
|
|
504
|
-
bundle_split = models.BooleanField(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
672
|
+
bundle_split = models.BooleanField(
|
|
673
|
+
default=True, verbose_name=_('Bundle Split Transactions')
|
|
674
|
+
)
|
|
675
|
+
activity = models.CharField(
|
|
676
|
+
choices=JournalEntryModel.ACTIVITIES,
|
|
677
|
+
max_length=20,
|
|
678
|
+
null=True,
|
|
679
|
+
blank=True,
|
|
680
|
+
verbose_name=_('Proposed Activity'),
|
|
681
|
+
)
|
|
682
|
+
amount = models.DecimalField(
|
|
683
|
+
decimal_places=2, max_digits=15, editable=False, null=True, blank=True
|
|
684
|
+
)
|
|
685
|
+
amount_split = models.DecimalField(
|
|
686
|
+
decimal_places=2, max_digits=15, null=True, blank=True
|
|
687
|
+
)
|
|
516
688
|
name = models.CharField(max_length=200, blank=True, null=True)
|
|
517
689
|
memo = models.CharField(max_length=200, blank=True, null=True)
|
|
518
690
|
|
|
519
|
-
account_model = models.ForeignKey(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
691
|
+
account_model = models.ForeignKey(
|
|
692
|
+
'django_ledger.AccountModel', on_delete=models.RESTRICT, null=True, blank=True
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
unit_model = models.ForeignKey(
|
|
696
|
+
'django_ledger.EntityUnitModel',
|
|
697
|
+
on_delete=models.RESTRICT,
|
|
698
|
+
null=True,
|
|
699
|
+
blank=True,
|
|
700
|
+
verbose_name=_('Entity Unit Model'),
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
transaction_model = models.OneToOneField(
|
|
704
|
+
'django_ledger.TransactionModel',
|
|
705
|
+
on_delete=models.SET_NULL,
|
|
706
|
+
null=True,
|
|
707
|
+
blank=True,
|
|
708
|
+
)
|
|
709
|
+
receipt_type = models.CharField(
|
|
710
|
+
choices=ReceiptModel.RECEIPT_TYPES,
|
|
711
|
+
max_length=20,
|
|
712
|
+
null=True,
|
|
713
|
+
blank=True,
|
|
714
|
+
verbose_name=_('Receipt Type'),
|
|
715
|
+
help_text=_('The receipt type of the transaction.'),
|
|
716
|
+
)
|
|
717
|
+
vendor_model = models.ForeignKey(
|
|
718
|
+
'django_ledger.VendorModel',
|
|
719
|
+
on_delete=models.RESTRICT,
|
|
720
|
+
null=True,
|
|
721
|
+
blank=True,
|
|
722
|
+
verbose_name=_('Associated Vendor Model'),
|
|
723
|
+
help_text=_('The Vendor associated with the transaction.'),
|
|
724
|
+
)
|
|
725
|
+
customer_model = models.ForeignKey(
|
|
726
|
+
'django_ledger.CustomerModel',
|
|
727
|
+
on_delete=models.RESTRICT,
|
|
728
|
+
null=True,
|
|
729
|
+
blank=True,
|
|
730
|
+
verbose_name=_('Associated Customer Model'),
|
|
731
|
+
help_text=_('The Customer associated with the transaction.'),
|
|
732
|
+
)
|
|
534
733
|
|
|
535
734
|
objects = StagedTransactionModelManager()
|
|
536
735
|
|
|
@@ -551,6 +750,13 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
551
750
|
def __str__(self):
|
|
552
751
|
return f'{self.__class__.__name__}: {self.get_amount()}'
|
|
553
752
|
|
|
753
|
+
def get_entity_slug(self) -> str:
|
|
754
|
+
try:
|
|
755
|
+
return getattr(self, 'entity_slug')
|
|
756
|
+
except AttributeError:
|
|
757
|
+
pass
|
|
758
|
+
return self.account_model.coa_model.entity.slug
|
|
759
|
+
|
|
554
760
|
def from_commit_dict(self, split_amount: Optional[Decimal] = None) -> List[Dict]:
|
|
555
761
|
"""
|
|
556
762
|
Converts a commit dictionary to a list of dictionaries containing
|
|
@@ -579,13 +785,15 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
579
785
|
the staged transaction model.
|
|
580
786
|
"""
|
|
581
787
|
amt = split_amount if split_amount else self.amount
|
|
582
|
-
return [
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
788
|
+
return [
|
|
789
|
+
{
|
|
790
|
+
'account': self.import_job.bank_account_model.account_model,
|
|
791
|
+
'amount': abs(amt),
|
|
792
|
+
'tx_type': DEBIT if not amt < 0.00 else CREDIT,
|
|
793
|
+
'description': self.name,
|
|
794
|
+
'staged_tx_model': self,
|
|
795
|
+
}
|
|
796
|
+
]
|
|
589
797
|
|
|
590
798
|
def to_commit_dict(self) -> List[Dict]:
|
|
591
799
|
"""
|
|
@@ -607,26 +815,33 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
607
815
|
children_qs = self.split_transaction_set.all().prefetch_related(
|
|
608
816
|
'split_transaction_set',
|
|
609
817
|
'split_transaction_set__account_model',
|
|
610
|
-
'split_transaction_set__unit_model'
|
|
818
|
+
'split_transaction_set__unit_model',
|
|
611
819
|
)
|
|
612
|
-
return [
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
820
|
+
return [
|
|
821
|
+
{
|
|
822
|
+
'account': child_txs_model.account_model,
|
|
823
|
+
'amount': abs(child_txs_model.amount_split),
|
|
824
|
+
'amount_staged': child_txs_model.amount_split,
|
|
825
|
+
'unit_model': child_txs_model.unit_model,
|
|
826
|
+
'tx_type': CREDIT
|
|
827
|
+
if not child_txs_model.amount_split < 0.00
|
|
828
|
+
else DEBIT,
|
|
829
|
+
'description': child_txs_model.name,
|
|
830
|
+
'staged_tx_model': child_txs_model,
|
|
831
|
+
}
|
|
832
|
+
for child_txs_model in children_qs
|
|
833
|
+
]
|
|
834
|
+
return [
|
|
835
|
+
{
|
|
836
|
+
'account': self.account_model,
|
|
837
|
+
'amount': abs(self.amount),
|
|
838
|
+
'amount_staged': self.amount,
|
|
839
|
+
'unit_model': self.unit_model,
|
|
840
|
+
'tx_type': CREDIT if not self.amount < 0.00 else DEBIT,
|
|
841
|
+
'description': self.name,
|
|
842
|
+
'staged_tx_model': self,
|
|
843
|
+
}
|
|
844
|
+
]
|
|
630
845
|
|
|
631
846
|
def commit_dict(self, split_txs: bool = False):
|
|
632
847
|
"""
|
|
@@ -651,9 +866,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
651
866
|
to_commit = self.to_commit_dict()
|
|
652
867
|
return [
|
|
653
868
|
[
|
|
654
|
-
self.from_commit_dict(split_amount=to_split['amount_staged'])[0],
|
|
655
|
-
|
|
656
|
-
|
|
869
|
+
self.from_commit_dict(split_amount=to_split['amount_staged'])[0],
|
|
870
|
+
to_split,
|
|
871
|
+
]
|
|
872
|
+
for to_split in to_commit
|
|
657
873
|
]
|
|
658
874
|
return [self.from_commit_dict() + self.to_commit_dict()]
|
|
659
875
|
|
|
@@ -675,6 +891,26 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
675
891
|
return self.amount_split
|
|
676
892
|
return self.amount
|
|
677
893
|
|
|
894
|
+
def is_sales(self) -> bool:
|
|
895
|
+
if self.is_children():
|
|
896
|
+
return self.parent.is_sales()
|
|
897
|
+
return any(
|
|
898
|
+
[
|
|
899
|
+
self.receipt_type == ReceiptModel.SALES_RECEIPT,
|
|
900
|
+
self.receipt_type == ReceiptModel.SALES_REFUND,
|
|
901
|
+
]
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
def is_expense(self) -> bool:
|
|
905
|
+
if self.is_children():
|
|
906
|
+
return self.parent.is_expense()
|
|
907
|
+
return any(
|
|
908
|
+
[
|
|
909
|
+
self.receipt_type == ReceiptModel.EXPENSE_RECEIPT,
|
|
910
|
+
self.receipt_type == ReceiptModel.EXPENSE_REFUND,
|
|
911
|
+
]
|
|
912
|
+
)
|
|
913
|
+
|
|
678
914
|
def is_imported(self) -> bool:
|
|
679
915
|
"""
|
|
680
916
|
Determines if the necessary models have been imported for the system to function
|
|
@@ -687,10 +923,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
687
923
|
True if both `account_model_id` and `transaction_model_id` are not None,
|
|
688
924
|
indicating that the models have been successfully imported. False otherwise.
|
|
689
925
|
"""
|
|
690
|
-
return all(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
926
|
+
return all(
|
|
927
|
+
[
|
|
928
|
+
self.account_model_id is not None,
|
|
929
|
+
self.transaction_model_id is not None,
|
|
930
|
+
]
|
|
931
|
+
)
|
|
694
932
|
|
|
695
933
|
def is_pending(self) -> bool:
|
|
696
934
|
"""
|
|
@@ -738,10 +976,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
738
976
|
bool
|
|
739
977
|
True if the entry is a single, standalone entry; False otherwise.
|
|
740
978
|
"""
|
|
741
|
-
return all([
|
|
742
|
-
not self.is_children(),
|
|
743
|
-
not self.has_children()
|
|
744
|
-
])
|
|
979
|
+
return all([not self.is_children(), not self.has_children()])
|
|
745
980
|
|
|
746
981
|
def is_children(self) -> bool:
|
|
747
982
|
"""
|
|
@@ -756,9 +991,11 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
756
991
|
True if the object has a valid `parent_id`, indicating it is a child entity;
|
|
757
992
|
False otherwise.
|
|
758
993
|
"""
|
|
759
|
-
return all(
|
|
760
|
-
|
|
761
|
-
|
|
994
|
+
return all(
|
|
995
|
+
[
|
|
996
|
+
self.parent_id is not None,
|
|
997
|
+
]
|
|
998
|
+
)
|
|
762
999
|
|
|
763
1000
|
def has_activity(self) -> bool:
|
|
764
1001
|
"""
|
|
@@ -795,6 +1032,51 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
795
1032
|
return False
|
|
796
1033
|
return getattr(self, 'children_count') > 0
|
|
797
1034
|
|
|
1035
|
+
@property
|
|
1036
|
+
def receipt_uuid(self):
|
|
1037
|
+
try:
|
|
1038
|
+
return getattr(self, '_receipt_uuid')
|
|
1039
|
+
except AttributeError:
|
|
1040
|
+
pass
|
|
1041
|
+
return self.receiptmodel.uuid
|
|
1042
|
+
|
|
1043
|
+
def can_migrate_receipt(self) -> bool:
|
|
1044
|
+
if self.is_children():
|
|
1045
|
+
return self.parent.receipt_type is not None
|
|
1046
|
+
return self.receipt_type is not None
|
|
1047
|
+
|
|
1048
|
+
def has_receipt(self) -> bool:
|
|
1049
|
+
if self.is_children():
|
|
1050
|
+
return all(
|
|
1051
|
+
[self.parent.receipt_type is not None, self.receipt_uuid is not None]
|
|
1052
|
+
)
|
|
1053
|
+
return all([self.receipt_type is not None, self.receipt_uuid is not None])
|
|
1054
|
+
|
|
1055
|
+
def has_mapped_receipt(self) -> bool:
|
|
1056
|
+
if all(
|
|
1057
|
+
[
|
|
1058
|
+
self.receipt_type is not None,
|
|
1059
|
+
any(
|
|
1060
|
+
[
|
|
1061
|
+
all(
|
|
1062
|
+
[
|
|
1063
|
+
self.vendor_model_id is not None,
|
|
1064
|
+
self.customer_model_id is None,
|
|
1065
|
+
]
|
|
1066
|
+
),
|
|
1067
|
+
all(
|
|
1068
|
+
[
|
|
1069
|
+
self.vendor_model_id is None,
|
|
1070
|
+
self.customer_model_id is not None,
|
|
1071
|
+
]
|
|
1072
|
+
),
|
|
1073
|
+
]
|
|
1074
|
+
),
|
|
1075
|
+
]
|
|
1076
|
+
):
|
|
1077
|
+
return True
|
|
1078
|
+
return False
|
|
1079
|
+
|
|
798
1080
|
def can_split(self) -> bool:
|
|
799
1081
|
"""
|
|
800
1082
|
Determines if the current object can be split based on its child status.
|
|
@@ -808,7 +1090,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
808
1090
|
`True` if the object has no children and can be split, otherwise
|
|
809
1091
|
`False`.
|
|
810
1092
|
"""
|
|
811
|
-
return not self.
|
|
1093
|
+
return all([not self.has_children(), not self.has_receipt()])
|
|
812
1094
|
|
|
813
1095
|
def can_have_unit(self) -> bool:
|
|
814
1096
|
"""
|
|
@@ -832,18 +1114,23 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
832
1114
|
if self.is_single():
|
|
833
1115
|
return True
|
|
834
1116
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1117
|
+
# parent transaction...
|
|
1118
|
+
if all(
|
|
1119
|
+
[
|
|
1120
|
+
self.has_children(),
|
|
1121
|
+
# self.has_activity(),
|
|
1122
|
+
# self.are_all_children_mapped(),
|
|
1123
|
+
self.bundle_split is True,
|
|
1124
|
+
]
|
|
1125
|
+
):
|
|
841
1126
|
return True
|
|
842
1127
|
|
|
843
|
-
if all(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1128
|
+
if all(
|
|
1129
|
+
[
|
|
1130
|
+
self.is_children(),
|
|
1131
|
+
self.parent.bundle_split is False if self.parent_id else False,
|
|
1132
|
+
]
|
|
1133
|
+
):
|
|
847
1134
|
return True
|
|
848
1135
|
|
|
849
1136
|
return False
|
|
@@ -863,7 +1150,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
863
1150
|
"""
|
|
864
1151
|
return not self.has_children()
|
|
865
1152
|
|
|
866
|
-
def
|
|
1153
|
+
def can_have_activity(self) -> bool:
|
|
1154
|
+
return self.account_model_id is not None
|
|
1155
|
+
|
|
1156
|
+
def can_migrate(self, as_split: bool = False) -> bool:
|
|
867
1157
|
"""
|
|
868
1158
|
Determines whether the object is ready for importing data and can optionally
|
|
869
1159
|
be split into "je" (journal entries) for import if applicable.
|
|
@@ -895,9 +1185,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
895
1185
|
can_split_into_je = getattr(self, 'can_split_into_je')
|
|
896
1186
|
if can_split_into_je and as_split:
|
|
897
1187
|
return True
|
|
898
|
-
return all([
|
|
899
|
-
|
|
900
|
-
|
|
1188
|
+
return all([self.is_role_mapping_valid(raise_exception=False)])
|
|
1189
|
+
|
|
1190
|
+
def can_import(self) -> bool:
|
|
1191
|
+
return self.can_migrate()
|
|
901
1192
|
|
|
902
1193
|
def add_split(self, raise_exception: bool = True, commit: bool = True, n: int = 1):
|
|
903
1194
|
"""
|
|
@@ -943,8 +1234,9 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
943
1234
|
date_posted=self.date_posted,
|
|
944
1235
|
amount=None,
|
|
945
1236
|
amount_split=Decimal('0.00'),
|
|
946
|
-
name=f'SPLIT: {self.name}'
|
|
947
|
-
)
|
|
1237
|
+
name=f'SPLIT: {self.name}',
|
|
1238
|
+
)
|
|
1239
|
+
for _ in range(n)
|
|
948
1240
|
]
|
|
949
1241
|
|
|
950
1242
|
for txs in new_txs:
|
|
@@ -1005,10 +1297,18 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1005
1297
|
if self.has_children():
|
|
1006
1298
|
split_txs_qs = self.split_transaction_set.all()
|
|
1007
1299
|
if all([txs.is_mapped() for txs in split_txs_qs]):
|
|
1008
|
-
return set(
|
|
1300
|
+
return set(
|
|
1301
|
+
[
|
|
1302
|
+
txs.account_model.role
|
|
1303
|
+
for txs in split_txs_qs
|
|
1304
|
+
if txs.account_model.role != ASSET_CA_CASH
|
|
1305
|
+
]
|
|
1306
|
+
)
|
|
1009
1307
|
return set()
|
|
1010
1308
|
|
|
1011
|
-
def get_prospect_je_activity_try(
|
|
1309
|
+
def get_prospect_je_activity_try(
|
|
1310
|
+
self, raise_exception: bool = True, force_update: bool = False
|
|
1311
|
+
) -> Optional[str]:
|
|
1012
1312
|
"""
|
|
1013
1313
|
Retrieve or attempt to fetch the journal entry activity for the current prospect object.
|
|
1014
1314
|
|
|
@@ -1035,11 +1335,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1035
1335
|
"""
|
|
1036
1336
|
ready_to_import = getattr(self, 'ready_to_import')
|
|
1037
1337
|
if (not self.has_activity() and ready_to_import) or force_update:
|
|
1038
|
-
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
1039
1338
|
role_set = self.get_import_role_set()
|
|
1040
1339
|
if role_set is not None:
|
|
1041
1340
|
try:
|
|
1042
|
-
self.activity = JournalEntryModel.get_activity_from_roles(
|
|
1341
|
+
self.activity = JournalEntryModel.get_activity_from_roles(
|
|
1342
|
+
role_set=role_set
|
|
1343
|
+
)
|
|
1043
1344
|
self.save(update_fields=['activity'])
|
|
1044
1345
|
return self.activity
|
|
1045
1346
|
except ValidationError as e:
|
|
@@ -1076,9 +1377,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1076
1377
|
otherwise None.
|
|
1077
1378
|
"""
|
|
1078
1379
|
activity = self.get_prospect_je_activity_try(raise_exception=False)
|
|
1079
|
-
if activity
|
|
1080
|
-
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
1081
|
-
return JournalEntryModel.MAP_ACTIVITIES[activity]
|
|
1380
|
+
return JournalEntryModel.MAP_ACTIVITIES[activity] if activity else None
|
|
1082
1381
|
|
|
1083
1382
|
def is_role_mapping_valid(self, raise_exception: bool = False) -> bool:
|
|
1084
1383
|
"""
|
|
@@ -1106,7 +1405,9 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1106
1405
|
"""
|
|
1107
1406
|
if not self.has_activity():
|
|
1108
1407
|
try:
|
|
1109
|
-
activity = self.get_prospect_je_activity_try(
|
|
1408
|
+
activity = self.get_prospect_je_activity_try(
|
|
1409
|
+
raise_exception=raise_exception
|
|
1410
|
+
)
|
|
1110
1411
|
if activity is None:
|
|
1111
1412
|
return False
|
|
1112
1413
|
self.activity = activity
|
|
@@ -1117,7 +1418,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1117
1418
|
return False
|
|
1118
1419
|
return True
|
|
1119
1420
|
|
|
1120
|
-
def
|
|
1421
|
+
def get_coa_account_model(self) -> AccountModel:
|
|
1422
|
+
return self.import_job.bank_account_model.account_model
|
|
1423
|
+
|
|
1424
|
+
def migrate_transactions(self, split_txs: bool = False):
|
|
1121
1425
|
"""
|
|
1122
1426
|
Migrate transactional data to the ledger model by processing the provided
|
|
1123
1427
|
transactions and committing them. This process involves using the provided
|
|
@@ -1138,27 +1442,128 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1138
1442
|
The saved objects are staged with appropriate models to retain the
|
|
1139
1443
|
transaction state.
|
|
1140
1444
|
"""
|
|
1141
|
-
if self.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1445
|
+
if self.has_receipt():
|
|
1446
|
+
raise StagedTransactionModelValidationError(
|
|
1447
|
+
'Migrate transactions can only be performed on non-receipt transactions. Use migrate_receipt() instead.'
|
|
1448
|
+
)
|
|
1449
|
+
if not self.can_migrate():
|
|
1450
|
+
raise StagedTransactionModelValidationError(
|
|
1451
|
+
f'Transaction {self.uuid} is not ready to be migrated'
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
commit_dict = self.commit_dict(split_txs=split_txs)
|
|
1455
|
+
import_job = self.import_job
|
|
1456
|
+
ledger_model = import_job.ledger_model
|
|
1145
1457
|
|
|
1146
|
-
|
|
1458
|
+
if len(commit_dict) > 0:
|
|
1459
|
+
with transaction.atomic():
|
|
1460
|
+
staged_to_save = list()
|
|
1147
1461
|
for je_data in commit_dict:
|
|
1148
|
-
unit_model =
|
|
1462
|
+
unit_model = (
|
|
1463
|
+
self.unit_model
|
|
1464
|
+
if not split_txs
|
|
1465
|
+
else commit_dict[0][1]['unit_model']
|
|
1466
|
+
)
|
|
1149
1467
|
_, _ = ledger_model.commit_txs(
|
|
1150
1468
|
je_timestamp=self.date_posted,
|
|
1151
1469
|
je_unit_model=unit_model,
|
|
1152
1470
|
je_txs=je_data,
|
|
1153
1471
|
je_desc=self.memo,
|
|
1154
1472
|
je_posted=False,
|
|
1155
|
-
force_je_retrieval=False
|
|
1473
|
+
force_je_retrieval=False,
|
|
1156
1474
|
)
|
|
1157
|
-
staged_to_save
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1475
|
+
staged_to_save += [i['staged_tx_model'] for i in je_data]
|
|
1476
|
+
# staged_to_save = set(i['staged_tx_model'] for i in je_data)
|
|
1477
|
+
# for i in staged_to_save:
|
|
1478
|
+
# i.save(update_fields=['transaction_model', 'updated'])
|
|
1479
|
+
staged_to_save = set(staged_to_save)
|
|
1480
|
+
for i in staged_to_save:
|
|
1481
|
+
i.save(update_fields=['transaction_model', 'updated'])
|
|
1482
|
+
|
|
1483
|
+
def migrate_receipt(self, receipt_date: Optional[date | datetime] = None):
|
|
1484
|
+
if not self.can_migrate_receipt():
|
|
1485
|
+
raise StagedTransactionModelValidationError(
|
|
1486
|
+
'Migrate receipts can only be performed on receipt transactions. Use migrate_transactions() instead.'
|
|
1487
|
+
)
|
|
1488
|
+
if not self.can_migrate():
|
|
1489
|
+
raise StagedTransactionModelValidationError(
|
|
1490
|
+
f'Transaction {self.uuid} is not ready to be migratedd'
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
with transaction.atomic():
|
|
1494
|
+
receipt_model: ReceiptModel = self.generate_receipt_model(
|
|
1495
|
+
receipt_date=receipt_date, commit=True
|
|
1496
|
+
)
|
|
1497
|
+
receipt_model.migrate_receipt()
|
|
1498
|
+
|
|
1499
|
+
def generate_receipt_model(
|
|
1500
|
+
self, receipt_date: Optional[date] = None, commit: bool = False
|
|
1501
|
+
) -> ReceiptModel:
|
|
1502
|
+
if receipt_date:
|
|
1503
|
+
if isinstance(receipt_date, datetime):
|
|
1504
|
+
receipt_date = receipt_date.date()
|
|
1505
|
+
|
|
1506
|
+
receipt_model = ReceiptModel()
|
|
1507
|
+
|
|
1508
|
+
if commit:
|
|
1509
|
+
receipt_model.configure(
|
|
1510
|
+
receipt_date=receipt_date,
|
|
1511
|
+
entity_model=self.entity_slug,
|
|
1512
|
+
amount=abs(self.amount),
|
|
1513
|
+
unit_model=self.unit_model,
|
|
1514
|
+
receipt_type=self.receipt_type,
|
|
1515
|
+
vendor_model=self.vendor_model if self.is_expense() else None,
|
|
1516
|
+
customer_model=self.customer_model if self.is_sales() else None,
|
|
1517
|
+
charge_account=self.get_coa_account_model(),
|
|
1518
|
+
receipt_account=self.account_model,
|
|
1519
|
+
staged_transaction_model=self,
|
|
1520
|
+
commit=True,
|
|
1521
|
+
)
|
|
1522
|
+
|
|
1523
|
+
return receipt_model
|
|
1524
|
+
|
|
1525
|
+
# UNDO
|
|
1526
|
+
def undo_import(self, raise_exception: bool = True):
|
|
1527
|
+
"""
|
|
1528
|
+
Undo import operation for a staged transaction. This method handles the deletion
|
|
1529
|
+
of related receipt or transaction models, as well as their associated data,
|
|
1530
|
+
if applicable. If no related data is available to undo, raises a validation
|
|
1531
|
+
error specifying that there is nothing to undo.
|
|
1532
|
+
|
|
1533
|
+
Raises
|
|
1534
|
+
------
|
|
1535
|
+
ValidationError
|
|
1536
|
+
If there is no receipt model or transaction model to undo.
|
|
1537
|
+
|
|
1538
|
+
"""
|
|
1539
|
+
with transaction.atomic():
|
|
1540
|
+
# Receipt import case...
|
|
1541
|
+
try:
|
|
1542
|
+
receipt_model = self.receiptmodel
|
|
1543
|
+
except ObjectDoesNotExist:
|
|
1544
|
+
receipt_model = None
|
|
1545
|
+
|
|
1546
|
+
if receipt_model is not None:
|
|
1547
|
+
receipt_model.delete()
|
|
1548
|
+
|
|
1549
|
+
if self.transaction_model_id:
|
|
1550
|
+
self.transaction_model = None
|
|
1551
|
+
self.save(update_fields=['transaction_model', 'updated'])
|
|
1552
|
+
return
|
|
1553
|
+
|
|
1554
|
+
# Transaction Import case....
|
|
1555
|
+
if self.transaction_model_id:
|
|
1556
|
+
tx_model = self.transaction_model
|
|
1557
|
+
journal_entry_model = tx_model.journal_entry
|
|
1558
|
+
journal_entry_model.delete()
|
|
1559
|
+
self.transaction_model = None
|
|
1560
|
+
self.save(update_fields=['transaction_model', 'updated'])
|
|
1561
|
+
return
|
|
1562
|
+
|
|
1563
|
+
if raise_exception:
|
|
1564
|
+
raise StagedTransactionModelValidationError(
|
|
1565
|
+
message=_('Nothing to undo for this staged transaction.')
|
|
1566
|
+
)
|
|
1162
1567
|
|
|
1163
1568
|
def clean(self, verify: bool = False):
|
|
1164
1569
|
if self.has_children():
|
|
@@ -1171,13 +1576,34 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1171
1576
|
if self.parent_id:
|
|
1172
1577
|
self.unit_model = self.parent.unit_model
|
|
1173
1578
|
|
|
1579
|
+
if not self.can_have_activity():
|
|
1580
|
+
self.activity = None
|
|
1581
|
+
|
|
1582
|
+
if self.is_children():
|
|
1583
|
+
self.vendor_model = None
|
|
1584
|
+
self.customer_model = None
|
|
1585
|
+
|
|
1586
|
+
if all([self.has_children(), self.has_receipt(), not self.bundle_split]):
|
|
1587
|
+
raise StagedTransactionModelValidationError(
|
|
1588
|
+
'Receipt transactions cannot be split into multiple receipts.'
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1174
1591
|
if verify:
|
|
1175
1592
|
self.is_role_mapping_valid(raise_exception=True)
|
|
1176
1593
|
|
|
1177
1594
|
|
|
1178
1595
|
class ImportJobModel(ImportJobModelAbstract):
|
|
1179
1596
|
"""
|
|
1180
|
-
|
|
1597
|
+
Represents the ImportJobModel class.
|
|
1598
|
+
|
|
1599
|
+
This class inherits from ImportJobModelAbstract and is specifically designed
|
|
1600
|
+
to provide implementations and metadata for import job entries. It defines the
|
|
1601
|
+
Meta subclass, which overrides the abstract attribute indicating whether this
|
|
1602
|
+
model is abstract or not.
|
|
1603
|
+
|
|
1604
|
+
Attributes
|
|
1605
|
+
----------
|
|
1606
|
+
None
|
|
1181
1607
|
"""
|
|
1182
1608
|
|
|
1183
1609
|
class Meta(ImportJobModelAbstract.Meta):
|
|
@@ -1185,10 +1611,35 @@ class ImportJobModel(ImportJobModelAbstract):
|
|
|
1185
1611
|
|
|
1186
1612
|
|
|
1187
1613
|
def importjobmodel_presave(instance: ImportJobModel, **kwargs):
|
|
1614
|
+
"""
|
|
1615
|
+
Handles pre-save validation for ImportJobModel instances.
|
|
1616
|
+
|
|
1617
|
+
This function ensures that the provided `ImportJobModel` instance is properly
|
|
1618
|
+
configured and validates its integrity with respect to related entities, such as
|
|
1619
|
+
the Bank Account Model and Ledger Model.
|
|
1620
|
+
|
|
1621
|
+
Parameters
|
|
1622
|
+
----------
|
|
1623
|
+
instance : ImportJobModel
|
|
1624
|
+
The instance of ImportJobModel being saved.
|
|
1625
|
+
**kwargs
|
|
1626
|
+
Additional arguments passed to the pre-save signal.
|
|
1627
|
+
|
|
1628
|
+
Raises
|
|
1629
|
+
------
|
|
1630
|
+
ImportJobModelValidationError
|
|
1631
|
+
If the Bank Account Model associated with the instance does not match the
|
|
1632
|
+
entity ID of the Ledger Model.
|
|
1633
|
+
"""
|
|
1188
1634
|
if instance.is_configured():
|
|
1189
|
-
if
|
|
1635
|
+
if (
|
|
1636
|
+
instance.bank_account_model.entity_model_id
|
|
1637
|
+
!= instance.ledger_model.entity_id
|
|
1638
|
+
):
|
|
1190
1639
|
raise ImportJobModelValidationError(
|
|
1191
|
-
message=_(
|
|
1640
|
+
message=_(
|
|
1641
|
+
'Invalid Bank Account for LedgerModel. No matching Entity Model found.'
|
|
1642
|
+
)
|
|
1192
1643
|
)
|
|
1193
1644
|
|
|
1194
1645
|
|
|
@@ -1197,8 +1648,52 @@ pre_save.connect(importjobmodel_presave, sender=ImportJobModel)
|
|
|
1197
1648
|
|
|
1198
1649
|
class StagedTransactionModel(StagedTransactionModelAbstract):
|
|
1199
1650
|
"""
|
|
1200
|
-
|
|
1651
|
+
Represents a concrete implementation of a staged transaction model.
|
|
1652
|
+
|
|
1653
|
+
This class is derived from `StagedTransactionModelAbstract` and provides
|
|
1654
|
+
a concrete implementation by overriding its meta-configuration. It is
|
|
1655
|
+
used to define the structure and behavior of the staged transaction
|
|
1656
|
+
records in the system.
|
|
1657
|
+
|
|
1658
|
+
Attributes
|
|
1659
|
+
----------
|
|
1660
|
+
Meta : class
|
|
1661
|
+
A nested class that extends the meta-configuration of
|
|
1662
|
+
the `StagedTransactionModelAbstract.Meta` class, specifying
|
|
1663
|
+
that the model is not abstract.
|
|
1201
1664
|
"""
|
|
1202
1665
|
|
|
1203
1666
|
class Meta(StagedTransactionModelAbstract.Meta):
|
|
1204
1667
|
abstract = False
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def stagedtransactionmodel_presave(instance: StagedTransactionModel, **kwargs):
|
|
1671
|
+
"""
|
|
1672
|
+
Validates the instance of StagedTransactionModel before saving.
|
|
1673
|
+
|
|
1674
|
+
This function ensures that either `customer_model_id` or `vendor_model_id`
|
|
1675
|
+
is set on the given instance but not both. If both attributes are present,
|
|
1676
|
+
an exception is raised to prevent invalid data from being saved.
|
|
1677
|
+
|
|
1678
|
+
Parameters
|
|
1679
|
+
----------
|
|
1680
|
+
instance : StagedTransactionModel
|
|
1681
|
+
The instance of the model to be validated.
|
|
1682
|
+
|
|
1683
|
+
kwargs : dict
|
|
1684
|
+
Additional keyword arguments, which are currently not used but
|
|
1685
|
+
are included for potential future extensibility.
|
|
1686
|
+
|
|
1687
|
+
Raises
|
|
1688
|
+
------
|
|
1689
|
+
StagedModelValidationError
|
|
1690
|
+
If both `customer_model_id` and `vendor_model_id` are set on the instance.
|
|
1691
|
+
|
|
1692
|
+
"""
|
|
1693
|
+
if all([instance.customer_model_id, instance.vendor_model_id]):
|
|
1694
|
+
raise StagedTransactionModelValidationError(
|
|
1695
|
+
message=_('Either customer or vendor model allowed.'),
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
pre_save.connect(stagedtransactionmodel_presave, sender=StagedTransactionModel)
|