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

Files changed (31) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/contrib/django_ledger_graphene/bank_account/schema.py +1 -1
  3. django_ledger/forms/bank_account.py +16 -12
  4. django_ledger/forms/data_import.py +70 -33
  5. django_ledger/io/io_core.py +945 -127
  6. django_ledger/io/io_generator.py +7 -3
  7. django_ledger/io/ofx.py +37 -16
  8. django_ledger/migrations/0020_remove_bankaccountmodel_django_ledg_cash_ac_59a8af_idx_and_more.py +44 -0
  9. django_ledger/migrations/0021_alter_bankaccountmodel_account_model_and_more.py +33 -0
  10. django_ledger/models/bank_account.py +14 -11
  11. django_ledger/models/customer.py +3 -13
  12. django_ledger/models/data_import.py +690 -35
  13. django_ledger/models/entity.py +39 -24
  14. django_ledger/models/journal_entry.py +18 -8
  15. django_ledger/models/mixins.py +17 -3
  16. django_ledger/models/vendor.py +2 -2
  17. django_ledger/settings.py +18 -22
  18. django_ledger/templates/django_ledger/bank_account/tags/bank_accounts_table.html +2 -2
  19. django_ledger/templates/django_ledger/data_import/data_import_job_txs.html +1 -1
  20. django_ledger/templates/django_ledger/data_import/import_job_create.html +11 -2
  21. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +1 -1
  22. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +5 -2
  23. django_ledger/templatetags/django_ledger.py +12 -12
  24. django_ledger/views/bank_account.py +1 -1
  25. django_ledger/views/data_import.py +60 -134
  26. {django_ledger-0.7.4.dist-info → django_ledger-0.7.5.dist-info}/METADATA +17 -17
  27. {django_ledger-0.7.4.dist-info → django_ledger-0.7.5.dist-info}/RECORD +31 -29
  28. {django_ledger-0.7.4.dist-info → django_ledger-0.7.5.dist-info}/WHEEL +1 -1
  29. {django_ledger-0.7.4.dist-info → django_ledger-0.7.5.dist-info}/top_level.txt +1 -0
  30. {django_ledger-0.7.4.dist-info → django_ledger-0.7.5.dist-info}/AUTHORS.md +0 -0
  31. {django_ledger-0.7.4.dist-info → django_ledger-0.7.5.dist-info}/LICENSE +0 -0
@@ -1,11 +1,19 @@
1
1
  """
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
+
5
+ This module provides core functionality for handling data import jobs and staged transactions in the `Django Ledger`
6
+ application. It introduces two primary models to facilitate the import and processing of transactions:
7
+
8
+ 1. `ImportJobModel` - Represents jobs that handle financial data import tasks.
9
+ 2. `StagedTransactionModel` - Represents individual transactions, including those that are staged for review, mapping,
10
+ or further processing.
11
+
4
12
  """
5
13
 
6
14
  from decimal import Decimal
7
15
  from typing import Optional, Set, Dict, List
8
- from uuid import uuid4
16
+ from uuid import uuid4, UUID
9
17
 
10
18
  from django.core.exceptions import ValidationError
11
19
  from django.db import models
@@ -29,16 +37,51 @@ class ImportJobModelQuerySet(QuerySet):
29
37
 
30
38
 
31
39
  class ImportJobModelManager(Manager):
40
+ """
41
+ Manages queryset operations related to import jobs.
42
+
43
+ This manager provides custom queryset handling for import job models, including
44
+ annotations for custom fields like transaction counts, user-specific filters,
45
+ and entity-specific filters. It is integrated with the ImportJobModel, designed
46
+ to support complex query requirements with field annotations and related object
47
+ optimizations for performance efficiency.
48
+
49
+ """
32
50
 
33
51
  def get_queryset(self):
34
- qs = super().get_queryset()
52
+ """
53
+ Generates a QuerySet with annotated data for ImportJobModel.
54
+
55
+ This method constructs a custom QuerySet for ImportJobModel with multiple
56
+ annotations and related fields. It includes counts for specific transaction
57
+ states, calculates pending transactions, and checks for completion status
58
+ of the import job. The QuerySet uses annotations and filters to derive
59
+ various properties required for processing.
60
+
61
+ Returns
62
+ -------
63
+ QuerySet
64
+ A QuerySet with additional annotations:
65
+ - _entity_uuid : UUID of the entity associated with the ledger model.
66
+ - _entity_slug : Slug of the entity associated with the ledger model.
67
+ - txs_count : Integer count of non-root transactions.
68
+ - txs_mapped_count : Integer count of mapped transactions based on specific
69
+ conditions.
70
+ - txs_pending : Integer count of pending transactions, calculated as
71
+ txs_count - txs_mapped_count.
72
+ - is_complete : Boolean value indicating if the import job is complete
73
+ (no pending transactions or total count is zero).
74
+ """
75
+ qs = ImportJobModelQuerySet(self.model, using=self._db)
35
76
  return qs.annotate(
77
+ _entity_uuid=F('ledger_model__entity__uuid'),
78
+ _entity_slug=F('ledger_model__entity__slug'),
36
79
  txs_count=Count('stagedtransactionmodel',
37
80
  filter=Q(stagedtransactionmodel__parent__isnull=False)),
38
81
  txs_mapped_count=Count(
39
82
  'stagedtransactionmodel__account_model_id',
40
- filter=Q(stagedtransactionmodel__parent__isnull=False) | Q(
41
- stagedtransactionmodel__parent__parent__isnull=False)
83
+ filter=Q(stagedtransactionmodel__parent__isnull=False) |
84
+ Q(stagedtransactionmodel__parent__parent__isnull=False)
42
85
 
43
86
  ),
44
87
  ).annotate(
@@ -52,11 +95,30 @@ class ImportJobModelManager(Manager):
52
95
  ),
