django-ledger 0.8.2.1__py3-none-any.whl → 0.8.2.2__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.

@@ -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,6 +361,71 @@ 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.
@@ -438,7 +508,7 @@ class StagedTransactionModelManager(Manager):
438
508
  Fetch and annotate the queryset with related fields and calculated annotations.
439
509
  """
440
510
 
441
- def get_queryset(self):
511
+ def get_queryset(self) -> StagedTransactionModelQuerySet:
442
512
  """
443
513
  Fetch and annotate the queryset for staged transaction models to include additional
444
514
  related fields and calculated annotations for further processing and sorting.
@@ -471,15 +541,13 @@ class StagedTransactionModelManager(Manager):
471
541
  'parent',
472
542
  'parent__account_model',
473
543
  'parent__unit_model',
544
+ 'receiptmodel',
474
545
  )
475
546
  .annotate(
476
- entity_slug=F('import_job__bank_account_model__entity_model__slug'),
547
+ _entity_slug=F('import_job__bank_account_model__entity_model__slug'),
477
548
  entity_unit=F('transaction_model__journal_entry__entity_unit__name'),
478
- _receipt_uuid=F('receiptmodel__uuid'),
479
549
  children_count=Count('split_transaction_set'),
480
- children_mapped_count=Count(
481
- 'split_transaction_set__account_model__uuid'
482
- ),
550
+ children_mapped_count=Count('split_transaction_set__account_model__uuid'),
483
551
  total_amount_split=Coalesce(
484
552
  Sum('split_transaction_set__amount_split'),
485
553
  Value(value=0.00, output_field=DecimalField()),
@@ -488,10 +556,10 @@ class StagedTransactionModelManager(Manager):
488
556
  When(parent_id__isnull=True, then=F('uuid')),
489
557
  When(parent_id__isnull=False, then=F('parent_id')),
490
558
  ),
559
+ _receipt_uuid=F('receiptmodel__uuid'),
491
560
  )
492
561
  .annotate(
493
- children_mapping_pending_count=F('children_count')
494
- - F('children_mapped_count'),
562
+ children_mapping_pending_count=F('children_count') - F('children_mapped_count'),
495
563
  )
496
564
  .annotate(
497
565
  children_mapping_done=Case(
@@ -504,6 +572,8 @@ class StagedTransactionModelManager(Manager):
504
572
  When(
505
573
  condition=(
506
574
  Q(children_count__exact=0)
575
+ & Q(bundle_split=True)
576
+ & Q(parent__isnull=True)
507
577
  & Q(account_model__isnull=False)
508
578
  & Q(parent__isnull=True)
509
579
  & Q(transaction_model__isnull=True)
@@ -515,19 +585,19 @@ class StagedTransactionModelManager(Manager):
515
585
  & Q(customer_model__isnull=True)
516
586
  )
517
587
  | (
518
- # transaction with receipt...
588
+ # sales/expense transaction...
519
589
  Q(receipt_type__isnull=False)
520
590
  & (
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
- )
591
+ (Q(vendor_model__isnull=False) & Q(customer_model__isnull=True))
592
+ | (Q(vendor_model__isnull=True) & Q(customer_model__isnull=False))
529
593
  )
530
594
  )
595
+ | (
596
+ # sales/expense transaction...
597
+ Q(receipt_type__exact=ReceiptModel.TRANSFER_RECEIPT)
598
+ & Q(vendor_model__isnull=True)
599
+ & Q(customer_model__isnull=True)
600
+ )
531
601
  )
532
602
  ),
533
603
  then=True,
@@ -539,31 +609,44 @@ class StagedTransactionModelManager(Manager):
539
609
  # will import the transaction as is...
540
610
  (
541
611
  Q(children_count__gt=0)
612
+ & Q(bundle_split=True)
542
613
  & Q(receipt_type__isnull=True)
543
614
  & Q(children_count=F('children_mapped_count'))
544
615
  & Q(total_amount_split__exact=F('amount'))
545
616
  & Q(parent__isnull=True)
546
617
  & Q(transaction_model__isnull=True)
618
+ & Q(customer_model__isnull=True)
619
+ & Q(vendor_model__isnull=False)
547
620
  )
