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

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

Potentially problematic release.


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

django_ledger/__init__.py CHANGED
@@ -9,7 +9,7 @@ Contributions to this module:
9
9
  default_app_config = 'django_ledger.apps.DjangoLedgerConfig'
10
10
 
11
11
  """Django Ledger"""
12
- __version__ = '0.6.0.1'
12
+ __version__ = '0.6.0.2'
13
13
  __license__ = 'GPLv3 License'
14
14
 
15
15
  __author__ = 'Miguel Sanda'
@@ -115,7 +115,9 @@ class LedgerModelAdmin(ModelAdmin):
115
115
  'is_locked',
116
116
  'is_extended',
117
117
  'journal_entry_count',
118
- 'earliest_journal_entry'
118
+ 'earliest_journal_entry',
119
+ 'created',
120
+ 'updated'
119
121
  ]
120
122
  actions = [
121
123
  'post',
@@ -259,6 +259,9 @@ class IODatabaseMixIn:
259
259
  helps minimize the number of transactions to aggregate for a given request.
260
260
  """
261
261
 
262
+ TRANSACTION_MODEL_CLASS = None
263
+ JOURNAL_ENTRY_MODEL_CLASS = None
264
+
262
265
  def is_entity_model(self):
263
266
  return isinstance(self, lazy_loader.get_entity_model())
264
267
 
@@ -276,6 +279,16 @@ class IODatabaseMixIn:
276
279
  elif self.is_entity_unit_model():
277
280
  return self.entity
278
281
 
282
+ def get_transaction_model(self):
283
+ if self.TRANSACTION_MODEL_CLASS is not None:
284
+ return self.TRANSACTION_MODEL_CLASS
285
+ return lazy_loader.get_txs_model()
286
+
287
+ def get_journal_entry_model(self):
288
+ if self.JOURNAL_ENTRY_MODEL_CLASS is not None:
289
+ return self.JOURNAL_ENTRY_MODEL_CLASS
290
+ return lazy_loader.get_journal_entry_model()
291
+
279
292
  def database_digest(self,
280
293
  entity_slug: Optional[str] = None,
281
294
  unit_slug: Optional[str] = None,
@@ -337,7 +350,7 @@ class IODatabaseMixIn:
337
350
  IOResult
338
351
  """
339
352
 
340
- TransactionModel = lazy_loader.get_txs_model()
353
+ TransactionModel = self.get_transaction_model()
341
354
 
342
355
  # get_initial txs_queryset... where the IO model is operating from??...
343
356
  if self.is_entity_model():
@@ -604,7 +617,7 @@ class IODatabaseMixIn:
604
617
  use_closing_entries=use_closing_entries,
605
618
  **kwargs)
606
619
 
607
- TransactionModel = lazy_loader.get_txs_model()
620
+ TransactionModel = self.get_transaction_model()
608
621
 
609
622
  for tx_model in io_result.txs_queryset:
610
623
  if tx_model['account__balance_type'] != tx_model['tx_type']:
@@ -801,8 +814,8 @@ class IODatabaseMixIn:
801
814
  force_je_retrieval: bool = False,
802
815
  **kwargs):
803
816
 
804
- JournalEntryModel = lazy_loader.get_journal_entry_model()
805
- TransactionModel = lazy_loader.get_txs_model()
817
+ TransactionModel = self.get_transaction_model()
818
+ JournalEntryModel = self.get_journal_entry_model()
806
819
 
807
820
  # Validates that credits/debits balance.
808
821
  check_tx_balance(je_txs, perform_correction=False)
@@ -897,7 +910,7 @@ class IODatabaseMixIn:
897
910
  if staged_tx_model:
898
911
  staged_tx_model.transaction_model = tx
899
912
 
900
- txs_models = je_model.transactionmodel_set.bulk_create(i[0] for i in txs_models)
913
+ txs_models = TransactionModel.objects.bulk_create(i[0] for i in txs_models)
901
914
  je_model.save(verify=True, post_on_verify=je_posted)
902
915
  return je_model, txs_models
903
916
 
@@ -7,12 +7,13 @@ Contributions to this module:
7
7
 
8
8
  This module contains classes and functions used to document, dispatch and commit new transaction into the database.
9
9
  """
10
+ import enum
10
11
  from collections import defaultdict
11
12
  from dataclasses import dataclass
12
13
  from datetime import date, datetime
13
14
  from decimal import Decimal
14
15
  from itertools import chain
15
- from typing import Union, Dict, Callable, Optional, List
16
+ from typing import Union, Dict, Callable, Optional, List, Set
16
17
  from uuid import UUID
17
18
 
18
19
  from django.core.exceptions import ValidationError
@@ -63,6 +64,11 @@ class IOCursorValidationError(ValidationError):
63
64
  pass
64
65
 
65
66
 
67
+ class IOCursorMode(enum.Enum):
68
+ STRICT = 'strict'
69
+ PERMISSIVE = 'permissive'
70
+
71
+
66
72
  class IOCursor:
67
73
  """
68
74
  Represents a Django Ledger cursor capable of dispatching transactions to the database.
@@ -86,18 +92,20 @@ class IOCursor:
86
92
  io_library,
87
93
  entity_model: EntityModel,
88
94
  user_model,
95
+ mode: IOCursorMode = IOCursorMode.PERMISSIVE,
89
96
  coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None):
90
97
  self.IO_LIBRARY = io_library
98
+ self.MODE = mode
91
99
  self.ENTITY_MODEL = entity_model
92
100
  self.USER_MODEL = user_model
93
101
  self.COA_MODEL = coa_model
94
- self.__COMMITTED: bool = False
95
102
  self.blueprints = defaultdict(list)
96
103
  self.ledger_model_qs: Optional[LedgerModelQuerySet] = None
97
104
  self.account_model_qs: Optional[AccountModelQuerySet] = None
98
105
  self.ledger_map = dict()
99
106
  self.commit_plan = dict()
100
107
  self.instructions = None
108
+ self.__COMMITTED: bool = False
101
109
 
102
110
  def get_ledger_model_qs(self) -> LedgerModelQuerySet:
103
111
  """
@@ -122,9 +130,9 @@ class IOCursor:
122
130
  """
123
131
  return self.ENTITY_MODEL.get_coa_accounts(
124
132
  coa_model=self.COA_MODEL
125
- )
133
+ ).can_transact()
126
134
 
127
- def resolve_account_model_qs(self, codes: List[str]) -> AccountModelQuerySet:
135
+ def resolve_account_model_qs(self, codes: Set[str]) -> AccountModelQuerySet:
128
136
  """
129
137
  Resolves the final AccountModelQuerySet associated with the given account codes used by the blueprint.
130
138
 
@@ -164,6 +172,12 @@ class IOCursor:
164
172
  )
165
173
  return self.ledger_model_qs
166
174
 
175
+ def is_permissive(self) -> bool:
176
+ return self.MODE == IOCursorMode.PERMISSIVE
177
+
178
+ def is_strict(self) -> bool:
179
+ return self.MODE == IOCursorMode.STRICT
180
+
167
181
  def dispatch(self,
168
182
  name,
169
183
  ledger_model: Optional[Union[str, LedgerModel, UUID]] = None,
@@ -183,13 +197,14 @@ class IOCursor:
183
197
  The keyword arguments to be passed to the blueprint function.
184
198
  """
185
199
 
186
- if not isinstance(ledger_model, (str, UUID, LedgerModel)):
187
- raise IOCursorValidationError(
188
- message=_('Ledger Model must be a string or UUID or LedgerModel')
189
- )
200
+ if ledger_model is not None:
201
+ if not isinstance(ledger_model, (str, UUID, LedgerModel)):
202
+ raise IOCursorValidationError(
203
+ message=_('Ledger Model must be a string or UUID or LedgerModel')
204
+ )
190
205
 
191
- if isinstance(ledger_model, LedgerModel):
192
- self.ENTITY_MODEL.validate_ledger_model_for_entity(ledger_model)
206
+ if isinstance(ledger_model, LedgerModel):
207
+ self.ENTITY_MODEL.validate_ledger_model_for_entity(ledger_model)
193
208
 
194
209
  blueprint_func = self.IO_LIBRARY.get_blueprint(name)
195
210
  blueprint_txs = blueprint_func(**kwargs)
@@ -238,6 +253,7 @@ class IOCursor:
238
253
 
239
254
  def commit(self,
240
255
  je_timestamp: Optional[Union[datetime, date, str]] = None,
256
+ je_description: Optional[str] = None,
241
257
  post_new_ledgers: bool = False,
242
258
  post_journal_entries: bool = False,
243
259
  **kwargs):
@@ -251,6 +267,8 @@ class IOCursor:
251
267
  ----------
252
268
  je_timestamp: Optional[Union[datetime, date, str]]
253
269
  The date or timestamp used for the committed journal entries. If none, localtime will be used.
270
+ je_description: Optional[str]
271
+ The description of the journal entries. If none, no description will be used.
254
272
  post_new_ledgers: bool