53
96
  ).select_related(
54
97
  'bank_account_model',
55
- 'bank_account_model__cash_account',
98
+ 'bank_account_model__account_model',
56
99
  'ledger_model'
57
100
  )
58
101
 
59
102
  def for_user(self, user_model):
103
+ """
104
+ Filters the queryset based on the user's permissions for accessing the data
105
+ related to bank accounts and entities they manage or administer.
106
+
107
+ This method first retrieves the default queryset. If the user is a superuser,
108
+ the query will return the full queryset without any filters. Otherwise, the
109
+ query will be limited to the entities that the user either administers or is
110
+ listed as a manager for.
111
+
112
+ Parameters
113
+ ----------
114
+ user_model : User
115
+ The user model instance whose permissions determine the filtering of the queryset.
116
+
117
+ Returns
118
+ -------
119
+ QuerySet
120
+ A filtered queryset based on the user's role and associated permissions.
121
+ """
60
122
  qs = self.get_queryset()
61
123
  if user_model.is_superuser:
62
124
  return qs
@@ -74,10 +136,33 @@ class ImportJobModelManager(Manager):
74
136
 
75
137
 
76
138
  class ImportJobModelAbstract(CreateUpdateMixIn):
139
+ """
140
+ Abstract model for managing import jobs within a financial system.
141
+
142
+ This abstract model serves as a foundational base for managing import jobs involving
143
+ bank accounts and ledger models. It provides functionalities such as linking to an
144
+ associated bank account and ledger model, determining completion status of the
145
+ import job, and properties for UUID and slug identifiers. Additionally, helper
146
+ methods are provided for configuration and deletion confirmation.
147
+
148
+ Attributes
149
+ ----------
150
+ uuid : UUID
151
+ Unique identifier for the import job instance.
152
+ description : str
153
+ Descriptive label or description for the import job.
154
+ bank_account_model : BankAccountModel
155
+ Foreign key linking the import job to a bank account model.
156
+ ledger_model : LedgerModel or None
157
+ One-to-one field linking the import job to a ledger model. Can be null or blank.
158
+ completed : bool
159
+ Indicates whether the import job has been completed.
160
+ objects : ImportJobModelManager
161
+ Manager for handling query operations and model lifecycle.
162
+ """
77
163
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
78
164
  description = models.CharField(max_length=200, verbose_name=_('Description'))
79
165
  bank_account_model = models.ForeignKey('django_ledger.BankAccountModel',
80
- editable=False,
81
166
  on_delete=models.CASCADE,
82
167
  verbose_name=_('Associated Bank Account Model'))
83
168
  ledger_model = models.OneToOneField('django_ledger.LedgerModel',
@@ -87,7 +172,7 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
87
172
  null=True,
88
173
  blank=True)
89
174
  completed = models.BooleanField(default=False, verbose_name=_('Import Job Completed'))
90
- objects = ImportJobModelManager.from_queryset(queryset_class=ImportJobModelQuerySet)()
175
+ objects = ImportJobModelManager()
91
176
 
92
177
  class Meta:
93
178
  abstract = True
@@ -98,53 +183,207 @@ class ImportJobModelAbstract(CreateUpdateMixIn):
98
183
  models.Index(fields=['completed']),
99
184
  ]
100
185
 
186
+ @property
187
+ def entity_uuid(self) -> UUID:
188
+ """
189
+ Get the UUID of the entity associated with the ledger model.
190
+
191
+ This property retrieves the UUID of the entity. If the `_entity_uuid`
192
+ attribute exists, it is returned. Otherwise, the UUID is fetched
193
+ from the `entity_model_id` attribute of the `ledger_model` instance.
194
+
195
+ Returns
196
+ -------
197
+ str
198
+ The UUID of the entity as a string.
199
+ """
200
+ try:
201
+ return getattr(self, '_entity_uuid')
202
+ except AttributeError:
203
+ pass
204
+ return self.ledger_model.entity_model_id
205
+
206
+ @property
207
+ def entity_slug(self) -> str:
208
+ """
209
+ Returns the slug identifier for the entity associated with the current instance.
210
+
211
+ The entity slug is a unique string that represents the associated entity in a
212
+ human-readable format. If the `_entity_slug` property is explicitly set, it is
213
+ returned. Otherwise, the slug associated with the `entity_model` within the
214
+ `ledger_model` is used as the default value.
215
+
216
+
217
+ Returns
218
+ -------
219
+ str
220
+ The slug identifier related to the entity.
221
+ """
222
+ try:
223
+ return getattr(self, '_entity_slug')
224
+ except AttributeError:
225
+ pass
226
+ return self.ledger_model.entity_model.slug
227
+
101
228
  def is_configured(self):