548
- # receipt type is assigned... at least a customer or vendor is selected...
621
+ # BUNDLED...
622
+ # a receipt type is assigned... at least a customer or vendor is selected...
549
623
  | (
550
624
  Q(children_count__gt=0)
625
+ & Q(parent__isnull=True)
626
+ & Q(bundle_split=True)
551
627
  & Q(receipt_type__isnull=False)
552
628
  & (
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
- )
629
+ (Q(vendor_model__isnull=False) & Q(customer_model__isnull=True))
630
+ | (Q(vendor_model__isnull=True) & Q(customer_model__isnull=False))
561
631
  )
562
632
  & Q(children_count=F('children_mapped_count'))
563
633
  & Q(total_amount_split__exact=F('amount'))
564
634
  & Q(parent__isnull=True)
565
635
  & Q(transaction_model__isnull=True)
566
636
  )
637
+ # NOT BUNDLED...
638
+ # a receipt type is assigned... at least a customer or vendor is selected...
639
+ | (
640
+ Q(children_count__gt=0)
641
+ & Q(parent__isnull=True)
642
+ & Q(bundle_split=False)
643
+ & Q(receipt_type__isnull=True)
644
+ & Q(vendor_model__isnull=True)
645
+ & Q(customer_model__isnull=True)
646
+ & Q(children_count=F('children_mapped_count'))
647
+ & Q(total_amount_split__exact=F('amount'))
648
+ & Q(transaction_model__isnull=True)
649
+ )
567
650
  ),
568
651
  then=True,
569
652
  ),
@@ -664,14 +747,10 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
664
747
  related_name='split_transaction_set',
665
748
  verbose_name=_('Parent Transaction'),
666
749
  )
667
- import_job = models.ForeignKey(
668
- 'django_ledger.ImportJobModel', on_delete=models.CASCADE
669
- )
750
+ import_job = models.ForeignKey('django_ledger.ImportJobModel', on_delete=models.CASCADE)
670
751
  fit_id = models.CharField(max_length=100)
671
752
  date_posted = models.DateField(verbose_name=_('Date Posted'))
672
- bundle_split = models.BooleanField(
673
- default=True, verbose_name=_('Bundle Split Transactions')
674
- )
753
+ bundle_split = models.BooleanField(default=True, verbose_name=_('Bundle Split Transactions'))
675
754
  activity = models.CharField(
676
755
  choices=JournalEntryModel.ACTIVITIES,
677
756
  max_length=20,
@@ -679,18 +758,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
679
758
  blank=True,
680
759
  verbose_name=_('Proposed Activity'),
681
760
  )
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
- )
761
+ amount = models.DecimalField(decimal_places=2, max_digits=15, editable=False, null=True, blank=True)
762
+ amount_split = models.DecimalField(decimal_places=2, max_digits=15, null=True, blank=True)
688
763
  name = models.CharField(max_length=200, blank=True, null=True)
689
764
  memo = models.CharField(max_length=200, blank=True, null=True)
690
765
 
691
- account_model = models.ForeignKey(
692
- 'django_ledger.AccountModel', on_delete=models.RESTRICT, null=True, blank=True
693
- )
766
+ account_model = models.ForeignKey('django_ledger.AccountModel', on_delete=models.RESTRICT, null=True, blank=True)
694
767
 
695
768
  unit_model = models.ForeignKey(
696
769
  'django_ledger.EntityUnitModel',
@@ -731,7 +804,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
731
804
  help_text=_('The Customer associated with the transaction.'),
732
805
  )
733
806
 
734
- objects = StagedTransactionModelManager()
807
+ objects = StagedTransactionModelManager.from_queryset(queryset_class=StagedTransactionModelQuerySet)()
735
808
 
736
809
  class Meta:
737
810
  abstract = True
@@ -823,9 +896,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
823
896
  'amount': abs(child_txs_model.amount_split),
824
897
  'amount_staged': child_txs_model.amount_split,
825
898
  'unit_model': child_txs_model.unit_model,
826
- 'tx_type': CREDIT
827
- if not child_txs_model.amount_split < 0.00
828
- else DEBIT,
899
+ 'tx_type': CREDIT if not child_txs_model.amount_split < 0.00 else DEBIT,
829
900
  'description': child_txs_model.name,
830
901
  'staged_tx_model': child_txs_model,
831
902
  }
@@ -892,7 +963,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
892
963
  return self.amount
893
964
 
894
965
  def is_sales(self) -> bool:
895
- if self.is_children():
966
+ if self.is_children() and self.is_bundled():
896
967
  return self.parent.is_sales()
