django-ledger 0.6.4__py3-none-any.whl → 0.7.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.

Files changed (93) hide show
  1. django_ledger/__init__.py +1 -4
  2. django_ledger/admin/__init__.py +1 -1
  3. django_ledger/admin/{coa.py → chart_of_accounts.py} +1 -1
  4. django_ledger/admin/entity.py +1 -1
  5. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
  6. django_ledger/forms/account.py +43 -38
  7. django_ledger/forms/bank_account.py +5 -2
  8. django_ledger/forms/bill.py +24 -36
  9. django_ledger/forms/chart_of_accounts.py +82 -0
  10. django_ledger/forms/customer.py +1 -1
  11. django_ledger/forms/data_import.py +3 -3
  12. django_ledger/forms/estimate.py +1 -1
  13. django_ledger/forms/invoice.py +5 -7
  14. django_ledger/forms/item.py +24 -15
  15. django_ledger/forms/transactions.py +3 -3
  16. django_ledger/io/io_core.py +4 -2
  17. django_ledger/io/io_library.py +1 -1
  18. django_ledger/io/io_middleware.py +5 -0
  19. django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
  20. django_ledger/migrations/0018_transactionmodel_cleared_transactionmodel_reconciled_and_more.py +37 -0
  21. django_ledger/models/__init__.py +1 -1
  22. django_ledger/models/accounts.py +229 -265
  23. django_ledger/models/bank_account.py +6 -6
  24. django_ledger/models/bill.py +7 -6
  25. django_ledger/models/{coa.py → chart_of_accounts.py} +187 -72
  26. django_ledger/models/closing_entry.py +5 -10
  27. django_ledger/models/coa_default.py +10 -9
  28. django_ledger/models/customer.py +6 -6
  29. django_ledger/models/data_import.py +12 -8
  30. django_ledger/models/entity.py +96 -39
  31. django_ledger/models/estimate.py +6 -10
  32. django_ledger/models/invoice.py +14 -11
  33. django_ledger/models/items.py +23 -14
  34. django_ledger/models/journal_entry.py +73 -30
  35. django_ledger/models/ledger.py +8 -8
  36. django_ledger/models/mixins.py +0 -3
  37. django_ledger/models/purchase_order.py +9 -9
  38. django_ledger/models/signals.py +0 -3
  39. django_ledger/models/transactions.py +24 -7
  40. django_ledger/models/unit.py +4 -3
  41. django_ledger/models/utils.py +0 -3
  42. django_ledger/models/vendor.py +4 -3
  43. django_ledger/settings.py +28 -3
  44. django_ledger/templates/django_ledger/account/account_create.html +2 -2
  45. django_ledger/templates/django_ledger/account/account_update.html +1 -1
  46. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
  47. django_ledger/templates/django_ledger/account/tags/accounts_table.html +29 -19
  48. django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
  49. django_ledger/templates/django_ledger/chart_of_accounts/coa_create.html +25 -0
  50. django_ledger/templates/django_ledger/chart_of_accounts/coa_list.html +25 -6
  51. django_ledger/templates/django_ledger/chart_of_accounts/coa_update.html +2 -2
  52. django_ledger/templates/django_ledger/chart_of_accounts/includes/coa_card.html +10 -4
  53. django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
  54. django_ledger/templates/django_ledger/financial_statements/tags/balance_sheet_statement.html +2 -2
  55. django_ledger/templates/django_ledger/includes/footer.html +2 -2
  56. django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
  57. django_ledger/templatetags/django_ledger.py +7 -1
  58. django_ledger/tests/base.py +23 -7
  59. django_ledger/tests/test_accounts.py +145 -9
  60. django_ledger/urls/account.py +17 -24
  61. django_ledger/urls/chart_of_accounts.py +6 -0
  62. django_ledger/utils.py +9 -36
  63. django_ledger/views/__init__.py +2 -2
  64. django_ledger/views/account.py +91 -116
  65. django_ledger/views/auth.py +1 -1
  66. django_ledger/views/bank_account.py +9 -11
  67. django_ledger/views/bill.py +91 -80
  68. django_ledger/views/{coa.py → chart_of_accounts.py} +49 -44
  69. django_ledger/views/closing_entry.py +8 -0
  70. django_ledger/views/customer.py +1 -1
  71. django_ledger/views/data_import.py +1 -1
  72. django_ledger/views/entity.py +1 -1
  73. django_ledger/views/estimate.py +13 -8
  74. django_ledger/views/feedback.py +1 -1
  75. django_ledger/views/financial_statement.py +1 -1
  76. django_ledger/views/home.py +1 -1
  77. django_ledger/views/inventory.py +9 -0
  78. django_ledger/views/invoice.py +5 -2
  79. django_ledger/views/item.py +58 -68
  80. django_ledger/views/journal_entry.py +1 -1
  81. django_ledger/views/ledger.py +3 -1
  82. django_ledger/views/mixins.py +25 -13
  83. django_ledger/views/purchase_order.py +1 -1
  84. django_ledger/views/transactions.py +1 -1
  85. django_ledger/views/unit.py +9 -0
  86. django_ledger/views/vendor.py +1 -1
  87. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/AUTHORS.md +8 -2
  88. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/METADATA +33 -44
  89. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/RECORD +92 -89
  90. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/WHEEL +1 -1
  91. django_ledger/forms/coa.py +0 -47
  92. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/LICENSE +0 -0
  93. {django_ledger-0.6.4.dist-info → django_ledger-0.7.1.dist-info}/top_level.txt +0 -0