229
+ """
230
+ Checks if the configuration for the instance is complete.
231
+
232
+ This method verifies whether the necessary attributes for ledger model ID
233
+ and bank account model ID are set. Only when both attributes are
234
+ non-None, the configuration is considered complete.
235
+
236
+ Returns
237
+ -------
238
+ bool
239
+ True if both `ledger_model_id` and `bank_account_model_id` attributes
240
+ are set (not None), otherwise False.
241
+ """
102
242
  return all([
103
243
  self.ledger_model_id is not None,
104
244
  self.bank_account_model_id is not None
105
245
  ])
106
246
 
107
247
  def configure(self, commit: bool = True):
248
+ """
249
+ Configures the ledger model if not already configured and optionally commits the changes.
250
+
251
+ This method checks if the ledger model is configured, and if not, it creates a new ledger
252
+ based on the associated bank account model's entity model. Additionally, it can commit
253
+ the changes to update the database based on the given parameter.
254
+
255
+ Parameters
256
+ ----------
257
+ commit : bool, optional
258
+ Determines whether to persist the changes to the database. Defaults to `True`.
259
+ """
108
260
  if not self.is_configured():
109
261
  if self.ledger_model_id is None:
110
262
  self.ledger_model = self.bank_account_model.entity_model.create_ledger(
111
263
  name=self.description
112
264
  )
113
265
  if commit:
114
- self.save(update_fields=[
115
- 'ledger_model'
116
- ])
266
+ self.save(
267
+ update_fields=[
268
+ 'ledger_model'
269
+ ])
117
270
 
118
271
  def get_delete_message(self) -> str:
119
272
  return _(f'Are you sure you want to delete Import Job {self.description}?')
120
273
 
121
274
 
122
275
  class StagedTransactionModelQuerySet(QuerySet):
276
+ """
277
+ Represents a custom QuerySet for handling staged transaction models.
278
+
279
+ This class extends the standard Django QuerySet to add custom filtering methods
280
+ for staged transaction models. These methods help in querying the data based
281
+ on certain conditions specific to the staged transaction model's state or
282
+ relationships.
283
+ """
123
284
 
124
285
  def is_pending(self):
286
+ """
287
+ Determines if there are any pending transactions.
288
+
289
+ This method filters the objects in the queryset to determine whether there
290
+ are any transactions that are pending (i.e., have a null transaction_model).
291
+ Pending transactions are identified by checking if the `transaction_model` is
292
+ null for any of the objects in the queryset.
293
+
294
+ Returns
295
+ -------
296
+ QuerySet
297
+ A QuerySet containing objects with a null `transaction_model`.
298
+
299
+ """
125
300
  return self.filter(transaction_model__isnull=True)
126
301
 
127
302
  def is_imported(self):
303
+ """
304
+ Filter method to determine if the objects in a queryset have been linked with a
305
+ related transaction model. This function checks whether the `transaction_model`
306
+ field in the related objects is non-null.
307
+
308
+ Returns
309
+ -------
310
+ QuerySet
311
+ A filtered queryset containing only objects where the `transaction_model`
312
+ is not null.
313
+ """
128
314
  return self.filter(transaction_model__isnull=False)
129
315
 
130
316
  def is_parent(self):
317
+ """
318
+ Determines whether the current queryset refers to parent objects based on a
319
+ null check for the `parent_id` field.
320
+
321
+ This method applies a filter to the queryset and restricts it to objects
322
+ where the `parent_id` is null. It is often used in hierarchical or
323
+ parent-child data structures to fetch only parent items in the structure.
324
+
325
+ Returns
326
+ -------
327
+ QuerySet
328
+ A filtered queryset containing only the objects with `parent_id` set
329
+ to null. The type of the queryset depends on the model class used
330
+ when invoking this method.
331
+ """
131
332
  return self.filter(parent_id__isnull=True)
132
333
 
133
334
  def is_ready_to_import(self):
335
+ """
336
+ Checks whether items are ready to be imported by applying a filter.
337
+
338
+ This function filters elements based on the `ready_to_import` attribute.
339
+ It is typically used to identify and retrieve items marked as ready for
340
+ further processing or importing.
341
+
342
+ Returns
343
+ -------
344
+ QuerySet
345
+ A QuerySet of elements that satisfy the `ready_to_import` condition.
346
+ """
134
347
  return self.filter(ready_to_import=True)
135
348
 
136
349
 
137
350
  class StagedTransactionModelManager(Manager):
138
351
 
139
352
  def get_queryset(self):