897
968
  return any(
898
969
  [
@@ -902,7 +973,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
902
973
  )
903
974
 
904
975
  def is_expense(self) -> bool:
905
- if self.is_children():
976
+ if self.is_children() and self.is_bundled():
906
977
  return self.parent.is_expense()
907
978
  return any(
908
979
  [
@@ -911,6 +982,14 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
911
982
  ]
912
983
  )
913
984
 
985
+ def is_transfer(self) -> bool:
986
+ return self.receipt_type == ReceiptModel.TRANSFER_RECEIPT
987
+
988
+ def is_debt_payment(self) -> bool:
989
+ if self.is_children() and self.is_bundled():
990
+ return self.parent.is_debt_payment()
991
+ return self.receipt_type == ReceiptModel.DEBT_PAYMENT
992
+
914
993
  def is_imported(self) -> bool:
915
994
  """
916
995
  Determines if the necessary models have been imported for the system to function
@@ -962,21 +1041,8 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
962
1041
  """
963
1042
  return self.account_model_id is not None
964
1043
 
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()])
1044
+ def is_parent(self) -> bool:
1045
+ return self.parent_id is None
980
1046
 
981
1047
  def is_children(self) -> bool:
982
1048
  """
@@ -991,11 +1057,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
991
1057
  True if the object has a valid `parent_id`, indicating it is a child entity;
992
1058
  False otherwise.
993
1059
  """
994
- return all(
995
- [
996
- self.parent_id is not None,
997
- ]
998
- )
1060
+ return self.parent_id is not None
1061
+
1062
+ def is_bundled(self) -> bool:
1063
+ if not self.parent_id:
1064
+ return self.bundle_split is True
1065
+ return self.parent.is_bundled()
999
1066
 
1000
1067
  def has_activity(self) -> bool:
