django-ledger 0.8.2.1__py3-none-any.whl → 0.8.2.3__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/data_import.py +104 -111
- django_ledger/io/roles.py +238 -303
- django_ledger/migrations/0027_alter_accountmodel_role_alter_receiptmodel_amount_and_more.py +159 -0
- django_ledger/models/__init__.py +1 -0
- django_ledger/models/chart_of_accounts.py +32 -90
- django_ledger/models/coa_default.py +1 -0
- django_ledger/models/customer.py +17 -26
- django_ledger/models/data_import.py +562 -182
- django_ledger/models/receipt.py +92 -123
- django_ledger/templates/django_ledger/data_import/data_import_job_txs.html +6 -0
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +68 -36
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +8 -10
- django_ledger/templates/django_ledger/receipt/receipt_detail.html +4 -2
- django_ledger/urls/data_import.py +3 -0
- django_ledger/views/data_import.py +47 -27
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.3.dist-info}/METADATA +1 -1
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.3.dist-info}/RECORD +22 -21
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.3.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.3.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.3.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.3.dist-info}/top_level.txt +0 -0
|
@@ -16,7 +16,7 @@ from decimal import Decimal
|
|
|
16
16
|
from typing import Dict, List, Optional, Set, Union
|
|
17
17
|
from uuid import UUID, uuid4
|
|
18
18
|
|
|
19
|
-
from django.core.exceptions import
|
|
19
|
+
from django.core.exceptions import ValidationError
|
|
20
20
|
from django.db import models, transaction
|
|
21
21
|
from django.db.models import (
|
|
22
22
|
BooleanField,
|
|
@@ -33,6 +33,7 @@ from django.db.models import (
|
|
|
33
33
|
)
|
|
34
34
|
from django.db.models.functions import Coalesce
|
|
35
35
|
from django.db.models.signals import pre_save
|
|
36
|
+
from django.urls import reverse
|
|
36
37
|
from django.utils.translation import gettext_lazy as _
|
|
37
38
|
|
|
38
39
|
from django_ledger.io import ASSET_CA_CASH, CREDIT, DEBIT
|
|
@@ -159,9 +160,7 @@ class ImportJobModelManager(Manager):
|
|
|
159
160
|
)
|
|
160
161
|
|
|
161
162
|
@deprecated_entity_slug_behavior
|
|
162
|
-
def for_entity(
|
|
163
|
-
self, entity_model: Union[EntityModel, str, UUID] = None, **kwargs
|
|
164
|
-
) -> ImportJobModelQuerySet:
|
|
163
|
+
def for_entity(self, entity_model: Union[EntityModel, str, UUID] = None, **kwargs) -> ImportJobModelQuerySet:
|
|
165
164
|
qs = self.get_queryset()
|
|
166
165
|
if 'user_model' in kwargs:
|
|
167
166
|
warnings.warn(
|
|
@@ -211,12 +210,6 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
211
210
|
Indicates whether the import job has been completed.
|
|
212
211
|
objects : ImportJobModelManager
|
|
213
212
|
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.
|
|
220
213
|
"""
|
|
221
214
|
|
|
222
215
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
@@ -234,9 +227,7 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
234
227
|
null=True,
|
|
235
228
|
blank=True,
|
|
236
229
|
)
|
|
237
|
-
completed = models.BooleanField(
|
|
238
|
-
default=False, verbose_name=_('Import Job Completed')
|
|
239
|
-
)
|
|
230
|
+
completed = models.BooleanField(default=False, verbose_name=_('Import Job Completed'))
|
|
240
231
|
objects = ImportJobModelManager()
|
|
241
232
|
|
|
242
233
|
class Meta:
|
|
@@ -304,9 +295,7 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
304
295
|
True if both `ledger_model_id` and `bank_account_model_id` attributes
|
|
305
296
|
are set (not None), otherwise False.
|
|
306
297
|
"""
|
|
307
|
-
return all(
|
|
308
|
-
[self.ledger_model_id is not None, self.bank_account_model_id is not None]
|
|
309
|
-
)
|
|
298
|
+
return all([self.ledger_model_id is not None, self.bank_account_model_id is not None])
|
|
310
299
|
|
|
311
300
|
def configure(self, commit: bool = True):
|
|
312
301
|
"""
|
|
@@ -323,15 +312,31 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
|
|
|
323
312
|
"""
|
|
324
313
|
if not self.is_configured():
|
|
325
314
|
if self.ledger_model_id is None:
|
|
326
|
-
self.ledger_model = self.bank_account_model.entity_model.create_ledger(
|
|
327
|
-
name=self.description
|
|
328
|
-
)
|
|
315
|
+
self.ledger_model = self.bank_account_model.entity_model.create_ledger(name=self.description)
|
|
329
316
|
if commit:
|
|
330
317
|
self.save(update_fields=['ledger_model'])
|
|
331
318
|
|
|
332
319
|
def get_delete_message(self) -> str:
|
|
333
320
|
return _(f'Are you sure you want to delete Import Job {self.description}?')
|
|
334
321
|
|
|
322
|
+
def get_data_import_url(self) -> str:
|
|
323
|
+
return reverse(
|
|
324
|
+
'django_ledger:data-import-job-txs',
|
|
325
|
+
kwargs={
|
|
326
|
+
'entity_slug': self.entity_slug,
|
|
327
|
+
'job_pk': self.uuid,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def get_data_import_reset_url(self) -> str:
|
|
332
|
+
return reverse(
|
|
333
|
+
'django_ledger:data-import-job-txs-undo',
|
|
334
|
+
kwargs={
|
|
335
|
+
'entity_slug': self.entity_slug,
|
|
336
|
+
'job_pk': self.uuid,
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
|
|
335
340
|
|
|
336
341
|
class StagedTransactionModelValidationError(ValidationError):
|
|
337
342
|
"""
|
|
@@ -356,22 +361,102 @@ class StagedTransactionModelQuerySet(QuerySet):
|
|
|
356
361
|
relationships.
|
|
357
362
|
"""
|
|
358
363
|
|
|
364
|
+
def for_entity(self, entity_model: 'Union[EntityModel, UUID, str]') -> 'StagedTransactionModelQuerySet':
|
|
365
|
+
"""
|
|
366
|
+
Filters the queryset based on the type of the provided entity model.
|
|
367
|
+
|
|
368
|
+
The method accepts entity identifiers of varying formats including instances
|
|
369
|
+
of `EntityModel`, UUIDs, or string slugs and filters the query accordingly.
|
|
370
|
+
If an invalid type is provided, a validation error is raised.
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
entity_model : Union[EntityModel, UUID, str]
|
|
375
|
+
The entity identifier used to filter the queryset. Can be an `EntityModel` instance,
|
|
376
|
+
a UUID, or a string representing the slug of the entity.
|
|
377
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
StagedTransactionModelQuerySet
|
|
381
|
+
A filtered queryset of staged transactions based on the provided entity model.
|
|
382
|
+
|
|
383
|
+
Raises
|
|
384
|
+
------
|
|
385
|
+
StagedTransactionModelValidationError
|
|
386
|
+
If the `entity_model` provided is not an instance of `EntityModel`, UUID, or string.
|
|
387
|
+
"""
|
|
388
|
+
if isinstance(entity_model, UUID):
|
|
389
|
+
return self.filter(import_job__ledger_model__entity_id=entity_model)
|
|
390
|
+
elif isinstance(entity_model, str):
|
|
391
|
+
return self.filter(import_job__ledger_model__entity__slug__exact=entity_model)
|
|
392
|
+
elif isinstance(entity_model, EntityModel):
|
|
393
|
+
return self.filter(import_job__ledger_model__entity=entity_model)
|
|
394
|
+
raise StagedTransactionModelValidationError(
|
|
395
|
+
message=f'Must pass an instance of EntityMode, UUID or str. Got {entity_model.__class__.__name__}'
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def for_import_job(self, import_job_model: 'Union[ImportJobModel | UUID]') -> 'StagedTransactionModelQuerySet':
|
|
399
|
+
"""
|
|
400
|
+
Filters the queryset based on the provided import job model or UUID.
|
|
401
|
+
|
|
402
|
+
This method evaluates whether the argument is an instance of ImportJobModel or
|
|
403
|
+
UUID and filters the queryset accordingly. If the argument is neither of these
|
|
404
|
+
types, it raises a validation error.
|
|
405
|
+
|
|
406
|
+
Parameters
|
|
407
|
+
----------
|
|
408
|
+
import_job_model : Union[ImportJobModel, UUID]
|
|
409
|
+
The import job model instance or UUID to filter the queryset by.
|
|
410
|
+
|
|
411
|
+
Returns
|
|
412
|
+
-------
|
|
413
|
+
StagedTransactionModelQuerySet
|
|
414
|
+
A queryset filtered by the given import job model or UUID.
|
|
415
|
+
|
|
416
|
+
Raises
|
|
417
|
+
------
|
|
418
|
+
StagedTransactionModelValidationError
|
|
419
|
+
If the provided argument is not an instance of ImportJobModel or UUID.
|
|
420
|
+
"""
|
|
421
|
+
if isinstance(import_job_model, ImportJobModel):
|
|
422
|
+
return self.filter(import_job=import_job_model)
|
|
423
|
+
elif isinstance(import_job_model, UUID):
|
|
424
|
+
return self.filter(import_job_id=import_job_model)
|
|
425
|
+
raise StagedTransactionModelValidationError(
|
|
426
|
+
message=f'Must pass an instance of ImportJobModel, UUID. Got {import_job_model.__class__.__name__}'
|
|
427
|
+
)
|
|
428
|
+
|
|
359
429
|
def is_pending(self):
|
|
360
430
|
"""
|
|
361
431
|
Determines if there are any pending transactions.
|
|
362
432
|
|
|
363
433
|
This method filters the objects in the queryset to determine whether there
|
|
364
434
|
are any transactions that are pending (i.e., have a null transaction_model).
|
|
365
|
-
|
|
366
|
-
|
|
435
|
+
Additionally, it includes parent transactions (not bundled) that have at least
|
|
436
|
+
one child transaction still pending import.
|
|
367
437
|
|
|
368
438
|
Returns
|
|
369
439
|
-------
|
|
370
440
|
QuerySet
|
|
371
|
-
A QuerySet containing objects with a null `transaction_model
|
|
441
|
+
A QuerySet containing objects with a null `transaction_model`, or parent
|
|
442
|
+
transactions (not bundled) with pending children.
|
|
372
443
|
|
|
373
444
|
"""
|
|
374
|
-
|
|
445
|
+
parents_with_pending_children = Q(
|
|
446
|
+
parent__isnull=True, bundle_split=False, children_mapping_done=False, children_import_pending_count__gt=0
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return self.filter(
|
|
450
|
+
Q(
|
|
451
|
+
transaction_model__isnull=True,
|
|
452
|
+
)
|
|
453
|
+
| parents_with_pending_children
|
|
454
|
+
).exclude(
|
|
455
|
+
bundle_split=False,
|
|
456
|
+
transaction_model__isnull=True,
|
|
457
|
+
children_count__gt=0,
|
|
458
|
+
children_import_pending_count=0
|
|
459
|
+
)
|
|
375
460
|
|
|
376
461
|
def is_imported(self):
|
|
377
462
|
"""
|
|
@@ -379,13 +464,19 @@ class StagedTransactionModelQuerySet(QuerySet):
|
|
|
379
464
|
related transaction model. This function checks whether the `transaction_model`
|
|
380
465
|
field in the related objects is non-null.
|
|
381
466
|
|
|
467
|
+
Additionally, it includes non-bundled parent transactions only if they have at
|
|
468
|
+
least one imported child (i.e., a child with a non-null `transaction_model`).
|
|
469
|
+
This ensures a parent is not considered imported until at least one child is
|
|
470
|
+
imported.
|
|
471
|
+
|
|
382
472
|
Returns
|
|
383
473
|
-------
|
|
384
474
|
QuerySet
|
|
385
|
-
A filtered queryset containing
|
|
386
|
-
|
|
475
|
+
A filtered queryset containing objects where the `transaction_model` is not
|
|
476
|
+
null, plus non-bundled parents that have at least one imported child.
|
|
387
477
|
"""
|
|
388
|
-
|
|
478
|
+
parents_with_imported_children = Q(parent__isnull=True, bundle_split=False, imported_count__gt=0)
|
|
479
|
+
return self.filter(Q(transaction_model__isnull=False) | parents_with_imported_children)
|
|
389
480
|
|
|
390
481
|
def is_parent(self):
|
|
391
482
|
"""
|
|
@@ -438,7 +529,7 @@ class StagedTransactionModelManager(Manager):
|
|
|
438
529
|
Fetch and annotate the queryset with related fields and calculated annotations.
|
|
439
530
|
"""
|
|
440
531
|
|
|
441
|
-
def get_queryset(self):
|
|
532
|
+
def get_queryset(self) -> StagedTransactionModelQuerySet:
|
|
442
533
|
"""
|
|
443
534
|
Fetch and annotate the queryset for staged transaction models to include additional
|
|
444
535
|
related fields and calculated annotations for further processing and sorting.
|
|
@@ -471,15 +562,14 @@ class StagedTransactionModelManager(Manager):
|
|
|
471
562
|
'parent',
|
|
472
563
|
'parent__account_model',
|
|
473
564
|
'parent__unit_model',
|
|
565
|
+
'receiptmodel',
|
|
474
566
|
)
|
|
475
567
|
.annotate(
|
|
476
|
-
|
|
568
|
+
_entity_slug=F('import_job__bank_account_model__entity_model__slug'),
|
|
477
569
|
entity_unit=F('transaction_model__journal_entry__entity_unit__name'),
|
|
478
|
-
_receipt_uuid=F('receiptmodel__uuid'),
|
|
479
570
|
children_count=Count('split_transaction_set'),
|
|
480
|
-
children_mapped_count=Count(
|
|
481
|
-
|
|
482
|
-
),
|
|
571
|
+
children_mapped_count=Count('split_transaction_set__account_model__uuid'),
|
|
572
|
+
imported_count=Count('split_transaction_set__transaction_model_id'),
|
|
483
573
|
total_amount_split=Coalesce(
|
|
484
574
|
Sum('split_transaction_set__amount_split'),
|
|
485
575
|
Value(value=0.00, output_field=DecimalField()),
|
|
@@ -488,10 +578,11 @@ class StagedTransactionModelManager(Manager):
|
|
|
488
578
|
When(parent_id__isnull=True, then=F('uuid')),
|
|
489
579
|
When(parent_id__isnull=False, then=F('parent_id')),
|
|
490
580
|
),
|
|
581
|
+
_receipt_uuid=F('receiptmodel__uuid'),
|
|
491
582
|
)
|
|
492
583
|
.annotate(
|
|
493
|
-
children_mapping_pending_count=F('children_count')
|
|
494
|
-
- F('
|
|
584
|
+
children_mapping_pending_count=F('children_count') - F('children_mapped_count'),
|
|
585
|
+
children_import_pending_count=F('imported_count') - F('children_count'),
|
|
495
586
|
)
|
|
496
587
|
.annotate(
|
|
497
588
|
children_mapping_done=Case(
|
|
@@ -504,6 +595,8 @@ class StagedTransactionModelManager(Manager):
|
|
|
504
595
|
When(
|
|
505
596
|
condition=(
|
|
506
597
|
Q(children_count__exact=0)
|
|
598
|
+
& Q(bundle_split=True)
|
|
599
|
+
& Q(parent__isnull=True)
|
|
507
600
|
& Q(account_model__isnull=False)
|
|
508
601
|
& Q(parent__isnull=True)
|
|
509
602
|
& Q(transaction_model__isnull=True)
|
|
@@ -515,55 +608,77 @@ class StagedTransactionModelManager(Manager):
|
|
|
515
608
|
& Q(customer_model__isnull=True)
|
|
516
609
|
)
|
|
517
610
|
| (
|
|
518
|
-
# transaction
|
|
611
|
+
# sales/expense transaction...
|
|
519
612
|
Q(receipt_type__isnull=False)
|
|
520
613
|
& (
|
|
521
|
-
(
|
|
522
|
-
|
|
523
|
-
& Q(customer_model__isnull=True)
|
|
524
|
-
)
|
|
525
|
-
| (
|
|
526
|
-
Q(vendor_model__isnull=True)
|
|
527
|
-
& Q(customer_model__isnull=False)
|
|
528
|
-
)
|
|
614
|
+
(Q(vendor_model__isnull=False) & Q(customer_model__isnull=True))
|
|
615
|
+
| (Q(vendor_model__isnull=True) & Q(customer_model__isnull=False))
|
|
529
616
|
)
|
|
530
617
|
)
|
|
618
|
+
| (
|
|
619
|
+
# sales/expense transaction...
|
|
620
|
+
Q(receipt_type__exact=ReceiptModel.TRANSFER_RECEIPT)
|
|
621
|
+
& Q(vendor_model__isnull=True)
|
|
622
|
+
& Q(customer_model__isnull=True)
|
|
623
|
+
)
|
|
531
624
|
)
|
|
532
625
|
),
|
|
533
626
|
then=True,
|
|
534
627
|
),
|
|
535
|
-
# is
|
|
628
|
+
# is parent, mapped and all parent amount is split...
|
|
536
629
|
When(
|
|
537
630
|
condition=(
|
|
538
631
|
# no receipt type selected...
|
|
539
632
|
# will import the transaction as is...
|
|
540
633
|
(
|
|
541
634
|
Q(children_count__gt=0)
|
|
635
|
+
& Q(bundle_split=True)
|
|
542
636
|
& Q(receipt_type__isnull=True)
|
|
543
|
-
& Q(
|
|
637
|
+
& Q(children_mapping_done=True)
|
|
544
638
|
& Q(total_amount_split__exact=F('amount'))
|
|
545
639
|
& Q(parent__isnull=True)
|
|
546
640
|
& Q(transaction_model__isnull=True)
|
|
641
|
+
& Q(customer_model__isnull=True)
|
|
642
|
+
& Q(vendor_model__isnull=True)
|
|
547
643
|
)
|
|
548
|
-
#
|
|
644
|
+
# BUNDLED...
|
|
645
|
+
# a receipt type is assigned... at least a customer or vendor is selected...
|
|
549
646
|
| (
|
|
550
647
|
Q(children_count__gt=0)
|
|
648
|
+
& Q(parent__isnull=True)
|
|
649
|
+
& Q(bundle_split=True)
|
|
551
650
|
& Q(receipt_type__isnull=False)
|
|
552
651
|
& (
|
|
553
|
-
(
|
|
554
|
-
|
|
555
|
-
& Q(customer_model__isnull=True)
|
|
556
|
-
)
|
|
557
|
-
| (
|
|
558
|
-
Q(vendor_model__isnull=True)
|
|
559
|
-
& Q(customer_model__isnull=False)
|
|
560
|
-
)
|
|
652
|
+
(Q(vendor_model__isnull=False) & Q(customer_model__isnull=True))
|
|
653
|
+
| (Q(vendor_model__isnull=True) & Q(customer_model__isnull=False))
|
|
561
654
|
)
|
|
562
655
|
& Q(children_count=F('children_mapped_count'))
|
|
563
656
|
& Q(total_amount_split__exact=F('amount'))
|
|
564
657
|
& Q(parent__isnull=True)
|
|
565
658
|
& Q(transaction_model__isnull=True)
|
|
566
659
|
)
|
|
660
|
+
# NOT BUNDLED...
|
|
661
|
+
# a receipt type is assigned... at least a customer or vendor is selected...
|
|
662
|
+
| (
|
|
663
|
+
Q(children_count__gt=0)
|
|
664
|
+
& Q(parent__isnull=True)
|
|
665
|
+
& Q(bundle_split=False)
|
|
666
|
+
& Q(receipt_type__isnull=True)
|
|
667
|
+
& Q(vendor_model__isnull=True)
|
|
668
|
+
& Q(customer_model__isnull=True)
|
|
669
|
+
& Q(children_mapping_done=True)
|
|
670
|
+
& Q(total_amount_split__exact=F('amount'))
|
|
671
|
+
& Q(transaction_model__isnull=True)
|
|
672
|
+
)
|
|
673
|
+
| (
|
|
674
|
+
Q(children_count__exact=0)
|
|
675
|
+
& Q(parent__isnull=False)
|
|
676
|
+
& Q(bundle_split=False)
|
|
677
|
+
& Q(receipt_type__isnull=False)
|
|
678
|
+
& (Q(vendor_model__isnull=False) | Q(customer_model__isnull=False))
|
|
679
|
+
& Q(children_mapping_done=True)
|
|
680
|
+
& Q(transaction_model__isnull=True)
|
|
681
|
+
)
|
|
567
682
|
),
|
|
568
683
|
then=True,
|
|
569
684
|
),
|
|
@@ -664,14 +779,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
664
779
|
related_name='split_transaction_set',
|
|
665
780
|
verbose_name=_('Parent Transaction'),
|
|
666
781
|
)
|
|
667
|
-
import_job = models.ForeignKey(
|
|
668
|
-
'django_ledger.ImportJobModel', on_delete=models.CASCADE
|
|
669
|
-
)
|
|
782
|
+
import_job = models.ForeignKey('django_ledger.ImportJobModel', on_delete=models.CASCADE)
|
|
670
783
|
fit_id = models.CharField(max_length=100)
|
|
671
784
|
date_posted = models.DateField(verbose_name=_('Date Posted'))
|
|
672
|
-
bundle_split = models.BooleanField(
|
|
673
|
-
default=True, verbose_name=_('Bundle Split Transactions')
|
|
674
|
-
)
|
|
785
|
+
bundle_split = models.BooleanField(default=True, verbose_name=_('Bundle Split Transactions'))
|
|
675
786
|
activity = models.CharField(
|
|
676
787
|
choices=JournalEntryModel.ACTIVITIES,
|
|
677
788
|
max_length=20,
|
|
@@ -679,18 +790,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
679
790
|
blank=True,
|
|
680
791
|
verbose_name=_('Proposed Activity'),
|
|
681
792
|
)
|
|
682
|
-
amount = models.DecimalField(
|
|
683
|
-
|
|
684
|
-
)
|
|
685
|
-
amount_split = models.DecimalField(
|
|
686
|
-
decimal_places=2, max_digits=15, null=True, blank=True
|
|
687
|
-
)
|
|
793
|
+
amount = models.DecimalField(decimal_places=2, max_digits=15, editable=False, null=True, blank=True)
|
|
794
|
+
amount_split = models.DecimalField(decimal_places=2, max_digits=15, null=True, blank=True)
|
|
688
795
|
name = models.CharField(max_length=200, blank=True, null=True)
|
|
689
796
|
memo = models.CharField(max_length=200, blank=True, null=True)
|
|
690
797
|
|
|
691
|
-
account_model = models.ForeignKey(
|
|
692
|
-
'django_ledger.AccountModel', on_delete=models.RESTRICT, null=True, blank=True
|
|
693
|
-
)
|
|
798
|
+
account_model = models.ForeignKey('django_ledger.AccountModel', on_delete=models.RESTRICT, null=True, blank=True)
|
|
694
799
|
|
|
695
800
|
unit_model = models.ForeignKey(
|
|
696
801
|
'django_ledger.EntityUnitModel',
|
|
@@ -731,7 +836,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
731
836
|
help_text=_('The Customer associated with the transaction.'),
|
|
732
837
|
)
|
|
733
838
|
|
|
734
|
-
objects = StagedTransactionModelManager()
|
|
839
|
+
objects = StagedTransactionModelManager.from_queryset(queryset_class=StagedTransactionModelQuerySet)()
|
|
735
840
|
|
|
736
841
|
class Meta:
|
|
737
842
|
abstract = True
|
|
@@ -823,9 +928,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
823
928
|
'amount': abs(child_txs_model.amount_split),
|
|
824
929
|
'amount_staged': child_txs_model.amount_split,
|
|
825
930
|
'unit_model': child_txs_model.unit_model,
|
|
826
|
-
'tx_type': CREDIT
|
|
827
|
-
if not child_txs_model.amount_split < 0.00
|
|
828
|
-
else DEBIT,
|
|
931
|
+
'tx_type': CREDIT if not child_txs_model.amount_split < 0.00 else DEBIT,
|
|
829
932
|
'description': child_txs_model.name,
|
|
830
933
|
'staged_tx_model': child_txs_model,
|
|
831
934
|
}
|
|
@@ -834,10 +937,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
834
937
|
return [
|
|
835
938
|
{
|
|
836
939
|
'account': self.account_model,
|
|
837
|
-
'amount': abs(self.amount),
|
|
838
|
-
'amount_staged': self.amount,
|
|
940
|
+
'amount': abs(self.amount if not self.is_children() else self.amount_split),
|
|
941
|
+
'amount_staged': self.amount if not self.is_children() else self.amount_split,
|
|
839
942
|
'unit_model': self.unit_model,
|
|
840
|
-
'tx_type': CREDIT
|
|
943
|
+
'tx_type': CREDIT
|
|
944
|
+
if not (self.amount if not self.is_children() else self.amount_split) < 0.00
|
|
945
|
+
else DEBIT,
|
|
841
946
|
'description': self.name,
|
|
842
947
|
'staged_tx_model': self,
|
|
843
948
|
}
|
|
@@ -892,7 +997,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
892
997
|
return self.amount
|
|
893
998
|
|
|
894
999
|
def is_sales(self) -> bool:
|
|
895
|
-
if self.is_children():
|
|
1000
|
+
if self.is_children() and self.is_bundled():
|
|
896
1001
|
return self.parent.is_sales()
|
|
897
1002
|
return any(
|
|
898
1003
|
[
|
|
@@ -902,7 +1007,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
902
1007
|
)
|
|
903
1008
|
|
|
904
1009
|
def is_expense(self) -> bool:
|
|
905
|
-
if self.is_children():
|
|
1010
|
+
if self.is_children() and self.is_bundled():
|
|
906
1011
|
return self.parent.is_expense()
|
|
907
1012
|
return any(
|
|
908
1013
|
[
|
|
@@ -911,40 +1016,71 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
911
1016
|
]
|
|
912
1017
|
)
|
|
913
1018
|
|
|
1019
|
+
def is_transfer(self) -> bool:
|
|
1020
|
+
return self.receipt_type == ReceiptModel.TRANSFER_RECEIPT
|
|
1021
|
+
|
|
1022
|
+
def is_debt_payment(self) -> bool:
|
|
1023
|
+
if self.is_children() and self.is_bundled():
|
|
1024
|
+
return self.parent.is_debt_payment()
|
|
1025
|
+
return self.receipt_type == ReceiptModel.DEBT_PAYMENT
|
|
1026
|
+
|
|
914
1027
|
def is_imported(self) -> bool:
|
|
915
1028
|
"""
|
|
916
1029
|
Determines if the necessary models have been imported for the system to function
|
|
917
1030
|
properly. This method checks whether both `account_model_id` and
|
|
918
1031
|
`transaction_model_id` are set.
|
|
919
1032
|
|
|
1033
|
+
Additionally, a parent transaction that is not bundled will be considered
|
|
1034
|
+
imported for listing purposes if it has at least one child transaction that is
|
|
1035
|
+
still pending import. This allows a parent to appear in both imported and
|
|
1036
|
+
pending states when its children are not fully imported.
|
|
1037
|
+
|
|
920
1038
|
Returns
|
|
921
1039
|
-------
|
|
922
1040
|
bool
|
|
923
1041
|
True if both `account_model_id` and `transaction_model_id` are not None,
|
|
924
|
-
indicating that the models have been successfully imported
|
|
1042
|
+
indicating that the models have been successfully imported; or if this is a
|
|
1043
|
+
non-bundled parent with at least one pending child. False otherwise.
|
|
925
1044
|
"""
|
|
926
|
-
|
|
1045
|
+
own_imported = all(
|
|
927
1046
|
[
|
|
928
1047
|
self.account_model_id is not None,
|
|
929
1048
|
self.transaction_model_id is not None,
|
|
930
1049
|
]
|
|
931
1050
|
)
|
|
1051
|
+
parent_with_imported_child = all(
|
|
1052
|
+
[
|
|
1053
|
+
self.is_parent(),
|
|
1054
|
+
not self.is_bundled(),
|
|
1055
|
+
self.split_transaction_set.filter(transaction_model__isnull=False).exists(),
|
|
1056
|
+
]
|
|
1057
|
+
)
|
|
1058
|
+
return own_imported or parent_with_imported_child
|
|
932
1059
|
|
|
933
1060
|
def is_pending(self) -> bool:
|
|
934
1061
|
"""
|
|
935
1062
|
Determine if the transaction is pending.
|
|
936
1063
|
|
|
937
1064
|
A transaction is considered pending if it has not been assigned a
|
|
938
|
-
`transaction_model_id`.
|
|
939
|
-
|
|
1065
|
+
`transaction_model_id`. Additionally, a parent transaction that is not
|
|
1066
|
+
bundled is also considered pending if any of its children are still
|
|
1067
|
+
pending import. This allows a parent to be both imported and pending
|
|
1068
|
+
while its children are not fully imported.
|
|
940
1069
|
|
|
941
1070
|
Returns
|
|
942
1071
|
-------
|
|
943
1072
|
bool
|
|
944
1073
|
True if the transaction is pending (i.e., `transaction_model_id`
|
|
945
|
-
is None),
|
|
1074
|
+
is None), or this is a non-bundled parent with at least one pending
|
|
1075
|
+
child. False otherwise.
|
|
946
1076
|
"""
|
|
947
|
-
return self.transaction_model_id is None
|
|
1077
|
+
return self.transaction_model_id is None or all(
|
|
1078
|
+
[
|
|
1079
|
+
self.is_parent(),
|
|
1080
|
+
not self.is_bundled(),
|
|
1081
|
+
self.split_transaction_set.filter(transaction_model__isnull=True).exists(),
|
|
1082
|
+
]
|
|
1083
|
+
)
|
|
948
1084
|
|
|
949
1085
|
def is_mapped(self) -> bool:
|
|
950
1086
|
"""
|
|
@@ -962,21 +1098,8 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
962
1098
|
"""
|
|
963
1099
|
return self.account_model_id is not None
|
|
964
1100
|
|
|
965
|
-
def
|
|
966
|
-
|
|
967
|
-
Checks whether the current instance represents a single entry.
|
|
968
|
-
|
|
969
|
-
This method determines if the current object qualifies as a single entry
|
|
970
|
-
by ensuring that it both does not have children and is not considered a
|
|
971
|
-
child of any other entry. The result is a boolean value indicating
|
|
972
|
-
whether the entry meets these criteria.
|
|
973
|
-
|
|
974
|
-
Returns
|
|
975
|
-
-------
|
|
976
|
-
bool
|
|
977
|
-
True if the entry is a single, standalone entry; False otherwise.
|
|
978
|
-
"""
|
|
979
|
-
return all([not self.is_children(), not self.has_children()])
|
|
1101
|
+
def is_parent(self) -> bool:
|
|
1102
|
+
return self.parent_id is None
|
|
980
1103
|
|
|
981
1104
|
def is_children(self) -> bool:
|
|
982
1105
|
"""
|
|
@@ -991,11 +1114,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
991
1114
|
True if the object has a valid `parent_id`, indicating it is a child entity;
|
|
992
1115
|
False otherwise.
|
|
993
1116
|
"""
|
|
994
|
-
return
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1117
|
+
return self.parent_id is not None
|
|
1118
|
+
|
|
1119
|
+
def is_bundled(self) -> bool:
|
|
1120
|
+
if not self.parent_id:
|
|
1121
|
+
return self.bundle_split is True
|
|
1122
|
+
return self.parent.is_bundled()
|
|
999
1123
|
|
|
1000
1124
|
def has_activity(self) -> bool:
|
|
1001
1125
|
"""
|
|
@@ -1032,25 +1156,196 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1032
1156
|
return False
|
|
1033
1157
|
return getattr(self, 'children_count') > 0
|
|
1034
1158
|
|
|
1159
|
+
# TX Cases...
|
|
1160
|
+
|
|
1161
|
+
def is_single(self) -> bool:
|
|
1162
|
+
"""
|
|
1163
|
+
Determine if the current object is an original import.
|
|
1164
|
+
|
|
1165
|
+
This method checks whether the current object is neither a child nor
|
|
1166
|
+
has any children associated with it. If both checks return False,
|
|
1167
|
+
the object is considered original.
|
|
1168
|
+
|
|
1169
|
+
Returns
|
|
1170
|
+
-------
|
|
1171
|
+
bool
|
|
1172
|
+
True if the object is original, otherwise False.
|
|
1173
|
+
"""
|
|
1174
|
+
return all([not self.is_children(), not self.has_children()])
|
|
1175
|
+
|
|
1176
|
+
def is_single_no_receipt(self) -> bool:
|
|
1177
|
+
return all(
|
|
1178
|
+
[
|
|
1179
|
+
self.is_single(),
|
|
1180
|
+
not self.has_receipt(),
|
|
1181
|
+
]
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
def is_single_has_receipt(self) -> bool:
|
|
1185
|
+
return all(
|
|
1186
|
+
[
|
|
1187
|
+
self.is_single(),
|
|
1188
|
+
self.has_receipt(),
|
|
1189
|
+
]
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
def is_parent_is_bundled_no_receipt(self) -> bool:
|
|
1193
|
+
return all(
|
|
1194
|
+
[
|
|
1195
|
+
self.is_parent(),
|
|
1196
|
+
self.has_children(),
|
|
1197
|
+
self.is_bundled(),
|
|
1198
|
+
not self.has_receipt(),
|
|
1199
|
+
]
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
def is_parent_is_bundled_has_receipt(self) -> bool:
|
|
1203
|
+
return all(
|
|
1204
|
+
[
|
|
1205
|
+
self.is_parent(),
|
|
1206
|
+
self.has_children(),
|
|
1207
|
+
self.is_bundled(),
|
|
1208
|
+
self.has_receipt(),
|
|
1209
|
+
]
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
def is_parent_not_bundled_has_receipt(self) -> bool:
|
|
1213
|
+
return all(
|
|
1214
|
+
[
|
|
1215
|
+
self.is_parent(),
|
|
1216
|
+
self.has_children(),
|
|
1217
|
+
not self.is_bundled(),
|
|
1218
|
+
self.has_receipt(),
|
|
1219
|
+
]
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
def is_parent_not_bundled_no_receipt(self) -> bool:
|
|
1223
|
+
return all(
|
|
1224
|
+
[
|
|
1225
|
+
self.is_parent(),
|
|
1226
|
+
self.has_children(),
|
|
1227
|
+
not self.is_bundled(),
|
|
1228
|
+
not self.has_receipt(),
|
|
1229
|
+
]
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
def is_child_is_bundled_no_receipt(self) -> bool:
|
|
1233
|
+
return all(
|
|
1234
|
+
[
|
|
1235
|
+
self.is_children(),
|
|
1236
|
+
self.is_bundled(),
|
|
1237
|
+
not self.has_receipt(),
|
|
1238
|
+
]
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
def is_child_is_bundled_has_receipt(self) -> bool:
|
|
1242
|
+
return all(
|
|
1243
|
+
[
|
|
1244
|
+
self.is_children(),
|
|
1245
|
+
self.is_bundled(),
|
|
1246
|
+
self.has_receipt(),
|
|
1247
|
+
]
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
def is_child_not_bundled_has_receipt(self) -> bool:
|
|
1251
|
+
return all(
|
|
1252
|
+
[
|
|
1253
|
+
self.is_children(),
|
|
1254
|
+
not self.is_bundled(),
|
|
1255
|
+
self.has_receipt(),
|
|
1256
|
+
]
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
def is_child_not_bundled_no_receipt(self) -> bool:
|
|
1260
|
+
return all(
|
|
1261
|
+
[
|
|
1262
|
+
self.is_children(),
|
|
1263
|
+
not self.is_bundled(),
|
|
1264
|
+
not self.has_receipt(),
|
|
1265
|
+
]
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
@property
|
|
1269
|
+
def entity_slug(self) -> str:
|
|
1270
|
+
return getattr(self, '_entity_slug')
|
|
1271
|
+
|
|
1035
1272
|
@property
|
|
1036
1273
|
def receipt_uuid(self):
|
|
1037
1274
|
try:
|
|
1038
1275
|
return getattr(self, '_receipt_uuid')
|
|
1039
1276
|
except AttributeError:
|
|
1040
1277
|
pass
|
|
1041
|
-
return
|
|
1278
|
+
return None
|
|
1042
1279
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1280
|
+
# Data Import Field Visibility...
|
|
1281
|
+
|
|
1282
|
+
def can_have_amount_split(self):
|
|
1283
|
+
if self.is_transfer():
|
|
1284
|
+
return False
|
|
1285
|
+
return self.is_children()
|
|
1286
|
+
|
|
1287
|
+
def can_have_bundle_split(self):
|
|
1288
|
+
if self.is_transfer():
|
|
1289
|
+
return False
|
|
1290
|
+
return all([self.is_parent()])
|
|
1291
|
+
|
|
1292
|
+
def can_have_receipt(self) -> bool:
|
|
1293
|
+
if any(
|
|
1294
|
+
[
|
|
1295
|
+
self.is_single_no_receipt(),
|
|
1296
|
+
self.is_single_has_receipt(),
|
|
1297
|
+
self.is_parent_is_bundled_no_receipt(),
|
|
1298
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1299
|
+
self.is_child_not_bundled_no_receipt(),
|
|
1300
|
+
self.is_child_not_bundled_has_receipt(),
|
|
1301
|
+
]
|
|
1302
|
+
):
|
|
1303
|
+
return True
|
|
1304
|
+
return False
|
|
1305
|
+
|
|
1306
|
+
def can_have_vendor(self) -> bool:
|
|
1307
|
+
if self.is_transfer():
|
|
1308
|
+
return False
|
|
1309
|
+
if all(
|
|
1310
|
+
[
|
|
1311
|
+
any(
|
|
1312
|
+
[
|
|
1313
|
+
self.is_expense(),
|
|
1314
|
+
self.is_debt_payment(),
|
|
1315
|
+
]
|
|
1316
|
+
),
|
|
1317
|
+
any(
|
|
1318
|
+
[
|
|
1319
|
+
self.is_single_has_receipt(),
|
|
1320
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1321
|
+
self.is_child_not_bundled_has_receipt(),
|
|
1322
|
+
]
|
|
1323
|
+
),
|
|
1324
|
+
]
|
|
1325
|
+
):
|
|
1326
|
+
return True
|
|
1327
|
+
return False
|
|
1328
|
+
|
|
1329
|
+
def can_have_customer(self) -> bool:
|
|
1330
|
+
if self.is_transfer():
|
|
1331
|
+
return False
|
|
1332
|
+
if all(
|
|
1333
|
+
[
|
|
1334
|
+
self.is_sales(),
|
|
1335
|
+
any(
|
|
1336
|
+
[
|
|
1337
|
+
self.is_single_has_receipt(),
|
|
1338
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1339
|
+
self.is_child_not_bundled_has_receipt(),
|
|
1340
|
+
]
|
|
1341
|
+
),
|
|
1342
|
+
]
|
|
1343
|
+
):
|
|
1344
|
+
return True
|
|
1345
|
+
return False
|
|
1047
1346
|
|
|
1048
1347
|
def has_receipt(self) -> bool:
|
|
1049
|
-
|
|
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])
|
|
1348
|
+
return self.receipt_type is not None
|
|
1054
1349
|
|
|
1055
1350
|
def has_mapped_receipt(self) -> bool:
|
|
1056
1351
|
if all(
|
|
@@ -1077,6 +1372,11 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1077
1372
|
return True
|
|
1078
1373
|
return False
|
|
1079
1374
|
|
|
1375
|
+
def can_unbundle(self) -> bool:
|
|
1376
|
+
if any([not self.is_single()]):
|
|
1377
|
+
return True
|
|
1378
|
+
return False
|
|
1379
|
+
|
|
1080
1380
|
def can_split(self) -> bool:
|
|
1081
1381
|
"""
|
|
1082
1382
|
Determines if the current object can be split based on its child status.
|
|
@@ -1090,7 +1390,16 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1090
1390
|
`True` if the object has no children and can be split, otherwise
|
|
1091
1391
|
`False`.
|
|
1092
1392
|
"""
|
|
1093
|
-
|
|
1393
|
+
if any(
|
|
1394
|
+
[
|
|
1395
|
+
self.is_single(),
|
|
1396
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1397
|
+
self.is_parent_is_bundled_no_receipt(),
|
|
1398
|
+
self.is_parent_not_bundled_no_receipt(),
|
|
1399
|
+
]
|
|
1400
|
+
):
|
|
1401
|
+
return True
|
|
1402
|
+
return False
|
|
1094
1403
|
|
|
1095
1404
|
def can_have_unit(self) -> bool:
|
|
1096
1405
|
"""
|
|
@@ -1151,7 +1460,24 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1151
1460
|
return not self.has_children()
|
|
1152
1461
|
|
|
1153
1462
|
def can_have_activity(self) -> bool:
|
|
1154
|
-
|
|
1463
|
+
if self.is_transfer():
|
|
1464
|
+
return False
|
|
1465
|
+
if all(
|
|
1466
|
+
[
|
|
1467
|
+
self.is_mapped(),
|
|
1468
|
+
any(
|
|
1469
|
+
[
|
|
1470
|
+
self.is_single(),
|
|
1471
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1472
|
+
self.is_parent_is_bundled_no_receipt(),
|
|
1473
|
+
self.is_child_not_bundled_has_receipt(),
|
|
1474
|
+
self.is_child_not_bundled_no_receipt(),
|
|
1475
|
+
]
|
|
1476
|
+
),
|
|
1477
|
+
]
|
|
1478
|
+
):
|
|
1479
|
+
return True
|
|
1480
|
+
return False
|
|
1155
1481
|
|
|
1156
1482
|
def can_migrate(self, as_split: bool = False) -> bool:
|
|
1157
1483
|
"""
|
|
@@ -1179,13 +1505,44 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1179
1505
|
otherwise False.
|
|
1180
1506
|
"""
|
|
1181
1507
|
ready_to_import = getattr(self, 'ready_to_import')
|
|
1508
|
+
|
|
1182
1509
|
if not ready_to_import:
|
|
1183
1510
|
return False
|
|
1511
|
+
is_role_valid = self.is_role_mapping_valid(raise_exception=False)
|
|
1512
|
+
if not is_role_valid:
|
|
1513
|
+
return False
|
|
1514
|
+
|
|
1515
|
+
if ready_to_import and is_role_valid:
|
|
1516
|
+
if self.is_bundled():
|
|
1517
|
+
return True
|
|
1518
|
+
|
|
1519
|
+
# not bundled....
|
|
1520
|
+
else:
|
|
1521
|
+
if any([self.is_child_not_bundled_no_receipt(), self.is_child_not_bundled_has_receipt()]):
|
|
1522
|
+
return True
|
|
1523
|
+
return False
|
|
1184
1524
|
|
|
1185
1525
|
can_split_into_je = getattr(self, 'can_split_into_je')
|
|
1186
1526
|
if can_split_into_je and as_split:
|
|
1187
1527
|
return True
|
|
1188
|
-
|
|
1528
|
+
|
|
1529
|
+
return False
|
|
1530
|
+
|
|
1531
|
+
def can_migrate_receipt(self) -> bool:
|
|
1532
|
+
if self.has_receipt():
|
|
1533
|
+
ready_to_import = getattr(self, 'ready_to_import')
|
|
1534
|
+
if ready_to_import:
|
|
1535
|
+
if self.is_transfer():
|
|
1536
|
+
return True
|
|
1537
|
+
if any(
|
|
1538
|
+
[
|
|
1539
|
+
self.is_single_has_receipt(),
|
|
1540
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1541
|
+
self.is_child_not_bundled_has_receipt(),
|
|
1542
|
+
]
|
|
1543
|
+
):
|
|
1544
|
+
return True
|
|
1545
|
+
return False
|
|
1189
1546
|
|
|
1190
1547
|
def can_import(self) -> bool:
|
|
1191
1548
|
return self.can_migrate()
|
|
@@ -1218,9 +1575,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1218
1575
|
"""
|
|
1219
1576
|
if not self.can_split():
|
|
1220
1577
|
if raise_exception:
|
|
1221
|
-
raise ImportJobModelValidationError(
|
|
1222
|
-
message=_(f'Staged Transaction {self.uuid} already split.')
|
|
1223
|
-
)
|
|
1578
|
+
raise ImportJobModelValidationError(message=_(f'Staged Transaction {self.uuid} already split.'))
|
|
1224
1579
|
return
|
|
1225
1580
|
|
|
1226
1581
|
if not self.has_children():
|
|
@@ -1294,20 +1649,16 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1294
1649
|
"""
|
|
1295
1650
|
if self.is_single() and self.is_mapped():
|
|
1296
1651
|
return {self.account_model.role}
|
|
1297
|
-
if self.
|
|
1652
|
+
if self.is_children() and not self.is_bundled() and self.is_mapped():
|
|
1653
|
+
return {self.account_model.role}
|
|
1654
|
+
if self.has_children() and self.is_bundled():
|
|
1298
1655
|
split_txs_qs = self.split_transaction_set.all()
|
|
1299
1656
|
if all([txs.is_mapped() for txs in split_txs_qs]):
|
|
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
|
-
)
|
|
1657
|
+
return set([txs.account_model.role for txs in split_txs_qs if txs.account_model.role != ASSET_CA_CASH])
|
|
1307
1658
|
return set()
|
|
1308
1659
|
|
|
1309
1660
|
def get_prospect_je_activity_try(
|
|
1310
|
-
self, raise_exception: bool = True, force_update: bool = False
|
|
1661
|
+
self, raise_exception: bool = True, force_update: bool = False, commit: bool = True
|
|
1311
1662
|
) -> Optional[str]:
|
|
1312
1663
|
"""
|
|
1313
1664
|
Retrieve or attempt to fetch the journal entry activity for the current prospect object.
|
|
@@ -1333,15 +1684,22 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1333
1684
|
The journal entry activity if successfully retrieved or updated; otherwise,
|
|
1334
1685
|
returns the existing activity or None if no activity is present.
|
|
1335
1686
|
"""
|
|
1336
|
-
|
|
1337
|
-
|
|
1687
|
+
if any(
|
|
1688
|
+
[
|
|
1689
|
+
force_update,
|
|
1690
|
+
self.is_single(),
|
|
1691
|
+
self.is_parent_is_bundled_no_receipt(),
|
|
1692
|
+
self.is_parent_is_bundled_has_receipt(),
|
|
1693
|
+
self.is_child_not_bundled_has_receipt(),
|
|
1694
|
+
self.is_child_not_bundled_no_receipt(),
|
|
1695
|
+
]
|
|
1696
|
+
):
|
|
1338
1697
|
role_set = self.get_import_role_set()
|
|
1339
1698
|
if role_set is not None:
|
|
1340
1699
|
try:
|
|
1341
|
-
self.activity = JournalEntryModel.get_activity_from_roles(
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
self.save(update_fields=['activity'])
|
|
1700
|
+
self.activity = JournalEntryModel.get_activity_from_roles(role_set=role_set)
|
|
1701
|
+
if commit:
|
|
1702
|
+
self.save(update_fields=['activity'])
|
|
1345
1703
|
return self.activity
|
|
1346
1704
|
except ValidationError as e:
|
|
1347
1705
|
if raise_exception:
|
|
@@ -1362,6 +1720,14 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1362
1720
|
Optional[str]
|
|
1363
1721
|
The activity of the prospect journal entry if available, otherwise `None`.
|
|
1364
1722
|
"""
|
|
1723
|
+
if all(
|
|
1724
|
+
[
|
|
1725
|
+
self.is_parent(),
|
|
1726
|
+
not self.is_bundled(),
|
|
1727
|
+
self.has_children(),
|
|
1728
|
+
]
|
|
1729
|
+
):
|
|
1730
|
+
return None
|
|
1365
1731
|
return self.get_prospect_je_activity_try(raise_exception=False)
|
|
1366
1732
|
|
|
1367
1733
|
def get_prospect_je_activity_display(self) -> Optional[str]:
|
|
@@ -1405,9 +1771,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1405
1771
|
"""
|
|
1406
1772
|
if not self.has_activity():
|
|
1407
1773
|
try:
|
|
1408
|
-
activity = self.get_prospect_je_activity_try(
|
|
1409
|
-
raise_exception=raise_exception
|
|
1410
|
-
)
|
|
1774
|
+
activity = self.get_prospect_je_activity_try(raise_exception=raise_exception)
|
|
1411
1775
|
if activity is None:
|
|
1412
1776
|
return False
|
|
1413
1777
|
self.activity = activity
|
|
@@ -1447,9 +1811,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1447
1811
|
'Migrate transactions can only be performed on non-receipt transactions. Use migrate_receipt() instead.'
|
|
1448
1812
|
)
|
|
1449
1813
|
if not self.can_migrate():
|
|
1450
|
-
raise StagedTransactionModelValidationError(
|
|
1451
|
-
f'Transaction {self.uuid} is not ready to be migrated'
|
|
1452
|
-
)
|
|
1814
|
+
raise StagedTransactionModelValidationError(f'Transaction {self.uuid} is not ready to be migrated')
|
|
1453
1815
|
|
|
1454
1816
|
commit_dict = self.commit_dict(split_txs=split_txs)
|
|
1455
1817
|
import_job = self.import_job
|
|
@@ -1459,11 +1821,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1459
1821
|
with transaction.atomic():
|
|
1460
1822
|
staged_to_save = list()
|
|
1461
1823
|
for je_data in commit_dict:
|
|
1462
|
-
unit_model =
|
|
1463
|
-
self.unit_model
|
|
1464
|
-
if not split_txs
|
|
1465
|
-
else commit_dict[0][1]['unit_model']
|
|
1466
|
-
)
|
|
1824
|
+
unit_model = self.unit_model if not split_txs else commit_dict[0][1]['unit_model']
|
|
1467
1825
|
_, _ = ledger_model.commit_txs(
|
|
1468
1826
|
je_timestamp=self.date_posted,
|
|
1469
1827
|
je_unit_model=unit_model,
|
|
@@ -1473,32 +1831,23 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1473
1831
|
force_je_retrieval=False,
|
|
1474
1832
|
)
|
|
1475
1833
|
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
1834
|
staged_to_save = set(staged_to_save)
|
|
1480
1835
|
for i in staged_to_save:
|
|
1481
1836
|
i.save(update_fields=['transaction_model', 'updated'])
|
|
1482
1837
|
|
|
1483
|
-
def migrate_receipt(self, receipt_date: Optional[date | datetime] = None):
|
|
1838
|
+
def migrate_receipt(self, receipt_date: Optional[date | datetime] = None, split_amount: bool = False):
|
|
1484
1839
|
if not self.can_migrate_receipt():
|
|
1485
1840
|
raise StagedTransactionModelValidationError(
|
|
1486
1841
|
'Migrate receipts can only be performed on receipt transactions. Use migrate_transactions() instead.'
|
|
1487
1842
|
)
|
|
1488
1843
|
if not self.can_migrate():
|
|
1489
|
-
raise StagedTransactionModelValidationError(
|
|
1490
|
-
f'Transaction {self.uuid} is not ready to be migratedd'
|
|
1491
|
-
)
|
|
1844
|
+
raise StagedTransactionModelValidationError(f'Transaction {self.uuid} is not ready to be migratedd')
|
|
1492
1845
|
|
|
1493
1846
|
with transaction.atomic():
|
|
1494
|
-
receipt_model: ReceiptModel = self.generate_receipt_model(
|
|
1495
|
-
|
|
1496
|
-
)
|
|
1497
|
-
receipt_model.migrate_receipt()
|
|
1847
|
+
receipt_model: ReceiptModel = self.generate_receipt_model(receipt_date=receipt_date, commit=True)
|
|
1848
|
+
receipt_model.migrate_receipt(split_amount=split_amount)
|
|
1498
1849
|
|
|
1499
|
-
def generate_receipt_model(
|
|
1500
|
-
self, receipt_date: Optional[date] = None, commit: bool = False
|
|
1501
|
-
) -> ReceiptModel:
|
|
1850
|
+
def generate_receipt_model(self, receipt_date: Optional[date] = None, commit: bool = False) -> ReceiptModel:
|
|
1502
1851
|
if receipt_date:
|
|
1503
1852
|
if isinstance(receipt_date, datetime):
|
|
1504
1853
|
receipt_date = receipt_date.date()
|
|
@@ -1509,20 +1858,26 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1509
1858
|
receipt_model.configure(
|
|
1510
1859
|
receipt_date=receipt_date,
|
|
1511
1860
|
entity_model=self.entity_slug,
|
|
1512
|
-
amount=abs(self.amount),
|
|
1861
|
+
amount=abs(self.amount_split if self.is_children() else self.amount),
|
|
1513
1862
|
unit_model=self.unit_model,
|
|
1514
1863
|
receipt_type=self.receipt_type,
|
|
1515
|
-
vendor_model=self.vendor_model if self.is_expense() else None,
|
|
1864
|
+
vendor_model=self.vendor_model if self.is_expense() or self.is_debt_payment() else None,
|
|
1516
1865
|
customer_model=self.customer_model if self.is_sales() else None,
|
|
1517
1866
|
charge_account=self.get_coa_account_model(),
|
|
1518
|
-
receipt_account=self.account_model,
|
|
1867
|
+
receipt_account=self.account_model if self.is_mapped() else None,
|
|
1519
1868
|
staged_transaction_model=self,
|
|
1520
1869
|
commit=True,
|
|
1521
1870
|
)
|
|
1522
1871
|
|
|
1523
1872
|
return receipt_model
|
|
1524
1873
|
|
|
1525
|
-
|
|
1874
|
+
def can_undo_import(self):
|
|
1875
|
+
if self.transaction_model_id is None:
|
|
1876
|
+
return False
|
|
1877
|
+
if all([self.is_children(), self.is_bundled()]):
|
|
1878
|
+
return False
|
|
1879
|
+
return True
|
|
1880
|
+
|
|
1526
1881
|
def undo_import(self, raise_exception: bool = True):
|
|
1527
1882
|
"""
|
|
1528
1883
|
Undo import operation for a staged transaction. This method handles the deletion
|
|
@@ -1536,12 +1891,15 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1536
1891
|
If there is no receipt model or transaction model to undo.
|
|
1537
1892
|
|
|
1538
1893
|
"""
|
|
1894
|
+
if not self.can_undo_import():
|
|
1895
|
+
if raise_exception:
|
|
1896
|
+
raise StagedTransactionModelValidationError(
|
|
1897
|
+
message='Cannot undo children bundled import. Must undo the parent import.'
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1539
1900
|
with transaction.atomic():
|
|
1540
1901
|
# Receipt import case...
|
|
1541
|
-
|
|
1542
|
-
receipt_model = self.receiptmodel
|
|
1543
|
-
except ObjectDoesNotExist:
|
|
1544
|
-
receipt_model = None
|
|
1902
|
+
receipt_model = getattr(self, 'receiptmodel', None)
|
|
1545
1903
|
|
|
1546
1904
|
if receipt_model is not None:
|
|
1547
1905
|
receipt_model.delete()
|
|
@@ -1555,15 +1913,23 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1555
1913
|
if self.transaction_model_id:
|
|
1556
1914
|
tx_model = self.transaction_model
|
|
1557
1915
|
journal_entry_model = tx_model.journal_entry
|
|
1916
|
+
|
|
1917
|
+
journal_entry_model.unpost(raise_exception=False)
|
|
1918
|
+
journal_entry_model.unlock(raise_exception=False)
|
|
1919
|
+
|
|
1558
1920
|
journal_entry_model.delete()
|
|
1921
|
+
|
|
1559
1922
|
self.transaction_model = None
|
|
1560
1923
|
self.save(update_fields=['transaction_model', 'updated'])
|
|
1561
1924
|
return
|
|
1562
1925
|
|
|
1563
1926
|
if raise_exception:
|
|
1564
|
-
raise StagedTransactionModelValidationError(
|
|
1565
|
-
|
|
1566
|
-
|
|
1927
|
+
raise StagedTransactionModelValidationError(message=_('Nothing to undo for this staged transaction.'))
|
|
1928
|
+
|
|
1929
|
+
def can_delete(self) -> bool:
|
|
1930
|
+
if self.is_children():
|
|
1931
|
+
return True
|
|
1932
|
+
return False
|
|
1567
1933
|
|
|
1568
1934
|
def clean(self, verify: bool = False):
|
|
1569
1935
|
if self.has_children():
|
|
@@ -1579,14 +1945,26 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
|
|
|
1579
1945
|
if not self.can_have_activity():
|
|
1580
1946
|
self.activity = None
|
|
1581
1947
|
|
|
1948
|
+
if self.is_sales():
|
|
1949
|
+
self.vendor_model = None
|
|
1950
|
+
|
|
1951
|
+
if self.is_expense():
|
|
1952
|
+
self.customer_model = None
|
|
1953
|
+
|
|
1954
|
+
if self.is_children() and self.is_bundled():
|
|
1955
|
+
self.vendor_model = None
|
|
1956
|
+
self.customer_model = None
|
|
1957
|
+
|
|
1582
1958
|
if self.is_children():
|
|
1959
|
+
self.bundle_split = self.parent.bundle_split
|
|
1960
|
+
|
|
1961
|
+
if all([self.is_parent(), not self.is_bundled()]):
|
|
1583
1962
|
self.vendor_model = None
|
|
1584
1963
|
self.customer_model = None
|
|
1585
1964
|
|
|
1586
|
-
if
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
)
|
|
1965
|
+
if self.is_transfer():
|
|
1966
|
+
self.vendor_model = None
|
|
1967
|
+
self.customer_model = None
|
|
1590
1968
|
|
|
1591
1969
|
if verify:
|
|
1592
1970
|
self.is_role_mapping_valid(raise_exception=True)
|
|
@@ -1632,14 +2010,9 @@ def importjobmodel_presave(instance: ImportJobModel, **kwargs):
|
|
|
1632
2010
|
entity ID of the Ledger Model.
|
|
1633
2011
|
"""
|
|
1634
2012
|
if instance.is_configured():
|
|
1635
|
-
if
|
|
1636
|
-
instance.bank_account_model.entity_model_id
|
|
1637
|
-
!= instance.ledger_model.entity_id
|
|
1638
|
-
):
|
|
2013
|
+
if instance.bank_account_model.entity_model_id != instance.ledger_model.entity_id:
|
|
1639
2014
|
raise ImportJobModelValidationError(
|
|
1640
|
-
message=_(
|
|
1641
|
-
'Invalid Bank Account for LedgerModel. No matching Entity Model found.'
|
|
1642
|
-
)
|
|
2015
|
+
message=_('Invalid Bank Account for LedgerModel. No matching Entity Model found.')
|
|
1643
2016
|
)
|
|
1644
2017
|
|
|
1645
2018
|
|
|
@@ -1697,3 +2070,10 @@ def stagedtransactionmodel_presave(instance: StagedTransactionModel, **kwargs):
|
|
|
1697
2070
|
|
|
1698
2071
|
|
|
1699
2072
|
pre_save.connect(stagedtransactionmodel_presave, sender=StagedTransactionModel)
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def stagedtransactionmodel_predelete(instance: StagedTransactionModel, **kwargs):
|
|
2076
|
+
if not instance.can_delete():
|
|
2077
|
+
raise StagedTransactionModelValidationError(
|
|
2078
|
+
message=_('Cannot delete parent Staged Transactions.'),
|
|
2079
|
+
)
|