django-ledger 0.5.6.5__py3-none-any.whl → 0.6.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ledger might be problematic. Click here for more details.

@@ -9,24 +9,23 @@ Contributions to this module:
9
9
  Chart Of Accounts
10
10
  _________________
11
11
 
12
- A Chart of Accounts (CoA) is a collection of accounts logically grouped into a distinct set within a
13
- ChartOfAccountModel. The CoA is the backbone of making of any financial statements and it consist of accounts of many
14
- roles, such as cash, accounts receivable, expenses, liabilities, income, etc. For instance, we can have a heading as
15
- "Fixed Assets" in the Balance Sheet, which will consists of Tangible, Intangible Assets. Further, the tangible assets
16
- will consists of multiple accounts like Building, Plant & Equipments, Machinery. So, aggregation of balances of
17
- individual accounts based on the Chart of Accounts and AccountModel roles, helps in preparation of the Financial
18
- Statements.
19
-
20
- All EntityModel must have a default CoA to be able to create any type of transaction. Throughout the application,
21
- when no explicit CoA is specified, the default behavior is to use the EntityModel default CoA. **Only ONE Chart of
22
- Accounts can be used when creating Journal Entries**. No commingling between CoAs is allowed in order to preserve the
23
- integrity of the Journal Entry.
12
+ A Chart of Accounts (CoA) is a crucial collection of logically grouped accounts within a ChartOfAccountModel,
13
+ forming the backbone of financial statements. The CoA includes various account roles such as cash, accounts receivable,
14
+ expenses, liabilities, and income. For example, the Balance Sheet may have a Fixed Assets heading consisting of
15
+ Tangible and Intangible Assets with multiple accounts like Building, Plant & Equipments, and Machinery under
16
+ tangible assets. Aggregation of individual account balances based on the Chart of Accounts and AccountModel roles is
17
+ essential for preparing Financial Statements.
18
+
19
+ All EntityModel must have a default CoA to create any type of transaction. When no explicit CoA is specified, the
20
+ default behavior is to use the EntityModel default CoA. Only ONE Chart of Accounts can be used when creating
21
+ Journal Entries. No commingling between CoAs is allowed to preserve the integrity of the Journal Entry.
24
22
  """
25
23
  from random import choices
26
24
  from string import ascii_lowercase, digits
27
- from typing import Optional, Union
25
+ from typing import Optional, Union, Dict
28
26
  from uuid import uuid4
29
27
 
28
+ from django.apps import apps
30
29
  from django.contrib.auth import get_user_model
31
30
  from django.core.exceptions import ValidationError
32
31
  from django.db import models
@@ -45,6 +44,8 @@ UserModel = get_user_model()
45
44
 
46
45
  SLUG_SUFFIX = ascii_lowercase + digits
47
46
 
47
+ app_config = apps.get_app_config('django_ledger')
48
+
48
49
 
49
50
  class ChartOfAccountsModelValidationError(ValidationError):
50
51
  pass
@@ -53,6 +54,9 @@ class ChartOfAccountsModelValidationError(ValidationError):
53
54
  class ChartOfAccountModelQuerySet(models.QuerySet):
54
55
 
55
56
  def active(self):
57
+ """
58
+ QuerySet method to retrieve active items.
59
+ """
56
60
  return self.filter(active=True)
57
61
 
58
62
 
@@ -74,13 +78,8 @@ class ChartOfAccountModelManager(models.Manager):
74
78
  user_model
75
79
  Logged in and authenticated django UserModel instance.
76
80
 
77
- Examples
78
- ________
79
- >>> request_user = self.request.user
80
- >>> coa_model_qs = ChartOfAccountModel.objects.for_user(user_model=request_user)
81
-
82
81
  Returns
83
- _______
82
+ -------
84
83
  ChartOfAccountQuerySet
85
84
  Returns a ChartOfAccountQuerySet with applied filters.
86
85
  """
@@ -106,15 +105,8 @@ class ChartOfAccountModelManager(models.Manager):
106
105
  user_model
107
106
  Logged in and authenticated django UserModel instance.
108
107
 
109
- Examples
110
- ________
111
-
112
- >>> request_user = self.request.user
113
- >>> slug = self.kwargs['entity_slug'] # may come from request kwargs
114
- >>> coa_model_qs = ChartOfAccountModelManager.objects.for_entity(user_model=request_user, entity_slug=slug)
115
-
116
108
  Returns
117
- _______
109
+ -------
118
110
  ChartOfAccountQuerySet
119
111
  Returns a ChartOfAccountQuerySet with applied filters.