140
- qs = super().get_queryset()
353
+ """
354
+ Fetch and annotate the queryset for staged transaction models to include additional
355
+ related fields and calculated annotations for further processing and sorting.
356
+
357
+ The method constructs a queryset with various related fields selected and annotated
358
+ for convenience. It includes fields for related account models, units, transactions,
359
+ journal entries, and import jobs. Annotations are added to calculate properties such
360
+ as the number of child transactions, the total amount split, and whether the transaction
361
+ is ready to import or can be split into journal entries.
362
+
363
+ Returns
364
+ -------
365
+ QuerySet
366
+ A Django QuerySet preconfigured with selected related fields and annotations
367
+ for staged transaction models.
368
+ """
369
+ qs = StagedTransactionModelQuerySet(self.model, using=self._db)
141
370
  return qs.select_related(
142
- 'parent',
143
371
  'account_model',
144
372
  'unit_model',
145
373
  'transaction_model',
146
374
  'transaction_model__journal_entry',
147
- 'transaction_model__account').annotate(
375
+ 'transaction_model__account',
376
+
377
+ 'import_job',
378
+ 'import_job__bank_account_model__account_model',
379
+
380
+ # selecting parent data....
381
+ 'parent',
382
+ 'parent__account_model',
383
+ 'parent__unit_model',
384
+ ).annotate(
385
+ entity_slug=F('import_job__bank_account_model__entity_model__slug'),
386
+ entity_unit=F('transaction_model__journal_entry__entity_unit__name'),
148
387
  children_count=Count('split_transaction_set'),
149
388
  children_mapped_count=Count('split_transaction_set__account_model_id'),
150
389
  total_amount_split=Coalesce(
@@ -156,7 +395,6 @@ class StagedTransactionModelManager(Manager):
156
395
  When(parent_id__isnull=False, then=F('parent_id'))
157
396
  ),
158
397
  ).annotate(
159
- entity_unit=F('transaction_model__journal_entry__entity_unit__name'),
160
398
  ready_to_import=Case(
161
399
  # is mapped singleton...
162
400
  When(
@@ -202,19 +440,51 @@ class StagedTransactionModelManager(Manager):
202
440
  '-children_count'
203
441
  )
204
442
 
205
- def for_job(self, entity_slug: str, user_model, job_pk):
206
- qs = self.get_queryset()
207
- return qs.filter(
208
- Q(import_job__bank_account_model__entity__slug__exact=entity_slug) &
209
- (
210
- Q(import_job__bank_account_model__entity__admin=user_model) |
211
- Q(import_job__bank_account_model__entity__managers__in=[user_model])
212
- ) &
213
- Q(import_job__uuid__exact=job_pk)
214
- ).prefetch_related('split_transaction_set')
215
-
216
443
 
217
444
  class StagedTransactionModelAbstract(CreateUpdateMixIn):
445
+ """
446
+ Represents an abstract model for staged transactions in a financial application.
447
+
448
+ This abstract class is designed to handle and manage staged transactions that may be
449
+ split into multiple child transactions for financial processing purposes. It includes
450
+ various attributes and methods to validate, process, and structure financial data for
451
+ import and transaction management. The model supports hierarchical relationships,
452
+ role mapping, unit handling, and other important functionalities required for staged
453
+ transactions.
454
+
455
+ Attributes
456
+ ----------
457
+ uuid : UUIDField
458
+ The unique identifier for the transaction.
459
+ parent : ForeignKey
460
+ Reference to the parent transaction if this is a child split transaction.
461
+ import_job : ForeignKey
462
+ Reference to the related import job that this staged transaction is part of.
463
+ fit_id : CharField
464
+ Identifier related to the financial institution transaction (FIT ID).
465
+ date_posted : DateField
466
+ The date on which the transaction was posted.
467
+ bundle_split : BooleanField
468
+ Indicates whether the transaction's split children are bundled into one record.
469
+ activity : CharField
470
+ Proposed activity for the staged transaction (e.g., spending, income categorization).
471
+ amount : DecimalField
472
+ The transaction amount, representing the value of the main transaction.
473
+ amount_split : DecimalField
474
+ The split amount for children when the transaction is split.
475
+ name : CharField
476
+ The name or title for the transaction description.
477
+ memo : CharField
478
+ Additional information or notes attached to the transaction.
479
+ account_model : ForeignKey
480
+ The related account model this transaction is associated with.
481
+ unit_model : ForeignKey
482
+ The unit model or entity associated with this transaction for accounting purposes.
483
+ transaction_model : OneToOneField
484
+ The actual transaction model associated with this staged transaction post-import.
485
+ objects : Manager
486
+ Custom manager for handling queries related to `StagedTransactionModel`.
487
+ """
218
488
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
219
489
  parent = models.ForeignKey('self',
220
490
  null=True,
@@ -257,7 +527,7 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
257
527
  null=True,
258
528
  blank=True)
259
529
 
260
- objects = StagedTransactionModelManager.from_queryset(queryset_class=StagedTransactionModelQuerySet)()
530
+ objects = StagedTransactionModelManager()
261
531
 
262
532
  class Meta:
263
533
  abstract = True
@@ -277,9 +547,35 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
277
547
  return f'{self.__class__.__name__}: {self.get_amount()}'
278
548
 
279
549
  def from_commit_dict(self, split_amount: Optional[Decimal] = None) -> List[Dict]:
550
+ """
551
+ Converts a commit dictionary to a list of dictionaries containing
552
+ transactional data. The method processes the transaction's amount,
553
+ determines its type (DEBIT or CREDIT), and bundles relevant information
554
+ from the current object's attributes.
555
+
556
+ Parameters
557
+ ----------
558
+ split_amount : Optional[Decimal], optional
559
+ A specific amount to override the transaction's default amount
560
+ (`self.amount`). If not provided, `self.amount` will be used.
561
+
562
+ Returns
563
+ -------
564
+ List[Dict]
565
+ A list containing a single dictionary with the following keys:
566
+ - 'account': The account associated with the transaction,
567
+ derived from `self.import_job.bank_account_model.account_model`.
568
+ - 'amount': The absolute value of the transaction amount.
569
+ - 'tx_type': The type of transaction, either DEBIT if the amount
570
+ is positive or CREDIT if negative.
571
+ - 'description': A descriptor for the transaction, taken from
572
+ `self.name`.
573
+ - 'staged_tx_model': A reference to the current object, representing
574
+ the staged transaction model.
575
+ """
280
576
  amt = split_amount if split_amount else self.amount
281
577
  return [{
282
- 'account': self.import_job.bank_account_model.cash_account,
578
+ 'account': self.import_job.bank_account_model.account_model,
283
579
  'amount': abs(amt),
284
580
  'tx_type': DEBIT if not amt < 0.00 else CREDIT,
285
581
  'description': self.name,
@@ -287,8 +583,27 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
287
583
  }]
288
584
 
289
585
  def to_commit_dict(self) -> List[Dict]:
586
+ """
587
+ Converts the current transaction or its children into a list of commit dictionaries.
588
+
589
+ Summarizes information about the transaction or its split children into
590
+ dictionaries containing essential details for committing the transaction.
591
+ Depending on whether the current transaction has child transactions, it processes
592
+ either the child transactions or itself to construct the dictionaries.
593
+
594
+ Returns
595
+ -------
596
+ List[Dict]
597
+ A list of dictionaries, each representing a transaction with fields such as
598
+ account, absolute amount, staged amount, unit model, transaction type, description,
599
+ and the corresponding staged transaction model.
600
+ """
290
601
  if self.has_children():
291
- children_qs = self.split_transaction_set.all()
602
+ children_qs = self.split_transaction_set.all().prefetch_related(
603
+ 'split_transaction_set',
604
+ 'split_transaction_set__account_model',
605
+ 'split_transaction_set__unit_model'
606
+ )
292
607
  return [{
293
608
  'account': child_txs_model.account_model,
294
609
  'amount': abs(child_txs_model.amount_split),
@@ -309,53 +624,202 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
309
624
  }]
310
625
 
311
626
  def commit_dict(self, split_txs: bool = False):
627
+ """
628
+ Generates a list of commit dictionaries or splits commit dictionaries based
629
+ on staged amounts if specified.
630
+
631
+ Parameters
632
+ ----------
633
+ split_txs : bool, optional
634
+ A flag indicating whether to split transactions by their staged amounts.
635
+ If True, the function will generate a split for each staged amount in
636
+ the commit dictionary. Defaults to False.
637
+
638
+ Returns
639
+ -------
640
+ list
641
+ A list representing the commit data. If `split_txs` is True, each entry
642
+ contains pairs of split commit dictionaries and their corresponding
643
+ data. Otherwise, it contains combined commit dictionary data.
644
+ """
312
645
  if split_txs:
313
646
  to_commit = self.to_commit_dict()
314
647
  return [
315
- [self.from_commit_dict(split_amount=to_split['amount_staged'])[0], to_split] for to_split in to_commit
648
+ [
649
+ self.from_commit_dict(split_amount=to_split['amount_staged'])[0], to_split
650
+ ] for to_split in
651
+ to_commit
316
652
  ]
317
653
  return [self.from_commit_dict() + self.to_commit_dict()]
318
654
 
319
655
  def get_amount(self) -> Decimal:
656
+ """
657
+ Returns the appropriate amount based on the object's state.
658
+
659
+ This method determines the amount to be returned based on whether the object
660
+ is classified as a "children" or not. If the `is_children` method returns True,
661
+ it returns the value of `amount_split`. Otherwise, it returns the value of the
662
+ `amount` attribute.
663
+
664
+ Returns
665
+ -------
666
+ Decimal
667
+ The calculated amount based on the object's state.
668
+ """
320
669
  if self.is_children():
321
670
  return self.amount_split
322
671
  return self.amount
323
672
 
324
673
  def is_imported(self) -> bool:
674
+ """
675
+ Determines if the necessary models have been imported for the system to function
676
+ properly. This method checks whether both `account_model_id` and
677
+ `transaction_model_id` are set.
678
+
679
+ Returns
680
+ -------
681
+ bool
682
+ True if both `account_model_id` and `transaction_model_id` are not None,
683
+ indicating that the models have been successfully imported. False otherwise.
684
+ """
325
685
  return all([
326
686
  self.account_model_id is not None,
327
687
  self.transaction_model_id is not None,
328
688
  ])
329
689
 
330
690
  def is_pending(self) -> bool:
691
+ """
692
+ Determine if the transaction is pending.
693
+
694
+ A transaction is considered pending if it has not been assigned a
695
+ `transaction_model_id`. This function checks the attribute and returns
696
+ a boolean indicating the status.
697
+
698
+ Returns
699
+ -------
700
+ bool
701
+ True if the transaction is pending (i.e., `transaction_model_id`
702
+ is None), False otherwise.
703
+ """
331
704
  return self.transaction_model_id is None
332
705
 
333
706
  def is_mapped(self) -> bool:
707
+ """
708
+ Determines if an account model is mapped.
709
+
710
+ This method checks whether the `account_model_id` is assigned a value,
711
+ indicating that the account model has been mapped. It returns a boolean
712
+ result based on the presence of the `account_model_id`.
713
+
714
+ Returns
715
+ -------
716
+ bool
717
+ True if `account_model_id` is not None, indicating the account
718
+ model is mapped. False otherwise.
719
+ """
334
720
  return self.account_model_id is not None
335
721
 
336
722
  def is_single(self) -> bool:
723
+ """
724
+ Checks whether the current instance represents a single entry.
725
+
726
+ This method determines if the current object qualifies as a single entry
727
+ by ensuring that it both does not have children and is not considered a
728
+ child of any other entry. The result is a boolean value indicating
729
+ whether the entry meets these criteria.
730
+
731
+ Returns
732
+ -------
733
+ bool
734
+ True if the entry is a single, standalone entry; False otherwise.
735
+ """
337
736
  return all([
338
737
  not self.is_children(),
339
738
  not self.has_children()
340
739
  ])
341
740
 
342
741
  def is_children(self) -> bool:
742
+ """
743
+ Determines if the current instance qualifies as a child entity based on the existence of a parent ID.
744
+
745
+ Checks whether the current object is associated with a parent by verifying the presence of `parent_id`.
746
+ The method returns `True` if the `parent_id` attribute is not `None`, indicating that the object is indeed a child.
747
+
748
+ Returns
749
+ -------
750
+ bool
751
+ True if the object has a valid `parent_id`, indicating it is a child entity;
752
+ False otherwise.
753
+ """
343
754
  return all([
344
755
  self.parent_id is not None,
345
756
  ])
346
757
 
347
758
  def has_activity(self) -> bool:
759
+ """
760
+ Determine if an activity is present.
761
+
762
+ This method checks whether the `activity` attribute is assigned a value
763
+ or not. If a value is set, it indicates that there is an associated
764
+ activity. Otherwise, no activity is present.
765
+
766
+ Returns
767
+ -------
768
+ bool
769
+ True if the `activity` attribute is not None, indicating the
770
+ presence of an activity. False otherwise.
771
+ """
348
772
  return self.activity is not None
349
773
 
350
774
  def has_children(self) -> bool:
775
+ """
776
+ Determines if the current instance has children.
777
+
778
+ The method checks the state of the instance to determine whether
779
+ it is in the process of adding. If so, it directly returns False,
780
+ signifying no children are present at that moment. Otherwise,
781
+ it evaluates the `children_count` attribute to decide.
782
+
783
+ Returns
784
+ -------
785
+ bool
786
+ True if the instance has children and is not in the process of
787
+ adding; otherwise, False.
788
+ """
351
789
  if self._state.adding:
352
790
  return False
353
791
  return getattr(self, 'children_count') > 0
354
792
 
355
793
  def can_split(self) -> bool:
794
+ """
795
+ Determines if the current object can be split based on its child status.
796
+
797
+ This method checks whether the object does not have any children and, as
798
+ a result, is capable of being split.
799
+
800
+ Returns
801
+ -------
802
+ bool
803
+ `True` if the object has no children and can be split, otherwise
804
+ `False`.
805
+ """
356
806
  return not self.is_children()
357
807
 
358
808
  def can_have_unit(self) -> bool:
809
+ """
810
+ Check if the entity can have a unit.
811
+
812
+ This method evaluates the conditions under which an entity may have a
813
+ unit assigned. It considers several factors including the state of
814
+ the entity, whether it has children, if all children are mapped, and
815
+ its relationship to its parent entity.
816
+
817
+ Returns
818
+ -------
819
+ bool
820
+ A boolean value indicating whether the entity can have a unit.
821
+ Returns `True` if the conditions are satisfied, otherwise `False`.
822
+ """
359
823
  if self._state.adding:
360
824
  return False
361
825
 
@@ -377,14 +841,48 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
377
841
  ]):
