django-ledger 0.8.0__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/forms/account.py +45 -46
- django_ledger/forms/data_import.py +182 -63
- django_ledger/io/io_core.py +507 -374
- django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
- django_ledger/models/__init__.py +2 -1
- django_ledger/models/bill.py +337 -300
- django_ledger/models/customer.py +47 -34
- django_ledger/models/data_import.py +770 -289
- django_ledger/models/entity.py +882 -637
- django_ledger/models/mixins.py +421 -280
- django_ledger/models/receipt.py +1083 -0
- django_ledger/models/transactions.py +105 -41
- django_ledger/models/unit.py +42 -30
- django_ledger/models/utils.py +12 -2
- django_ledger/models/vendor.py +85 -66
- django_ledger/settings.py +1 -0
- 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 +3 -1
- 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/layouts/base.html +1 -1
- 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 +3 -2
- django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
- django_ledger/templatetags/django_ledger.py +338 -191
- django_ledger/urls/__init__.py +1 -0
- 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/customer.py +56 -14
- django_ledger/views/data_import.py +119 -66
- django_ledger/views/mixins.py +112 -86
- django_ledger/views/receipt.py +294 -0
- django_ledger/views/vendor.py +53 -14
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/METADATA +1 -1
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/RECORD +51 -40
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.0.dist-info → django_ledger-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -8,33 +8,60 @@ 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
|
"""
|
|
12
|
+
|
|
13
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
|
|
30
45
|
from django_ledger.settings import DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR
|
|
31
46
|
|
|
32
47
|
|
|
33
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
|
+
|
|
34
60
|
pass
|
|
35
61
|
|
|
36
62
|
|
|
37
63
|
class ImportJobModelQuerySet(QuerySet):
|
|
64
|
+
""" """
|
|
38
65
|
|
|
39
66
|
def for_user(self, user_model) -> 'ImportJobModelQuerySet':
|
|
40
67
|
"""
|
|
@@ -59,22 +86,21 @@ class ImportJobModelQuerySet(QuerySet):
|
|
|
59
86
|
if user_model.is_superuser:
|
|
60
87
|
return self
|
|
61
88
|
return self.filter(
|
|
62
|
-
Q(bank_account_model__entity_model__admin=user_model)
|
|
63
|
-
Q(bank_account_model__entity_model__managers__in=[user_model])
|
|
64
|
-
|
|
89
|
+
Q(bank_account_model__entity_model__admin=user_model)
|
|
90
|
+
| Q(bank_account_model__entity_model__managers__in=[user_model])
|
|
65
91
|
)
|
|
66
92
|
|
|
67
93
|
|
|
68
94
|
class ImportJobModelManager(Manager):
|
|
69
95
|
"""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
This
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
78
104
|
"""
|
|
79
105
|
|
|
80
106
|
def get_queryset(self) -> ImportJobModelQuerySet:
|
|
@@ -102,41 +128,47 @@ class ImportJobModelManager(Manager):
|
|
|
102
128
|
(no pending transactions or total count is zero).
|
|
103
129
|
"""
|
|
104
130
|
qs = ImportJobModelQuerySet(self.model, using=self._db)
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
)
|
|
129
159
|
)
|
|
130
160
|
|
|
131
161
|
@deprecated_entity_slug_behavior
|
|
132
|
-
def for_entity(
|
|
162
|
+
def for_entity(
|
|
163
|
+
self, entity_model: Union[EntityModel, str, UUID] = None, **kwargs
|
|
164
|
+
) -> ImportJobModelQuerySet:
|
|
133
165
|
qs = self.get_queryset()
|
|
134
166
|
if 'user_model' in kwargs:
|
|
135
167
|
warnings.warn(
|
|
136
168
|
'user_model parameter is deprecated and will be removed in a future release. '
|
|
137
169
|
'Use for_user(user_model).for_entity(entity_model) instead to keep current behavior.',
|
|
138
170
|
DeprecationWarning,
|
|
139
|
-
stacklevel=2
|
|
171
|
+
stacklevel=2,
|
|
140
172
|
)
|
|
141
173
|
if DJANGO_LEDGER_USE_DEPRECATED_BEHAVIOR:
|
|
142
174
|
qs = qs.for_user(kwargs['user_model'])
|
|
@@ -156,41 +188,55 @@ class ImportJobModelManager(Manager):
|
|
|
156
188
|
|
|
157
189
|
class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
158
190
|
"""
|
|
159
|
-
|
|
191
|
+
Represents an abstract model for managing import jobs.
|
|
160
192
|
|
|
161
|
-
This
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
166
198
|
|
|
167
199
|
Attributes
|
|
168
200
|
----------
|
|
169
201
|
uuid : UUID
|
|
170
|
-
|
|
202
|
+
The universally unique identifier for the import job.
|
|
171
203
|
description : str
|
|
172
|
-
|
|
173
|
-
bank_account_model : BankAccountModel
|
|
174
|
-
|
|
175
|
-
ledger_model : LedgerModel
|
|
176
|
-
|
|
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.
|
|
177
210
|
completed : bool
|
|
178
211
|
Indicates whether the import job has been completed.
|
|
179
212
|
objects : ImportJobModelManager
|
|
180
|
-
|
|
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.
|
|
181
220
|
"""
|
|
221
|
+
|
|
182
222
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
183
223
|
description = models.CharField(max_length=200, verbose_name=_('Description'))
|
|
184
|
-
bank_account_model = models.ForeignKey(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
)
|
|
194
240
|
objects = ImportJobModelManager()
|
|
195
241
|
|
|
196
242
|
class Meta:
|
|
@@ -258,10 +304,9 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
258
304
|
True if both `ledger_model_id` and `bank_account_model_id` attributes
|
|
259
305
|
are set (not None), otherwise False.
|
|
260
306
|
"""
|
|
261
|
-
return all(
|
|
262
|
-
self.ledger_model_id is not None,
|
|
263
|
-
|
|
264
|
-
])
|
|
307
|
+
return all(
|
|
308
|
+
[self.ledger_model_id is not None, self.bank_account_model_id is not None]
|
|
309
|
+
)
|
|
265
310
|
|
|
266
311
|
def configure(self, commit: bool = True):
|
|
267
312
|
"""
|
|
@@ -282,15 +327,25 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
282
327
|
name=self.description
|
|
283
328
|
)
|
|
284
329
|
if commit:
|
|
285
|
-
self.save(
|
|
286
|
-
update_fields=[
|
|
287
|
-
'ledger_model'
|
|
288
|
-
])
|
|
330
|
+
self.save(update_fields=['ledger_model'])
|
|
289
331
|
|
|
290
332
|
def get_delete_message(self) -> str:
|
|
291
333
|
return _(f'Are you sure you want to delete Import Job {self.description}?')
|
|
292
334
|
|
|
293
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
|
+
|
|
294
349
|
class StagedTransactionModelQuerySet(QuerySet):
|
|
295
350
|
"""
|
|
296
351
|
Represents a custom QuerySet for handling staged transaction models.
|
|
@@ -367,6 +422,21 @@ class StagedTransactionModelQuerySet(QuerySet):
|
|
|
367
422
|
|
|
368
423
|
|
|
369
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
|
+
"""
|
|
370
440
|
|
|
371
441
|
def get_queryset(self):
|
|
372
442
|
"""
|
|
@@ -386,165 +456,280 @@ class StagedTransactionModelManager(Manager):
|
|
|
386
456
|
for staged transaction models.
|
|
387
457
|
"""
|
|
388
458
|
qs = StagedTransactionModelQuerySet(self.model, using=self._db)
|
|
389
|
-
return
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
condition=(
|
|
421
|
-
Q(children_count__exact=0) &
|
|
422
|
-
Q(account_model__isnull=False) &
|
|
423
|
-
Q(parent__isnull=True) &
|
|
424
|
-
Q(transaction_model__isnull=True)
|
|
425
|
-
),
|
|
426
|
-
then=True
|
|
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')),
|
|
427
490
|
),
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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(),
|
|
501
|
+
),
|
|
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,
|
|
534
|
+
),
|
|
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,
|
|
436
569
|
),
|
|
437
|
-
|
|
570
|
+
default=False,
|
|
571
|
+
output_field=BooleanField(),
|
|
438
572
|
),
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
Q(
|
|
446
|
-
Q(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
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,
|
|
450
583
|
),
|
|
451
|
-
|
|
584
|
+
default=False,
|
|
585
|
+
output_field=BooleanField(),
|
|
452
586
|
),
|
|
453
|
-
default=False,
|
|
454
|
-
output_field=BooleanField()
|
|
455
587
|
)
|
|
456
|
-
|
|
457
|
-
'date_posted',
|
|
458
|
-
'group_uuid',
|
|
459
|
-
'-children_count'
|
|
588
|
+
.order_by('date_posted', 'group_uuid', '-children_count')
|
|
460
589
|
)
|
|
461
590
|
|
|
462
591
|
|
|
463
592
|
class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
464
593
|
"""
|
|
465
|
-
|
|
594
|
+
Abstract model representing a staged transaction within the application.
|
|
466
595
|
|
|
467
|
-
This
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
role mapping, unit handling, and other important functionalities required for staged
|
|
472
|
-
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.
|
|
473
600
|
|
|
474
601
|
Attributes
|
|
475
602
|
----------
|
|
476
603
|
uuid : UUIDField
|
|
477
|
-
The unique identifier for the transaction.
|
|
604
|
+
The unique identifier for the staged transaction.
|
|
478
605
|
parent : ForeignKey
|
|
479
|
-
|
|
606
|
+
The parent transaction associated with this transaction in case of split transactions.
|
|
480
607
|
import_job : ForeignKey
|
|
481
|
-
Reference to the
|
|
608
|
+
Reference to the import job this transaction belongs to.
|
|
482
609
|
fit_id : CharField
|
|
483
|
-
|
|
610
|
+
A unique identifier for the financial institution's transaction ID.
|
|
484
611
|
date_posted : DateField
|
|
485
612
|
The date on which the transaction was posted.
|
|
486
613
|
bundle_split : BooleanField
|
|
487
|
-
Indicates whether
|
|
488
|
-
activity : CharField
|
|
489
|
-
|
|
490
|
-
amount : DecimalField
|
|
491
|
-
The transaction amount
|
|
492
|
-
amount_split : DecimalField
|
|
493
|
-
The
|
|
494
|
-
name : CharField
|
|
495
|
-
The name or
|
|
496
|
-
memo : CharField
|
|
497
|
-
|
|
498
|
-
account_model : ForeignKey
|
|
499
|
-
The
|
|
500
|
-
unit_model : ForeignKey
|
|
501
|
-
The unit model
|
|
502
|
-
transaction_model : OneToOneField
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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.
|
|
506
655
|
"""
|
|
656
|
+
|
|
507
657
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
508
|
-
parent = models.ForeignKey(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
+
)
|
|
516
670
|
fit_id = models.CharField(max_length=100)
|
|
517
671
|
date_posted = models.DateField(verbose_name=_('Date Posted'))
|
|
518
|
-
bundle_split = models.BooleanField(
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
)
|
|
530
688
|
name = models.CharField(max_length=200, blank=True, null=True)
|
|
531
689
|
memo = models.CharField(max_length=200, blank=True, null=True)
|
|
532
690
|
|
|
533
|
-
account_model = models.ForeignKey(
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
)
|
|
548
733
|
|
|
549
734
|
objects = StagedTransactionModelManager()
|
|
550
735
|
|
|
@@ -565,6 +750,13 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
565
750
|
def __str__(self):
|
|
566
751
|
return f'{self.__class__.__name__}: {self.get_amount()}'
|
|
567
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
|
+
|
|
568
760
|
def from_commit_dict(self, split_amount: Optional[Decimal] = None) -> List[Dict]:
|
|
569
761
|
"""
|
|
570
762
|
Converts a commit dictionary to a list of dictionaries containing
|
|
@@ -593,13 +785,15 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
593
785
|
the staged transaction model.
|
|
594
786
|
"""
|
|
595
787
|
amt = split_amount if split_amount else self.amount
|
|
596
|
-
return [
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
]
|
|
603
797
|
|
|
604
798
|
def to_commit_dict(self) -> List[Dict]:
|
|
605
799
|
"""
|
|
@@ -621,26 +815,33 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
621
815
|
children_qs = self.split_transaction_set.all().prefetch_related(
|
|
622
816
|
'split_transaction_set',
|
|
623
817
|
'split_transaction_set__account_model',
|
|
624
|
-
'split_transaction_set__unit_model'
|
|
818
|
+
'split_transaction_set__unit_model',
|
|
625
819
|
)
|
|
626
|
-
return [
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
]
|
|
644
845
|
|
|
645
846
|
def commit_dict(self, split_txs: bool = False):
|
|
646
847
|
"""
|
|
@@ -665,9 +866,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
665
866
|
to_commit = self.to_commit_dict()
|
|
666
867
|
return [
|
|
667
868
|
[
|
|
668
|
-
self.from_commit_dict(split_amount=to_split['amount_staged'])[0],
|
|
669
|
-
|
|
670
|
-
|
|
869
|
+
self.from_commit_dict(split_amount=to_split['amount_staged'])[0],
|
|
870
|
+
to_split,
|
|
871
|
+
]
|
|
872
|
+
for to_split in to_commit
|
|
671
873
|
]
|
|
672
874
|
return [self.from_commit_dict() + self.to_commit_dict()]
|
|
673
875
|
|
|
@@ -689,6 +891,26 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
689
891
|
return self.amount_split
|
|
690
892
|
return self.amount
|
|
691
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
|
+
|
|
692
914
|
def is_imported(self) -> bool:
|
|
693
915
|
"""
|
|
694
916
|
Determines if the necessary models have been imported for the system to function
|
|
@@ -701,10 +923,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
701
923
|
True if both `account_model_id` and `transaction_model_id` are not None,
|
|
702
924
|
indicating that the models have been successfully imported. False otherwise.
|
|
703
925
|
"""
|
|
704
|
-
return all(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
926
|
+
return all(
|
|
927
|
+
[
|
|
928
|
+
self.account_model_id is not None,
|
|
929
|
+
self.transaction_model_id is not None,
|
|
930
|
+
]
|
|
931
|
+
)
|
|
708
932
|
|
|
709
933
|
def is_pending(self) -> bool:
|
|
710
934
|
"""
|
|
@@ -752,10 +976,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
752
976
|
bool
|
|
753
977
|
True if the entry is a single, standalone entry; False otherwise.
|
|
754
978
|
"""
|
|
755
|
-
return all([
|
|
756
|
-
not self.is_children(),
|
|
757
|
-
not self.has_children()
|
|
758
|
-
])
|
|
979
|
+
return all([not self.is_children(), not self.has_children()])
|
|
759
980
|
|
|
760
981
|
def is_children(self) -> bool:
|
|
761
982
|
"""
|
|
@@ -770,9 +991,11 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
770
991
|
True if the object has a valid `parent_id`, indicating it is a child entity;
|
|
771
992
|
False otherwise.
|
|
772
993
|
"""
|
|
773
|
-
return all(
|
|
774
|
-
|
|
775
|
-
|
|
994
|
+
return all(
|
|
995
|
+
[
|
|
996
|
+
self.parent_id is not None,
|
|
997
|
+
]
|
|
998
|
+
)
|
|
776
999
|
|
|
777
1000
|
def has_activity(self) -> bool:
|
|
778
1001
|
"""
|
|
@@ -809,6 +1032,51 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
809
1032
|
return False
|
|
810
1033
|
return getattr(self, 'children_count') > 0
|
|
811
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
|
+
|
|
812
1080
|
def can_split(self) -> bool:
|
|
813
1081
|
"""
|
|
814
1082
|
Determines if the current object can be split based on its child status.
|
|
@@ -822,7 +1090,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
822
1090
|
`True` if the object has no children and can be split, otherwise
|
|
823
1091
|
`False`.
|
|
824
1092
|
"""
|
|
825
|
-
return not self.
|
|
1093
|
+
return all([not self.has_children(), not self.has_receipt()])
|
|
826
1094
|
|
|
827
1095
|
def can_have_unit(self) -> bool:
|
|
828
1096
|
"""
|
|
@@ -846,18 +1114,23 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
846
1114
|
if self.is_single():
|
|
847
1115
|
return True
|
|
848
1116
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
+
):
|
|
855
1126
|
return True
|
|
856
1127
|
|
|
857
|
-
if all(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1128
|
+
if all(
|
|
1129
|
+
[
|
|
1130
|
+
self.is_children(),
|
|
1131
|
+
self.parent.bundle_split is False if self.parent_id else False,
|
|
1132
|
+
]
|
|
1133
|
+
):
|
|
861
1134
|
return True
|
|
862
1135
|
|
|
863
1136
|
return False
|
|
@@ -877,7 +1150,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
877
1150
|
"""
|
|
878
1151
|
return not self.has_children()
|
|
879
1152
|
|
|
880
|
-
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:
|
|
881
1157
|
"""
|
|
882
1158
|
Determines whether the object is ready for importing data and can optionally
|
|
883
1159
|
be split into "je" (journal entries) for import if applicable.
|
|
@@ -909,9 +1185,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
909
1185
|
can_split_into_je = getattr(self, 'can_split_into_je')
|
|
910
1186
|
if can_split_into_je and as_split:
|
|
911
1187
|
return True
|
|
912
|
-
return all([
|
|
913
|
-
|
|
914
|
-
|
|
1188
|
+
return all([self.is_role_mapping_valid(raise_exception=False)])
|
|
1189
|
+
|
|
1190
|
+
def can_import(self) -> bool:
|
|
1191
|
+
return self.can_migrate()
|
|
915
1192
|
|
|
916
1193
|
def add_split(self, raise_exception: bool = True, commit: bool = True, n: int = 1):
|
|
917
1194
|
"""
|
|
@@ -957,8 +1234,9 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
957
1234
|
date_posted=self.date_posted,
|
|
958
1235
|
amount=None,
|
|
959
1236
|
amount_split=Decimal('0.00'),
|
|
960
|
-
name=f'SPLIT: {self.name}'
|
|
961
|
-
)
|
|
1237
|
+
name=f'SPLIT: {self.name}',
|
|
1238
|
+
)
|
|
1239
|
+
for _ in range(n)
|
|
962
1240
|
]
|
|
963
1241
|
|
|
964
1242
|
for txs in new_txs:
|
|
@@ -1019,10 +1297,18 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1019
1297
|
if self.has_children():
|
|
1020
1298
|
split_txs_qs = self.split_transaction_set.all()
|
|
1021
1299
|
if all([txs.is_mapped() for txs in split_txs_qs]):
|
|
1022
|
-
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
|
+
)
|
|
1023
1307
|
return set()
|
|
1024
1308
|
|
|
1025
|
-
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]:
|
|
1026
1312
|
"""
|
|
1027
1313
|
Retrieve or attempt to fetch the journal entry activity for the current prospect object.
|
|
1028
1314
|
|
|
@@ -1049,11 +1335,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1049
1335
|
"""
|
|
1050
1336
|
ready_to_import = getattr(self, 'ready_to_import')
|
|
1051
1337
|
if (not self.has_activity() and ready_to_import) or force_update:
|
|
1052
|
-
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
1053
1338
|
role_set = self.get_import_role_set()
|
|
1054
1339
|
if role_set is not None:
|
|
1055
1340
|
try:
|
|
1056
|
-
self.activity = JournalEntryModel.get_activity_from_roles(
|
|
1341
|
+
self.activity = JournalEntryModel.get_activity_from_roles(
|
|
1342
|
+
role_set=role_set
|
|
1343
|
+
)
|
|
1057
1344
|
self.save(update_fields=['activity'])
|
|
1058
1345
|
return self.activity
|
|
1059
1346
|
except ValidationError as e:
|
|
@@ -1090,9 +1377,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1090
1377
|
otherwise None.
|
|
1091
1378
|
"""
|
|
1092
1379
|
activity = self.get_prospect_je_activity_try(raise_exception=False)
|
|
1093
|
-
if activity
|
|
1094
|
-
JournalEntryModel = lazy_loader.get_journal_entry_model()
|
|
1095
|
-
return JournalEntryModel.MAP_ACTIVITIES[activity]
|
|
1380
|
+
return JournalEntryModel.MAP_ACTIVITIES[activity] if activity else None
|
|
1096
1381
|
|
|
1097
1382
|
def is_role_mapping_valid(self, raise_exception: bool = False) -> bool:
|
|
1098
1383
|
"""
|
|
@@ -1120,7 +1405,9 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1120
1405
|
"""
|
|
1121
1406
|
if not self.has_activity():
|
|
1122
1407
|
try:
|
|
1123
|
-
activity = self.get_prospect_je_activity_try(
|
|
1408
|
+
activity = self.get_prospect_je_activity_try(
|
|
1409
|
+
raise_exception=raise_exception
|
|
1410
|
+
)
|
|
1124
1411
|
if activity is None:
|
|
1125
1412
|
return False
|
|
1126
1413
|
self.activity = activity
|
|
@@ -1131,7 +1418,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1131
1418
|
return False
|
|
1132
1419
|
return True
|
|
1133
1420
|
|
|
1134
|
-
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):
|
|
1135
1425
|
"""
|
|
1136
1426
|
Migrate transactional data to the ledger model by processing the provided
|
|
1137
1427
|
transactions and committing them. This process involves using the provided
|
|
@@ -1152,27 +1442,128 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1152
1442
|
The saved objects are staged with appropriate models to retain the
|
|
1153
1443
|
transaction state.
|
|
1154
1444
|
"""
|
|
1155
|
-
if self.
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
|
1159
1457
|
|
|
1160
|
-
|
|
1458
|
+
if len(commit_dict) > 0:
|
|
1459
|
+
with transaction.atomic():
|
|
1460
|
+
staged_to_save = list()
|
|
1161
1461
|
for je_data in commit_dict:
|
|
1162
|
-
unit_model =
|
|
1462
|
+
unit_model = (
|
|
1463
|
+
self.unit_model
|
|
1464
|
+
if not split_txs
|
|
1465
|
+
else commit_dict[0][1]['unit_model']
|
|
1466
|
+
)
|
|
1163
1467
|
_, _ = ledger_model.commit_txs(
|
|
1164
1468
|
je_timestamp=self.date_posted,
|
|
1165
1469
|
je_unit_model=unit_model,
|
|
1166
1470
|
je_txs=je_data,
|
|
1167
1471
|
je_desc=self.memo,
|
|
1168
1472
|
je_posted=False,
|
|
1169
|
-
force_je_retrieval=False
|
|
1473
|
+
force_je_retrieval=False,
|
|
1170
1474
|
)
|
|
1171
|
-
staged_to_save
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
+
)
|
|
1176
1567
|
|
|
1177
1568
|
def clean(self, verify: bool = False):
|
|
1178
1569
|
if self.has_children():
|
|
@@ -1185,13 +1576,34 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1185
1576
|
if self.parent_id:
|
|
1186
1577
|
self.unit_model = self.parent.unit_model
|
|
1187
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
|
+
|
|
1188
1591
|
if verify:
|
|
1189
1592
|
self.is_role_mapping_valid(raise_exception=True)
|
|
1190
1593
|
|
|
1191
1594
|
|
|
1192
1595
|
class ImportJobModel(ImportJobModelAbstract):
|
|
1193
1596
|
"""
|
|
1194
|
-
|
|
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
|
|
1195
1607
|
"""
|
|
1196
1608
|
|
|
1197
1609
|
class Meta(ImportJobModelAbstract.Meta):
|
|
@@ -1199,10 +1611,35 @@ class ImportJobModel(ImportJobModelAbstract):
|
|
|
1199
1611
|
|
|
1200
1612
|
|
|
1201
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
|
+
"""
|
|
1202
1634
|
if instance.is_configured():
|
|
1203
|
-
if
|
|
1635
|
+
if (
|
|
1636
|
+
instance.bank_account_model.entity_model_id
|
|
1637
|
+
!= instance.ledger_model.entity_id
|
|
1638
|
+
):
|
|
1204
1639
|
raise ImportJobModelValidationError(
|
|
1205
|
-
message=_(
|
|
1640
|
+
message=_(
|
|
1641
|
+
'Invalid Bank Account for LedgerModel. No matching Entity Model found.'
|
|
1642
|
+
)
|
|
1206
1643
|
)
|
|
1207
1644
|
|
|
1208
1645
|
|
|
@@ -1211,8 +1648,52 @@ pre_save.connect(importjobmodel_presave, sender=ImportJobModel)
|
|
|
1211
1648
|
|
|
1212
1649
|
class StagedTransactionModel(StagedTransactionModelAbstract):
|
|
1213
1650
|
"""
|
|
1214
|
-
|
|
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.
|
|
1215
1664
|
"""
|
|
1216
1665
|
|
|
1217
1666
|
class Meta(StagedTransactionModelAbstract.Meta):
|
|
1218
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)
|