1001
1068
  """
@@ -1032,25 +1099,198 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1032
1099
  return False
1033
1100
  return getattr(self, 'children_count') > 0
1034
1101
 
1102
+ # TX Cases...
1103
+
1104
+ def is_single(self) -> bool:
1105
+ """
1106
+ Determine if the current object is an original import.
1107
+
1108
+ This method checks whether the current object is neither a child nor
1109
+ has any children associated with it. If both checks return False,
1110
+ the object is considered original.
1111
+
1112
+ Returns
1113
+ -------
1114
+ bool
1115
+ True if the object is original, otherwise False.
1116
+ """
1117
+ return all([not self.is_children(), not self.has_children()])
1118
+
1119
+ def is_single_no_receipt(self) -> bool:
1120
+ return all(
1121
+ [
1122
+ self.is_single(),
1123
+ not self.has_receipt(),
1124
+ ]
1125
+ )
1126
+
1127
+ def is_single_has_receipt(self) -> bool:
1128
+ return all(
1129
+ [
1130
+ self.is_single(),
1131
+ self.has_receipt(),
1132
+ ]
1133
+ )
1134
+
1135
+ def is_parent_is_bundled_no_receipt(self) -> bool:
1136
+ return all(
1137
+ [
1138
+ self.is_parent(),
1139
+ self.has_children(),
1140
+ self.is_bundled(),
1141
+ not self.has_receipt(),
1142
+ ]
1143
+ )
1144
+
1145
+ def is_parent_is_bundled_has_receipt(self) -> bool:
1146
+ return all(
1147
+ [
1148
+ self.is_parent(),
1149
+ self.has_children(),
1150
+ self.is_bundled(),
1151
+ self.has_receipt(),
1152
+ ]
1153
+ )
1154
+
1155
+ def is_parent_not_bundled_has_receipt(self) -> bool:
1156
+ return all(
1157
+ [
1158
+ self.is_parent(),
1159
+ self.has_children(),
1160
+ not self.is_bundled(),
1161
+ self.has_receipt(),
1162
+ ]
1163
+ )
1164
+
1165
+ def is_parent_not_bundled_no_receipt(self) -> bool:
1166
+ return all(
1167
+ [
1168
+ self.is_parent(),
1169
+ self.has_children(),
1170
+ not self.is_bundled(),
1171
+ not self.has_receipt(),
1172
+ ]
1173
+ )
1174
+
1175
+ def is_child_is_bundled_no_receipt(self) -> bool:
1176
+ return all(
1177
+ [
1178
+ self.is_children(),
1179
+ self.is_bundled(),
1180
+ not self.has_receipt(),
1181
+ ]
1182
+ )
1183
+
1184
+ def is_child_is_bundled_has_receipt(self) -> bool:
1185
+ return all(
1186
+ [
1187
+ self.is_children(),
1188
+ self.is_bundled(),
1189
+ self.has_receipt(),
1190
+ ]
1191
+ )
1192
+
1193
+ def is_child_not_bundled_has_receipt(self) -> bool:
1194
+ return all(
1195
+ [
1196
+ self.is_children(),
1197
+ not self.is_bundled(),
1198
+ self.has_receipt(),
1199
+ ]
1200
+ )
1201
+
1202
+ def is_child_not_bundled_no_receipt(self) -> bool:
1203
+ return all(
1204
+ [
1205
+ self.is_children(),
1206
+ not self.is_bundled(),
1207
+ not self.has_receipt(),
1208
+ ]
1209
+ )
1210
+
1211
+ @property
1212
+ def entity_slug(self) -> str:
1213
+ return getattr(self, '_entity_slug')
1214
+
1035
1215
  @property
1036
1216
  def receipt_uuid(self):
1037
1217
  try:
1038
1218
  return getattr(self, '_receipt_uuid')
1039
1219
  except AttributeError:
1040
1220
  pass
1041
- return self.receiptmodel.uuid
1221
+ return None
1042
1222
 
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
1223
+ # Data Import Field Visibility...
1224
+
1225
+ def can_have_amount_split(self):
1226
+ if self.is_transfer():
1227
+ return False
1228
+ return self.is_children()
1229
+
1230
+ def can_have_bundle_split(self):
1231
+ if self.is_transfer():
1232
+ return False
1233
+ return all([self.is_parent()])
1234
+
1235
+ def can_have_receipt(self) -> bool:
1236
+ if any(
1237
+ [
1238
+ self.is_single_no_receipt(),
1239
+ self.is_single_has_receipt(),
1240
+ self.is_parent_is_bundled_no_receipt(),
1241
+ self.is_parent_is_bundled_has_receipt(),
1242
+ self.is_child_not_bundled_no_receipt(),
1243
+ self.is_child_not_bundled_has_receipt(),
1244
+ ]
1245
+ ):
1246
+ return True
1247
+ return False
1248
+
1249
+ def can_have_vendor(self) -> bool:
1250
+ if self.is_transfer():
1251
+ return False
1252
+ if all(
1253
+ [
1254
+ any(
1255
+ [
1256
+ self.is_expense(),
1257
+ self.is_debt_payment(),
1258
+ ]
1259
+ ),
1260
+ any(
1261
+ [
1262
+ self.is_single_has_receipt(),
1263
+ self.is_parent_is_bundled_has_receipt(),
1264
+ self.is_child_not_bundled_has_receipt(),
1265
+ ]
1266
+ ),
1267
+ ]
1268
+ ):
1269
+ return True
1270
+ return False
1271
+
1272
+ def can_have_customer(self) -> bool:
1273
+ if self.is_transfer():
1274
+ return False
1275
+ if all(
1276
+ [
1277
+ self.is_sales(),
1278
+ any(
1279
+ [
1280
+ self.is_single_has_receipt(),
1281
+ self.is_parent_is_bundled_has_receipt(),
1282
+ self.is_child_not_bundled_has_receipt(),
1283
+ ]
1284
+ ),
1285
+ ]
1286
+ ):
1287
+ return True
1288
+ return False
1047
1289
 
1048
1290
  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])
1291
+ # if self.is_children() and self.is_bundled():
1292
+ # return self.parent.receipt_type is not None
1293
+ return self.receipt_type is not None
1054
1294
 
1055
1295
  def has_mapped_receipt(self) -> bool:
1056
1296
  if all(
@@ -1077,6 +1317,13 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1077
1317
  return True
1078
1318
  return False
1079
1319
 
1320
+ def can_unbundle(self) -> bool:
1321
+ if any([
1322
+ not self.is_single()
1323
+ ]):
1324
+ return True
1325
+ return False
1326
+
1080
1327
  def can_split(self) -> bool:
1081
1328
  """
1082
1329
  Determines if the current object can be split based on its child status.
@@ -1090,7 +1337,15 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1090
1337
  `True` if the object has no children and can be split, otherwise
1091
1338
  `False`.
