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.1 #204

Merged
merged 11 commits into from
May 10, 2024
288 changes: 136 additions & 152 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.6.0.2'
__version__ = '0.6.1'
__license__ = 'GPLv3 License'

__author__ = 'Miguel Sanda'
Expand Down
4 changes: 1 addition & 3 deletions django_ledger/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
Miguel Sanda <msanda@arrobalytics.com>
"""

from django_ledger.io.io_digest import *
from django_ledger.io.io_context import *
from django_ledger.io.io_middleware import *
from django_ledger.io.ratios import *
from django_ledger.io.roles import *
# due to circular import
# from django_ledger.io.io_library import IOLibrary
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def get_io_txs_queryset(self):
def get_strftime_format(self):
return self.STRFTIME_FORMAT

@property
def from_datetime(self):
return self.get_from_datetime()

def get_from_datetime(self, as_str: bool = False, fmt=None) -> Optional[datetime]:
from_date = self.IO_DATA['from_date']
if from_date:
Expand All @@ -47,6 +51,10 @@ def get_from_datetime(self, as_str: bool = False, fmt=None) -> Optional[datetime
return from_date.strftime(fmt)
return from_date

@property
def to_datetime(self):
return self.get_to_datetime()

def get_to_datetime(self, as_str: bool = False, fmt=None) -> datetime:
if as_str:
if not fmt:
Expand Down
2 changes: 1 addition & 1 deletion django_ledger/io/io_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from django_ledger import settings
from django_ledger.exceptions import InvalidDateInputError, TransactionNotInBalanceError
from django_ledger.io import roles as roles_module
from django_ledger.io.io_digest import IODigestContextManager
from django_ledger.io.io_context import IODigestContextManager
from django_ledger.io.io_middleware import (
AccountRoleIOMiddleware,
AccountGroupIOMiddleware,
Expand Down
6 changes: 4 additions & 2 deletions django_ledger/models/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def get_queryset(self):
qs = EntityModelQuerySet(self.model, using=self._db).order_by('path')
return qs.order_by('path').select_related('admin', 'default_coa')

def for_user(self, user_model):
def for_user(self, user_model, authorized_superuser: bool = False):
"""
This QuerySet guarantees that Users do not access or operate on EntityModels that don't have access to.
This is the recommended initial QuerySet.
Expand All @@ -125,6 +125,8 @@ def for_user(self, user_model):
----------
user_model
The Django User Model making the request.
authorized_superuser
Allows any superuser to access the EntityModel. Default is False.

Returns
-------
Expand All @@ -134,7 +136,7 @@ def for_user(self, user_model):
2. Is a manager.
"""
qs = self.get_queryset()
if user_model.is_superuser:
if user_model.is_superuser and authorized_superuser:
return qs
return qs.filter(
Q(admin=user_model) |
Expand Down
4 changes: 1 addition & 3 deletions django_ledger/models/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,8 +881,6 @@ class ItemTransactionModelManager(models.Manager):

def for_user(self, user_model):
qs = self.get_queryset()
if user_model.is_superuser:
return qs
return qs.filter(
Q(item_model__entity__admin=user_model) |
Q(item_model__entity__managers__in=[user_model])
Expand All @@ -891,7 +889,7 @@ def for_user(self, user_model):
def for_entity(self, user_model, entity_slug):
qs = self.for_user(user_model)
if isinstance(entity_slug, lazy_loader.get_entity_model()):
qs.filter(
return qs.filter(
Q(item_model__entity=entity_slug)
)
return qs.filter(
Expand Down
23 changes: 14 additions & 9 deletions django_ledger/models/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ def __str__(self):
ledger_str = f'LedgerModel: {self.uuid}'
return f'{ledger_str} | Posted: {self.posted} | Locked: {self.locked}'


def has_wrapped_model_info(self):
if self.additional_info is not None:
return self._WRAPPED_MODEL_KEY in self.additional_info
Expand Down Expand Up @@ -523,7 +522,7 @@ def lock_journal_entries(self, commit: bool = True, **kwargs):
je_model_qs.bulk_update(objs=je_model_qs, fields=['locked', 'updated'])
return je_model_qs

def unlock(self, commit: bool = False, **kwargs):
def unlock(self, commit: bool = False, raise_exception: bool = True, **kwargs):
"""
Un-locks the LedgerModel.