120
112
  """
@@ -126,26 +118,20 @@ class ChartOfAccountModelManager(models.Manager):
126
118
 
127
119
  class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
128
120
  """
129
- Base implementation of Chart of Accounts Model as an Abstract.
130
-
131
- 2. :func:`CreateUpdateMixIn <django_ledger.models.mixins.SlugMixIn>`
132
- 2. :func:`CreateUpdateMixIn <django_ledger.models.mixins.CreateUpdateMixIn>`
133
-
121
+ Abstract base class for the Chart of Account model.
122
+
134
123
  Attributes
135
124
  ----------
136
- uuid : UUID
137
- This is a unique primary key generated for the table. The default value of this field is uuid4().
138
-
139
- entity: EntityModel
140
- The EntityModel associated with this Chart of Accounts.
141
-
142
- active: bool
143
- This determines whether any changes can be done to the Chart of Accounts.
144
- Inactive Chart of Accounts will not be able to be used in new Transactions.
145
- Default value is set to False (inactive).
146
-
147
- description: str
148
- A user generated description for this Chart of Accounts.
125
+ uuid : UUIDField
126
+ UUID field for the chart of account model (primary key).
127
+ entity : ForeignKey
128
+ ForeignKey to the EntityModel.
129
+ active : BooleanField
130
+ BooleanField indicating whether the chart of account is active or not.
131
+ description : TextField
132
+ TextField storing the description of the chart of account.
133
+ objects : ChartOfAccountModelManager
134
+ Manager for the ChartOfAccountModel.
149
135
  """
150
136
 
151
137
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
@@ -172,16 +158,58 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
172
158
  return self.slug
173
159
 
174
160
  def get_coa_root_accounts_qs(self) -> AccountModelQuerySet:
161
+ """
162
+ Retrieves the root accounts in the chart of accounts.
163
+
164
+ Returns:
165
+ AccountModelQuerySet: A queryset containing the root accounts in the chart of accounts.
166
+ """
175
167
  return self.accountmodel_set.all().is_coa_root()
176
168
 
177
- def get_coa_root_account(self) -> AccountModel:
169
+ def get_coa_root_node(self) -> AccountModel:
170
+ """
171
+ Retrieves the root node of the chart of accounts.
172
+
173
+ Returns:
174
+ AccountModel: The root node of the chart of accounts.
175
+
176
+ """
178
177
  qs = self.get_coa_root_accounts_qs()
179
178
  return qs.get(role__exact=ROOT_COA)
180
179
 
181
- def get_coa_l2_root(self,
182
- account_model: AccountModel,
183
- root_account_qs: Optional[AccountModelQuerySet] = None,
184
- as_queryset: bool = False) -> Union[AccountModelQuerySet, AccountModel]:
180
+ def get_account_root_node(self,
181
+ account_model: AccountModel,
182
+ root_account_qs: Optional[AccountModelQuerySet] = None,
183
+ as_queryset: bool = False) -> AccountModel:
184
+ """
185
+ Fetches the root node of the ChartOfAccountModel instance. The root node is the highest level of the CoA
186
+ hierarchy. It can be used to traverse the hierarchy of the CoA structure downstream.
187
+
188
+
189
+ Parameters
190
+ ----------
191
+ account_model : AccountModel
192
+ The account model for which to find the root node.
193
+ root_account_qs : Optional[AccountModelQuerySet], optional
194
+ The queryset of root accounts. If not provided, it will be retrieved using `get_coa_root_accounts_qs` method.
195
+ as_queryset : bool, optional
196
+ If True, return the root account queryset instead of a single root account. Default is False.
197
+
198
+ Returns
199
+ -------
200
+ Union[AccountModelQuerySet, AccountModel]
201
+ If `as_queryset` is True, returns the root account queryset. Otherwise, returns a single root account.
202
+
203
+ Raises
204
+ ------
205
+ ChartOfAccountsModelValidationError
206
+ If the account model is not part of the chart of accounts.
207
+ """
208
+
209
+ if account_model.coa_model_id != self.uuid:
210
+ raise ChartOfAccountsModelValidationError(
211
+ message=_(f'The account model {account_model} is not part of the chart of accounts {self.name}.'),
212
+ )
185
213
 
186
214
  if not account_model.is_root_account():
187
215
 
@@ -209,13 +237,72 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
209
237
  return qs.get()
210
238
 
211
239
  def get_non_root_coa_accounts_qs(self) -> AccountModelQuerySet:
240
+ """
241
+ Returns a query set of non-root accounts in the chart of accounts.
242
+
243
+ Returns
244
+ -------
245
+ AccountModelQuerySet
246
+ A query set of non-root accounts in the chart of accounts.
247
+ """
212
248
  return self.accountmodel_set.all().not_coa_root()
213
249
 
214
- def get_coa_account_tree(self):
215
- root_account = self.get_coa_root_account()
250
+ def get_coa_accounts(self, active_only: bool = True) -> AccountModelQuerySet:
251
+ """
252
+ Returns the AccountModelQuerySet associated with the ChartOfAccounts model instance.
253
+
254
+ Parameters
255
+ ----------
256
+ active_only : bool, optional
257
+ Flag to indicate whether to retrieve only active accounts or all accounts.
258
+ Default is True.
259
+
260
+ Returns
261
+ -------
262
+ AccountModelQuerySet
263
+ A queryset containing accounts from the chart of accounts.
264
+
265
+ """
266
+ qs = self.get_non_root_coa_accounts_qs()
267
+ if active_only:
268
+ return qs.active()
269
+ return qs
270
+
271
+ def get_coa_account_tree(self) -> Dict:
272
+ """
273
+ Performs a bulk dump of the ChartOfAccounts model instance accounts to a dictionary.
274
+ The method invokes the`dump_bulk` method on the ChartOfAccount model instance root node.
275
+ See Django Tree Beard documentation for more information.
276
+
277
+ Returns
278
+ -------
279
+ Dict
280
+ A dictionary containing all accounts from the chart of accounts in a nested structure.
281
+ """
282
+ root_account = self.get_coa_root_node()
216
283
  return AccountModel.dump_bulk(parent=root_account)
217
284
 
218
285
  def generate_slug(self, raise_exception: bool = False) -> str:
286
+ """
287
+ Generates and assigns a slug based on the ChartOfAccounts model instance EntityModel information.
288
+
289
+
290
+ Parameters
291
+ ----------
292
+ raise_exception : bool, optional
293
+ If set to True, it will raise a ChartOfAccountsModelValidationError if the `self.slug` is already set.
294
+
295
+ Returns
296
+ -------
297
+ str
298
+ The generated slug for the Chart of Accounts.
299
+
300
+ Raises
301
+ ------
302
+ ChartOfAccountsModelValidationError
303
+ If `raise_exception` is set to True and `self.slug` is already set.
304
+
305
+ """
219
306
  if self.slug:
220
307
  if raise_exception:
221
308
  raise ChartOfAccountsModelValidationError(
@@ -225,7 +312,17 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
225
312
  self.slug = f'coa-{self.entity.slug[-5:]}-' + ''.join(choices(SLUG_SUFFIX, k=15))
226
313
 
227
314
  def configure(self, raise_exception: bool = True):
315
+ """
316
+ A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
317
+ to support the insertion of new accounts into the chart of account model tree.
318
+ This method must be called every time the ChartOfAccounts model is created.
228
319
 