@@ -2,14 +2,8 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
- * Pranav P Tulshyan <ptulshyan77@gmail.com>
8
-
9
-
10
5
  AccountModel
11
6
  ------------
12
-
13
7
  The AccountModel is a fundamental component of the Django Ledger system, responsible for categorizing and organizing
14
8
  financial transactions related to an entity's assets, liabilities, and equity.
15
9
 
@@ -60,18 +54,19 @@ from uuid import uuid4
60
54
 
61
55
  from django.core.exceptions import ValidationError
62
56
  from django.db import models
63
- from django.db.models import Q
57
+ from django.db.models import Q, F, UniqueConstraint
64
58
  from django.db.models.signals import pre_save
65
59
  from django.urls import reverse
66
60
  from django.utils.translation import gettext_lazy as _
67
61
  from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
68
62
 
69
- from django_ledger.io.io_core import get_localdate
70
- from django_ledger.io.roles import (ACCOUNT_ROLE_CHOICES, BS_ROLES, GROUP_INVOICE, GROUP_BILL, validate_roles,
71
- GROUP_ASSETS,
72
- GROUP_LIABILITIES, GROUP_CAPITAL, GROUP_INCOME, GROUP_EXPENSES, GROUP_COGS,
73
- ROOT_GROUP, BS_BUCKETS, ROOT_ASSETS, ROOT_LIABILITIES,
74
- ROOT_CAPITAL, ROOT_INCOME, ROOT_EXPENSES, ROOT_COA, VALID_PARENTS)
63
+ from django_ledger.io.roles import (
64
+ ACCOUNT_ROLE_CHOICES, BS_ROLES, GROUP_INVOICE, GROUP_BILL, validate_roles,
65
+ GROUP_ASSETS, GROUP_LIABILITIES, GROUP_CAPITAL, GROUP_INCOME, GROUP_EXPENSES, GROUP_COGS,
66
+ ROOT_GROUP, BS_BUCKETS, ROOT_ASSETS, ROOT_LIABILITIES,
67
+ ROOT_CAPITAL, ROOT_INCOME, ROOT_EXPENSES, ROOT_COA, VALID_PARENTS,
68
+ ROLES_ORDER_ALL
69
+ )
75
70
  from django_ledger.models.mixins import CreateUpdateMixIn
76
71
  from django_ledger.models.utils import lazy_loader
77
72
  from django_ledger.settings import DJANGO_LEDGER_ACCOUNT_CODE_GENERATE, DJANGO_LEDGER_ACCOUNT_CODE_USE_PREFIX
@@ -155,11 +150,17 @@ class AccountModelQuerySet(MP_NodeQuerySet):
155
150
  AccountModelQuerySet
156
151
  A QuerySet of accounts filtered by the provided roles.
157
152
  """
153
+ roles = validate_roles(roles)
158
154
  if isinstance(roles, str):
159
155
  roles = [roles]
160
156
  roles = validate_roles(roles)
161
157
  return self.filter(role__in=roles)
162
158
 
159
+ def with_codes(self, codes: Union[List, str]):
160
+ if isinstance(codes, str):
161
+ codes = [codes]
162
+ return self.filter(code__in=codes)
163
+
163
164
  def expenses(self):
164
165
  """