255
273
  If a new ledger is created, the ledger model will be posted to the database.
256
274
  post_journal_entries: bool
@@ -275,29 +293,39 @@ class IOCursor:
275
293
  for k, txs in self.blueprints.items():
276
294
  if k is None:
277
295
 
278
- # no specified xid, ledger or UUID... create one...
279
- self.commit_plan[
280
- self.ENTITY_MODEL.create_ledger(
281
- name='Blueprint Commitment',
282
- commit=False,
283
- posted=post_new_ledgers
296
+ if self.is_permissive():
297
+ # no specified xid, ledger or UUID... create one...
298
+ self.commit_plan[
299
+ self.ENTITY_MODEL.create_ledger(
300
+ name='Blueprint Commitment',
301
+ commit=False,
302
+ posted=post_new_ledgers
303
+ )
304
+ ] = txs
305
+ else:
306
+ raise IOCursorValidationError(
307
+ message=_('Cannot commit transactions to a non-existing ledger')
284
308
  )
285
- ] = txs
286
309
 
287
310
  elif isinstance(k, str):
288
311
  try:
289
312
  # ledger with xid already exists...
290
313
  self.commit_plan[self.ledger_map[k]] = txs
291
314
  except KeyError:
292
- # create ledger with xid provided...
293
- self.commit_plan[
294
- self.ENTITY_MODEL.create_ledger(
295
- name=f'Blueprint Commitment {k}',
296
- ledger_xid=k,
297
- commit=False,
298
- posted=post_new_ledgers
315
+ if self.is_permissive():
316
+ # create ledger with xid provided...
317
+ self.commit_plan[
318
+ self.ENTITY_MODEL.create_ledger(
319
+ name=f'Blueprint Commitment {k}',
320
+ ledger_xid=k,
321
+ commit=False,
322
+ posted=post_new_ledgers
323
+ )
324
+ ] = txs
325
+ else:
326
+ raise IOCursorValidationError(
327
+ message=_(f'Cannot commit transactions to a non-existing ledger_xid {k}')
299
328
  )
300
- ] = txs
301
329
 
302
330
  elif isinstance(k, UUID):
303
331
  try:
@@ -315,12 +343,18 @@ class IOCursor:
315
343
 
316
344
  instructions = self.compile_instructions()
317
345
  account_codes = set(tx.account_code for tx in chain.from_iterable(tr for _, tr in instructions.items()))
346
+ account_model_qs = self.resolve_account_model_qs(codes=account_codes)
318
347
  account_models = {
319
- acc.code: acc for acc in self.resolve_account_model_qs(codes=account_codes)
348
+ acc.code: acc for acc in account_model_qs
320
349
  }
321
350
 
322
351
  for tx in chain.from_iterable(tr for _, tr in instructions.items()):
323
- tx.account_model = account_models[tx.account_code]
352
+ try:
353
+ tx.account_model = account_models[tx.account_code]
354
+ except KeyError:
355
+ raise IOCursorValidationError(
356
+ message=_(f'Account code {tx.account_code} not found. Is account available and not locked?')
357
+ )
324
358
 
325
359
  results = dict()
326
360
  for ledger_model, tr_items in instructions.items():
@@ -333,15 +367,20 @@ class IOCursor:
333
367
  je_timestamp=je_timestamp if je_timestamp else get_localtime(),
334
368
  je_txs=je_txs,
335
369
  je_posted=post_journal_entries,
370
+ je_desc=je_description,
336
371
  **kwargs
337
372
  )
338
373
 
374
+ je.txs_models = txs_models
375
+
339
376
  results[ledger_model] = {
377
+ 'ledger_model': ledger_model,
340
378
  'journal_entry': je,
341
379
  'txs_models': txs_models,
342
- 'instructions': tr_items
380
+ 'instructions': tr_items,
381
+ 'account_model_qs': self.account_model_qs
343
382
  }
344
- results['account_model_qs'] = self.account_model_qs
383
+
345
384
  self.__COMMITTED = True
346
385
  return results
347
386
 
@@ -518,6 +557,8 @@ class IOLibrary:
518
557
  The human-readable name of the library (i.e. PayRoll, Expenses, Rentals, etc...)
519
558
  """
520
559
 
560
+ IO_CURSOR_CLASS = IOCursor
561
+
521
562
  def __init__(self, name: str):
522
563
  self.name = name
523
564
  self.registry: Dict[str, Callable] = {}
@@ -545,10 +586,14 @@ class IOLibrary:
545
586
  raise IOLibraryError(message=f'Function "{name}" is not registered in IO library {self.name}')
546
587
  return self.registry[name]
547
588
 
589
+ def get_io_cursor_class(self):
590
+ return self.IO_CURSOR_CLASS
591
+
548
592
  def get_cursor(
549
593
  self,
550
594
  entity_model: EntityModel,
551
595
  user_model,
596
+ mode: IOCursorMode = IOCursorMode.PERMISSIVE,
552
597
  coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None
553
598
  ) -> IOCursor:
554
599
  """
@@ -562,14 +607,18 @@ class IOLibrary:
562
607
  The user model instance executing the transactions.
563
608
  coa_model: ChartOfAccountModel or UUID or str, optional
564
609
  The ChartOfAccountsModel instance or identifier used to determine the AccountModelQuerySet used for the transactions.
610
+ mode: IOCursorMode
611
+ The Mode of the cursor instance. Defaults to IOCursorMode.PERMISSIVE.
565
612
 
566
613
  Returns
567
614
  -------
568
615
  IOCursor
569
616
  """
570
- return IOCursor(
617
+ io_cursor_class = self.get_io_cursor_class()
618
+ return io_cursor_class(
571
619
  io_library=self,
572
620
  entity_model=entity_model,
573
621
  user_model=user_model,
574
622
  coa_model=coa_model,
623
+ mode=mode
575
624
  )
django_ledger/io/roles.py CHANGED
@@ -517,92 +517,86 @@ ACCOUNT_ROLE_CHOICES = [
517
517
  ))
518
518
  ]
519
519
 
520
- # ACCOUNT_ROLE_CHOICES = [
521
- # (BS_ASSET_ROLE.capitalize(), (
522
- # # CURRENT ASSETS ----
523
- # (ASSET_CA_CASH, _('Current Asset')),
524
- # (ASSET_CA_MKT_SECURITIES, _('Marketable Securities')),
525
- # (ASSET_CA_RECEIVABLES, _('Receivables')),
526
- # (ASSET_CA_INVENTORY, _('Inventory')),
527
- # (ASSET_CA_UNCOLLECTIBLES, _('Uncollectibles')),
528
- # (ASSET_CA_PREPAID, _('Prepaid')),
529
- # (ASSET_CA_OTHER, _('Other Liquid Assets')),
530
- #
531
- # # LONG TERM INVESTMENTS ---
532
- # (ASSET_LTI_NOTES_RECEIVABLE, _('Notes Receivable')),
533
- # (ASSET_LTI_LAND, _('Land')),
534
- # (ASSET_LTI_SECURITIES, _('Securities')),
535
- #
536
- # # PPE ...
537
- # (ASSET_PPE_BUILDINGS, _('Buildings')),
538
- # (ASSET_PPE_BUILDINGS_ACCUM_DEPRECIATION, _('Buildings - Accum. Depreciation')),
539
- # (ASSET_PPE_PLANT, _('Plant')),
540
- # (ASSET_PPE_PLANT_ACCUM_DEPRECIATION, _('Plant - Accum. Depreciation')),
541
- # (ASSET_PPE_EQUIPMENT, _('Equipment')),
542
- # (ASSET_PPE_EQUIPMENT_ACCUM_DEPRECIATION, _('Equipment - Accum. Depreciation')),
543
- #
544
- # # Other Assets ...
545
- # (ASSET_INTANGIBLE_ASSETS, _('Intangible Assets')),
546
- # (ASSET_INTANGIBLE_ASSETS_ACCUM_AMORTIZATION, _('Intangible Assets - Accum. Amortization')),
547
- # (ASSET_ADJUSTMENTS, _('Other Assets')),
548
- # )),
549
- # (BS_LIABILITIES_ROLE.capitalize(), (
550
- #
551
- # # CURRENT LIABILITIES ---
552
- # (LIABILITY_CL_ACC_PAYABLE, _('Accounts Payable')),
553
- # (LIABILITY_CL_WAGES_PAYABLE, _('Wages Payable')),
554
- # (LIABILITY_CL_INTEREST_PAYABLE, _('Interest Payable')),
555
- # (LIABILITY_CL_TAXES_PAYABLE, _('Taxes Payable')),
556
- # (LIABILITY_CL_ST_NOTES_PAYABLE, _('Short Term Notes Payable')),
557
- # (LIABILITY_CL_LTD_MATURITIES, _('Current Maturities of Long Tern Debt')),
558
- # (LIABILITY_CL_DEFERRED_REVENUE, _('Deferred Revenue')),
559
- # (LIABILITY_CL_OTHER, _('Other Liabilities')),
560
- #
561
- # # LONG TERM LIABILITIES ----
562
- # (LIABILITY_LTL_NOTES_PAYABLE, _('Long Term Notes Payable')),
563
- # (LIABILITY_LTL_BONDS_PAYABLE, _('Bonds Payable')),
564
- # (LIABILITY_LTL_MORTGAGE_PAYABLE, _('Mortgage Payable')),
565
- # )),
566
- # (BS_EQUITY_ROLE.capitalize(), (
567
- #
568
- # # EQUITY ---
569
- # (EQUITY_CAPITAL, _('Capital')),
570
- # (EQUITY_COMMON_STOCK, _('Common Stock')),
571
- # (EQUITY_PREFERRED_STOCK, _('Preferred Stock')),
572
- # (EQUITY_ADJUSTMENT, _('Other Equity Adjustments')),
573
- # (EQUITY_DIVIDENDS, _('Dividends & Distributions to Shareholders')),
574
- # )),
575
- # ('Income', (
576
- # # INCOME ---
577
- # (INCOME_OPERATIONAL, _('Operational Income')),
578
- # (INCOME_INVESTING, _('Investing/Passive Income')),
579
- # (INCOME_INTEREST, _('Interest Income')),
580
- # (INCOME_CAPITAL_GAIN_LOSS, _('Capital Gain/Loss Income')),
581
- # (INCOME_OTHER, _('Other Income')),
582
- # )),
583
- # ('Expenses', (
584
- # # COGS ----
585
- # (COGS, _('Cost of Goods Sold')),
586
- #
587
- # # EXPENSES ----
588
- # (EXPENSE_REGULAR, _('Regular Expense')),
589
- # (EXPENSE_INTEREST, _('Interest Expense')),
590
- # (EXPENSE_TAXES, _('Tax Expense')),
591
- # (EXPENSE_CAPITAL, _('Capital Expense')),
592
- # (EXPENSE_DEPRECIATION, _('Depreciation Expense')),
593
- # (EXPENSE_AMORTIZATION, _('Amortization Expense')),
594
- # (EXPENSE_OTHER, _('Other Expense')),
595
- # )),
596
- # ('Root', (
597
- # (ROOT_COA, 'CoA Root Account'),
598
- # (ROOT_ASSETS, 'Assets Root Account'),
599
- # (ROOT_LIABILITIES, 'Liabilities Root Account'),
600
- # (ROOT_CAPITAL, 'Capital Root Account'),
601
- # (ROOT_INCOME, 'Income Root Account'),
602
- # (ROOT_COGS, 'COGS Root Account'),
603
- # (ROOT_EXPENSES, 'Expenses Root Account'),
604
- # ))
605
- # ]
520
+ ACCOUNT_ROLE_CHOICES_FOR_FORMS = [
521
+ ('Asset', (
522
+ # CURRENT ASSETS ----
523
+ (ASSET_CA_CASH, _('Current Asset')),
524
+ (ASSET_CA_MKT_SECURITIES, _('Marketable Securities')),
525
+ (ASSET_CA_RECEIVABLES, _('Receivables')),
526
+ (ASSET_CA_INVENTORY, _('Inventory')),
527
+ (ASSET_CA_UNCOLLECTIBLES, _('Uncollectibles')),
528
+ (ASSET_CA_PREPAID, _('Prepaid')),
529
+ (ASSET_CA_OTHER, _('Other Liquid Assets')),
530
+
531
+ # LONG TERM INVESTMENTS ---
532
+ (ASSET_LTI_NOTES_RECEIVABLE, _('Notes Receivable')),
533
+ (ASSET_LTI_LAND, _('Land')),
534
+ (ASSET_LTI_SECURITIES, _('Securities')),
535
+
536
+ # PPE ...
537
+ (ASSET_PPE_BUILDINGS, _('Buildings')),
538
+ (ASSET_PPE_BUILDINGS_ACCUM_DEPRECIATION, _('Buildings - Accum. Depreciation')),
539
+ (ASSET_PPE_PLANT, _('Plant')),
540
+ (ASSET_PPE_PLANT_ACCUM_DEPRECIATION, _('Plant - Accum. Depreciation')),
541
+ (ASSET_PPE_EQUIPMENT, _('Equipment')),
542
+ (ASSET_PPE_EQUIPMENT_ACCUM_DEPRECIATION, _('Equipment - Accum. Depreciation')),
543
+
544
+ # Other Assets ...
545
+ (ASSET_INTANGIBLE_ASSETS, _('Intangible Assets')),
546
+ (ASSET_INTANGIBLE_ASSETS_ACCUM_AMORTIZATION, _('Intangible Assets - Accum. Amortization')),
547
+ (ASSET_ADJUSTMENTS, _('Other Assets')),
548
+ )),
549
+ ('Liabilities', (
550
+
551
+ # CURRENT LIABILITIES ---
552
+ (LIABILITY_CL_ACC_PAYABLE, _('Accounts Payable')),
553
+ (LIABILITY_CL_WAGES_PAYABLE, _('Wages Payable')),
554
+ (LIABILITY_CL_INTEREST_PAYABLE, _('Interest Payable')),
555
+ (LIABILITY_CL_TAXES_PAYABLE, _('Taxes Payable')),
556
+ (LIABILITY_CL_ST_NOTES_PAYABLE, _('Short Term Notes Payable')),
557
+ (LIABILITY_CL_LTD_MATURITIES, _('Current Maturities of Long Tern Debt')),
558
+ (LIABILITY_CL_DEFERRED_REVENUE, _('Deferred Revenue')),
559
+ (LIABILITY_CL_OTHER, _('Other Liabilities')),
560
+
561
+ # LONG TERM LIABILITIES ----
562
+ (LIABILITY_LTL_NOTES_PAYABLE, _('Long Term Notes Payable')),
563
+ (LIABILITY_LTL_BONDS_PAYABLE, _('Bonds Payable')),
564
+ (LIABILITY_LTL_MORTGAGE_PAYABLE, _('Mortgage Payable')),
565
+ )),
566
+ ('Capital', (
567
+
568
+ # EQUITY ---
569
+ (EQUITY_CAPITAL, _('Capital')),
570
+ (EQUITY_COMMON_STOCK, _('Common Stock')),
571
+ (EQUITY_PREFERRED_STOCK, _('Preferred Stock')),
572
+ (EQUITY_ADJUSTMENT, _('Other Equity Adjustments')),
573
+ (EQUITY_DIVIDENDS, _('Dividends & Distributions to Shareholders')),
574
+ )),
575
+
576
+ ('Income', (
577
+ # INCOME ---
578
+ (INCOME_OPERATIONAL, _('Operational Income')),
579
+ (INCOME_PASSIVE, _('Investing/Passive Income')),
580
+ (INCOME_INTEREST, _('Interest Income')),
581
+ (INCOME_CAPITAL_GAIN_LOSS, _('Capital Gain/Loss Income')),
582
+ (INCOME_OTHER, _('Other Income')),
583
+ )),
584
+
585
+ ('Expense', (
586
+ # COGS ----
587
+ (COGS, _('Cost of Goods Sold')),
588
+
589
+ # EXPENSES ----
590
+ (EXPENSE_OPERATIONAL, _('Regular Expense')),
591
+ (EXPENSE_INTEREST_ST, _('Interest Expense - Short Term Debt')),
592
+ (EXPENSE_INTEREST_LT, _('Interest Expense - Long Term Debt')),
593
+ (EXPENSE_TAXES, _('Tax Expense')),
594
+ (EXPENSE_CAPITAL, _('Capital Expense')),
595
+ (EXPENSE_DEPRECIATION, _('Depreciation Expense')),
596
+ (EXPENSE_AMORTIZATION, _('Amortization Expense')),
597
+ (EXPENSE_OTHER, _('Other Expense')),
598
+ ))
599
+ ]
606
600
 
607
601
  ACCOUNT_CHOICES_NO_ROOT = [c for c in ACCOUNT_ROLE_CHOICES if c[0] != 'Root']
608
602
 
@@ -0,0 +1,44 @@
1
+ # Generated by Django 5.0.4 on 2024-04-25 13:41
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('django_ledger', '0015_remove_chartofaccountmodel_locked_and_more'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RemoveIndex(
15
+ model_name='accountmodel',
16
+ name='django_ledg_coa_mod_e19964_idx',
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='ledgermodel',
20
+ name='entity',
21
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_ledger.entitymodel', verbose_name='Ledger Entity'),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='ledgermodel',
25
+ name='ledger_xid',
26
+ field=models.SlugField(allow_unicode=True, blank=True, help_text='User Defined Ledger ID', max_length=150, null=True, verbose_name='Ledger External ID'),
27
+ ),
28
+ migrations.AddIndex(
29
+ model_name='accountmodel',
30
+ index=models.Index(fields=['coa_model', 'code'], name='django_ledg_coa_mod_e073bc_idx'),
31
+ ),
32
+ migrations.AddIndex(
33
+ model_name='accountmodel',
34
+ index=models.Index(fields=['code'], name='django_ledg_code_081adc_idx'),
35
+ ),
36
+ migrations.AddIndex(
37
+ model_name='ledgermodel',
38
+ index=models.Index(fields=['entity', 'ledger_xid'], name='django_ledg_entity__7be095_idx'),
39
+ ),
40
+ migrations.AddIndex(
41
+ model_name='ledgermodel',
42
+ index=models.Index(fields=['ledger_xid'], name='django_ledg_ledger__05f099_idx'),
43
+ ),
44
+ ]
@@ -87,6 +87,26 @@ class AccountModelQuerySet(MP_NodeQuerySet):
87
87
  """
88
88
  return self.filter(active=False)
89
89
 
90
+ def locked(self):
91
+ """
92
+ Filter locked elements.
93
+
94
+ This method filters the elements based on the `locked` attribute and returns a filtered queryset.
95
+
96
+ Returns:
97
+ A filtered queryset containing the locked elements.
98
+ """
99
+ return self.filter(locked=True)
100
+
101
+ def unlocked(self):
102
+ """
103
+ Returns a filtered version of an object, excluding any locked items.
104
+
105
+ Returns:
106
+ A filtered version of the object, excluding any locked items.
107
+ """
108
+ return self.filter(locked=False)
109
+
90
110
  def with_roles(self, roles: Union[List, str]):
91
111
  """
92
112
  This method is used to make query of accounts with a certain role. For instance, the fixed assets like
@@ -110,12 +130,25 @@ class AccountModelQuerySet(MP_NodeQuerySet):
110
130
  return self.filter(role__in=roles)
111
131
 
112
132
  def expenses(self):
133
+ """
134
+ Return the expenses filtered by the roles specified in GROUP_EXPENSES.
135
+
136
+ Returns:
137
+ QuerySet: A queryset containing the expenses filtered by the GROUP_EXPENSES roles..
138
+ """
113
139
  return self.filter(role__in=GROUP_EXPENSES)
114
140
 
115
141
  def is_coa_root(self):
142
+ """
143
+ Check if the account model instance is the Chart of Account Root.
144
+
145
+ Returns:
146
+ bool: True if the Account is the CoA Root, False otherwise.
147
+ """
116
148
  return self.filter(role__in=ROOT_GROUP)
117
149
 
118
150
  def not_coa_root(self):
151
+
119
152
  return self.exclude(role__in=ROOT_GROUP)
120
153
 
121
154
  def for_entity(self, entity_slug, user_model):
@@ -146,12 +179,16 @@ class AccountModelQuerySet(MP_NodeQuerySet):
146
179
  def is_role_default(self):
147
180
  return self.not_coa_root().filter(role_default=True)
148
181
 
182
+ def can_transact(self):
183
+ return self.filter(
184
+ Q(locked=False) & Q(active=True)
185
+ )
186
+
149
187
 
150
188
  class AccountModelManager(MP_NodeManager):
151
189
  """
152
- This Model Manager will be used as interface through which the database query operations can be provided to the
153
- Account Model. It uses the custom defined AccountModelQuerySet and hence overrides the normal get_queryset
154
- function which return all rows of a model.
190
+ AccountModelManager class provides methods to manage and retrieve AccountModel objects.
191
+ It inherits from MP_NodeManager for tree-like model implementation.
155
192
  """
156
193
 
157
194
  def get_queryset(self) -> AccountModelQuerySet:
@@ -407,49 +444,41 @@ def account_code_validator(value: str):
407
444
 
408
445
 
409
446
  class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
410
- """
411
- Django Ledger Base Account Model Abstract. This is the main abstract class which the Account Model database will
412
- inherit, and it contains the fields/columns/attributes which the said ledger table will have. In addition to the
413
- attributes mentioned below, it also has the fields/columns/attributes mentioned in the ParentChileMixin & the
414
- CreateUpdateMixIn. Read about these mixin here.
447
+ """ AccountModelAbstract
415
448
 
416
- Below are the fields specific to the accounts model.
449
+ Abstract class representing an Account Model.
417
450
 
418
451
  Attributes
419
452
  ----------
420
- uuid: UUID
421
- This is a unique primary key generated for the table. The default value of this field is uuid4().
422
-
423
- code: str
424
- Each account will have its own alphanumeric code.
425
- For example:
426
- * Cash Account -> Code 1010.
427
- * Inventory -> 1200.
428
- * Maximum Length allowed is 10.
429
-
430
- name: str
431
- This is the user defined name of the Account. the maximum length for Name of the ledger allowed is 100
432
-
433
- role: str
434
- Each Account needs to be assigned a certain Role. The exhaustive list of ROLES is defined in io.roles.
435
-
436
- balance_type: str
437
- Each account will have a default Account type i.e. Either Debit or Credit.
438
- For example:
439
- * Assets like Cash, Inventory, Accounts Receivable or Expenses like Rent, Salary will have balance_type=DEBIT.
440
- * Liabilities, Equities and Income like Payables, Loans, Income, Sales, Reserves will have balance_type=CREDIT.
441
-
442
- locked: bool
443
- This determines whether any transactions can be added in the account. Before making any update to the
444
- account, the account needs to be unlocked. Default value is set to False i.e. Unlocked.
445
-
446
- active: bool
447
- Determines whether the concerned account is active. Any Account can be used only when it is unlocked and
448
- Active. Default value is set to True.
449
-
450
- coa_model: ChartOfAccountsModel
451
- Each Accounts must be assigned a ChartOfAccountsModel. By default, one CoA will be created for each entity.
452
- However, the creating of a new AccountModel must have an explicit assignment of a ChartOfAccountModel.
453
+ BALANCE_TYPE : list
454
+ List of choices for the balance type of the account.
455
+
456
+ uuid : UUIDField
457
+ UUID field representing the primary key of the account.
458
+
459
+ code : CharField
460
+ CharField representing the account code.
461
+
462
+ name : CharField
463
+ CharField representing the account name.
464
+
465
+ role : CharField
466
+ CharField representing the account role.
467
+
468
+ role_default : BooleanField
469
+ BooleanField representing whether the account is a default account for the role.
470
+
471
+ balance_type : CharField
472
+ CharField representing the balance type of the account. Must be 'debit' or 'credit'.
473
+
474
+ locked : BooleanField
475
+ BooleanField representing whether the account is locked.
476
+
477
+ active : BooleanField
478
+ BooleanField representing whether the account is active.
479
+
480
+ coa_model : ForeignKey
481
+ ForeignKey representing the associated ChartOfAccountModel.
453
482
  """
454
483
  BALANCE_TYPE = [
455
484
  (CREDIT, _('Credit')),
@@ -485,7 +514,8 @@ class AccountModelAbstract(MP_Node, CreateUpdateMixIn):
485
514
  models.Index(fields=['balance_type']),
486
515
  models.Index(fields=['active']),
487
516
  models.Index(fields=['locked']),
488
- models.Index(fields=['coa_model'])
517
+ models.Index(fields=['coa_model', 'code']),
518
+ models.Index(fields=['code'])
489
519
  ]
490
520
 
491
521
  def __str__(self):
@@ -486,6 +486,8 @@ class BillModelAbstract(
486
486
 
487
487
  if self.can_generate_bill_number():
488
488
  self.generate_bill_number(commit=commit)
489
+ ledger_model.ledger_xid = f'bill-{self.bill_number.lower()}-{str(ledger_model.entity_id)[-5:]}'
490
+ ledger_model.save(update_fields=['ledger_xid'])
489
491
 
490
492
  self.clean()
491
493
 
@@ -460,6 +460,8 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
460
460
  raise ChartOfAccountsModelValidationError(
461
461
  message=f'Invalid Account Model {account_model} for CoA {self}'
462
462
  )
463
+ else:
464
+ account_model.coa_model = self
463
465
 
464
466
  if not root_account_qs:
465
467
  root_account_qs = self.get_coa_root_accounts_qs()
@@ -113,7 +113,7 @@ class EntityModelManager(MP_NodeManager):
113
113
 
114
114
  def get_queryset(self):
115
115
  """Sets the custom queryset as the default."""
116
- qs = EntityModelQuerySet(self.model).order_by('path')
116
+ qs = EntityModelQuerySet(self.model, using=self._db).order_by('path')
117
117
  return qs.order_by('path').select_related('admin', 'default_coa')
118
118
 
119
119
  def for_user(self, user_model):
@@ -446,10 +446,11 @@ class InvoiceModelAbstract(
446
446
 
447
447
  if self.can_generate_invoice_number():
448
448
  self.generate_invoice_number(commit=commit)
449
+ ledger_model.ledger_xid=f'invoice-{self.invoice_number.lower()}-{str(ledger_model.entity_id)[-5:]}'
450
+ ledger_model.save(update_fields=['ledger_xid'])
449
451
 
450
452
  self.clean()
451
453
 
452
-
453
454
  if commit:
454
455
  self.save()
455
456
 
@@ -890,6 +890,10 @@ class ItemTransactionModelManager(models.Manager):
890
890
 
891
891
  def for_entity(self, user_model, entity_slug):
892
892
  qs = self.for_user(user_model)
893
+ if isinstance(entity_slug, lazy_loader.get_entity_model()):
894
+ qs.filter(
895
+ Q(item_model__entity=entity_slug)
896
+ )
893
897
  return qs.filter(
894
898
  Q(item_model__entity__slug__exact=entity_slug)
895
899
  )
@@ -184,13 +184,11 @@ class LedgerModelAbstract(CreateUpdateMixIn, IOMixIn):
184
184
  _WRAPPED_MODEL_KEY = 'wrapped_model'
185
185
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
186
186
  ledger_xid = models.SlugField(allow_unicode=True, max_length=150, null=True, blank=True,
187
- verbose_name=_('Ledger Slug'),
187
+ verbose_name=_('Ledger External ID'),
188
188
  help_text=_('User Defined Ledger ID'))
189
189
  name = models.CharField(max_length=150, null=True, blank=True, verbose_name=_('Ledger Name'))
190
190
 
191
- # todo: rename to entity_model...
192
191
  entity = models.ForeignKey('django_ledger.EntityModel',
193
- editable=False,
194
192
  on_delete=models.CASCADE,
195
193
  verbose_name=_('Ledger Entity'))
196
194
  posted = models.BooleanField(default=False, verbose_name=_('Posted Ledger'))
@@ -212,13 +210,22 @@ class LedgerModelAbstract(CreateUpdateMixIn, IOMixIn):
212
210
  models.Index(fields=['entity']),
213
211
  models.Index(fields=['entity', 'posted']),
214
212
  models.Index(fields=['entity', 'locked']),
213
+ models.Index(fields=['entity', 'ledger_xid']),
214
+ models.Index(fields=['ledger_xid']),
215
215
  ]
216
216
  unique_together = [
217
217
  ('entity', 'ledger_xid')
218
218
  ]
219
219
 
220
220
  def __str__(self):
221
- return self.name
221
+ if self.name is not None:
222
+ ledger_str = f'LedgerModel: {self.name}'
223
+ elif self.ledger_xid is not None:
224
+ ledger_str = f'LedgerModel: {self.ledger_xid}'
225
+ else:
226
+ ledger_str = f'LedgerModel: {self.uuid}'
227
+ return f'{ledger_str} | Posted: {self.posted} | Locked: {self.locked}'
228
+
222
229
 
223
230
  def has_wrapped_model_info(self):
224
231
  if self.additional_info is not None:
@@ -24,7 +24,7 @@ from django.contrib.auth import get_user_model
24
24
  from django.core.exceptions import ValidationError
25
25
  from django.core.validators import MinValueValidator
26
26
  from django.db import models
27
- from django.db.models import Q, QuerySet
27
+ from django.db.models import Q, QuerySet, F
28
28
  from django.db.models.signals import pre_save
29
29
  from django.utils.translation import gettext_lazy as _
30
30
 
@@ -238,7 +238,7 @@ class TransactionModelQuerySet(QuerySet):
238
238
  return self.filter(journal_entry__is_closing_entry=True)
239
239
 
240
240
 
241
- class TransactionModelAdmin(models.Manager):
241
+ class TransactionModelManager(models.Manager):
242
242
  """
243
243
  A manager class for the TransactionModel.
244
244
  """
@@ -531,7 +531,7 @@ class TransactionModelAbstract(CreateUpdateMixIn):
531
531
  verbose_name=_('Tx Description'),
532
532
  help_text=_('A description to be included with this individual transaction'))
533
533
 
534
- objects = TransactionModelAdmin()
534
+ objects = TransactionModelManager()
535
535
 
536
536
  class Meta:
537
537
  abstract = True
django_ledger/settings.py CHANGED
@@ -34,6 +34,7 @@ logger.info(f'Django Ledger GraphQL Enabled: {DJANGO_LEDGER_GRAPHQL_SUPPORT_ENAB
34
34
  DJANGO_LEDGER_USE_CLOSING_ENTRIES = getattr(settings, 'DJANGO_LEDGER_USE_CLOSING_ENTRIES', True)
35
35
  DJANGO_LEDGER_DEFAULT_CLOSING_ENTRY_CACHE_TIMEOUT = getattr(settings,
36
36
  'DJANGO_LEDGER_DEFAULT_CLOSING_ENTRY_CACHE_TIMEOUT', 3600)
37
+ DJANGO_LEDGER_AUTHORIZED_SUPERUSER = getattr(settings, 'DJANGO_LEDGER_AUTHORIZED_SUPERUSER', False)
37
38
  DJANGO_LEDGER_LOGIN_URL = getattr(settings, 'DJANGO_LEDGER_LOGIN_URL', settings.LOGIN_URL)
38
39
  DJANGO_LEDGER_BILL_NUMBER_LENGTH = getattr(settings, 'DJANGO_LEDGER_BILL_NUMBER_LENGTH', 10)
39
40
  DJANGO_LEDGER_INVOICE_NUMBER_LENGTH = getattr(settings, 'DJANGO_LEDGER_INVOICE_NUMBER_LENGTH', 10)
@@ -9,7 +9,7 @@ urlpatterns = [
9
9
 
10
10
  # DASHBOARD Views...
11
11
  path('<slug:entity_slug>/dashboard/',
12
- views.EntityModelDetailView.as_view(),
12
+ views.EntityModelDetailHandlerView.as_view(),
13
13
  name='entity-dashboard'),
14
14
  path('<slug:entity_slug>/dashboard/year/<int:year>/',
15
15
  views.FiscalYearEntityModelDashboardView.as_view(),
@@ -19,7 +19,7 @@ urlpatterns = [
19
19
 
20
20
  # DASHBOARD Views ...
21
21
  path('<slug:entity_slug>/dashboard/<slug:unit_slug>/',
22
- views.EntityModelDetailView.as_view(),
22
+ views.EntityModelDetailHandlerView.as_view(),
23
23
  name='unit-dashboard'),
24
24
  path('<slug:entity_slug>/dashboard/<slug:unit_slug>/year/<int:year>/',
25
25
  views.FiscalYearEntityModelDashboardView.as_view(),
@@ -159,9 +159,9 @@ class EntityDeleteView(DjangoLedgerSecurityMixIn, EntityModelModelViewQuerySetMi
159
159
 
160
160
 
161
161
  # DASHBOARD VIEWS START ----
162
- class EntityModelDetailView(DjangoLedgerSecurityMixIn,
163
- EntityUnitMixIn,
164
- RedirectView):
162
+ class EntityModelDetailHandlerView(DjangoLedgerSecurityMixIn,
163
+ EntityUnitMixIn,
164
+ RedirectView):
165
165
 
166
166
  def get_redirect_url(self, *args, **kwargs):
167
167
  loc_date = get_localdate()
@@ -182,14 +182,14 @@ class EntityModelDetailView(DjangoLedgerSecurityMixIn,
182
182
  })
183
183
 
184
184
 
185
- class FiscalYearEntityModelDashboardView(DjangoLedgerSecurityMixIn,
186
- EntityModelModelViewQuerySetMixIn,
187
- BaseDateNavigationUrlMixIn,
188
- UnpaidElementsMixIn,
189
- EntityUnitMixIn,
190
- DigestContextMixIn,
191
- YearlyReportMixIn,
192
- DetailView):
185
+ class EntityModelDetailBaseView(DjangoLedgerSecurityMixIn,
186
+ EntityModelModelViewQuerySetMixIn,
187
+ BaseDateNavigationUrlMixIn,
188
+ UnpaidElementsMixIn,
189
+ EntityUnitMixIn,
190
+ DigestContextMixIn,
191
+ YearlyReportMixIn,
192
+ DetailView):
193
193
  context_object_name = 'entity'
194
194
  slug_url_kwarg = 'entity_slug'
195
195
  template_name = 'django_ledger/entity/entity_dashboard.html'
@@ -203,7 +203,7 @@ class FiscalYearEntityModelDashboardView(DjangoLedgerSecurityMixIn,
203
203
  IO_DIGEST_EQUITY = True
204
204
 
205
205
  def get_context_data(self, **kwargs):
206
- context = super(FiscalYearEntityModelDashboardView, self).get_context_data(**kwargs)
206
+ context = super().get_context_data(**kwargs)
207
207
  entity_model: EntityModel = self.object
208
208
  context['page_title'] = entity_model.name
209
209
  context['header_title'] = entity_model.name
@@ -228,6 +228,12 @@ class FiscalYearEntityModelDashboardView(DjangoLedgerSecurityMixIn,
228
228
  return context
229
229
 
230
230
 
231
+ class FiscalYearEntityModelDashboardView(EntityModelDetailBaseView):
232
+ """
233
+ Entity Fiscal Year Dashboard View.
234
+ """
235
+
236
+
231
237
  class QuarterlyEntityDashboardView(FiscalYearEntityModelDashboardView, QuarterlyReportMixIn):
232
238
  """
233
239
  Entity Quarterly Dashboard View.
@@ -107,7 +107,6 @@ class LedgerModelCreateView(DjangoLedgerSecurityMixIn, LedgerModelModelViewQuery
107
107
  def form_valid(self, form):
108
108
  instance = form.save(commit=False)
109
109
  instance.entity = self.AUTHORIZED_ENTITY_MODEL
110
- self.object = form.save()
111
110
  return super().form_valid(form)
112
111
 
113
112
  def get_success_url(self):
@@ -10,8 +10,8 @@ from calendar import monthrange
10
10
  from datetime import timedelta, date
11
11
  from typing import Tuple, Optional
12
12
 
13
- from django.contrib.auth.mixins import PermissionRequiredMixin
14
- from django.core.exceptions import ValidationError, ObjectDoesNotExist
13
+ from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
14
+ from django.core.exceptions import ValidationError, ObjectDoesNotExist, ImproperlyConfigured
15
15
  from django.db.models import Q
16
16
  from django.http import Http404, HttpResponse, HttpResponseNotFound
17
17
  from django.urls import reverse
@@ -21,7 +21,7 @@ from django.views.generic.dates import YearMixin, MonthMixin, DayMixin
21
21
 
22
22
  from django_ledger.models import EntityModel, InvoiceModel, BillModel
23
23
  from django_ledger.models.entity import EntityModelFiscalPeriodMixIn
24
- from django_ledger.settings import DJANGO_LEDGER_PDF_SUPPORT_ENABLED
24
+ from django_ledger.settings import DJANGO_LEDGER_PDF_SUPPORT_ENABLED, DJANGO_LEDGER_AUTHORIZED_SUPERUSER
25
25
 
26
26
 
27
27
  class YearlyReportMixIn(YearMixin, EntityModelFiscalPeriodMixIn):
@@ -292,19 +292,34 @@ class SuccessUrlNextMixIn:
292
292
  return reverse('django_ledger:home')
293
293
 
294
294
 
295
- class DjangoLedgerSecurityMixIn(PermissionRequiredMixin):
295
+ class DjangoLedgerSecurityMixIn(LoginRequiredMixin, PermissionRequiredMixin):
296
+ ENTITY_SLUG_URL_KWARG = 'entity_slug'
296
297
  AUTHORIZED_ENTITY_MODEL: Optional[EntityModel] = None
298
+ AUTHORIZE_SUPERUSER: bool = DJANGO_LEDGER_AUTHORIZED_SUPERUSER
297
299
  permission_required = []
298
300
 
299
301
  def get_login_url(self):
300
302
  return reverse('django_ledger:login')
301
303
 
304
+ def get_entity_slug(self):
305
+ return self.kwargs[self.ENTITY_SLUG_URL_KWARG]
306
+
307
+ def get_entity_slug_kwarg(self):
308
+ if self.ENTITY_SLUG_URL_KWARG is None:
309
+ raise ImproperlyConfigured(
310
+ _('ENTITY_SLUG_URL_KWARG must be provided.')
311
+ )
312
+ return self.ENTITY_SLUG_URL_KWARG
313
+
302
314
  def has_permission(self):
315
+ entity_slug_kwarg = self.get_entity_slug_kwarg()
303
316
  if self.request.user.is_superuser:
304
- if 'entity_slug' in self.kwargs:
317
+ if not self.AUTHORIZE_SUPERUSER:
318
+ return False
319
+ if entity_slug_kwarg in self.kwargs:
305
320
  try:
306
321
  entity_model_qs = self.get_authorized_entity_queryset()
307
- self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs['entity_slug'])
322
+ self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
308
323
  except ObjectDoesNotExist:
309
324
  return False
310
325
  return True
@@ -312,10 +327,10 @@ class DjangoLedgerSecurityMixIn(PermissionRequiredMixin):
312
327
  has_perm = super().has_permission()
313
328
  if not has_perm:
314
329
  return False
315
- if 'entity_slug' in self.kwargs:
330
+ if entity_slug_kwarg in self.kwargs:
316
331
  try:
317
332
  entity_model_qs = self.get_authorized_entity_queryset()
318
- self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs['entity_slug'])
333
+ self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
319
334
  except ObjectDoesNotExist:
320
335
  return False
321
336
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-ledger
3
- Version: 0.6.0.1
3
+ Version: 0.6.0.2
4
4
  Summary: Double entry accounting system built on the Django Web Framework.
5
5
  Author-email: Miguel Sanda <msanda@arrobalytics.com>
6
6
  Maintainer-email: Miguel Sanda <msanda@arrobalytics.com>
@@ -44,9 +44,10 @@ Requires-Dist: furo ; extra == 'dev'
44
44
 
45
45
  Introducing __Django Ledger__, a powerful double entry accounting system designed for financially driven applications
46
46
  using the [Django Web Framework](https://www.djangoproject.com). Developed by lead developer Miguel Sanda, this system
47
- offers a simplified, high-level API, making it easier for users to navigate the complexities of accounting. If you have
48
- prior experience with Django, you'll find this software even more effective. And, for those interested in contributing,
49
- consider joining our new discord channel for further collaboration and discussions.
47
+ offers a simplified, high-level API, making it easier for users to navigate the complexities of accounting.
48
+
49
+ If you have prior experience with Django, you'll find this software even more effective. And, for those interested
50
+ in contributing, consider joining our new discord channel for further collaboration and discussions.
50
51
 
51
52
  ### Questions? Join our Discord Channel [Here](https://discord.gg/c7PZcbYgrc)
52
53
 
@@ -82,13 +83,14 @@ Feel free to initiate an Issue describing your new feature request.
82
83
  Finance and Accounting is a complicated subject. Django Ledger stands out from other Django projects due to its focus
83
84
  on providing a developer-friendly accounting engine and a reliable, extensible API for financially driven applications.
84
85
  The project requires expertise in Python, Django programming, finance, and accounting. In essence, the project is
85
- seeking assistance from individuals with the specific skill set needed to contribute effectively. So, it's clear that
86
- they are in need of support from individuals with the right expertise.
86
+ seeking assistance from individuals with the specific skill set needed to contribute effectively.
87
87
 
88
88
  The project is actively seeking contributors with financial and/or accounting experience. Prior accounting experience
89
89
  is a big plus for potential contributors. If you have the relevant experience and want to contribute, feel free to
90
- reach out to me. You can find the contribution guidelines at the specified link. The project welcomes anyone interested
91
- in making a contribution.
90
+ reach out to me or submit your pull request.
91
+
92
+ You can find the contribution guidelines at the specified link.
93
+ The project welcomes anyone interested in making a contribution.
92
94
 
93
95
  See __[contribution guidelines](https://github.com/arrobalytics/django-ledger/blob/develop/Contribute.md)__.
94
96
 
@@ -48,15 +48,15 @@ assets/node_modules/node-gyp/gyp/tools/pretty_gyp.py,sha256=2ZCRPW-MZfK7gdnCIaqh
48
48
  assets/node_modules/node-gyp/gyp/tools/pretty_sln.py,sha256=b_Fxm-SXUCPL3Tix4EyNwZNmQ-zkeRIFFmuL0R5wFhw,5482
49
49
  assets/node_modules/node-gyp/gyp/tools/pretty_vcproj.py,sha256=AwQrxK1F-jhjsbbT35XQjrvWNbc3IBFaKXoJogqMh_o,10633
50
50
  assets/node_modules/node-gyp/test/fixtures/test-charmap.py,sha256=5raXzaQnO2eJnrlFtlDtWftryhZX7Fj0amFW3hdSnhE,547
51
- django_ledger/__init__.py,sha256=jnHCdsWYYamrH0NaMS2Fo0Mw33kRl-BEzEmJJxs6HAw,458
51
+ django_ledger/__init__.py,sha256=c-3dvReg1GgIIrDElZzpUfB38BXg2TAKf5d9uhe5WMQ,458
52
52
  django_ledger/apps.py,sha256=H-zEWUjKGakgSDSZmLIoXChZ2h6e0dth0ZO5SpoT-8U,163
53
53
  django_ledger/exceptions.py,sha256=rML8sQQ0Hq-DYMLZ76dfw2RYSAsXWUoyHuyC_yP9o1o,491
54
- django_ledger/settings.py,sha256=KLujLYwEC3fxfj5u9HnVDsr1rBjE5OAI--bu0MYi4JE,6221
54
+ django_ledger/settings.py,sha256=bZyPKgjmRcO_Rj7hDi4gGlW0VFr_LP2yKeUVIkmWgQM,6321
55
55
  django_ledger/utils.py,sha256=l8xq-uSvUdJNpyDjC_0UrsSfjeEpwf7B-tavbnt40a8,4305
56
56
  django_ledger/admin/__init__.py,sha256=MipzxmBhXswpx63uf3Ai2amyBMAP5fZL7mKXKxjNRIY,458
57
57
  django_ledger/admin/coa.py,sha256=BcBsvNs4Z1hOyZy4YqCtIfk1aw8DejrI1bAEH93Tkjc,3542
58
58
  django_ledger/admin/entity.py,sha256=DhH-6o3kjUdkhVPHzwOSF3crtvf5MCzcc1vPCk9O2Bk,6287
59
- django_ledger/admin/ledger.py,sha256=ecwmnuW4119StZDR_1QaK9jdZXw2dEvza-dnx1bHWDM,7876
59
+ django_ledger/admin/ledger.py,sha256=z33FYDT50ahrK4AGs-bZhnrvdIt-imG0QJpZ_KRGUWw,7914
60
60
  django_ledger/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  django_ledger/contrib/django_ledger_graphene/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  django_ledger/contrib/django_ledger_graphene/api.py,sha256=exrsmMcX21-Vhpe2_9X0eRLcdlnoE2ut0KUxBLu-TM8,871
@@ -115,14 +115,14 @@ django_ledger/forms/unit.py,sha256=rXUefjpuAmUU0vPOqu1ObO4k-bN-_Q6kOqHJ4kp_Vlg,1
115
115
  django_ledger/forms/utils.py,sha256=sgkwBZs15_rZ5NT7h-8Z7wi3-ItM1E1sqoVDo3NQ5Jc,513
116
116
  django_ledger/forms/vendor.py,sha256=Nuh8MmSpz4ycMZwiVe--U9Ec6ezIsfACHDkhA2SyiZ4,2215
117
117
  django_ledger/io/__init__.py,sha256=Y9R-mY4peg8EpxmlXKaBER1IHMU-Nos8_dII41Kj0Ho,445
118
- django_ledger/io/io_core.py,sha256=fvMZYLlutL7Pn6F_p7U3whRA4ZeS7IDangm1v6Hcjvw,46479
118
+ django_ledger/io/io_core.py,sha256=Cutbj5WQQ0Mrja2_kLw02KL6_JygDdJvDp7JF5KzWJU,46923
119
119
  django_ledger/io/io_digest.py,sha256=W_bCH6JxGw6eASDb1k43JuGAejvOVfyA7WkCS7AEqDQ,4280
120
120
  django_ledger/io/io_generator.py,sha256=JF4plsABUkCIrtI2X-YD7o5eNghRIgLUseNcBIGOj3U,34613
121
- django_ledger/io/io_library.py,sha256=kZt61TV6McxH2Ly1INYRmb-T1hNuEKe4WI0YB_YHeKk,20564
121
+ django_ledger/io/io_library.py,sha256=vvQm3IQRLFdH7HS_DYX46Xe-U9IvgZ6MQnHjy0-fyjk,22480
122
122
  django_ledger/io/io_middleware.py,sha256=c-vwpcjg2HbYbb4O36fdf6011dFOnoNsDHOAQXmJgB8,20052
123
123
  django_ledger/io/ofx.py,sha256=JnmDjhIpLySoixK1WVe6DivRuu02rYsBjqI8yi5Opzs,1488
124
124
  django_ledger/io/ratios.py,sha256=dsuCv9-r73SMLv3OrxeoT5JebfRmrDsRKG_YzHggWFw,3542
125
- django_ledger/io/roles.py,sha256=RrErn0-cDOr90UrMBGl-PM_PxG2o_KYbGYeK4Dpwsek,20690
125
+ django_ledger/io/roles.py,sha256=J9Z8WtunOQShKORCY97HpFtlAHG4N4hPfBkpUtRQDIY,20223
126
126
  django_ledger/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
127
  django_ledger/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
128
  django_ledger/management/commands/generate_oauth2_codes.py,sha256=H92pSOMA0VFjdCLXOqdIWql-aKU12uaAPdXgz2DB9Go,1495
@@ -141,25 +141,26 @@ django_ledger/migrations/0012_stagedtransactionmodel_activity.py,sha256=Tv0rXC1H
141
141
  django_ledger/migrations/0013_stagedtransactionmodel_bundle_split.py,sha256=7Uxd5JEKbP31vSnlP1Us_JA6mtJzAwFnr0XNCKYJDao,469
142
142
  django_ledger/migrations/0014_ledgermodel_ledger_xid_and_more.py,sha256=UHuEQrnFr1dV4p2mxeUtWk39psSnqwBymDz_xt57sZc,663
143
143
  django_ledger/migrations/0015_remove_chartofaccountmodel_locked_and_more.py,sha256=GZDKJDjpqo52pY7sXusHpyvXsUwsuvoZqTQNda9Eo1I,560
144
+ django_ledger/migrations/0016_remove_accountmodel_django_ledg_coa_mod_e19964_idx_and_more.py,sha256=wpapkPycqZ9drUMlPGBs1IRy7pz6HyDgNvZBaf-E86o,1655
144
145
  django_ledger/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
146
  django_ledger/models/__init__.py,sha256=8mn-OGhAVgLs8YASEBwo8dpX6tHyGtMxRHVPGDGECVU,793
146
- django_ledger/models/accounts.py,sha256=GQoVBCN_UeXnA2zj4bejXthr2mmL9a_8iS-gDPKI5KY,28937
147
+ django_ledger/models/accounts.py,sha256=0OWMrv89fUdec7RF1EiWE6xZdJMOdEpgYPWechAJYrM,28881
147
148
  django_ledger/models/bank_account.py,sha256=0-eTBxxRyvUKOVVNcGqWV1kiOKcXA2KPQIdiVHDUDCY,7678
148
- django_ledger/models/bill.py,sha256=vaXdTEfow9SPzactNs6mZdm4tJc8uamKufT9tQGFpJg,63541
149
+ django_ledger/models/bill.py,sha256=ZC6PmPYeSFMUBSUqTqebQOZrWEpo59PH6Y4evTO5uy8,63717
149
150
  django_ledger/models/closing_entry.py,sha256=557vVKhrRZOdzqmmvtVlU48VbzJk8tTV018b0dTfpek,17746
150
- django_ledger/models/coa.py,sha256=Eue4XTRjM9irR-2z2P-Ynrw0ZrfAMub1m69mBdqok7s,27097
151
+ django_ledger/models/coa.py,sha256=o-VM2XK64djM3px6pJlGrUVTXu5qNb4ENESS70I___0,27154
151
152
  django_ledger/models/coa_default.py,sha256=4Zj8OMhgBiYuREjM82cFfyGWd8uCAeqggVkeNhg4SLU,27338
152
153
  django_ledger/models/customer.py,sha256=JQOktcYKUlENJv4frek9rAW6sRerrQ0xXHlC5KPmhWk,11807
153
154
  django_ledger/models/data_import.py,sha256=2H-4oTVLa7qXq03m9fd7T5zSQLkZKOAn2OAeOQBzMPA,19477
154
- django_ledger/models/entity.py,sha256=1btxPOJxKmSpT1T12bisVE_QVZmQoniyfpya1JURBt4,121692
155
+ django_ledger/models/entity.py,sha256=YYcgiVsC4aDl-htStJRQ6_1Hdmw5oHK4mBP2L0XanwI,121708
155
156
  django_ledger/models/estimate.py,sha256=-qB5t2cEdyYpFUq7tOUQnFqvE6EDUiVdTtzjEbESwEQ,55829
156
- django_ledger/models/invoice.py,sha256=FZ7ZxAjyrM2NUwiR3wIX0PnbPXV27F63u8sESb4qM20,61389
157
- django_ledger/models/items.py,sha256=JLPFGPQvTKiq3r09wgG2cxBB4ZmcEXK6LyqMUhzWj3k,54938
157
+ django_ledger/models/invoice.py,sha256=h5Jh5KOfYr31Eu9gFW1mdoGoVzx7nW8qBdx7vyiXnZU,61568
158
+ django_ledger/models/items.py,sha256=O9ujsut2jiISwKEmYcCTdUdsFywm0S-RKUDPCeXvPgY,55093
158
159
  django_ledger/models/journal_entry.py,sha256=VfXXvm3tUFuy2Z6j3PLlDk9ndHqsZgn_PuhrxTNqaiY,50918
159
- django_ledger/models/ledger.py,sha256=qjORKpXHn7d393OLS62F2joyyfZyh7tCg7wc67nAu50,23042
160
+ django_ledger/models/ledger.py,sha256=q6yWf9jGK05kah_19Cbijc8HZErPM1koPejKO3ZbYXI,23382
160
161
  django_ledger/models/mixins.py,sha256=SBcSMfFuFzLqFQv298aDOfAJrF5kT91oXyo384auKqc,51903
161
162
  django_ledger/models/purchase_order.py,sha256=CDibi90e7Yhpv_UiyP32mMcsQ0EUElXJ2r8pLzuS7yE,42729
162
- django_ledger/models/transactions.py,sha256=lCwJ68vPy2ePX8dTzDsEwHPk87VN-vYGdxfwSNF60Po,24229
163
+ django_ledger/models/transactions.py,sha256=aRZuP-zg-ZrxBi6rt3wx2ELXhsRC-BS9NtNUDCU2DV0,24236
163
164
  django_ledger/models/unit.py,sha256=x5FFJXgOi1OdajQejIakW6wGY4DjrJhL3S0Pm5OimMk,8074
164
165
  django_ledger/models/utils.py,sha256=3gkdCrfJp9qwN3Sf8R96AliilzwcKBm31UEao4WJO9o,8436
165
166
  django_ledger/models/vendor.py,sha256=akJCO86GIwjlZ_jPUZCDXlMeuJe-8zKTm-52aJXGFpg,11320
@@ -376,7 +377,7 @@ django_ledger/urls/closing_entry.py,sha256=3W0fCuAWGB3h8cWg0cxOb9EClVrydeIdHEX0q
376
377
  django_ledger/urls/customer.py,sha256=I3tWSb5Gmdr-boBSlCst_5cvCHz6JhpGxuwglqJeaG0,426
377
378
  django_ledger/urls/data_import.py,sha256=bOi6U8gN2adxQUjOeNCJGuDRB--hwWeUIQOYTMbFarw,780
378
379
  django_ledger/urls/djl_api.py,sha256=4BOkWI8MyfJlHXRn21hL08KYF39j7Njd1l7FIxTcsuc,952
379
- django_ledger/urls/entity.py,sha256=OjTvKuwlqtZVdcnQbr1JcLk5imyop-0Fs-p1wqgqKPQ,1279
380
+ django_ledger/urls/entity.py,sha256=8ysVslj0KhzGeOZyfRMJW6SYROyGM_azwGxFkkG4ICQ,1286
380
381
  django_ledger/urls/estimate.py,sha256=4dmIv-IElYgl88HsEuwIYBr6XK4Dhbhtj09TmDa5H8k,2058
381
382
  django_ledger/urls/feedback.py,sha256=TY7UWFHHdN6teL6HiLibmjGCx4pXSijYZWaLt3L7-qs,273
382
383
  django_ledger/urls/financial_statement.py,sha256=frEM-gPH3r9QkkyqmpQc3xf5IdqoCAdVQ5PgjvHD_PU,8565
@@ -388,7 +389,7 @@ django_ledger/urls/journal_entry.py,sha256=sKuYtKDSaAVcW--iIe8GQecuVZ7wF6W3vOtgA
388
389
  django_ledger/urls/ledger.py,sha256=9OD7jvR3D3F6KY9RU-Hj6asvH4OiapQzvwaG3IS7haY,2801
389
390
  django_ledger/urls/purchase_order.py,sha256=iUNdzy8dcxkkmDAOt2fO4Up3e0pHDqZNSf9JOKbO4Wo,2388
390
391
  django_ledger/urls/transactions.py,sha256=e_x_z5qbkR6i7o8OWWdXshDiY_WVmu9WVhR9A96fnhI,80
391
- django_ledger/urls/unit.py,sha256=EaBd1EcSeQYbOH1rTQZdyDEEtLVi7-QfC_wpRPwTpuE,1499
392
+ django_ledger/urls/unit.py,sha256=QEVKrgcw2dqMaaXsUHfqYecTa5-iaPlS9smrYJ1QsgM,1506
392
393
  django_ledger/urls/vendor.py,sha256=ODHpAwe5lomluj8ZCqbMtugTeeRsv0Yo9SqkZEmfYaw,393
393
394
  django_ledger/views/__init__.py,sha256=l5Pm2_oAW6Q_jJbXf-BiHA3ilCbiGb6gkXCm73K5DGY,1158
394
395
  django_ledger/views/account.py,sha256=d2pzYXKPOF74hCD4ehsQ_WNFsgqyGXXekCh22gDawAM,10523
@@ -400,7 +401,7 @@ django_ledger/views/coa.py,sha256=WnWQVz-4Ik9v28KHzD_WiKcgix7l6bBj1A60p4k-eos,49
400
401
  django_ledger/views/customer.py,sha256=RoBsXBxZC9b79DSNNHaoSZtQ2AoXf7DJAGmZEO3xdxs,3672
401
402
  django_ledger/views/data_import.py,sha256=_H8gjGRIE2Jm97ivvEQn0uEWrM3VvKkYQeXQ1GbKn3g,11950
402
403
  django_ledger/views/djl_api.py,sha256=6ADX9fBK8DroTeg8UIeCf2x4wt6-AF5xLlDQnqXBfsM,4411
403
- django_ledger/views/entity.py,sha256=YCAcgzlg66pk8mYCnhnVHbkWkGFuYHkJ-Vy6H8ea9og,9422
404
+ django_ledger/views/entity.py,sha256=egJoB4-HAyzKd_5tZ8gOh8nxMKA09_Ds2H7elGt5_DE,9457
404
405
  django_ledger/views/estimate.py,sha256=ZFG0k2_nAV10EjO-p8yp7EVMa4x2qOcFSHl2xFpNDaM,12811
405
406
  django_ledger/views/feedback.py,sha256=qoIN44fJnblPx-pJFe5yYeO-dMqp-FReFZiyw0qQb_s,2460
406
407
  django_ledger/views/financial_statement.py,sha256=B4FE9qyBYs8tJvBJ1n9-7kR-pH2EJWn6SnjBdtbRfuE,7335
@@ -409,15 +410,15 @@ django_ledger/views/inventory.py,sha256=ZyCmrkdYLu-D7Fnt0if0-wW6-wSWMgK9EQivVATA
409
410
  django_ledger/views/invoice.py,sha256=iUzTG-EbdYqNX-eYwHBnQRUD_1wTOGutw0BfDMKcI6s,20304
410
411
  django_ledger/views/item.py,sha256=FY53vk_giTRgvJ47FRqChQ8vyDYPDp4DGTvVhGAb36E,21347
411
412
  django_ledger/views/journal_entry.py,sha256=21kuiRBlhlkgv8xZKM4mj9djv0Fu0BhB80QOEOHCa-w,12135
412
- django_ledger/views/ledger.py,sha256=k3cK9zgGNnPwMPx0uqj_pRMFbM71lbYi7bi-l6B2M5s,12681
413
- django_ledger/views/mixins.py,sha256=Zgx85WJ87IQ0yTRdVgVQp70puWaRloUObLgqeCoQLTM,21283
413
+ django_ledger/views/ledger.py,sha256=Yk6uDoYhJs5vf5JRqsy8n0nUNDEHk7NzjR1PglyqaAM,12647
414
+ django_ledger/views/mixins.py,sha256=pXiEEen4rKrAluaROMAZC7nLR967LUV5fOiOAfq22tY,21966
414
415
  django_ledger/views/purchase_order.py,sha256=1J3u4QnCkM7z1Y6DePijVdM67x4CQgfmQJcs3Y4kclU,21082
415
416
  django_ledger/views/transactions.py,sha256=5taQRGLSMkM_N8paQJ07HMspI_Nl7PawF8OohCiRmao,206
416
417
  django_ledger/views/unit.py,sha256=_RgPJO9mR6v5ohBXlnL3T8nTWgS1lwlCvERQcHk0wHE,10232
417
418
  django_ledger/views/vendor.py,sha256=gUdBPTFLeSwlNfdHSA1KFdE_y3QpwpkFhEB0r3-UYdI,3461
418
- django_ledger-0.6.0.1.dist-info/AUTHORS.md,sha256=SRM2cynD89ZfEsL09zrbUVeO17r9zE2ZM7y6ReMqVRo,713
419
- django_ledger-0.6.0.1.dist-info/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
420
- django_ledger-0.6.0.1.dist-info/METADATA,sha256=OrUy7UxR0pq7BC-igJag8bYQi_LujihzrpEEvxBwnVM,9700
421
- django_ledger-0.6.0.1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
422
- django_ledger-0.6.0.1.dist-info/top_level.txt,sha256=fmHWehb2HfoDncQ3eQtYzeYc-gJMywf6q_ZpKBjwzoQ,38
423
- django_ledger-0.6.0.1.dist-info/RECORD,,
419
+ django_ledger-0.6.0.2.dist-info/AUTHORS.md,sha256=SRM2cynD89ZfEsL09zrbUVeO17r9zE2ZM7y6ReMqVRo,713
420
+ django_ledger-0.6.0.2.dist-info/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
421
+ django_ledger-0.6.0.2.dist-info/METADATA,sha256=lx74R33LSCnxzSR4EcoICDKCz8vpi04rr1d31KbzoVg,9643
422
+ django_ledger-0.6.0.2.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
423
+ django_ledger-0.6.0.2.dist-info/top_level.txt,sha256=0U3SjF63ND36grQNWDONVe-T9-T07lFl5e6QkG7bR2E,44
424
+ django_ledger-0.6.0.2.dist-info/RECORD,,
@@ -2,3 +2,4 @@ assets
2
2
  django_ledger
3
3
  docs
4
4
  screenshots
5
+ tests