Expand All @@ -532,13 +531,19 @@ def unlock(self, commit: bool = False, **kwargs):
commit: bool
If True, saves the LedgerModel instance instantly. Defaults to False.
"""
if self.can_unlock():
self.locked = False
if commit:
self.save(update_fields=[
'locked',
'updated'
])
if not self.can_unlock():
if raise_exception:
raise LedgerModelValidationError(
message=_(f'Ledger {self.name} cannot be un-locked. UUID: {self.uuid}')
)
return

self.locked = False
if commit:
self.save(update_fields=[
'locked',
'updated'
])

def hide(self, commit: bool = False, raise_exception: bool = True, **kwargs):
if not self.can_hide():
Expand Down
18 changes: 11 additions & 7 deletions django_ledger/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ def lock_ledger(self, commit: bool = False, raise_exception: bool = True, **kwar
if ledger_model.locked:
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is already locked...')
ledger_model.lock(commit)
return
ledger_model.lock(commit, raise_exception=raise_exception)

def unlock_ledger(self, commit: bool = False, raise_exception: bool = True, **kwargs):
"""
Expand All @@ -501,10 +502,11 @@ def unlock_ledger(self, commit: bool = False, raise_exception: bool = True, **kw
If True, raises ValidationError if LedgerModel already locked.
"""
ledger_model = self.ledger
if not ledger_model.locked:
if not ledger_model.is_locked():
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is already unlocked...')
ledger_model.unlock(commit)
return
ledger_model.unlock(commit, raise_exception=raise_exception)

# POST/UNPOST Ledger...
def post_ledger(self, commit: bool = False, raise_exception: bool = True, **kwargs):
Expand All @@ -522,7 +524,8 @@ def post_ledger(self, commit: bool = False, raise_exception: bool = True, **kwar
if ledger_model.posted:
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is already posted...')
ledger_model.post(commit)
return
ledger_model.post(commit, raise_exception=raise_exception)

def unpost_ledger(self, commit: bool = False, raise_exception: bool = True, **kwargs):
"""
Expand All @@ -536,13 +539,14 @@ def unpost_ledger(self, commit: bool = False, raise_exception: bool = True, **kw
If True, raises ValidationError if LedgerModel already locked.
"""
ledger_model = self.ledger
if not ledger_model.posted:
if not ledger_model.is_posted():
if raise_exception:
raise ValidationError(f'Bill ledger {ledger_model.name} is not posted...')
ledger_model.post(commit)
return
ledger_model.post(commit, raise_exception=raise_exception)

def migrate_state(self,
# todo: remove usermodel param...
# todo: remove usermodel param...?
user_model,
entity_slug: str,
itemtxs_qs: Optional[QuerySet] = None,
Expand Down
33 changes: 1 addition & 32 deletions django_ledger/models/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,35 +49,6 @@ class TransactionModelQuerySet(QuerySet):
"""
A custom QuerySet class for TransactionModels implementing methods to effectively and safely read
TransactionModels from the database.

Methods
-------
posted() -> TransactionModelQuerySet:
Fetches a QuerySet of posted transactions only.

for_accounts(account_list: List[str or AccountModel]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels which AccountModel has a specific role.

for_roles(role_list: Union[str, List[str]]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels which AccountModel has a specific role.

for_unit(unit_slug: Union[str, EntityUnitModel]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a specific EntityUnitModel.

for_activity(activity_list: Union[str, List[str]]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a specific activity or list of activities.

to_date(to_date: Union[str, date, datetime]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a maximum date or timestamp filter.

from_date(from_date: Union[str, date, datetime]) -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels associated with a minimum date or timestamp filter.

not_closing_entry() -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels that are not part of a closing entry.

is_closing_entry() -> TransactionModelQuerySet:
Fetches a QuerySet of TransactionModels that are part of a closing entry.
"""

def posted(self) -> QuerySet:
Expand Down Expand Up @@ -111,7 +82,7 @@ def for_accounts(self, account_list: List[str or AccountModel]):
TransactionModelQuerySet
Returns a TransactionModelQuerySet with applied filters.
"""
if len(account_list) > 0 and isinstance(account_list[0], str):
if isinstance(account_list, list) > 0 and isinstance(account_list[0], str):
return self.filter(account__code__in=account_list)
return self.filter(account__in=account_list)

Expand Down Expand Up @@ -276,8 +247,6 @@ def for_user(self, user_model) -> TransactionModelQuerySet:
ledger or the user is one of the managers of the entity associated with the transaction's ledger.
"""
qs = self.get_queryset()
if user_model.is_superuser:
return qs
return qs.filter(
Q(journal_entry__ledger__entity__admin=user_model) |
Q(journal_entry__ledger__entity__managers__in=[user_model])
Expand Down
67 changes: 41 additions & 26 deletions django_ledger/views/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,25 +311,20 @@ def get_entity_slug_kwarg(self):
)
return self.ENTITY_SLUG_URL_KWARG

def get_superuser_authorization(self):
return self.AUTHORIZE_SUPERUSER

def has_permission(self):
has_perm = super().has_permission()
if not has_perm:
return False

entity_slug_kwarg = self.get_entity_slug_kwarg()
if self.request.user.is_superuser:
if not self.AUTHORIZE_SUPERUSER:
return False
if entity_slug_kwarg in self.kwargs:
try:
entity_model_qs = self.get_authorized_entity_queryset()
self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
except ObjectDoesNotExist:
return False
return True
elif self.request.user.is_authenticated:
has_perm = super().has_permission()
if not has_perm:
return False
entity_model_qs = self.get_authorized_entity_queryset()

if self.request.user.is_authenticated:
if entity_slug_kwarg in self.kwargs:
try:
entity_model_qs = self.get_authorized_entity_queryset()
self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
except ObjectDoesNotExist:
return False
Expand All @@ -338,7 +333,9 @@ def has_permission(self):

def get_authorized_entity_queryset(self):
return EntityModel.objects.for_user(
user_model=self.request.user).only(
user_model=self.request.user,
authorized_superuser=self.get_superuser_authorization(),
).only(
'uuid', 'slug', 'name', 'default_coa', 'admin')

def get_authorized_entity_instance(self) -> Optional[EntityModel]:
Expand Down Expand Up @@ -372,8 +369,26 @@ def get_context_data(self, **kwargs):


class DigestContextMixIn:
IO_DIGEST = False
IO_DIGEST_EQUITY = False
IO_DIGEST_UNBOUNDED = False
IO_DIGEST_BOUNDED = False

IO_DIGEST_UNBOUNDED_CONTEXT_NAME = 'tx_digest'
IO_MANAGER_UNBOUNDED_CONTEXT_NAME = 'tx_digest_context'

IO_DIGEST_BOUNDED_CONTEXT_NAME = 'equity_digest'
IO_MANAGER_BOUNDED_CONTEXT_NAME = 'equity_digest_context'

def get_io_digest_unbounded_context_name(self):
return self.IO_DIGEST_UNBOUNDED_CONTEXT_NAME

def get_io_manager_unbounded_context_name(self):
return self.IO_MANAGER_UNBOUNDED_CONTEXT_NAME

def get_io_digest_bounded_context_name(self):
return self.IO_DIGEST_BOUNDED_CONTEXT_NAME

def get_io_manager_bounded_context_name(self):
return self.IO_MANAGER_BOUNDED_CONTEXT_NAME

def get_context_data(self, **kwargs):
context = super(DigestContextMixIn, self).get_context_data(**kwargs)
Expand All @@ -385,8 +400,8 @@ def get_io_digest(self,
to_date=None,
**kwargs):

if any([self.IO_DIGEST,
self.IO_DIGEST_EQUITY]):
if any([self.IO_DIGEST_UNBOUNDED,
self.IO_DIGEST_BOUNDED]):

by_period = self.request.GET.get('by_period')
entity_model: EntityModel = self.object
Expand All @@ -401,7 +416,7 @@ def get_io_digest(self,
else:
unit_slug = None

if self.IO_DIGEST:
if self.IO_DIGEST_UNBOUNDED:
io_digest = entity_model.digest(user_model=self.request.user,
to_date=to_date,
unit_slug=unit_slug,
Expand All @@ -410,10 +425,10 @@ def get_io_digest(self,
process_roles=True,
process_groups=True)

context['tx_digest_context'] = io_digest
context['tx_digest'] = io_digest.get_io_data()
context[self.get_io_manager_unbounded_context_name()] = io_digest
context[self.get_io_digest_unbounded_context_name()] = io_digest.get_io_data()

if self.IO_DIGEST_EQUITY:
if self.IO_DIGEST_BOUNDED:
io_digest_equity = entity_model.digest(user_model=self.request.user,
equity_only=True,
to_date=to_date,
Expand All @@ -424,8 +439,8 @@ def get_io_digest(self,
process_roles=False,
process_groups=True)

context['equity_digest_context'] = io_digest_equity
context['equity_digest'] = io_digest_equity.get_io_data()
context[self.get_io_manager_bounded_context_name()] = io_digest_equity
context[self.get_io_digest_bounded_context_name()] = io_digest_equity.get_io_data()

# todo: how is this used??....
context['date_filter'] = to_date
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "django-ledger"
version = "0.6.0.2"
version = "0.6.1"
readme = "README.md"
requires-python = ">=3.10"
description = "Double entry accounting system built on the Django Web Framework."
Expand Down