320
+ Parameters
321
+ ----------
322
+ raise_exception : bool, optional
323
+ Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
324
+ This indicates that the ChartOfAccountModel instance is already configured.
325
+ """
229
326
  self.generate_slug()
230
327
 
231
328
  root_accounts_qs = self.get_coa_root_accounts_qs()
@@ -274,6 +371,14 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
274
371
  ))
275
372
 
276
373
  def is_default(self) -> bool:
374
+ """
375
+ Check if the ChartOfAccountModel instance is set as the default for the EntityModel.
376
+
377
+ Returns
378
+ -------
379
+ bool
380
+ True if the ChartOfAccountModel instance is set as the default for the EntityModel. Else, False.
381
+ """
277
382
  if not self.entity_id:
278
383
  return False
279
384
  if not self.entity.default_coa_id:
@@ -281,9 +386,29 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
281
386
  return self.entity.default_coa_id == self.uuid
282
387
 
283
388
  def is_active(self) -> bool:
389
+ """
390
+ Check if the ChartOfAccountModel instance is active.
391
+
392
+ Returns:
393
+ bool: True if the ChartOfAccountModel instance is active, False otherwise.
394
+ """
284
395
  return self.active is True
285
396
 
286
397
  def validate_account_model_qs(self, account_model_qs: AccountModelQuerySet):
398
+ """
399
+ Validates the given AccountModelQuerySet for the ChartOfAccountsModel.
400
+
401
+ Parameters
402
+ ----------
403
+ account_model_qs : AccountModelQuerySet
404
+ The AccountModelQuerySet to validate.
405
+
406
+ Raises
407
+ ------
408
+ ChartOfAccountsModelValidationError
409
+ If the account_model_qs is not an instance of AccountModelQuerySet or if it contains an account model with a different coa_model_id than the current CoA model.
410
+
411
+ """
287
412
  if not isinstance(account_model_qs, AccountModelQuerySet):
288
413
  raise ChartOfAccountsModelValidationError(
289
414
  message='Must pass an instance of AccountModelQuerySet'
@@ -294,24 +419,40 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
294
419
  message=f'Invalid root queryset for CoA {self.name}'
295
420
  )
296
421
 
297
- def allocate_account(self,
298
- account_model: AccountModel,
299
- root_account_qs: Optional[AccountModelQuerySet] = None):
422
+ def insert_account(self,
423
+ account_model: AccountModel,
424
+ root_account_qs: Optional[AccountModelQuerySet] = None):
300
425
  """
