Skip to content

Commit 411a6dc

Browse files
elarrobapablo
andauthored
v0.7.8 (#273)
* v.0.7.7 * Enhance account list views and update icon styles. Implemented dynamic page titles and subtitles for account pages by overriding `get_context_data`. Updated template text and classes for greater consistency, including standardizing icon colors for active, locked, and default roles. * Adding TransactionModel attributes during commit_txs. * Fixed a couple of hard coded dollar "$" symbols and changed them with `{% currency_symbol %}` tag to render the correct symbol when `DJANGO_LEDGER_CURRENCY_SYMBOL` is other than default ("$"). (#265) * Dependency update * Update contribution guidelines in README and Contribute.md Clarified the types of pull requests that are encouraged, emphasizing those that address bug fixes, enhancements, or valuable additions. Added a note discouraging submissions focused only on cosmetic changes like linting or refactoring. * Update ManyToManyField configurations and bump version to 0.7.8 Adjusted `ManyToManyField` relationships in `BillModel`, `InvoiceModel`, and `PurchaseOrderModel` to include `through` and `through_fields` for `ItemTransactionModel`. Incremented package version to `0.7.8`. * Add support for bank account type validation and retrieval based on OFX standards - Introduced `bank_account_type` field in `BankAccountModel` with predefined choices. - Added methods to retrieve routing number, account type, and account type validation in `OFXImport` class. - Enhanced account queries with a new `.cash()` method to filter accounts with `ASSET_CA_CASH` role. - Updated indexing and unique constraints for `BankAccountModel`. * Migration Update * Refactor bank account type handling and account type mapping logic - Replaced `BankAccountModel.BANK_ACCOUNT_TYPES` with explicit OFX types. - Renamed `ACCOUNT_TYPE_ROLE_MAPPING` to `ACCOUNT_TYPE_DEFAULT_ROLE_MAPPING`. - Centralized OFX type mappings in `ACCOUNT_TYPE_OFX_MAPPING`. - Removed `bank_account_type` field from `BankAccountModel`. - Added `get_account_type_from_ofx` method for retrieving account type from OFX data. * Add financial institution field and utility methods to account models - Introduced `financial_institution` field in account mixin for storing bank details. - Added `get_account_last_digits` utility for partial account number retrieval. - Implemented `can_hide` and `can_unhide` methods in `BankAccountModel`. * Refactor account handling and enhance validation methods - Renamed `get_account_type` to `get_ofx_account_type` for clarity in OFX implementation. - Added `get_account_type` method to map OFX account types to internal account types. - Introduced `get_routing_last_digits` method for masked routing number retrieval. - Improved handling of missing account and routing numbers in utility methods. --------- Co-authored-by: Pablo Santa Cruz <pablo@roshka.com.py>
1 parent 19a0e8a commit 411a6dc

File tree

9 files changed

+157
-17
lines changed

9 files changed

+157
-17
lines changed

django_ledger/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
default_app_config = 'django_ledger.apps.DjangoLedgerConfig'
77

88
"""Django Ledger"""
9-
__version__ = '0.7.7'
9+
__version__ = '0.7.8'
1010
__license__ = 'GPLv3 License'
1111

1212
__author__ = 'Miguel Sanda'

django_ledger/io/ofx.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
from typing import List, Optional, Dict
10+
from django_ledger.models.bank_account import BankAccountModel
1011

1112
from django.core.exceptions import ValidationError
1213
from ofxtools import OFXTree
@@ -64,6 +65,30 @@ def get_account_data(self):
6465
def get_account_number(self):
6566
return self.get_account_data()['account'].acctid
6667

68+
def get_routing_number(self):
69+
return self.get_account_data()['account'].bankid
70+
71+
def get_ofx_account_type(self):
72+
"""
73+
Gets the account type as defined in the OFX (Open Financial Exchange) specification.
74+
75+
Returns:
76+
str: One of the following standardized account types:
77+
- 'CHECKING' - Standard checking account
78+
- 'SAVINGS' - Savings account
79+
- 'MONEYMRKT' - Money Market account
80+
- 'CREDITLINE' - Credit line account
81+
- 'CD' - Certificate of Deposit
82+
"""
83+
acc_type = self.get_account_data()['account'].accttype
84+
85+
if acc_type not in ['CHECKING', 'SAVINGS', 'MONEYMRKT', 'CREDITLINE', 'CD']:
86+
raise OFXImportValidationError(f'Account type "{acc_type}" is not supported.')
87+
return acc_type
88+
89+
def get_account_type(self):
90+
return BankAccountModel.ACCOUNT_TYPE_OFX_MAPPING[self.get_ofx_account_type()]
91+
6792
def get_account_txs(self):
6893
acc_statement = next(iter(
6994
st for st in self.ofx_data.statements if st.account.acctid == self.get_account_number()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 5.2.1 on 2025-06-24 21:54
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='bankaccountmodel',
15+
name='financial_institution',
16+
field=models.CharField(blank=True, help_text='Name of the financial institution (i.e. Bank Name).', max_length=100, null=True, verbose_name='Financial Institution'),
17+
),
18+
migrations.AddField(
19+
model_name='vendormodel',
20+
name='financial_institution',
21+
field=models.CharField(blank=True, help_text='Name of the financial institution (i.e. Bank Name).', max_length=100, null=True, verbose_name='Financial Institution'),
22+
),
23+
migrations.AlterField(
24+
model_name='bankaccountmodel',
25+
name='account_type',
26+
field=models.CharField(choices=[('checking', 'Checking'), ('savings', 'Savings'), ('money_market', 'Money Market'), ('cert_deposit', 'Certificate of Deposit'), ('credit_card', 'Credit Card'), ('st_loan', 'Short Term Loan'), ('lt_loan', 'Long Term Loan'), ('mortgage', 'Mortgage'), ('other', 'Other')], default='checking', max_length=20, verbose_name='Account Type'),
27+
),
28+
migrations.AlterField(
29+
model_name='billmodel',
30+
name='bill_items',
31+
field=models.ManyToManyField(through='django_ledger.ItemTransactionModel', through_fields=('bill_model', 'item_model'), to='django_ledger.itemmodel', verbose_name='Bill Items'),
32+
),
33+
migrations.AlterField(
34+
model_name='invoicemodel',
35+
name='invoice_items',
36+
field=models.ManyToManyField(through='django_ledger.ItemTransactionModel', through_fields=('invoice_model', 'item_model'), to='django_ledger.itemmodel', verbose_name='Invoice Items'),
37+
),
38+
migrations.AlterField(
39+
model_name='purchaseordermodel',
40+
name='po_items',
41+
field=models.ManyToManyField(through='django_ledger.ItemTransactionModel', through_fields=('po_model', 'item_model'), to='django_ledger.itemmodel', verbose_name='Purchase Order Items'),
42+
),
43+
migrations.AlterField(
44+
model_name='vendormodel',
45+
name='account_type',
46+
field=models.CharField(choices=[('checking', 'Checking'), ('savings', 'Savings'), ('money_market', 'Money Market'), ('cert_deposit', 'Certificate of Deposit'), ('credit_card', 'Credit Card'), ('st_loan', 'Short Term Loan'), ('lt_loan', 'Long Term Loan'), ('mortgage', 'Mortgage'), ('other', 'Other')], default='checking', max_length=20, verbose_name='Account Type'),
47+
),
48+
migrations.AddIndex(
49+
model_name='bankaccountmodel',
50+
index=models.Index(fields=['entity_model'], name='django_ledg_entity__6ad006_idx'),
51+
),
52+
]

django_ledger/models/accounts.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
GROUP_ASSETS, GROUP_LIABILITIES, GROUP_CAPITAL, GROUP_INCOME, GROUP_EXPENSES, GROUP_COGS,
6666
ROOT_GROUP, BS_BUCKETS, ROOT_ASSETS, ROOT_LIABILITIES,
6767
ROOT_CAPITAL, ROOT_INCOME, ROOT_EXPENSES, ROOT_COA, VALID_PARENTS,
68-
ROLES_ORDER_ALL
68+
ROLES_ORDER_ALL, ASSET_CA_CASH
6969
)
7070
from django_ledger.models.mixins import CreateUpdateMixIn
7171
from django_ledger.models.utils import lazy_loader
@@ -161,6 +161,10 @@ def with_codes(self, codes: Union[List, str]):
161161
codes = [codes]
162162
return self.filter(code__in=codes)
163163

164+
def cash(self):
165+
"""Retrieve accounts that are of type ASSET_CA_CASH."""
166+
return self.filter(role__exact=ASSET_CA_CASH)
167+
164168
def expenses(self):
165169
"""
166170
Retrieve a queryset containing expenses filtered by specified roles.

django_ledger/models/bank_account.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.contrib.auth import get_user_model
1313
from django.core.exceptions import ValidationError
1414
from django.db import models
15-
from django.db.models import Q, QuerySet
15+
from django.db.models import Q, QuerySet, Manager
1616
from django.shortcuts import get_object_or_404
1717
from django.utils.translation import gettext_lazy as _
1818

@@ -55,7 +55,7 @@ def hidden(self) -> QuerySet:
5555
return self.filter(hidden=True)
5656

5757

58-
class BankAccountModelManager(models.Manager):
58+
class BankAccountModelManager(Manager):
5959
"""
6060
Custom defined Model Manager for the BankAccountModel.
6161
"""
@@ -126,10 +126,12 @@ class BankAccountModelAbstract(FinancialAccountInfoMixin, CreateUpdateMixIn):
126126
entity_model = models.ForeignKey('django_ledger.EntityModel',
127127
on_delete=models.CASCADE,
128128
verbose_name=_('Entity Model'))
129+
129130
account_model = models.ForeignKey('django_ledger.AccountModel',
130131
on_delete=models.RESTRICT,
131132
help_text=_(
132-
'Account model be used to map transactions from financial institution'),
133+
'Account model be used to map transactions from financial institution'
134+
),
133135
verbose_name=_('Associated Account Model'))
134136
active = models.BooleanField(default=False)
135137
hidden = models.BooleanField(default=False)
@@ -168,7 +170,8 @@ class Meta:
168170
verbose_name = _('Bank Account')
169171
indexes = [
170172
models.Index(fields=['account_type']),
171-
models.Index(fields=['account_model'])
173+
models.Index(fields=['account_model']),
174+
models.Index(fields=['entity_model'])
172175
]
173176
unique_together = [
174177
('entity_model', 'account_number'),
@@ -178,6 +181,12 @@ class Meta:
178181
def __str__(self):
179182
return f'{self.get_account_type_display()} Bank Account: {self.name}'
180183

184+
def can_hide(self) -> bool:
185+
return self.hidden is False
186+
187+
def can_unhide(self) -> bool:
188+
return self.hidden is True
189+
181190
def can_activate(self) -> bool:
182191
return self.active is False
183192

django_ledger/models/entity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2048,7 +2048,7 @@ def create_bank_account(self,
20482048
account_model_qs = self.get_coa_accounts(coa_model=coa_model, active=True)
20492049
account_model_qs = account_model_qs.with_roles(
20502050
roles=[
2051-
BankAccountModel.ACCOUNT_TYPE_ROLE_MAPPING[account_type]
2051+
BankAccountModel.ACCOUNT_TYPE_DEFAULT_ROLE_MAPPING[account_type]
20522052
]
20532053
).is_role_default()
20542054

django_ledger/models/mixins.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from django.utils.translation import gettext_lazy as _
2424
from markdown import markdown
2525

26-
from django_ledger.io import ASSET_CA_CASH, LIABILITY_CL_ST_NOTES_PAYABLE, LIABILITY_LTL_MORTGAGE_PAYABLE
26+
from django_ledger.io import (
27+
ASSET_CA_CASH, LIABILITY_CL_ST_NOTES_PAYABLE, LIABILITY_LTL_MORTGAGE_PAYABLE,
28+
LIABILITY_CL_ACC_PAYABLE, LIABILITY_CL_OTHER, LIABILITY_LTL_NOTES_PAYABLE
29+
)
2730
from django_ledger.io.io_core import validate_io_timestamp, check_tx_balance, get_localtime, get_localdate
2831
from django_ledger.models.utils import lazy_loader
2932

@@ -1123,25 +1126,55 @@ class FinancialAccountInfoMixin(models.Model):
11231126

11241127
ACCOUNT_CHECKING = 'checking'
11251128
ACCOUNT_SAVINGS = 'savings'
1129+
ACCOUNT_MONEY_MKT = 'money_market'
1130+
ACCOUNT_CERT_DEPOSIT = 'cert_deposit'
11261131
ACCOUNT_CREDIT_CARD = 'credit_card'
1132+
ACCOUNT_ST_LOAN = 'st_loan'
1133+
ACCOUNT_LT_LOAN = 'lt_loan'
11271134
ACCOUNT_MORTGAGE = 'mortgage'
1135+
ACCOUNT_OTHER = 'other'
11281136

1129-
ACCOUNT_TYPE_ROLE_MAPPING = {
1137+
ACCOUNT_TYPE_DEFAULT_ROLE_MAPPING = {
11301138
ACCOUNT_CHECKING: ASSET_CA_CASH,
11311139
ACCOUNT_SAVINGS: ASSET_CA_CASH,
1132-
ACCOUNT_CREDIT_CARD: LIABILITY_CL_ST_NOTES_PAYABLE,
1133-
ACCOUNT_MORTGAGE: LIABILITY_LTL_MORTGAGE_PAYABLE
1140+
ACCOUNT_MONEY_MKT: ASSET_CA_CASH,
1141+
ACCOUNT_CERT_DEPOSIT: ASSET_CA_CASH,
1142+
ACCOUNT_CREDIT_CARD: LIABILITY_CL_ACC_PAYABLE,
1143+
ACCOUNT_ST_LOAN: LIABILITY_CL_ST_NOTES_PAYABLE,
1144+
ACCOUNT_LT_LOAN: LIABILITY_LTL_NOTES_PAYABLE,
1145+
ACCOUNT_MORTGAGE: LIABILITY_LTL_MORTGAGE_PAYABLE,
1146+
ACCOUNT_OTHER: LIABILITY_CL_OTHER
11341147
}
11351148

11361149
ACCOUNT_TYPE_CHOICES = [
11371150
(ACCOUNT_CHECKING, _('Checking')),
11381151
(ACCOUNT_SAVINGS, _('Savings')),
1152+
(ACCOUNT_MONEY_MKT, _('Money Market')),
1153+
(ACCOUNT_CERT_DEPOSIT, _('Certificate of Deposit')),
11391154
(ACCOUNT_CREDIT_CARD, _('Credit Card')),
1155+
(ACCOUNT_ST_LOAN, _('Short Term Loan')),
1156+
(ACCOUNT_LT_LOAN, _('Long Term Loan')),
11401157
(ACCOUNT_MORTGAGE, _('Mortgage')),
1158+
(ACCOUNT_OTHER, _('Other')),
11411159
]
11421160

1161+
ACCOUNT_TYPE_OFX_MAPPING = {
1162+
'CHECKING': ACCOUNT_CHECKING,
1163+
'SAVINGS': ACCOUNT_SAVINGS,
1164+
'MONEYMRKT': ACCOUNT_MONEY_MKT,
1165+
'CREDITLINE': ACCOUNT_CREDIT_CARD,
1166+
'CD': ACCOUNT_CERT_DEPOSIT
1167+
}
1168+
11431169
VALID_ACCOUNT_TYPES = tuple(atc[0] for atc in ACCOUNT_TYPE_CHOICES)
11441170

1171+
financial_institution = models.CharField(
1172+
max_length=100,
1173+
blank=True,
1174+
null=True,
1175+
verbose_name=_('Financial Institution'),
1176+
help_text=_('Name of the financial institution (i.e. Bank Name).')
1177+
)
11451178
account_number = models.CharField(max_length=30, null=True, blank=True,
11461179
validators=[
11471180
int_list_validator(sep='', message=_('Only digits allowed'))
@@ -1152,14 +1185,31 @@ class FinancialAccountInfoMixin(models.Model):
11521185
], verbose_name=_('Routing Number'))
11531186
aba_number = models.CharField(max_length=30, null=True, blank=True, verbose_name=_('ABA Number'))
11541187
swift_number = models.CharField(max_length=30, null=True, blank=True, verbose_name=_('SWIFT Number'))
1155-
account_type = models.CharField(choices=ACCOUNT_TYPE_CHOICES,
1156-
max_length=20,
1157-
default=ACCOUNT_CHECKING,
1158-
verbose_name=_('Account Type'))
1188+
account_type = models.CharField(
1189+
choices=ACCOUNT_TYPE_CHOICES,
1190+
max_length=20,
1191+
default=ACCOUNT_CHECKING,
1192+
verbose_name=_('Account Type')
1193+
)
11591194

11601195
class Meta:
11611196
abstract = True
11621197

1198+
def get_account_last_digits(self, n=4) -> str:
1199+
if not self.account_number:
1200+
return 'Not Available'
1201+
return f'*{self.account_number[-n:]}'
1202+
1203+
def get_routing_last_digits(self, n=4) -> str:
1204+
if not self.routing_number:
1205+
return 'Not Available'
1206+
return f'*{self.routing_number[-n:]}'
1207+
1208+
def get_account_type_from_ofx(self, ofx_type):
1209+
return self.ACCOUNT_TYPE_OFX_MAPPING.get(
1210+
ofx_type, self.ACCOUNT_OTHER
1211+
)
1212+
11631213

11641214
class TaxInfoMixIn(models.Model):
11651215
tax_id_number = models.CharField(max_length=30,

django_ledger/views/data_import.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def get_queryset(self):
3232
'bank_account_model__entity_model',
3333
'bank_account_model__account_model',
3434
'bank_account_model__account_model__coa_model')
35-
return super().get_queryset()
35+
return self.queryset
3636

3737

3838
class ImportJobModelCreateView(ImportJobModelViewBaseView, FormView):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "django-ledger"
3-
version = "0.7.7"
3+
version = "0.7.8"
44
readme = "README.md"
55
requires-python = ">=3.10"
66
description = "Double entry accounting system built on the Django Web Framework."

0 commit comments

Comments
 (0)