1092
1339
  """
1093
- return all([not self.has_children(), not self.has_receipt()])
1340
+ if any(
1341
+ [
1342
+ self.is_single(),
1343
+ self.is_parent_is_bundled_has_receipt(),
1344
+ self.is_parent_is_bundled_no_receipt()
1345
+ ]
1346
+ ):
1347
+ return True
1348
+ return False
1094
1349
 
1095
1350
  def can_have_unit(self) -> bool:
1096
1351
  """
@@ -1151,7 +1406,24 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1151
1406
  return not self.has_children()
1152
1407
 
1153
1408
  def can_have_activity(self) -> bool:
1154
- return self.account_model_id is not None
1409
+ if self.is_transfer():
1410
+ return False
1411
+ if all(
1412
+ [
1413
+ self.is_mapped(),
1414
+ any(
1415
+ [
1416
+ self.is_single(),
1417
+ self.is_parent_is_bundled_has_receipt(),
1418
+ self.is_parent_is_bundled_no_receipt(),
1419
+ self.is_child_not_bundled_has_receipt(),
1420
+ self.is_child_not_bundled_no_receipt(),
1421
+ ]
1422
+ ),
1423
+ ]
1424
+ ):
1425
+ return True
1426
+ return False
1155
1427
 
1156
1428
  def can_migrate(self, as_split: bool = False) -> bool:
1157
1429
  """
@@ -1179,13 +1451,33 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1179
1451
  otherwise False.
1180
1452
  """
1181
1453
  ready_to_import = getattr(self, 'ready_to_import')
1454
+
1182
1455
  if not ready_to_import:
1183
1456
  return False
1184
1457
 
1458
+ if ready_to_import:
1459
+ is_role_valid = self.is_role_mapping_valid(raise_exception=False)
1460
+ if is_role_valid:
1461
+ return True
1462
+
1185
1463
  can_split_into_je = getattr(self, 'can_split_into_je')
1186
1464
  if can_split_into_je and as_split:
1187
1465
  return True
1188
- return all([self.is_role_mapping_valid(raise_exception=False)])
1466
+
1467
+ return False
1468
+
1469
+ def can_migrate_receipt(self) -> bool:
1470
+ if self.has_receipt():
1471
+ ready_to_import = getattr(self, 'ready_to_import')
1472
+ if ready_to_import:
1473
+ if self.is_transfer():
1474
+ return True
1475
+ if any([
1476
+ self.is_single_has_receipt(),
1477
+ self.is_parent_is_bundled_has_receipt()
1478
+ ]):
1479
+ return True
1480
+ return False
1189
1481
 
1190
1482
  def can_import(self) -> bool:
1191
1483
  return self.can_migrate()
@@ -1218,9 +1510,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1218
1510
  """
1219
1511
  if not self.can_split():
1220
1512
  if raise_exception:
1221
- raise ImportJobModelValidationError(
1222
- message=_(f'Staged Transaction {self.uuid} already split.')
1223
- )
1513
+ raise ImportJobModelValidationError(message=_(f'Staged Transaction {self.uuid} already split.'))
1224
1514
  return
1225
1515
 
1226
1516
  if not self.has_children():
@@ -1294,20 +1584,16 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1294
1584
  """
1295
1585
  if self.is_single() and self.is_mapped():
1296
1586
  return {self.account_model.role}
1297
- if self.has_children():
1587
+ if self.is_children() and not self.is_bundled() and self.is_mapped():
1588
+ return {self.account_model.role}
1589
+ if self.has_children() and self.is_bundled():
1298
1590
  split_txs_qs = self.split_transaction_set.all()
1299
1591
  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
- )
1592
+ return set([txs.account_model.role for txs in split_txs_qs if txs.account_model.role != ASSET_CA_CASH])
1307
1593
  return set()
1308
1594
 
1309
1595
  def get_prospect_je_activity_try(
1310
- self, raise_exception: bool = True, force_update: bool = False
1596
+ self, raise_exception: bool = True, force_update: bool = False, commit: bool = True
1311
1597
  ) -> Optional[str]:
1312
1598
  """
1313
1599
  Retrieve or attempt to fetch the journal entry activity for the current prospect object.
@@ -1333,15 +1619,17 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1333
1619
  The journal entry activity if successfully retrieved or updated; otherwise,
1334
1620
  returns the existing activity or None if no activity is present.
1335
1621
  """