378
842
  return True
379
843
 
380
- # if getattr(self.parent, 'can_split_into_je'):
381
- # return True
382
844
  return False
383
845
 
384
846
  def can_have_account(self) -> bool:
847
+ """
848
+ Determines if an account can be created based on the current state.
849
+
850
+ This method assesses whether an account can be created for an entity
851
+ by checking if the entity has any children. The account creation is
852
+ prohibited if the entity has children and allowed otherwise.
853
+
854
+ Returns
855
+ -------
856
+ bool
857
+ True if the entity can have an account, False otherwise.
858
+ """
385
859
  return not self.has_children()
386
860
 
387
861
  def can_import(self, as_split: bool = False) -> bool:
862
+ """
863
+ Determines whether the object is ready for importing data and can optionally
864
+ be split into "je" (journal entries) for import if applicable.
865
+
866
+ This method evaluates the readiness of the object for importing based on
867
+ its attributes and conditions. It first checks if the object is marked as
868
+ ready for import. If the object supports splitting into journal entries
869
+ and the `as_split` argument is True, the method considers it eligible for
870
+ import as split entries. If neither of the above conditions are met, it
871
+ checks whether the role mapping is valid without raising exceptions and
872
+ returns the result.
873
+
874
+ Parameters
875
+ ----------
876
+ as_split : bool, optional
877
+ Specifies if the object should be checked for readiness to be split
878
+ into "je" (journal entries) for import. Defaults to False.
879
+
880
+ Returns
881
+ -------
882
+ bool
883
+ True if the object is ready to import (optionally as split entries),
884
+ otherwise False.
885
+ """
388
886
  ready_to_import = getattr(self, 'ready_to_import')
