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.

@@ -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 ObjectDoesNotExist, ValidationError
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
- Pending transactions are identified by checking if the `transaction_model` is
366
- null for any of the objects in the queryset.
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
- return self.filter(transaction_model__isnull=True)
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 only objects where the `transaction_model`
386
- is not null.
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
- return self.filter(transaction_model__isnull=False)
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
- entity_slug=F('import_job__bank_account_model__entity_model__slug'),
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
- 'split_transaction_set__account_model__uuid'
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('children_mapped_count'),
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 with receipt...
611
+ # sales/expense transaction...
519
612
  Q(receipt_type__isnull=False)
520
613
  & (
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
- )
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 children, mapped and all parent amount is split...
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(children_count=F('children_mapped_count'))
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
- # receipt type is assigned... at least a customer or vendor is selected...
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
- 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
- )
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
- 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
- )
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 if not self.amount < 0.00 else DEBIT,
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. False otherwise.
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
- return all(
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`. This function checks the attribute and returns
939
- a boolean indicating the status.
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), False otherwise.
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 is_single(self) -> bool:
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 all(
995
- [
996
- self.parent_id is not None,
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 self.receiptmodel.uuid
1278
+ return None
1042
1279
 
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
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
- 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])
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
- return all([not self.has_children(), not self.has_receipt()])
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
- return self.account_model_id is not None
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
- return all([self.is_role_mapping_valid(raise_exception=False)])
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.has_children():
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
- ready_to_import = getattr(self, 'ready_to_import')
1337
- if (not self.has_activity() and ready_to_import) or force_update:
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
- role_set=role_set
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
- receipt_date=receipt_date, commit=True
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
- # UNDO
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
- try:
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
- message=_('Nothing to undo for this staged transaction.')
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 all([self.has_children(), self.has_receipt(), not self.bundle_split]):
1587
- raise StagedTransactionModelValidationError(
1588
- 'Receipt transactions cannot be split into multiple receipts.'
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
+ )