1336
- ready_to_import = getattr(self, 'ready_to_import')
1337
- if (not self.has_activity() and ready_to_import) or force_update:
1622
+ if (
1623
+ force_update
1624
+ or all([self.is_children() and self.parent.has_activity(), not self.is_bundled(), not self.has_activity()])
1625
+ or all([not self.has_activity(), getattr(self, 'ready_to_import')])
1626
+ ):
1338
1627
  role_set = self.get_import_role_set()
1339
1628
  if role_set is not None:
1340
1629
  try:
1341
- self.activity = JournalEntryModel.get_activity_from_roles(
1342
- role_set=role_set
1343
- )
1344
- self.save(update_fields=['activity'])
1630
+ self.activity = JournalEntryModel.get_activity_from_roles(role_set=role_set)
1631
+ if commit:
1632
+ self.save(update_fields=['activity'])
1345
1633
  return self.activity
1346
1634
  except ValidationError as e:
1347
1635
  if raise_exception:
@@ -1362,6 +1650,14 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1362
1650
  Optional[str]
1363
1651
  The activity of the prospect journal entry if available, otherwise `None`.
1364
1652
  """
1653
+ if all(
1654
+ [
1655
+ self.is_parent(),
1656
+ not self.is_bundled(),
1657
+ self.has_children(),
1658
+ ]
1659
+ ):
1660
+ return None
1365
1661
  return self.get_prospect_je_activity_try(raise_exception=False)
1366
1662
 
1367
1663
  def get_prospect_je_activity_display(self) -> Optional[str]:
@@ -1405,9 +1701,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1405
1701
  """
1406
1702
  if not self.has_activity():
1407
1703
  try:
1408
- activity = self.get_prospect_je_activity_try(
1409
- raise_exception=raise_exception
1410
- )
1704
+ activity = self.get_prospect_je_activity_try(raise_exception=raise_exception)
1411
1705
  if activity is None:
1412
1706
  return False
1413
1707
  self.activity = activity
@@ -1447,9 +1741,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1447
1741
  'Migrate transactions can only be performed on non-receipt transactions. Use migrate_receipt() instead.'
1448
1742
  )
1449
1743
  if not self.can_migrate():
1450
- raise StagedTransactionModelValidationError(
1451
- f'Transaction {self.uuid} is not ready to be migrated'
1452
- )
1744
+ raise StagedTransactionModelValidationError(f'Transaction {self.uuid} is not ready to be migrated')
1453
1745
 
1454
1746
  commit_dict = self.commit_dict(split_txs=split_txs)
1455
1747
  import_job = self.import_job
@@ -1459,11 +1751,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1459
1751
  with transaction.atomic():
1460
1752
  staged_to_save = list()
1461
1753
  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
- )
1754
+ unit_model = self.unit_model if not split_txs else commit_dict[0][1]['unit_model']
1467
1755
  _, _ = ledger_model.commit_txs(
1468
1756
  je_timestamp=self.date_posted,
1469
1757
  je_unit_model=unit_model,
@@ -1486,19 +1774,13 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1486
1774
  'Migrate receipts can only be performed on receipt transactions. Use migrate_transactions() instead.'
1487
1775
  )
1488
1776
  if not self.can_migrate():
1489
- raise StagedTransactionModelValidationError(
1490
- f'Transaction {self.uuid} is not ready to be migratedd'
1491
- )
1777
+ raise StagedTransactionModelValidationError(f'Transaction {self.uuid} is not ready to be migratedd')
1492
1778
 
1493
1779
  with transaction.atomic():
1494
- receipt_model: ReceiptModel = self.generate_receipt_model(
1495
- receipt_date=receipt_date, commit=True
1496
- )
1780
+ receipt_model: ReceiptModel = self.generate_receipt_model(receipt_date=receipt_date, commit=True)
1497
1781
  receipt_model.migrate_receipt()
1498
1782
 
1499
- def generate_receipt_model(
1500
- self, receipt_date: Optional[date] = None, commit: bool = False
1501
- ) -> ReceiptModel:
1783
+ def generate_receipt_model(self, receipt_date: Optional[date] = None, commit: bool = False) -> ReceiptModel:
1502
1784
  if receipt_date:
1503
1785
  if isinstance(receipt_date, datetime):
1504
1786
  receipt_date = receipt_date.date()
@@ -1512,16 +1794,22 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1512
1794
  amount=abs(self.amount),