389
887
  if not ready_to_import:
390
888
  return False
@@ -397,6 +895,31 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
397
895
  ])
398
896
 
399
897
  def add_split(self, raise_exception: bool = True, commit: bool = True, n: int = 1):
898
+ """
899
+ Adds a specified number of split transactions to the staged transaction.
900
+
901
+ The method checks whether the staged transaction can be split and ensures it
902
+ has no children before proceeding. If requested, it raises an exception if a split
903
+ transaction is not allowed. New split transactions are created and validated before
904
+ optionally committing them to the database.
905
+
906
+ Parameters
907
+ ----------
908
+ raise_exception : bool
909
+ Determines if an exception should be raised when splitting is not allowed.
910
+ Default is True.
911
+ commit : bool
912
+ Indicates whether to commit the newly created split transactions to the
913
+ database. Default is True.
914
+ n : int, optional
915
+ The number of split transactions to create. If the staged transaction has
916
+ no children, one additional split transaction is created. Default is 1.
917
+
918
+ Returns
919
+ -------
920
+ list of StagedTransactionModel
921
+ List of newly created staged transactions in the split.
922
+ """
400
923
  if not self.can_split():
401
924
  if raise_exception:
402
925
  raise ImportJobModelValidationError(
@@ -428,20 +951,83 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
428
951
  return new_txs
429
952
 
430
953
  def is_total_amount_split(self) -> bool:
954
+ """
955
+ Indicates whether the total amount is distributed as per the split rules.
956
+
957
+ Returns
958
+ -------
959
+ bool
960
+ True if the `amount` attribute equals the `total_amount_split` attribute,
961
+ indicating the total amount is split correctly; False otherwise.
962
+ """
431
963
  return self.amount == getattr(self, 'total_amount_split')
432
964
 
433
965
  def are_all_children_mapped(self) -> bool:
966
+ """
967
+ Determines whether all children have been mapped.
968
+
969
+ This method compares the total number of children with the number
970
+ of mapped children to check whether all children have been mapped.
971
+
972
+ Returns
973
+ -------
974
+ bool
975
+ True if the number of children equals the number of mapped children,
976
+ otherwise False.
977
+ """
434
978
  return getattr(self, 'children_count') == getattr(self, 'children_mapped_count')
435
979
 
436
- def get_import_role_set(self) -> Optional[Set[str]]:
980
+ def get_import_role_set(self) -> Set[str]:
981
+ """
982
+ Retrieves the set of roles associated with import.
983
+
984
+ This method determines the role(s) based on the current instance's state and
985
+ its associated transactions. If the instance is single and mapped, the role
986
+ directly tied to its account model is returned. If the instance has child
987
+ split transactions and all of them are mapped, the roles associated with
988
+ each transaction's account model, excluding a specific type of role, are
989
+ aggregated and returned as a set.
990
+
991
+ Returns
992
+ -------
993
+ Set[str]
994
+ A set of roles derived from the account model(s) relating to the
995
+ instance or its child transactions. Returns empty set if no roles
996
+ can be determined.
997
+ """
437
998
  if self.is_single() and self.is_mapped():
438
999
  return {self.account_model.role}
439
1000
  if self.has_children():
440
1001
  split_txs_qs = self.split_transaction_set.all()
441
1002
  if all([txs.is_mapped() for txs in split_txs_qs]):
442
1003
  return set([txs.account_model.role for txs in split_txs_qs if txs.account_model.role != ASSET_CA_CASH])
1004
+ return set()
443
1005
 
444
1006
  def get_prospect_je_activity_try(self, raise_exception: bool = True, force_update: bool = False) -> Optional[str]:
1007
+ """
1008
+ Retrieve or attempt to fetch the journal entry activity for the current prospect object.
1009
+
1010
+ The method determines whether the activity should be updated or fetched based on the
1011
+ current state. If the `force_update` flag is set to True or required conditions are met,
1012
+ it attempts to retrieve the activity from the associated roles of the journal entry.
1013
+ The activity is then saved and optionally returned. If an exception occurs during this
1014
+ process and `raise_exception` is set to True, the exception is propagated.
1015
+
1016
+ Parameters
1017
+ ----------
1018
+ raise_exception : bool, optional
1019
+ Specifies whether to raise exceptions in case of validation errors.
1020
+ Default is True.
1021
+ force_update : bool, optional
1022
+ Forces the method to fetch and update the activity even if it already exists.
1023
+ Default is False.
1024
+
1025
+ Returns
1026
+ -------
1027
+ Optional[str]
1028
+ The journal entry activity if successfully retrieved or updated; otherwise,
1029
+ returns the existing activity or None if no activity is present.
1030
+ """
445
1031
  ready_to_import = getattr(self, 'ready_to_import')
446
1032
  if (not self.has_activity() and ready_to_import) or force_update:
447
1033
  JournalEntryModel = lazy_loader.get_journal_entry_model()
@@ -457,15 +1043,62 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
457
1043
  return self.activity
458
1044
 
459
1045
  def get_prospect_je_activity(self) -> Optional[str]:
1046
+ """
1047
+ Gets the activity of the prospect JE (Journal Entry) in a safe manner.
1048
+
1049
+ This method retrieves the activity of the prospect journal entry (JE). If the
1050
+ activity cannot be retrieved, it will return `None` instead of raising an
1051
+ exception. It serves as a wrapper for the `get_prospect_je_activity_try`
1052
+ method by specifying that exceptions should not be raised during retrieval.
1053
+
1054
+ Returns
1055
+ -------
1056
+ Optional[str]
1057
+ The activity of the prospect journal entry if available, otherwise `None`.
1058
+ """
460
1059
  return self.get_prospect_je_activity_try(raise_exception=False)
461
1060
 
462
1061
  def get_prospect_je_activity_display(self) -> Optional[str]:
1062
+ """
1063
+ Provides functionality to retrieve and display the prospect journal entry activity
1064
+ based on the mapped activity associated with a prospect. The method attempts to
1065
+ fetch the journal entry activity safely and returns its display name if available.
1066
+
1067
+ Returns
1068
+ -------
1069
+ Optional[str]
1070
+ The display name of the prospect journal entry activity if it exists,
1071
+ otherwise None.
1072
+ """
463
1073
  activity = self.get_prospect_je_activity_try(raise_exception=False)
464
1074
  if activity is not None:
465
1075
  JournalEntryModel = lazy_loader.get_journal_entry_model()
466
1076
  return JournalEntryModel.MAP_ACTIVITIES[activity]
467
1077
 
468
1078
  def is_role_mapping_valid(self, raise_exception: bool = False) -> bool:
1079
+ """
1080
+ Determines if the role mapping is valid by verifying associated activities.
1081
+
1082
+ The method checks for the presence of an activity linked to the object.
1083
+ If no activity is found, it attempts to fetch one. The validity of the
1084
+ role mapping is determined by the success of this process.
1085
+
1086
+ Parameters
1087
+ ----------
1088
+ raise_exception : bool, optional
1089
+ Determines whether to raise an exception if validation fails
1090
+ (default is False).
1091
+
1092
+ Returns
1093
+ -------
1094
+ bool
1095
+ True if the role mapping is valid, otherwise False.
1096
+
1097
+ Raises
1098
+ ------
1099
+ ValidationError
1100
+ If raise_exception is set to True and the validation process fails.
1101
+ """
469
1102
  if not self.has_activity():
470
1103
  try:
471
1104
  activity = self.get_prospect_je_activity_try(raise_exception=raise_exception)
@@ -480,15 +1113,35 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
480
1113
  return True
481
1114
 
482
1115
  def migrate(self, split_txs: bool = False):
1116
+ """
1117
+ Migrate transactional data to the ledger model by processing the provided
1118
+ transactions and committing them. This process involves using the provided
1119
+ parameter to determine transaction splitting and subsequently saving the
1120
+ processed transactional data for each entry in the commit dictionary.
1121
+
1122
+ Parameters
1123
+ ----------
1124
+ split_txs : bool, optional
1125
+ A flag that determines whether the transactions should be split into
1126
+ multiple entries based on the associated commit data. Defaults to False.
1127
+
1128
+ Notes
1129
+ -----
1130
+ The method checks if the transactional data can be imported using the
1131
+ `can_import` method. If successful, it creates a commit dictionary and
1132
+ processes it by committing all transactional data to the ledger model.
1133
+ The saved objects are staged with appropriate models to retain the
1134
+ transaction state.
1135
+ """
483
1136
  if self.can_import(as_split=split_txs):
484
1137
  commit_dict = self.commit_dict(split_txs=split_txs)
485
1138
  import_job = self.import_job
486
1139
  ledger_model = import_job.ledger_model
487
1140
 
488
- if len(commit_dict):
1141
+ if len(commit_dict) > 0:
489
1142
  for je_data in commit_dict:
490
1143
  unit_model = self.unit_model if not split_txs else commit_dict[0][1]['unit_model']
491
- je_model, txs_models = ledger_model.commit_txs(
1144
+ _, _ = ledger_model.commit_txs(
492
1145
  je_timestamp=self.date_posted,
493
1146
  je_unit_model=unit_model,
494
1147
  je_txs=je_data,
@@ -498,7 +1151,9 @@ class StagedTransactionModelAbstract(CreateUpdateMixIn):
498
1151
  )
499
1152
  staged_to_save = [i['staged_tx_model'] for i in je_data]
500
1153
  for i in staged_to_save:
501
- i.save(update_fields=['transaction_model'])
1154
+ i.save(
1155
+ update_fields=['transaction_model', 'updated']
1156
+ )
502
1157
 
503
1158
  def clean(self, verify: bool = False):
504
1159
  if self.has_children():