Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0.6.0.2 #202

Merged
merged 5 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion django_ledger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
default_app_config = 'django_ledger.apps.DjangoLedgerConfig'

"""Django Ledger"""
__version__ = '0.6.0.1'
__version__ = '0.6.0.2'
__license__ = 'GPLv3 License'

__author__ = 'Miguel Sanda'
Expand Down
4 changes: 3 additions & 1 deletion django_ledger/admin/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ class LedgerModelAdmin(ModelAdmin):
'is_locked',
'is_extended',
'journal_entry_count',
'earliest_journal_entry'
'earliest_journal_entry',
'created',
'updated'
]
actions = [
'post',
Expand Down
23 changes: 18 additions & 5 deletions django_ledger/io/io_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ class IODatabaseMixIn:
helps minimize the number of transactions to aggregate for a given request.
"""

TRANSACTION_MODEL_CLASS = None
JOURNAL_ENTRY_MODEL_CLASS = None

def is_entity_model(self):
return isinstance(self, lazy_loader.get_entity_model())

Expand All @@ -276,6 +279,16 @@ def get_entity_model_from_io(self):
elif self.is_entity_unit_model():
return self.entity

def get_transaction_model(self):
if self.TRANSACTION_MODEL_CLASS is not None:
return self.TRANSACTION_MODEL_CLASS
return lazy_loader.get_txs_model()

def get_journal_entry_model(self):
if self.JOURNAL_ENTRY_MODEL_CLASS is not None:
return self.JOURNAL_ENTRY_MODEL_CLASS
return lazy_loader.get_journal_entry_model()

def database_digest(self,
entity_slug: Optional[str] = None,
unit_slug: Optional[str] = None,
Expand Down Expand Up @@ -337,7 +350,7 @@ def database_digest(self,
IOResult
"""

TransactionModel = lazy_loader.get_txs_model()
TransactionModel = self.get_transaction_model()

# get_initial txs_queryset... where the IO model is operating from??...
if self.is_entity_model():
Expand Down Expand Up @@ -604,7 +617,7 @@ def python_digest(self,
use_closing_entries=use_closing_entries,
**kwargs)

TransactionModel = lazy_loader.get_txs_model()
TransactionModel = self.get_transaction_model()

for tx_model in io_result.txs_queryset:
if tx_model['account__balance_type'] != tx_model['tx_type']:
Expand Down Expand Up @@ -801,8 +814,8 @@ def commit_txs(self,
force_je_retrieval: bool = False,
**kwargs):

JournalEntryModel = lazy_loader.get_journal_entry_model()
TransactionModel = lazy_loader.get_txs_model()
TransactionModel = self.get_transaction_model()
JournalEntryModel = self.get_journal_entry_model()

# Validates that credits/debits balance.
check_tx_balance(je_txs, perform_correction=False)
Expand Down Expand Up @@ -897,7 +910,7 @@ def commit_txs(self,
if staged_tx_model:
staged_tx_model.transaction_model = tx

txs_models = je_model.transactionmodel_set.bulk_create(i[0] for i in txs_models)
txs_models = TransactionModel.objects.bulk_create(i[0] for i in txs_models)
je_model.save(verify=True, post_on_verify=je_posted)
return je_model, txs_models

Expand Down
109 changes: 79 additions & 30 deletions django_ledger/io/io_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@

This module contains classes and functions used to document, dispatch and commit new transaction into the database.
"""
import enum
from collections import defaultdict
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal
from itertools import chain
from typing import Union, Dict, Callable, Optional, List
from typing import Union, Dict, Callable, Optional, List, Set
from uuid import UUID

from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -63,6 +64,11 @@ class IOCursorValidationError(ValidationError):
pass


class IOCursorMode(enum.Enum):
STRICT = 'strict'
PERMISSIVE = 'permissive'


class IOCursor:
"""
Represents a Django Ledger cursor capable of dispatching transactions to the database.
Expand All @@ -86,18 +92,20 @@ def __init__(self,
io_library,
entity_model: EntityModel,
user_model,
mode: IOCursorMode = IOCursorMode.PERMISSIVE,
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None):
self.IO_LIBRARY = io_library
self.MODE = mode
self.ENTITY_MODEL = entity_model
self.USER_MODEL = user_model
self.COA_MODEL = coa_model
self.__COMMITTED: bool = False
self.blueprints = defaultdict(list)
self.ledger_model_qs: Optional[LedgerModelQuerySet] = None
self.account_model_qs: Optional[AccountModelQuerySet] = None
self.ledger_map = dict()
self.commit_plan = dict()
self.instructions = None
self.__COMMITTED: bool = False

def get_ledger_model_qs(self) -> LedgerModelQuerySet:
"""
Expand All @@ -122,9 +130,9 @@ def get_account_model_qs(self) -> AccountModelQuerySet:
"""
return self.ENTITY_MODEL.get_coa_accounts(
coa_model=self.COA_MODEL
)
).can_transact()

def resolve_account_model_qs(self, codes: List[str]) -> AccountModelQuerySet:
def resolve_account_model_qs(self, codes: Set[str]) -> AccountModelQuerySet:
"""
Resolves the final AccountModelQuerySet associated with the given account codes used by the blueprint.

Expand Down Expand Up @@ -164,6 +172,12 @@ def resolve_ledger_model_qs(self) -> LedgerModelQuerySet:
)
return self.ledger_model_qs

def is_permissive(self) -> bool:
return self.MODE == IOCursorMode.PERMISSIVE

def is_strict(self) -> bool:
return self.MODE == IOCursorMode.STRICT

def dispatch(self,
name,
ledger_model: Optional[Union[str, LedgerModel, UUID]] = None,
Expand All @@ -183,13 +197,14 @@ def dispatch(self,
The keyword arguments to be passed to the blueprint function.
"""

if not isinstance(ledger_model, (str, UUID, LedgerModel)):
raise IOCursorValidationError(
message=_('Ledger Model must be a string or UUID or LedgerModel')
)
if ledger_model is not None:
if not isinstance(ledger_model, (str, UUID, LedgerModel)):
raise IOCursorValidationError(
message=_('Ledger Model must be a string or UUID or LedgerModel')
)

if isinstance(ledger_model, LedgerModel):
self.ENTITY_MODEL.validate_ledger_model_for_entity(ledger_model)
if isinstance(ledger_model, LedgerModel):
self.ENTITY_MODEL.validate_ledger_model_for_entity(ledger_model)

blueprint_func = self.IO_LIBRARY.get_blueprint(name)
blueprint_txs = blueprint_func(**kwargs)
Expand Down Expand Up @@ -238,6 +253,7 @@ def is_committed(self) -> bool:

def commit(self,
je_timestamp: Optional[Union[datetime, date, str]] = None,
je_description: Optional[str] = None,
post_new_ledgers: bool = False,
post_journal_entries: bool = False,
**kwargs):
Expand All @@ -251,6 +267,8 @@ def commit(self,
----------
je_timestamp: Optional[Union[datetime, date, str]]
The date or timestamp used for the committed journal entries. If none, localtime will be used.
je_description: Optional[str]
The description of the journal entries. If none, no description will be used.
post_new_ledgers: bool
If a new ledger is created, the ledger model will be posted to the database.
post_journal_entries: bool
Expand All @@ -275,29 +293,39 @@ def commit(self,
for k, txs in self.blueprints.items():
if k is None:

# no specified xid, ledger or UUID... create one...
self.commit_plan[
self.ENTITY_MODEL.create_ledger(
name='Blueprint Commitment',
commit=False,
posted=post_new_ledgers
if self.is_permissive():
# no specified xid, ledger or UUID... create one...
self.commit_plan[
self.ENTITY_MODEL.create_ledger(
name='Blueprint Commitment',
commit=False,
posted=post_new_ledgers
)
] = txs
else:
raise IOCursorValidationError(
message=_('Cannot commit transactions to a non-existing ledger')
)
] = txs

elif isinstance(k, str):
try:
# ledger with xid already exists...
self.commit_plan[self.ledger_map[k]] = txs
except KeyError:
# create ledger with xid provided...
self.commit_plan[
self.ENTITY_MODEL.create_ledger(
name=f'Blueprint Commitment {k}',
ledger_xid=k,
commit=False,
posted=post_new_ledgers
if self.is_permissive():
# create ledger with xid provided...
self.commit_plan[
self.ENTITY_MODEL.create_ledger(
name=f'Blueprint Commitment {k}',
ledger_xid=k,
commit=False,
posted=post_new_ledgers
)
] = txs
else:
raise IOCursorValidationError(
message=_(f'Cannot commit transactions to a non-existing ledger_xid {k}')
)
] = txs

elif isinstance(k, UUID):
try:
Expand All @@ -315,12 +343,18 @@ def commit(self,

instructions = self.compile_instructions()
account_codes = set(tx.account_code for tx in chain.from_iterable(tr for _, tr in instructions.items()))
account_model_qs = self.resolve_account_model_qs(codes=account_codes)
account_models = {
acc.code: acc for acc in self.resolve_account_model_qs(codes=account_codes)
acc.code: acc for acc in account_model_qs
}

for tx in chain.from_iterable(tr for _, tr in instructions.items()):
tx.account_model = account_models[tx.account_code]
try:
tx.account_model = account_models[tx.account_code]
except KeyError:
raise IOCursorValidationError(
message=_(f'Account code {tx.account_code} not found. Is account available and not locked?')
)

results = dict()
for ledger_model, tr_items in instructions.items():
Expand All @@ -333,15 +367,20 @@ def commit(self,
je_timestamp=je_timestamp if je_timestamp else get_localtime(),
je_txs=je_txs,
je_posted=post_journal_entries,
je_desc=je_description,
**kwargs
)

je.txs_models = txs_models

results[ledger_model] = {
'ledger_model': ledger_model,
'journal_entry': je,
'txs_models': txs_models,
'instructions': tr_items
'instructions': tr_items,
'account_model_qs': self.account_model_qs
}
results['account_model_qs'] = self.account_model_qs

self.__COMMITTED = True
return results

Expand Down Expand Up @@ -518,6 +557,8 @@ class IOLibrary:
The human-readable name of the library (i.e. PayRoll, Expenses, Rentals, etc...)
"""

IO_CURSOR_CLASS = IOCursor

def __init__(self, name: str):
self.name = name
self.registry: Dict[str, Callable] = {}
Expand Down Expand Up @@ -545,10 +586,14 @@ def get_blueprint(self, name: str) -> Callable:
raise IOLibraryError(message=f'Function "{name}" is not registered in IO library {self.name}')
return self.registry[name]

def get_io_cursor_class(self):
return self.IO_CURSOR_CLASS

def get_cursor(
self,
entity_model: EntityModel,
user_model,
mode: IOCursorMode = IOCursorMode.PERMISSIVE,
coa_model: Optional[Union[ChartOfAccountModel, UUID, str]] = None
) -> IOCursor:
"""
Expand All @@ -562,14 +607,18 @@ def get_cursor(
The user model instance executing the transactions.
coa_model: ChartOfAccountModel or UUID or str, optional
The ChartOfAccountsModel instance or identifier used to determine the AccountModelQuerySet used for the transactions.
mode: IOCursorMode
The Mode of the cursor instance. Defaults to IOCursorMode.PERMISSIVE.

Returns
-------
IOCursor
"""
return IOCursor(
io_cursor_class = self.get_io_cursor_class()
return io_cursor_class(
io_library=self,
entity_model=entity_model,
user_model=user_model,
coa_model=coa_model,
mode=mode
)
Loading