1513
1795
  unit_model=self.unit_model,
1514
1796
  receipt_type=self.receipt_type,
1515
- vendor_model=self.vendor_model if self.is_expense() else None,
1797
+ vendor_model=self.vendor_model if self.is_expense() or self.is_debt_payment() else None,
1516
1798
  customer_model=self.customer_model if self.is_sales() else None,
1517
1799
  charge_account=self.get_coa_account_model(),
1518
- receipt_account=self.account_model,
1800
+ receipt_account=self.account_model if self.is_mapped() else None,
1519
1801
  staged_transaction_model=self,
1520
1802
  commit=True,
1521
1803
  )
1522
1804
 
1523
1805
  return receipt_model
1524
1806
 
1807
+ def can_undo_import(self):
1808
+ if all([self.is_children(), self.is_bundled()]):
1809
+ return False
1810
+
1811
+ return True
1812
+
1525
1813
  # UNDO
1526
1814
  def undo_import(self, raise_exception: bool = True):
1527
1815
  """
@@ -1536,6 +1824,12 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1536
1824
  If there is no receipt model or transaction model to undo.
1537
1825
 
1538
1826
  """
1827
+ if not self.can_undo_import():
1828
+ if raise_exception:
1829
+ raise StagedTransactionModelValidationError(
1830
+ message='Cannot undo children bundled import. Must undo the parent import.'
1831
+ )
1832
+
1539
1833
  with transaction.atomic():
1540
1834
  # Receipt import case...
1541
1835
  try:
@@ -1555,15 +1849,18 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1555
1849
  if self.transaction_model_id:
1556
1850
  tx_model = self.transaction_model
1557
1851
  journal_entry_model = tx_model.journal_entry
1558
- journal_entry_model.delete()
1852
+
1853
+ if journal_entry_model.can_unlock() and journal_entry_model.can_unpost():
1854
+ journal_entry_model.unlock(raise_exception=False)
1855
+ journal_entry_model.unpost(raise_exception=False)
1856
+ journal_entry_model.delete()
1857
+
1559
1858
  self.transaction_model = None
1560
1859
  self.save(update_fields=['transaction_model', 'updated'])
1561
1860
  return
1562
1861
 
1563
1862
  if raise_exception:
1564
- raise StagedTransactionModelValidationError(
1565
- message=_('Nothing to undo for this staged transaction.')
1566
- )
1863
+ raise StagedTransactionModelValidationError(message=_('Nothing to undo for this staged transaction.'))
1567
1864
 
1568
1865
  def clean(self, verify: bool = False):
1569
1866
  if self.has_children():
@@ -1579,14 +1876,26 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
1579
1876
  if not self.can_have_activity():
1580
1877
  self.activity = None
1581
1878
 
1879
+ if self.is_sales():
1880
+ self.vendor_model = None
1881
+
1882
+ if self.is_expense():
1883
+ self.customer_model = None
1884
+
1885
+ if self.is_children() and self.is_bundled():
1886
+ self.vendor_model = None
1887
+ self.customer_model = None
1888
+
1582
1889
  if self.is_children():
1890
+ self.bundle_split = self.parent.bundle_split
1891
+
1892
+ if all([self.is_parent(), not self.is_bundled()]):
1583
1893
  self.vendor_model = None
1584
1894
  self.customer_model = None
1585
1895
 
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
- )
1896
+ if self.is_transfer():
1897
+ self.vendor_model = None
1898
+ self.customer_model = None
1590
1899
 
1591
1900
  if verify:
1592
1901
  self.is_role_mapping_valid(raise_exception=True)
@@ -1632,14 +1941,9 @@ def importjobmodel_presave(instance: ImportJobModel, **kwargs):
1632
1941
  entity ID of the Ledger Model.
1633
1942
  """
1634
1943
  if instance.is_configured():
1635
- if (
1636
- instance.bank_account_model.entity_model_id
1637
- != instance.ledger_model.entity_id
1638
- ):
1944
+ if instance.bank_account_model.entity_model_id != instance.ledger_model.entity_id:
1639
1945
  raise ImportJobModelValidationError(
1640
- message=_(
1641
- 'Invalid Bank Account for LedgerModel. No matching Entity Model found.'
1642
- )
1946
+ message=_('Invalid Bank Account for LedgerModel. No matching Entity Model found.')
1643
1947
  )
1644
1948
 
1645
1949