165
166
  Retrieve a queryset containing expenses filtered by specified roles.
@@ -200,36 +201,6 @@ class AccountModelQuerySet(MP_NodeQuerySet):
200
201
  """
201
202
  return self.exclude(role__in=ROOT_GROUP)
202
203
 
203
- def for_entity(self, entity_slug, user_model):
204
- """
205
- Parameters
206
- ----------
207
- entity_slug : str
208
- The slug identifier for the entity.
209
- user_model : UserModel
210
- The user model instance to use for filtering.
211
-
212
- Returns
213
- -------
214
- AccountModelQuerySet
215
- A Django QuerySet filtered by the specified entity and user permissions, ordered by 'code'.
216
- """
217
- if isinstance(self, lazy_loader.get_entity_model()):
218
- return self.filter(
219
- Q(coa_model__entity=entity_slug) &
220
- (
221
- Q(coa_model__entity__admin=user_model) |
222
- Q(coa_model__entity__managers__in=[user_model])
223
- )
224
- ).order_by('code')
225
- return self.filter(
226
- Q(coa_model__entity__slug__exact=entity_slug) &
227
- (
228
- Q(coa_model__entity__admin=user_model) |
229
- Q(coa_model__entity__managers__in=[user_model])
230
- )
231
- ).order_by('code')
232
-
233
204
  def gb_bs_role(self):
234
205
  """
235
206
  Groups accounts by Balance Sheet Bucket and then further groups them by role.
@@ -241,10 +212,14 @@ class AccountModelQuerySet(MP_NodeQuerySet):
241
212
  and the second element is a list of tuples where each sub-tuple contains a role display
242
213
  and a list of accounts that fall into that role within the BS bucket.
243
214
  """
244
- accounts_gb = list((r, list(gb)) for r, gb in groupby(self, key=lambda acc: acc.get_bs_bucket()))
215
+ accounts_gb = list(
216
+ (r, sorted(list(gb), key=lambda acc: ROLES_ORDER_ALL.index(acc.role))) for r, gb in
217
+ groupby(self, key=lambda acc: acc.get_bs_bucket())
218
+ )
245
219
  return [
246
220
  (bsr, [
247
- (r, list(l)) for r, l in groupby(gb, key=lambda a: a.get_role_display())
221
+ (r, sorted(list(l), key=lambda acc: acc.code)) for r, l in
222
+ groupby(gb, key=lambda a: a.get_role_display())
248
223
  ]) for bsr, gb in accounts_gb
249
224
  ]
250
225
 
@@ -270,9 +245,47 @@ class AccountModelQuerySet(MP_NodeQuerySet):
270
245
  A QuerySet containing the filtered results.
271
246
  """
272
247
  return self.filter(
273
- Q(locked=False) & Q(active=True)
248
+ Q(locked=False) &
249
+ Q(active=True) &
250
+ Q(coa_model__active=True)
251
+ )
252
+
253
+ def available(self):
254
+ return self.filter(
255
+ Q(locked=False) &
256
+ Q(active=True) &
257
+ Q(coa_model__active=True)
274
258
  )
275
259
 
260
+ def for_bill(self):
261
+ """
262
+ Retrieves only available and unlocked AccountModels for a specific EntityModel,
263
+ specifically for the creation and management of Bills. Roles within the 'GROUP_BILL'
264
+ context include: ASSET_CA_CASH, ASSET_CA_PREPAID, and LIABILITY_CL_ACC_PAYABLE.
265
+
266
+ Returns
267
+ -------
268
+ AccountModelQuerySet
269
+ A QuerySet of the requested EntityModel's chart of accounts.
270
+ """
271
+ return self.available().filter(role__in=GROUP_BILL)
272
+
273
+ def for_invoice(self):
274
+ """
275
+ Retrieves available and unlocked AccountModels for a specific EntityModel, specifically for the creation
276
+ and management of Invoices.
277
+
278
+ This method ensures that only relevant accounts are pulled, as defined under the roles in `GROUP_INVOICE`.
279
+ These roles include: ASSET_CA_CASH, ASSET_CA_RECEIVABLES, and LIABILITY_CL_DEFERRED_REVENUE.
280
+
281
+ Returns
282
+ -------
283
+ AccountModelQuerySet
284
+ A QuerySet containing the AccountModels relevant for the specified EntityModel and the roles defined
285
+ in `GROUP_INVOICE`.
286
+ """
287
+ return self.available().filter(role__in=GROUP_INVOICE)
288
+
276
289
 