301
- Allocates a given account model to the appropriate root account depending on the Account Model Role.
426
+ This method inserts the given account model into the chart of accounts (COA) instance.
427
+ It first verifies if the account model's COA model ID matches the COA's UUID. If not, it
428
+ raises a `ChartOfAccountsModelValidationError`. If the `root_account_qs` is not provided, it retrieves the
429
+ root account query set using the `get_coa_root_accounts_qs` method. Providing a pre-fetched `root_account_qs`
430
+ avoids unnecessary retrieval of the root account query set every an account model is inserted into the CoA.
431
+
432
+ Next, it validates the provided `root_account_qs` if it is not None. Then, it obtains the root node for the
433
+ account model using the `get_account_root_node` method and assigns it to `account_root_node`.
434
+
435
+ Finally, it adds the account model as a child to the `account_root_node` and retrieves the updated COA accounts
436
+ query set using the `get_non_root_coa_accounts_qs` method. It returns the inserted account model found in the
437
+ COA accounts query set.
302
438
 
303
439
  Parameters
304
440
  ----------
305
- account_model: AccountModel
306
- The Account Model to Allocate
307
- root_account_qs:
308
- The Root Account QuerySet of the Chart Of Accounts to use.
309
- Will be validated against current CoA Model.
441
+ account_model : AccountModel
442
+ The account model to be inserted into the chart of accounts.
443
+ root_account_qs : Optional[AccountModelQuerySet], default=None
444
+ The root account query set. If not provided, it will be obtained using the `get_coa_root_accounts_qs`
445
+ method.
310
446
 
311
447
  Returns
312
448
  -------
313
449
  AccountModel
314
- The saved and allocated AccountModel.
450
+ The inserted account model.
451
+
452
+ Raises
453
+ ------
454
+ ChartOfAccountsModelValidationError
455
+ If the provided account model has an invalid COA model ID for the current COA.
315
456
  """
316
457
 
317
458
  if account_model.coa_model_id:
@@ -325,13 +466,12 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
325
466
  else:
326
467
  self.validate_account_model_qs(root_account_qs)
327
468
 
328
- l2_root_node: AccountModel = self.get_coa_l2_root(
469
+ account_root_node: AccountModel = self.get_account_root_node(
329
470
  account_model=account_model,
330
471
  root_account_qs=root_account_qs
331
472
  )
332
473
 
333
- account_model.coa_model = self
334
- l2_root_node.add_child(instance=account_model)
474
+ account_root_node.add_child(instance=account_model)
335
475
  coa_accounts_qs = self.get_non_root_coa_accounts_qs()
336
476
  return coa_accounts_qs.get(uuid__exact=account_model.uuid)
337
477
 
@@ -342,17 +482,41 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
342
482
  balance_type: str,
343
483
  active: bool,
344
484
  root_account_qs: Optional[AccountModelQuerySet] = None):
485
+ """
486
+ Proper method for inserting a new Account Model into a CoA.
487
+ Use this in liu of the direct instantiation of the AccountModel of using the django related manager.
488
+
489
+ Parameters
490
+ ----------
491
+ code : str
492
+ The code of the account to be created.
493
+ role : str
494
+ The role of the account. This can be a user-defined value.
495
+ name : str
496
+ The name of the account.
497
+ balance_type : str
498
+ The balance type of the account. This can be a user-defined value.
499
+ active : bool
500
+ Specifies whether the account is active or not.
501
+ root_account_qs : Optional[AccountModelQuerySet], optional
502
+ The query set of root accounts to which the created account should be linked. Defaults to None.
345
503
 
