Skip to content

Commit

Permalink
v0.5.6.3 (#188)
Browse files Browse the repository at this point in the history
* Date/Datetime Compatibility Improvements
CFS Purchase or Sale of PPE recognizes depreciation roles.

* v0.5.6.3
AccountModelCreateForm now accepts active and locked fields.
Account transaction table now shows entity unit and JE number information.
Closing Entry optimizations and bugfixes.
Financial Statements report format bugfixes.
LedgerModel Hide/UnHide Actions.
CFS Activity Grouping Updates

* LedgerModelQuerySet current() now compares dates instead of datetime.

* LedgerModelQuerySet current() now compares dates instead of datetime.

* Django Compatibility Update
Tests Updates
  • Loading branch information
elarroba authored Mar 1, 2024
1 parent 0cfc1b9 commit 4d4a41f
Show file tree
Hide file tree
Showing 27 changed files with 1,563 additions and 532 deletions.
6 changes: 3 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"

[packages]
django = ">=2.2"
django = ">=4.2"
django-treebeard = ">=4.5.1"
ofxtools = ">=0.9.5"
markdown = ">=3.4.1"
Expand All @@ -24,14 +24,14 @@ fpdf2 = ">=2.7.4"
sphinx = "*"
behave = "*"
twine = "*"
#jupyterlab = "*"
jupyterlab = "*"
pandas = "*"
#pipenv-setup = "*"
#pylint = "*"
#furo = "*"
#python-dotenv = "*"
#tabulate = "*"
#myst_parser = "*"
#pandas = "*"

[requires]
python_version = "3.11"
Expand Down
1,516 changes: 1,249 additions & 267 deletions Pipfile.lock

Large diffs are not rendered by default.

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.5.6.2'
__version__ = '0.5.6.3'
__license__ = 'GPLv3 License'

__author__ = 'Miguel Sanda'
Expand Down
2 changes: 2 additions & 0 deletions django_ledger/forms/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class Meta:
'role',
'role_default',
'balance_type',
'active',
'locked'
]
widgets = {
'code': TextInput(attrs={
Expand Down
167 changes: 90 additions & 77 deletions django_ledger/io/io_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from django.conf import settings as global_settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db.models import Sum, QuerySet
from django.db.models import Sum, QuerySet, F, DecimalField, When, Case
from django.db.models.functions import TruncMonth
from django.http import Http404
from django.utils.dateparse import parse_date, parse_datetime
Expand Down Expand Up @@ -142,16 +142,13 @@ def get_localdate() -> date:
return datetime.today()


def validate_io_date(
def validate_io_timestamp(
dt: Union[str, date, datetime],
no_parse_localdate: bool = True) -> Optional[Union[datetime, date]]:
if not dt:
return

if isinstance(dt, date):
return dt

elif isinstance(dt, datetime):
if isinstance(dt, datetime):
if is_naive(dt):
return make_aware(
value=dt,
Expand All @@ -178,15 +175,22 @@ def validate_io_date(
))
return datetime.combine(fdt, datetime.min.time())

elif isinstance(dt, date):
if global_settings.USE_TZ:
return make_aware(
value=datetime.combine(dt, datetime.min.time())
)
return datetime.combine(dt, datetime.min.time())

if no_parse_localdate:
return localtime()


def validate_dates(
from_date: Union[str, datetime, date] = None,
to_date: Union[str, datetime, date] = None) -> Tuple[date, date]:
from_date = validate_io_date(from_date, no_parse_localdate=False)
to_date = validate_io_date(to_date)
from_date = validate_io_timestamp(from_date, no_parse_localdate=False)
to_date = validate_io_timestamp(to_date)
return from_date, to_date


Expand Down Expand Up @@ -226,6 +230,13 @@ class IOResult:
# the aggregated account balance...
accounts_digest: Optional[List[Dict]] = None

@property
def is_bounded(self) -> bool:
return all([
self.ce_from_date is not None,
self.ce_to_date is not None,
])


class IODatabaseMixIn:
"""
Expand Down Expand Up @@ -275,7 +286,7 @@ def database_digest(self,
accounts: Optional[Union[str, List[str], Set[str]]] = None,
posted: bool = True,
exclude_zero_bal: bool = True,
force_closing_entry_use: bool = False,
use_closing_entries: bool = False,
**kwargs) -> IOResult:
"""
Performs the appropriate database aggregation query for a given request.
Expand Down Expand Up @@ -314,57 +325,67 @@ def database_digest(self,
Returns results aggregated by accounting if needed. Defaults to False.
by_unit: bool
Returns results aggregated by unit if needed. Defaults to False.
force_closing_entry_use: bool
Forces the use of closing entries if DJANGO_LEDGER_USE_CLOSING_ENTRIES setting is set to False.
use_closing_entry: bool
Overrides the DJANGO_LEDGER_USE_CLOSING_ENTRIES setting.
Returns
-------
IOResult
"""

TransactionModel = lazy_loader.get_txs_model()
io_result = IOResult(db_to_date=to_date, db_from_date=from_date)
txs_queryset_closing_entry = TransactionModel.objects.none()

# where the IO model is operating from??...
# get_initial txs_queryset... where the IO model is operating from??...
if self.is_entity_model():
if entity_slug:
if entity_slug != self.slug:
raise IOValidationError('Inconsistent entity_slug. '
f'Provided {entity_slug} does not match actual {self.slug}')
if unit_slug:
txs_queryset = TransactionModel.objects.for_unit(
txs_queryset_init = TransactionModel.objects.for_unit(
user_model=user_model,
entity_slug=entity_slug or self.slug,
unit_slug=unit_slug
)
else:
txs_queryset = TransactionModel.objects.for_entity(
txs_queryset_init = TransactionModel.objects.for_entity(
user_model=user_model,
entity_slug=self
)
elif self.is_ledger_model():
elif self.is_entity_unit_model():
if not entity_slug:
raise IOValidationError(
'Calling digest from Ledger Model requires entity_slug explicitly for safety')
txs_queryset = TransactionModel.objects.for_ledger(
'Calling digest from Entity Unit requires entity_slug explicitly for safety')
txs_queryset_init = TransactionModel.objects.for_unit(
user_model=user_model,
entity_slug=entity_slug,
ledger_model=self
unit_slug=unit_slug or self
)
elif self.is_entity_unit_model():
elif self.is_ledger_model():
if not entity_slug:
raise IOValidationError(
'Calling digest from Entity Unit requires entity_slug explicitly for safety')
txs_queryset = TransactionModel.objects.for_unit(
'Calling digest from Ledger Model requires entity_slug explicitly for safety')
txs_queryset_init = TransactionModel.objects.for_ledger(
user_model=user_model,
entity_slug=entity_slug,
unit_slug=unit_slug or self
ledger_model=self
)
else:
txs_queryset = TransactionModel.objects.none()
raise IOValidationError(
message=f'Cannot call digest from {self.__class__.__name__}'
)

io_result = IOResult(db_to_date=to_date, db_from_date=from_date)
txs_queryset_agg = txs_queryset_init.not_closing_entry()
txs_queryset_from_closing_entry = txs_queryset_init.none()
txs_queryset_to_closing_entry = txs_queryset_init.none()

USE_CLOSING_ENTRIES = settings.DJANGO_LEDGER_USE_CLOSING_ENTRIES
if use_closing_entries is not None:
USE_CLOSING_ENTRIES = use_closing_entries

# use closing entries to minimize DB aggregation if activated...
if settings.DJANGO_LEDGER_USE_CLOSING_ENTRIES or force_closing_entry_use:
# use closing entries to minimize DB aggregation if possible and activated...
if USE_CLOSING_ENTRIES:
txs_queryset_closing_entry = txs_queryset_init.is_closing_entry()
entity_model = self.get_entity_model_from_io()

# looking up available dates...
Expand All @@ -378,57 +399,55 @@ def database_digest(self,

# if there's a suitable closing entry...
if ce_alt_from_date:
txs_queryset_closing_entry = txs_queryset.is_closing_entry().filter(
journal_entry__timestamp__date=ce_alt_from_date
)
txs_queryset_from_closing_entry = txs_queryset_closing_entry.filter(
journal_entry__timestamp__date=ce_alt_from_date)
io_result.ce_match = True
io_result.ce_from_date = ce_alt_from_date

# limit db aggregation to unclosed entries...
io_result.db_from_date = ce_alt_from_date + timedelta(days=1)
io_result.db_to_date = to_date

# print(f'Unbounded lookup no date match. Closest from_dt: {ce_alt_from_date}...')

# unbounded lookup, exact to_date match...
elif not ce_from_date and ce_to_date:
txs_queryset_closing_entry = txs_queryset.is_closing_entry().filter(
elif not from_date and ce_to_date:
txs_queryset_to_closing_entry = txs_queryset_closing_entry.filter(
journal_entry__timestamp__date=ce_to_date)
io_result.ce_match = True
io_result.ce_to_date = ce_to_date

# no need to DB aggregate...
# no need to DB aggregate, just use closing entry...
io_result.db_from_date = None
io_result.db_to_date = None
# print(f'Unbounded lookup EXACT date match. Closest to_dt: {ce_to_date}...')
txs_queryset_agg = TransactionModel.objects.none()

# bounded exact from_date and to_date match...
elif ce_from_date and ce_to_date:
txs_queryset_closing_entry = txs_queryset.is_closing_entry().filter(
journal_entry__timestamp__date__in=[
ce_from_date,
ce_to_date
])
txs_queryset_from_closing_entry = txs_queryset_closing_entry.filter(
journal_entry__timestamp__date=ce_from_date)

txs_queryset_to_closing_entry = txs_queryset_closing_entry.filter(
journal_entry__timestamp__date=ce_to_date)

io_result.ce_match = True
io_result.ce_from_date = ce_from_date
io_result.ce_to_date = ce_to_date

# no need to aggregate...
# no need to aggregate, use both closing entries...
io_result.db_from_date = None
io_result.db_to_date = None
# print(f'Bounded lookup EXACT date match. Closest from_dt: {ce_from_date} '
# f'| to_dt: {ce_to_date}...')
txs_queryset_agg = TransactionModel.objects.none()

# no suitable closing entries to use...
else:
txs_queryset = txs_queryset.not_closing_entry()
else:
# not using closing entries...
txs_queryset = txs_queryset.not_closing_entry()
txs_queryset_closing_entry = txs_queryset_from_closing_entry | txs_queryset_to_closing_entry

if io_result.db_from_date:
txs_queryset = txs_queryset.from_date(from_date=io_result.db_from_date)
txs_queryset_agg = txs_queryset_agg.from_date(from_date=io_result.db_from_date)

if io_result.db_to_date:
txs_queryset = txs_queryset.to_date(to_date=io_result.db_to_date)
txs_queryset_agg = txs_queryset_agg.to_date(to_date=io_result.db_to_date)

txs_queryset = txs_queryset_agg | txs_queryset_closing_entry

if exclude_zero_bal:
txs_queryset = txs_queryset.filter(amount__gt=0.00)
Expand All @@ -449,6 +468,16 @@ def database_digest(self,
if role:
txs_queryset = txs_queryset.for_roles(role_list=role)

if io_result.is_bounded:
txs_queryset = txs_queryset.annotate(
amount_io=Case(
When(
journal_entry__timestamp__date=ce_from_date,
then=-F('amount')),
default=F('amount'),
output_field=DecimalField()
))

VALUES = [
'account__uuid',
'account__balance_type',
Expand All @@ -457,7 +486,11 @@ def database_digest(self,
'account__name',
'account__role',
]

ANNOTATE = {'balance': Sum('amount')}
if io_result.is_bounded:
ANNOTATE = {'balance': Sum('amount_io')}

ORDER_BY = ['account__uuid']

if by_unit:
Expand All @@ -476,8 +509,6 @@ def database_digest(self,
ORDER_BY.append('tx_type')
VALUES.append('tx_type')

txs_queryset = txs_queryset | txs_queryset_closing_entry

io_result.txs_queryset = txs_queryset.values(*VALUES).annotate(**ANNOTATE).order_by(*ORDER_BY)
return io_result

Expand All @@ -496,7 +527,7 @@ def python_digest(self,
by_activity: bool = False,
by_tx_type: bool = False,
by_period: bool = False,
force_closing_entry_use: bool = False,
use_closing_entries: bool = False,
force_queryset_sorting: bool = False,
**kwargs) -> IOResult:
"""
Expand Down Expand Up @@ -565,7 +596,7 @@ def python_digest(self,
activity=activity,
role=role,
accounts=accounts,
force_closing_entry_use=force_closing_entry_use,
use_closing_entries=use_closing_entries,
**kwargs)

for tx_model in io_result.txs_queryset:
Expand Down Expand Up @@ -649,6 +680,7 @@ def digest(self,
balance_sheet_statement: bool = False,
income_statement: bool = False,
cash_flow_statement: bool = False,
use_closing_entry: Optional[bool] = None,
**kwargs) -> IODigestContextManager:

if balance_sheet_statement:
Expand Down Expand Up @@ -690,6 +722,7 @@ def digest(self,
by_unit=by_unit,
by_activity=by_activity,
by_tx_type=by_tx_type,
use_closing_entry=use_closing_entry,
**kwargs
)

Expand Down Expand Up @@ -759,35 +792,15 @@ def commit_txs(self,
je_unit_model=None,
je_desc=None,
je_origin=None,
force_je_retrieval: bool = False):
"""
Creates JE from TXS list using provided account_id.
TXS = List[{
'account': Account Database UUID
'tx_type': credit/debit,
'amount': Decimal/Float/Integer,
'description': string,
'staged_tx_model': StagedTransactionModel or None
}]
:param je_timestamp:
:param je_txs:
:param je_activity:
:param je_posted:
:param je_ledger_model:
:param je_desc:
:param je_origin:
:param je_parent:
:return:
"""
force_je_retrieval: bool = False,
**kwargs):

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

# Validates that credits/debits balance.
check_tx_balance(je_txs, perform_correction=False)
je_timestamp = validate_io_date(dt=je_timestamp)
je_timestamp = validate_io_timestamp(dt=je_timestamp)

entity_model = self.get_entity_model_from_io()

Expand Down
Loading

0 comments on commit 4d4a41f

Please sign in to comment.