277
290
  class AccountModelManager(MP_NodeManager):
278
291
  """
@@ -295,7 +308,12 @@ class AccountModelManager(MP_NodeManager):
295
308
  return AccountModelQuerySet(
296
309
  self.model,
297
310
  using=self._db
298
- ).order_by('path').select_related('coa_model')
311
+ ).order_by('path').select_related(
312
+ 'coa_model').annotate(
313
+ _coa_slug=F('coa_model__slug'),
314
+ _coa_active=F('coa_model__active'),
315
+ _entity_slug=F('coa_model__entity__slug'),
316
+ )
299
317
 
300
318
  def for_user(self, user_model) -> AccountModelQuerySet:
301
319
  """
@@ -318,235 +336,54 @@ class AccountModelManager(MP_NodeManager):
318
336
  Q(coa_model__entity__managers__in=[user_model])
319
337
  )
320
338
 
321
- # todo: search for uses and pass EntityModel whenever possible.
322
339
  def for_entity(
323
340
  self,
324
341
  user_model,
325
- entity_slug,
326
- coa_slug: Optional[str] = None,
327
- select_coa_model: bool = True
342
+ entity_model,
343
+ coa_slug: Optional[str] = None
328
344
  ) -> AccountModelQuerySet:
329
345
  """
330
- Retrieves accounts associated with the specified EntityModel.
346
+ Retrieve accounts associated with a specified EntityModel and Chart of Accounts.
331
347
 
332
348
  Parameters
333
349
  ----------
334
- user_model: User
335
- The Django User Model making the request to check for permissions.
336
- entity_slug: Union[EntityModel, str]
337
- The EntityModel instance or its slug to filter accounts by. If a slug is provided and `coa_slug` is None,
338
- an additional
339
- database query will be executed to determine the default Chart of Accounts.
340
- coa_slug: Optional[str], default=None
341
- The slug of the specific Chart of Accounts to use. If None, the default Chart of Accounts is selected.
342
- select_coa_model: bool, default=True
343
- If True, prefetches the CoA Model information in the QuerySet.
350
+ user_model : User
351
+ The Django User instance initiating the request. Used to check for required permissions.
352
+ entity_model : Union[EntityModel, str]
353
+ An instance of EntityModel or its slug. This determines the entity whose accounts are being retrieved.
354
+ A database query will be carried out to identify the default Chart of Accounts.
355
+ coa_slug : Optional[str], default=None
356
+ The slug for a specific Chart of Accounts to be used. If None, the default Chart of Accounts will be selected.
344
357
 
345
358
  Returns
346
359
  -------
347
360
  AccountModelQuerySet
348
361
  A QuerySet containing accounts associated with the specified EntityModel and Chart of Accounts.
362
+
363
+ Raises
364
+ ------
365
+ AccountModelValidationError
366
+ If the entity_model is neither an instance of EntityModel nor a string.
349
367
  """
350
368
  qs = self.for_user(user_model)
351
- if select_coa_model:
352
- qs = qs.select_related('coa_model')
353
-
354
369
  EntityModel = lazy_loader.get_entity_model()
355
- if isinstance(entity_slug, EntityModel):
356
- entity_model = entity_slug
370
+
371
+ if isinstance(entity_model, EntityModel):
372
+ entity_model = entity_model
357
373
  qs = qs.filter(coa_model__entity=entity_model)
358
- elif isinstance(entity_slug, str):
359
- qs = qs.filter(coa_model__entity__slug__exact=entity_slug)
374
+ elif isinstance(entity_model, str):
375
+ qs = qs.filter(coa_model__entity__slug__exact=entity_model)
360
376
  else:
361
- raise AccountModelValidationError(message='Must pass an instance of EntityModel or String for entity_slug.')
362
-
363
- if coa_slug:
364
- qs = qs.filter(coa_model__slug__exact=coa_slug)
365
- return qs.order_by('coa_model')
366
-
367
- def for_entity_available(self, user_model, entity_slug, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
368
- """
369
- Retrieve available and unlocked AccountModels for a specific EntityModel.
370
-
371
- This method filters AccountModels associated with the specified EntityModel
372
- that are active, not locked, and have an active Chart of Accounts.
373
-
374
- Parameters
375
- ----------
376
- user_model: User
377
- The Django User Model instance making the request, used to validate permissions.
378
-
379
- entity_slug: EntityModel or str
380
- The EntityModel instance or its slug to pull accounts from. If entity_slug is passed
381
- and coa_slug is None, an additional database query will be performed to determine
382
- the default Chart of Accounts.
383
-
384
- coa_slug: str, optional
385
- The specific Chart of Accounts to use. If None, the default Chart of Accounts will be pulled.
377
+ raise AccountModelValidationError(
378
+ message='Must pass an instance of EntityModel or String for entity_slug.'
379
+ )
386
380
 
387
- Returns
388
- -------
389
- AccountModelQuerySet
390
- A QuerySet containing available and unlocked AccountModels for the specified EntityModel and Chart of Accounts.
391
- """
392
- qs = self.for_entity(
393
- user_model=user_model,
394
- entity_slug=entity_slug,
395
- coa_slug=coa_slug)
396
381
  return qs.filter(
397
- Q(active=True) &
398
- Q(locked=False) &
399
- Q(coa_model__active=True)
382
+ coa_model__slug__exact=coa_slug
383
+ ) if coa_slug else qs.filter(
384
+ coa_model__slug__exact=F('coa_model__entity__default_coa__slug')
400
385
  )
401
386
 
402
- def with_roles(self, roles: Union[list, str], entity_slug, user_model) -> AccountModelQuerySet:
403
- """
404
- Retrieve accounts based on specific roles.
405
-
406
- This method filters accounts associated with a given role or a list of roles. For example, if you need to
407
- find all accounts under the "asset_ppe_build" role, which includes all buildings fixed assets, this method
408
- can be used.
409
-
410
- Parameters
411
- ----------
412
- entity_slug: EntityModel or str
413
- The EntityModel instance or its slug to fetch accounts from. If only the slug is provided and coa_slug is
414
- not specified, an additional database query will be performed to determine the default chart of accounts.
415
- user_model: User
416
- The Django User model instance making the request to ensure appropriate permissions are checked.
417
- roles: list or str
418
- Accepts either a single role as a string or a list of roles. Refer to io.roles.py for a comprehensive
419
- list of roles.
420
-
421
- Returns
422
- -------
423
- AccountModelQuerySet
424
- A QuerySet of accounts filtered by the specified roles.
425
- """
426
- roles = validate_roles(roles)
427
- if isinstance(roles, str):
428
- roles = [roles]
429
- qs = self.for_entity(entity_slug=entity_slug, user_model=user_model)
430
- return qs.filter(role__in=roles)
431
-
432
- def with_roles_available(self, roles: Union[list, str],
433
- entity_slug,
434
- user_model,
435
- coa_slug: Optional[str]) -> AccountModelQuerySet:
436
- """
437
- Retrieve available and unlocked AccountModels for a specified EntityModel and list of roles.
438
-
439
- Parameters
440
- ----------
441
- roles : Union[list, str]
442
- A single role as a string or a list of roles.
443
- entity_slug : Union[str, 'EntityModel']
444
- The EntityModel object or its slug. If a slug is provided and `coa_slug` is None, an additional
445
- database query will be executed to fetch the default Chart of Accounts.
446
- user_model : 'UserModel'
447
- The Django UserModel instance making the request, used to check permissions.
448
- coa_slug : Optional[str], default None
449
- The specific Chart of Accounts slug. If None, the default Chart of Accounts will be used.
450
- This parameter assists in identifying the complete Chart of Accounts for the EntityModel.
451
-
452
- Returns
453
- -------
454
- AccountModelQuerySet
455
- A QuerySet containing available and unlocked AccountModel instances for the specified
456
- EntityModel and roles.
457
- """
458
-
459
- if isinstance(roles, str):
460
- roles = [roles]
461
- roles = validate_roles(roles)
462
- qs = self.for_entity_available(entity_slug=entity_slug, user_model=user_model)
463
- return qs.filter(role__in=roles)
464
-
465
- def coa_roots(self, user_model, entity_slug, coa_slug) -> AccountModelQuerySet:
466
- """
467
- Retrieves the root accounts of a specified Code of Accounts (CoA).
468
-
469
- Parameters
470
- ----------
471
- user_model: object
472
- The Django User model instance requesting the data, used for permission checking.
473
- entity_slug: Union[EntityModel, str]
474
- The entity or its slug from which to fetch accounts. If a slug is provided and `coa_slug` is None,
475
- an additional database query is performed to determine the default Code of Accounts.
476
- coa_slug: Optional[str]
477
- The specific chart of accounts to retrieve. If None, the default chart of accounts for the entity
478
- will be used. This is crucial for identifying the complete set of accounts for a given entity.
479
-
480
- Returns
481
- -------
482
- AccountModelQuerySet
483
- A queryset of root accounts for the specified Code of Accounts.
484
- """
485
- qs = self.for_entity(user_model=user_model, entity_slug=entity_slug, coa_slug=coa_slug)
486
- return qs.is_coa_root()
487
-
488
- def for_invoice(self, user_model, entity_slug: str, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
489
- """
490
- Retrieves available and unlocked AccountModels for a specific EntityModel, specifically for the creation
491
- and management of Invoices.
492
-
493
- This method ensures that only relevant accounts are pulled, as defined under the roles in `GROUP_INVOICE`.
494
- These roles include: ASSET_CA_CASH, ASSET_CA_RECEIVABLES, and LIABILITY_CL_DEFERRED_REVENUE.
495
-
496
- Parameters
497
- ----------
498
- user_model: User
499
- The Django User Model instance requesting access. It is used to check the necessary permissions.
500
-
501
- entity_slug: Union[EntityModel, str]
502
- Specifies the EntityModel or its slug to pull accounts from. If a slug is provided and `coa_slug` is `None`,
503
- the method will perform an additional database query to determine the default chart of accounts.
504
-
505
- coa_slug: Optional[str], default=None
506
- Explicitly specifies which chart of accounts to use. If `None`, the method will default to using
507
- the EntityModel's default chart of accounts.
508
-
509
- Returns
510
- -------
511
- AccountModelQuerySet
512
- A QuerySet containing the AccountModels relevant for the specified EntityModel and the roles defined
513
- in `GROUP_INVOICE`.
514
- """
515
- qs = self.for_entity_available(
516
- user_model=user_model,
517
- entity_slug=entity_slug,
518
- coa_slug=coa_slug)
519
- return qs.filter(role__in=GROUP_INVOICE)
520
-
521
- def for_bill(self, user_model, entity_slug, coa_slug: Optional[str] = None) -> AccountModelQuerySet:
522
- """
523
- Retrieves only available and unlocked AccountModels for a specific EntityModel,
524
- specifically for the creation and management of Bills. Roles within the 'GROUP_BILL'
525
- context include: ASSET_CA_CASH, ASSET_CA_PREPAID, and LIABILITY_CL_ACC_PAYABLE.
526
-
527
- Parameters
528
- ----------
529
- user_model : Django User Model
530
- The Django User Model that is making the request, used to check for permissions.
531
-
532
- entity_slug : Union[EntityModel, str]
533
- The EntityModel or EntityModel slug from which to pull accounts. If given a slug and coa_slug
534
- is None, an additional database query will be made to determine the default chart of accounts.
535
-
536
- coa_slug : Optional[str]
537
- The specific chart of accounts to use. If None, it will default to the EntityModel's default chart of accounts.
538
-
539
- Returns
540
- -------
541
- AccountModelQuerySet
542
- A QuerySet of the requested EntityModel's chart of accounts.
543
- """
544
- qs = self.for_entity_available(
545
- user_model=user_model,
546
- entity_slug=entity_slug,
547
- coa_slug=coa_slug)
548
- return qs.filter(role__in=GROUP_BILL)
549
-
550
387
 
551
388
  def account_code_validator(value: str):
552
389
  if not value.isalnum():
@@ -596,7 +433,6 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
596
433
  active = models.BooleanField(default=False, verbose_name=_('Active'))
597
434
  coa_model = models.ForeignKey('django_ledger.ChartOfAccountModel',
598
435
  on_delete=models.CASCADE,
599
- editable=False,
600
436
  verbose_name=_('Chart of Accounts'))
601
437
  objects = AccountModelManager()
602
438
  node_order_by = ['uuid']
@@ -606,9 +442,17 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
606
442
  ordering = ['-created']
607
443
  verbose_name = _('Account')
608
444
  verbose_name_plural = _('Accounts')
609
- unique_together = [
610
- ('coa_model', 'code'),
611
- ('coa_model', 'role', 'role_default')
445
+ constraints = [
446
+ UniqueConstraint(
447
+ fields=('coa_model', 'code'),
448
+ name='unique_code_for_coa_model',
449
+ violation_error_message=_('Account codes must be unique for each Chart of Accounts Model.')
450
+ ),
451
+ UniqueConstraint(
452
+ fields=('coa_model', 'role', 'role_default'),
453
+ name='only_one_account_assigned_as_default_for_role',
454
+ violation_error_message=_('Only one default account for role permitted.')
455
+ )
612
456
  ]
613
457
  indexes = [
614
458
  models.Index(fields=['role']),
@@ -628,6 +472,17 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
628
472
  x5=self.code
629
473
  )
630
474
 
475
+ @property
476
+ def coa_slug(self):
477
+ try:
478
+ return getattr(self, '_coa_slug')
479
+ except AttributeError:
480
+ return self.coa_model.slug
481
+
482
+ @property
483
+ def entity_slug(self):
484
+ return getattr(self, '_entity_slug')
485
+
631
486
  @classmethod
632
487
  def create_account(cls,
633
488
  name: str,
@@ -838,6 +693,13 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
838
693
  """
839
694
  return self.locked is True
840
695
 
696
+ def is_coa_active(self) -> bool:
697
+ try:
698
+ return getattr(self, '_coa_active')
699
+ except AttributeError:
700
+ pass
701
+ return self.coa_model.active
702
+
841
703
  def can_activate(self):
842
704
  """
843
705
  Determines if the object can be activated.
@@ -866,6 +728,46 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
866
728
  self.active is True
867
729
  ])
868
730
 
731
+ def can_lock(self):
732
+ return all([
733
+ self.locked is False
734
+ ])
735
+
736
+ def can_unlock(self):
737
+ return all([
738
+ self.locked is True
739
+ ])
740
+
741
+ def lock(self, commit: bool = True, raise_exception: bool = True, **kwargs):
742
+ if not self.can_lock():
743
+ if raise_exception:
744
+ raise AccountModelValidationError(
745
+ message=_(f'Cannot lock account {self.code}: {self.name}. Active: {self.is_active()}')
746
+ )
747
+ return
748
+
749
+ self.locked = True
750
+ if commit:
751
+ self.save(update_fields=[
752
+ 'locked',
753
+ 'updated'
754
+ ])
755
+
756
+ def unlock(self, commit: bool = True, raise_exception: bool = True, **kwargs):
757
+ if not self.can_unlock():
758
+ if raise_exception:
759
+ raise AccountModelValidationError(
760
+ message=_(f'Cannot unlock account {self.code}: {self.name}. Active: {self.is_active()}')
761
+ )
762
+ return
763
+
764
+ self.locked = False
765
+ if commit:
766
+ self.save(update_fields=[
767
+ 'locked',
768
+ 'updated'
769
+ ])
770
+
869
771
  def activate(self, commit: bool = True, raise_exception: bool = True, **kwargs):
870
772
  """
871
773
  Checks if the Account Model instance can be activated, then Activates the AccountModel instance.
@@ -936,9 +838,8 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
936
838
  3. The entity itself must be active.
937
839
  """
938
840
  return all([
939
- self.coa_model.is_active(),
940
- not self.is_locked(),
941
- self.is_active()
841
+ self.is_coa_active(),
842
+ not self.is_locked()
942
843
  ])
943
844
 
944
845
  def get_code_prefix(self) -> str:
@@ -1072,16 +973,75 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
1072
973
  ri = randint(10000, 99999)
1073
974
  return f'{prefix}{ri}'
1074
975
 
1075
- def get_absolute_url(self):
976
+ # URLS...
977
+ def get_absolute_url(self) -> str:
978
+ return reverse(
979
+ viewname='django_ledger:account-detail',
980
+ kwargs={
981
+ 'account_pk': self.uuid,
982
+ 'entity_slug': self.entity_slug,
983
+ 'coa_slug': self.coa_slug
984
+ }
985
+ )
986
+
987
+ def get_update_url(self) -> str:
988
+ return reverse(
989
+ viewname='django_ledger:account-update',
990
+ kwargs={
991
+ 'account_pk': self.uuid,
992
+ 'entity_slug': self.entity_slug,
993
+ 'coa_slug': self.coa_slug
994
+ }
995
+ )
996
+
997
+ def get_action_deactivate_url(self) -> str:
998
+ return reverse(
999
+ viewname='django_ledger:account-action-deactivate',
1000
+ kwargs={
1001
+ 'account_pk': self.uuid,
1002
+ 'entity_slug': self.entity_slug,
1003
+ 'coa_slug': self.coa_slug
1004
+ }
1005
+ )
1006
+
1007
+ def get_action_activate_url(self) -> str:
1076
1008
  return reverse(
1077
- viewname='django_ledger:account-detail-year',
1009
+ viewname='django_ledger:account-action-activate',
1078
1010
  kwargs={
1079
1011
  'account_pk': self.uuid,
1080
- 'entity_slug': self.coa_model.entity.slug,
1081
- 'year': get_localdate().year
1012
+ 'entity_slug': self.entity_slug,
1013
+ 'coa_slug': self.coa_slug
1082
1014
  }
1083
1015
  )
1084
1016
 
1017
+ def get_action_lock_url(self) -> str:
1018
+ return reverse(
1019
+ viewname='django_ledger:account-action-lock',
1020
+ kwargs={
1021
+ 'account_pk': self.uuid,
1022
+ 'entity_slug': self.entity_slug,
1023
+ 'coa_slug': self.coa_slug
1024
+ }
1025
+ )
1026
+
1027
+ def get_action_unlock_url(self) -> str:
1028
+ return reverse(
1029
+ viewname='django_ledger:account-action-unlock',
1030
+ kwargs={
1031
+ 'account_pk': self.uuid,
1032
+ 'entity_slug': self.entity_slug,
1033
+ 'coa_slug': self.coa_slug
1034
+ }
1035
+ )
1036
+
1037
+ def get_coa_account_list_url(self) -> str:
1038
+ return reverse(
1039
+ viewname='django_ledger:account-list',
1040
+ kwargs={
1041
+ 'entity_slug': self.entity_slug,
1042
+ 'coa_slug': self.coa_slug
1043
+ })
1044
+
1085
1045
  def clean(self):
1086
1046
  if not self.code and DJANGO_LEDGER_ACCOUNT_CODE_GENERATE:
1087
1047
  self.code = self.generate_random_code()
@@ -1098,6 +1058,10 @@ class AccountModel(AccountModelAbstract):
1098
1058
  Base Account Model from Account Model Abstract Class
1099
1059
  """
1100
1060
 
1061
+ class Meta(AccountModelAbstract.Meta):
1062
+ swappable = 'DJANGO_LEDGER_ACCOUNT_MODEL'
1063
+ abstract = False
1064
+
1101
1065
 
1102
1066
  def accountmodel_presave(instance: AccountModel, **kwargs):
1103
1067
  if instance.role_default is False:
@@ -2,10 +2,6 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
- * Pranav P Tulshyan <ptulshyan77@gmail.com>
8
-
9
5
  A Bank Account refers to the financial institution which holds financial assets for the EntityModel.
10
6
  A bank account usually holds cash, which is a Current Asset. Transactions may be imported using the open financial
11
7
  format specification OFX into a staging area for final disposition into the EntityModel ledger.
@@ -95,7 +91,7 @@ class BankAccountModelManager(models.Manager):
95
91
  )
96
92
 
97
93
 
98
- class BackAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn):
94
+ class BankAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn):
99
95
  """
100
96
  This is the main abstract class which the BankAccountModel database will inherit from.
101
97
  The BankAccountModel inherits functionality from the following MixIns:
@@ -208,7 +204,11 @@ class BackAccountModelAbstract(BankAccountInfoMixIn, CreateUpdateMixIn):
208
204
  ])
209
205
 
210
206
 
211
- class BankAccountModel(BackAccountModelAbstract):
207
+ class BankAccountModel(BankAccountModelAbstract):
212
208
  """
213
209
  Base Bank Account Model Implementation
214
210
  """
211
+
212
+ class Meta(BankAccountModelAbstract.Meta):
213
+ swappable = 'DJANGO_LEDGER_BANK_ACCOUNT_MODEL'
214
+ abstract = False