504
+ Returns
505
+ -------
506
+ AccountModel
507
+ The created account model instance.
508
+ """
346
509
  account_model = AccountModel(
347
510
  code=code,
348
511
  name=name,
349
512
  role=role,
350
513
  active=active,
351
- balance_type=balance_type
514
+ balance_type=balance_type,
515
+ coa_model=self
352
516
  )
353
517
  account_model.clean()
354
518
 
355
- account_model = self.allocate_account(
519
+ account_model = self.insert_account(
356
520
  account_model=account_model,
357
521
  root_account_qs=root_account_qs
358
522
  )
@@ -361,14 +525,14 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
361
525
  # ACTIONS -----
362
526
  # todo: use these methods once multi CoA features are enabled...
363
527
  def lock_all_accounts(self) -> AccountModelQuerySet:
364
- non_root_accounts_qs = self.get_non_root_coa_accounts_qs()
365
- non_root_accounts_qs.update(locked=True)
366
- return non_root_accounts_qs
528
+ account_qs = self.get_coa_accounts()
529
+ account_qs.update(locked=True)
530
+ return account_qs
367
531
 
368
532
  def unlock_all_accounts(self) -> AccountModelQuerySet:
369
- non_root_accounts_qs = self.get_non_root_coa_accounts_qs()
370
- non_root_accounts_qs.update(locked=False)
371
- return non_root_accounts_qs
533
+ account_qs = self.get_non_root_coa_accounts_qs()
534
+ account_qs.update(locked=False)
535
+ return account_qs
372
536
 
373
537
  def mark_as_default(self, commit: bool = False, raise_exception: bool = False, **kwargs):
374
538
  """
@@ -415,9 +579,23 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
415
579
  )
416
580
 
417
581
  def can_activate(self) -> bool:
582
+ """
583
+ Check if the ChartOffAccountModel instance can be activated.
584
+
585
+ Returns
586
+ -------
587
+ True if the object can be activated, False otherwise.
588
+ """
418
589
  return self.active is False
419
590
 
420
591
  def can_deactivate(self) -> bool:
592
+ """
593
+ Check if the ChartOffAccountModel instance can be deactivated.
594
+
595
+ Returns
596
+ -------
597
+ True if the object can be deactivated, False otherwise.
598
+ """
421
599
  return all([
422
600
  self.is_active(),
423
601
  not self.is_default()
@@ -548,7 +726,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
548
726
 
549
727
  def clean(self):
550
728
  self.generate_slug()
551
-
552
729
  if self.is_default() and not self.active:
553
730
  raise ChartOfAccountsModelValidationError(
554
731
  _('Default Chart of Accounts cannot be deactivated.')
@@ -1100,7 +1100,7 @@ class EntityModelAbstract(MP_Node,
1100
1100
  role=a['role'],
1101
1101
  balance_type=a['balance_type'],
1102
1102
  active=activate_accounts,
1103
- # coa_model=chart_of_accounts,
1103
+ coa_model=coa_model,
1104
1104
  ) for a in v] for k, v in CHART_OF_ACCOUNTS_ROOT_MAP.items()
1105
1105
  }
1106
1106
 
@@ -1115,7 +1115,7 @@ class EntityModelAbstract(MP_Node,
1115
1115
  pass
1116
1116
 
1117
1117
  account_model.clean()
1118
- coa_model.allocate_account(account_model, root_account_qs=root_account_qs)
1118
+ coa_model.insert_account(account_model, root_account_qs=root_account_qs)
1119
1119
 
1120
1120
  else:
1121
1121
  if not ignore_if_default_coa:
@@ -1124,6 +1124,25 @@ class EntityModelAbstract(MP_Node,
1124
1124
  'Use force=True to bypass this check'
1125
1125
  )
1126
1126
 
1127
+ def get_coa_model_qs(self, active: bool = True):
1128
+ """
1129
+ Fetches the current Entity Model instance Chart of Accounts Model Queryset.
1130
+
1131
+ Parameters
1132
+ ----------
1133
+ active: bool
1134
+ Returns only active Chart of Account Models. Defaults to True.
1135
+
1136
+ Returns
1137
+ -------
1138
+ ChartOfAccountModelQuerySet
1139
+ """
1140
+
1141
+ coa_model_qs = self.chartofaccountmodel_set.all()
1142
+ if active:
1143
+ return coa_model_qs.active()
1144
+ return coa_model_qs
1145
+
1127
1146
  # Model Validators....
1128
1147
  def validate_chart_of_accounts_for_entity(self,
1129
1148
  coa_model: ChartOfAccountModel,
@@ -743,7 +743,7 @@ class AccrualMixIn(models.Model):
743
743
 
744
744
  if commit:
745
745
  JournalEntryModel = lazy_loader.get_journal_entry_model()
746
- TransactionModel = lazy_loader.get_transaction_model()
746
+ TransactionModel = lazy_loader.get_txs_model()
747
747
 
748
748
  unit_uuids = list(set(k[1] for k in idx_keys))
749
749