Added type hints to the accounting application.

This commit is contained in:
依瑪貓 2020-08-13 07:25:35 +08:00
parent 9cb6f25ee5
commit d4e7458117
8 changed files with 302 additions and 287 deletions

View File

@ -142,14 +142,17 @@ class CashAccountConverter:
"""The path converter for the cash account."""
regex = "0|(11|12|21|22)[1-9]{1,3}"
def to_python(self, value):
def to_python(self, value: str) -> Account:
"""Returns the cash account by the account code.
Args:
value (str): The account code.
value: The account code.
Returns:
Account: The account.
The account.
Raises:
ValueError: When the value is invalid
"""
if value == "0":
return Account(
@ -164,14 +167,14 @@ class CashAccountConverter:
raise ValueError
return account
def to_url(self, value):
def to_url(self, value: Account) -> str:
"""Returns the code of an account.
Args:
value (Account): The account.
value: The account.
Returns:
str: The account code.
The account code.
"""
return value.code
@ -180,14 +183,17 @@ class LedgerAccountConverter:
"""The path converter for the ledger account."""
regex = "[1-9]{1,5}"
def to_python(self, value):
def to_python(self, value: str) -> Account:
"""Returns the ledger account by the account code.
Args:
value (str): The account code.
value: The account code.
Returns:
Account: The account.
The account.
Raises:
ValueError: When the value is invalid
"""
try:
account = Account.objects.get(code=value)
@ -197,14 +203,14 @@ class LedgerAccountConverter:
raise ValueError
return account
def to_url(self, value):
def to_url(self, value: Account) -> str:
"""Returns the code of an account.
Args:
value (Account): The account.
value: The account.
Returns:
str: The account code.
The account code.
"""
return value.code
@ -213,27 +219,30 @@ class TransactionConverter:
"""The path converter for the accounting transactions."""
regex = "[1-9][0-9]{8}"
def to_python(self, value):
def to_python(self, value: str) -> Transaction:
"""Returns the transaction by the transaction ID.
Args:
value (str): The transaction ID.
value: The transaction ID.
Returns:
Transaction: The account.
The account.
Raises:
ValueError: When the value is invalid
"""
try:
return Transaction.objects.get(pk=value)
except Transaction.DoesNotExist:
raise ValueError
def to_url(self, value):
def to_url(self, value: Transaction) -> str:
"""Returns the ID of an account.
Args:
value (Transaction): The transaction.
value: The transaction.
Returns:
str: The transaction ID.
The transaction ID.
"""
return value.pk

View File

@ -19,6 +19,7 @@
"""
import re
from typing import Optional
from django import forms
from django.core.validators import RegexValidator
@ -67,12 +68,12 @@ class RecordForm(forms.Form):
self.txn_form = None
self.is_credit = None
def account_title(self):
def account_title(self) -> Optional[str]:
"""Returns the title of the specified account, if any.
Returns:
str: The title of the specified account, or None if the specified
account is not available.
The title of the specified account, or None if the specified
account is not available.
"""
try:
return Account.objects.get(code=self["account"].value()).title
@ -101,7 +102,7 @@ class RecordForm(forms.Form):
if errors:
raise forms.ValidationError(errors)
def _validate_transaction(self):
def _validate_transaction(self) -> None:
"""Validates whether the transaction matches the transaction form.
Raises:
@ -122,7 +123,7 @@ class RecordForm(forms.Form):
_("This record is not for this transaction."),
code="not_belong")
def _validate_account_type(self):
def _validate_account_type(self) -> None:
"""Validates whether the account is a correct debit or credit account.
Raises:
@ -145,7 +146,7 @@ class RecordForm(forms.Form):
self.add_error("account", error)
raise error
def _validate_is_credit(self):
def _validate_is_credit(self) -> None:
"""Validates whether debit and credit records are submitted correctly
as corresponding debit and credit records.
@ -250,7 +251,7 @@ class TransactionForm(forms.Form):
if errors:
raise forms.ValidationError(errors)
def _validate_has_debit_records(self):
def _validate_has_debit_records(self) -> None:
"""Validates whether there is any debit record.
Raises:
@ -268,7 +269,7 @@ class TransactionForm(forms.Form):
_("Please fill in accounting records."),
code="has_debit_records")
def _validate_has_credit_records(self):
def _validate_has_credit_records(self) -> None:
"""Validates whether there is any credit record.
Raises:
@ -286,7 +287,7 @@ class TransactionForm(forms.Form):
_("Please fill in accounting records."),
code="has_debit_records")
def _validate_balance(self):
def _validate_balance(self) -> None:
"""Validates whether the total amount of debit and credit records are
consistent.
@ -301,20 +302,20 @@ class TransactionForm(forms.Form):
_("The total of the debit and credit amounts are inconsistent."),
code="balance")
def is_valid(self):
if not super(TransactionForm, self).is_valid():
def is_valid(self) -> bool:
if not super().is_valid():
return False
for x in self.debit_records + self.credit_records:
if not x.is_valid():
return False
return True
def balance_error(self):
def balance_error(self) -> Optional[str]:
"""Returns the error message when the transaction is imbalanced.
Returns:
str: The error message when the transaction is imbalanced, or
None otherwise.
The error message when the transaction is imbalanced, or None
otherwise.
"""
errors = [x for x in self.non_field_errors().data
if x.code == "balance"]
@ -322,20 +323,20 @@ class TransactionForm(forms.Form):
return errors[0].message
return None
def debit_total(self):
def debit_total(self) -> int:
"""Returns the total amount of the debit records.
Returns:
int: The total amount of the credit records.
The total amount of the credit records.
"""
return sum([int(x.data["amount"]) for x in self.debit_records
if "amount" in x.data and "amount" not in x.errors])
def credit_total(self):
def credit_total(self) -> int:
"""Returns the total amount of the credit records.
Returns:
int: The total amount of the credit records.
The total amount of the credit records.
"""
return sum([int(x.data["amount"]) for x in self.credit_records
if "amount" in x.data and "amount" not in x.errors])
@ -366,7 +367,8 @@ class AccountForm(forms.Form):
self.account = None
@property
def parent(self):
def parent(self) -> Optional[Account]:
"""The parent account, or None if this is the topmost account."""
code = self["code"].value()
if code is None or len(code) < 2:
return None
@ -391,7 +393,7 @@ class AccountForm(forms.Form):
if errors:
raise forms.ValidationError(errors)
def _validate_code_not_under_myself(self):
def _validate_code_not_under_myself(self) -> None:
"""Validates whether the code is under itself.
Raises:
@ -411,7 +413,7 @@ class AccountForm(forms.Form):
self.add_error("code", error)
raise error
def _validate_code_unique(self):
def _validate_code_unique(self) -> None:
"""Validates whether the code is unique.
Raises:
@ -432,7 +434,7 @@ class AccountForm(forms.Form):
self.add_error("code", error)
raise error
def _validate_code_parent_exists(self):
def _validate_code_parent_exists(self) -> None:
"""Validates whether the parent account exists.
Raises:
@ -452,7 +454,7 @@ class AccountForm(forms.Form):
raise error
return
def _validate_code_descendant_code_size(self):
def _validate_code_descendant_code_size(self) -> None:
"""Validates whether the codes of the descendants will be too long.
Raises:
@ -467,9 +469,9 @@ class AccountForm(forms.Form):
~Q(pk=self.account.pk))\
.aggregate(max_len=Max(Length("code")))["max_len"]
if cur_max_len is None:
return True
return
new_max_len = cur_max_len - len(self.account.code)\
+ len(self.data["code"])
+ len(self.data["code"])
if new_max_len <= 5:
return
error = forms.ValidationError(

View File

@ -62,7 +62,7 @@ class Command(BaseCommand):
user.save()
p = Populator(user)
p.add_accounts((
p.add_accounts([
(1, "資產", "assets", "资产"),
(2, "負債", "liabilities", "负债"),
(3, "業主權益", "owners equity", "业主权益"),
@ -124,7 +124,7 @@ class Command(BaseCommand):
"管理及总务费用"),
(6272, "伙食費", "meal (expenses)", "伙食费"),
(6273, "職工福利", "employee benefits/welfare", "职工福利"),
))
])
income = random.randint(40000, 50000)
pension = 882 if income <= 40100\
@ -143,49 +143,49 @@ class Command(BaseCommand):
month = (date.replace(day=1) - timezone.timedelta(days=1)).month
p.add_transfer_transaction(
date,
((1113, "薪資轉帳", savings),
[(1113, "薪資轉帳", savings),
(1314, F"勞保{month}", pension),
(6262, F"健保{month}", insurance),
(1255, "代扣所得稅", tax)),
((4611, F"{month}月份薪水", income),))
(1255, "代扣所得稅", tax)],
[(4611, F"{month}月份薪水", income)])
p.add_income_transaction(
-15,
((1113, "ATM提款", 2000),))
[(1113, "ATM提款", 2000)])
p.add_transfer_transaction(
-14,
((6254, "高鐵—台北→左營", 1490),),
((2141, "高鐵—台北→左營", 1490),))
[(6254, "高鐵—台北→左營", 1490)],
[(2141, "高鐵—台北→左營", 1490)])
p.add_transfer_transaction(
-14,
((6273, "電影—復仇者聯盟", 80),),
((2141, "電影—復仇者聯盟", 80),))
[(6273, "電影—復仇者聯盟", 80)],
[(2141, "電影—復仇者聯盟", 80)])
p.add_transfer_transaction(
-13,
((6273, "電影—2001太空漫遊", 80),),
((2141, "電影—2001太空漫遊", 80),))
[(6273, "電影—2001太空漫遊", 80)],
[(2141, "電影—2001太空漫遊", 80)])
p.add_transfer_transaction(
-11,
((2141, "電影—復仇者聯盟", 80),),
((1113, "電影—復仇者聯盟", 80),))
[(2141, "電影—復仇者聯盟", 80)],
[(1113, "電影—復仇者聯盟", 80)])
p.add_expense_transaction(
-13,
((6273, "公車—262—民生社區→頂溪捷運站", 30),))
[(6273, "公車—262—民生社區→頂溪捷運站", 30)])
p.add_expense_transaction(
-2,
((6272, "午餐—排骨飯", random.randint(40, 200)),
(6272, "飲料—紅茶", random.randint(40, 200))))
[(6272, "午餐—排骨飯", random.randint(40, 200)),
(6272, "飲料—紅茶", random.randint(40, 200))])
p.add_expense_transaction(
-1,
((6272, "午餐—牛肉麵", random.randint(40, 200)),
(6272, "飲料—紅茶", random.randint(40, 200))))
([(6272, "午餐—牛肉麵", random.randint(40, 200)),
(6272, "飲料—紅茶", random.randint(40, 200))]))
p.add_expense_transaction(
-1,
((6272, "午餐—排骨飯", random.randint(40, 200)),
(6272, "飲料—冬瓜茶", random.randint(40, 200))))
[(6272, "午餐—排骨飯", random.randint(40, 200)),
(6272, "飲料—冬瓜茶", random.randint(40, 200))])
p.add_expense_transaction(
0,
((6272, "午餐—雞腿飯", random.randint(40, 200)),
(6272, "飲料—咖啡", random.randint(40, 200))))
[(6272, "午餐—雞腿飯", random.randint(40, 200)),
(6272, "飲料—咖啡", random.randint(40, 200))])

View File

@ -18,6 +18,9 @@
"""The data models of the accounting application.
"""
import datetime
from typing import Dict, List, Optional
from dirtyfields import DirtyFieldsMixin
from django.conf import settings
from django.db import models, transaction
@ -83,51 +86,44 @@ class Account(DirtyFieldsMixin, models.Model):
db_table = "accounting_accounts"
@property
def title(self):
def title(self) -> str:
"""The title in the current language."""
return get_multi_lingual_attr(self, "title")
@title.setter
def title(self, value):
def title(self, value: str) -> None:
set_multi_lingual_attr(self, "title", value)
@property
def option_data(self):
def option_data(self) -> Dict[str, str]:
"""The data as an option."""
return {
"code": self.code,
"title": self.title,
}
@property
def is_parent_and_in_use(self):
"""Whether this is a parent account and is in use.
Returns:
bool: True if this is a parent account and is in use, or false
otherwise
"""
def is_parent_and_in_use(self) -> bool:
"""Whether this is a parent account and is in use."""
if self._is_parent_and_in_use is None:
self._is_parent_and_in_use = self.child_set.count() > 0\
and self.record_set.count() > 0
return self._is_parent_and_in_use
@is_parent_and_in_use.setter
def is_parent_and_in_use(self, value):
def is_parent_and_in_use(self, value: bool) -> None:
self._is_parent_and_in_use = value
@property
def is_in_use(self):
"""Whether this account is in use.
Returns:
bool: True if this account is in use, or false otherwise.
"""
def is_in_use(self) -> bool:
"""Whether this account is in use."""
if self._is_in_use is None:
self._is_in_use = self.child_set.count() > 0\
or self.record_set.count() > 0
return self._is_in_use
@is_in_use.setter
def is_in_use(self, value):
def is_in_use(self, value: bool) -> None:
self._is_in_use = value
@ -157,7 +153,7 @@ class Transaction(DirtyFieldsMixin, models.Model):
transaction."""
return self.date.__str__() + " #" + self.ord.__str__()
def get_absolute_url(self):
def get_absolute_url(self) -> str:
"""Returns the URL to view this transaction."""
if self.is_cash_expense:
return reverse(
@ -169,13 +165,13 @@ class Transaction(DirtyFieldsMixin, models.Model):
return reverse(
"accounting:transactions.detail", args=("transfer", self))
def is_dirty(self, check_relationship=False, check_m2m=None):
def is_dirty(self, check_relationship=False, check_m2m=None) -> bool:
"""Returns whether the data of this transaction is changed and need
to be saved into the database.
Returns:
bool: True if the data of this transaction is changed and need
to be saved into the database, or False otherwise.
True if the data of this transaction is changed and need to be
saved into the database, or False otherwise.
"""
if super().is_dirty(check_relationship=check_relationship,
check_m2m=check_m2m):
@ -189,8 +185,9 @@ class Transaction(DirtyFieldsMixin, models.Model):
return True
return False
def save(self, current_user=None, old_date=None, force_insert=False,
force_update=False, using=None, update_fields=None):
def save(self, current_user=None, old_date: datetime.date = None,
force_insert=False, force_update=False, using=None,
update_fields=None):
# When the date is changed, the orders of the transactions in the same
# day need to be reordered
txn_to_sort = []
@ -262,7 +259,7 @@ class Transaction(DirtyFieldsMixin, models.Model):
"""The records of the transaction.
Returns:
list[Record]: The records.
List[Record]: The records.
"""
if self._records is None:
self._records = list(self.record_set.all())
@ -278,22 +275,18 @@ class Transaction(DirtyFieldsMixin, models.Model):
"""The debit records of this transaction.
Returns:
list[Record]: The records.
List[Record]: The records.
"""
return [x for x in self.records if not x.is_credit]
def debit_total(self):
def debit_total(self) -> int:
"""The total amount of the debit records."""
return sum([x.amount for x in self.debit_records
if isinstance(x.amount, int)])
@property
def debit_summaries(self):
"""The summaries of the debit records.
Returns:
list[str]: The summaries of the debit records.
"""
def debit_summaries(self) -> List[str]:
"""The summaries of the debit records."""
return [x.account.title if x.summary is None else x.summary
for x in self.debit_records]
@ -302,36 +295,28 @@ class Transaction(DirtyFieldsMixin, models.Model):
"""The credit records of this transaction.
Returns:
list[Record]: The records.
List[Record]: The records.
"""
return [x for x in self.records if x.is_credit]
def credit_total(self):
def credit_total(self) -> int:
"""The total amount of the credit records."""
return sum([x.amount for x in self.credit_records
if isinstance(x.amount, int)])
@property
def credit_summaries(self):
"""The summaries of the credit records.
Returns:
list[str]: The summaries of the credit records.
"""
def credit_summaries(self) -> List[str]:
"""The summaries of the credit records."""
return [x.account.title if x.summary is None else x.summary
for x in self.credit_records]
@property
def amount(self):
"""The amount of this transaction.
Returns:
int: The amount of this transaction.
"""
def amount(self) -> int:
"""The amount of this transaction."""
return self.debit_total()
@property
def is_balanced(self):
def is_balanced(self) -> bool:
"""Whether the sum of the amounts of the debit records is the
same as the sum of the amounts of the credit records. """
if self._is_balanced is None:
@ -341,16 +326,16 @@ class Transaction(DirtyFieldsMixin, models.Model):
return self._is_balanced
@is_balanced.setter
def is_balanced(self, value):
def is_balanced(self, value: bool) -> None:
self._is_balanced = value
def has_many_same_day(self):
def has_many_same_day(self) -> bool:
"""whether there are more than one transactions at this day,
so that the user can sort their orders. """
return Transaction.objects.filter(date=self.date).count() > 1
@property
def has_order_hole(self):
def has_order_hole(self) -> bool:
"""Whether the order of the transactions on this day is not
1, 2, 3, 4, 5..., and should be reordered. """
if self._has_order_hole is None:
@ -369,11 +354,11 @@ class Transaction(DirtyFieldsMixin, models.Model):
return self._has_order_hole
@has_order_hole.setter
def has_order_hole(self, value):
def has_order_hole(self, value: bool) -> None:
self._has_order_hole = value
@property
def is_cash_income(self):
def is_cash_income(self) -> bool:
"""Whether this transaction is a cash income transaction."""
debit_records = self.debit_records
return (len(debit_records) == 1
@ -381,7 +366,7 @@ class Transaction(DirtyFieldsMixin, models.Model):
and debit_records[0].summary is None)
@property
def is_cash_expense(self):
def is_cash_expense(self) -> bool:
"""Whether this transaction is a cash expense transaction."""
credit_records = self.credit_records
return (len(credit_records) == 1
@ -389,7 +374,7 @@ class Transaction(DirtyFieldsMixin, models.Model):
and credit_records[0].summary is None)
@property
def type(self):
def type(self) -> str:
"""The transaction type."""
if self.is_cash_expense:
return "expense"
@ -444,40 +429,40 @@ class Record(DirtyFieldsMixin, models.Model):
db_table = "accounting_records"
@property
def debit_amount(self):
def debit_amount(self) -> Optional[int]:
"""The debit amount of this accounting record."""
if self._debit_amount is None:
self._debit_amount = self.amount if not self.is_credit else None
return self._debit_amount
@debit_amount.setter
def debit_amount(self, value):
def debit_amount(self, value: Optional[int]) -> None:
self._debit_amount = value
@property
def credit_amount(self):
def credit_amount(self) -> Optional[int]:
"""The credit amount of this accounting record."""
if self._credit_amount is None:
self._credit_amount = self.amount if self.is_credit else None
return self._credit_amount
@credit_amount.setter
def credit_amount(self, value):
def credit_amount(self, value: Optional[int]):
self._credit_amount = value
@property
def is_balanced(self):
def is_balanced(self) -> bool:
"""Whether the transaction of this record is balanced. """
if self._is_balanced is None:
self._is_balanced = self.transaction.is_balanced
return self._is_balanced
@is_balanced.setter
def is_balanced(self, value):
def is_balanced(self, value: bool) -> None:
self._is_balanced = value
@property
def has_order_hole(self):
def has_order_hole(self) -> bool:
"""Whether the order of the transactions on this day is not
1, 2, 3, 4, 5..., and should be reordered. """
if self._has_order_hole is None:
@ -485,5 +470,5 @@ class Record(DirtyFieldsMixin, models.Model):
return self._has_order_hole
@has_order_hole.setter
def has_order_hole(self, value):
def has_order_hole(self, value: bool) -> None:
self._has_order_hole = value

View File

@ -19,6 +19,7 @@
"""
import re
from typing import Union, Optional
from django import template
@ -30,7 +31,7 @@ register = template.Library()
@register.filter
def accounting_amount(value):
def accounting_amount(value: Union[str, int]) -> str:
if value is None:
return ""
if value == 0:
@ -47,13 +48,15 @@ def accounting_amount(value):
@register.simple_tag
def report_url(cash_account, ledger_account, period):
def report_url(cash_account: Optional[Account],
ledger_account: Optional[Account],
period: Optional[Period]) -> ReportUrl:
"""Returns accounting report URL helper.
Args:
cash_account (Account): The current cash account.
ledger_account (Account): The current ledger account.
period (Period): The period.
cash_account: The current cash account.
ledger_account: The current ledger account.
period: The period.
Returns:
ReportUrl: The accounting report URL helper.

View File

@ -21,6 +21,7 @@
import datetime
import json
import re
from typing import Union, Tuple, List, Optional, Iterable, Mapping, Dict
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
@ -37,6 +38,9 @@ from mia_core.utils import new_pk
from .forms import TransactionForm, RecordForm
from .models import Account, Transaction, Record
AccountData = Tuple[Union[str, int], str, str, str]
RecordData = Tuple[Union[str, int], Optional[str], int]
DEFAULT_CASH_ACCOUNT = "1111"
CASH_SHORTCUT_ACCOUNTS = ["0", "1111"]
DEFAULT_LEDGER_ACCOUNT = "1111"
@ -48,12 +52,12 @@ class MonthlySummary:
"""A summary record.
Args:
month (datetime.date): The month.
label (str): The text label.
credit (int): The credit amount.
debit (int): The debit amount.
balance (int): The balance.
cumulative_balance (int): The cumulative balance.
month: The month.
label: The text label.
credit: The credit amount.
debit: The debit amount.
balance: The balance.
cumulative_balance: The cumulative balance.
Attributes:
month (datetime.date): The month.
@ -64,8 +68,9 @@ class MonthlySummary:
cumulative_balance (int): The cumulative balance.
"""
def __init__(self, month=None, label=None, credit=None, debit=None,
balance=None, cumulative_balance=None):
def __init__(self, month: datetime.date = None, label: str = None,
credit: int = None, debit: int = None, balance: int = None,
cumulative_balance: int = None):
self.month = month
self.label = label
self.credit = credit
@ -80,43 +85,42 @@ class ReportUrl:
"""The URL of the accounting reports.
Args:
cash (Account): The currently-specified account of the
cash: The currently-specified account of the
cash account or cash summary.
ledger (Account): The currently-specified account of the
ledger: The currently-specified account of the
ledger or leger summary.
period (Period): The currently-specified period.
period: The currently-specified period.
"""
def __init__(self, cash=None, ledger=None, period=None):
def __init__(self, cash: Account = None, ledger: Account = None,
period: Period = None):
self._period = Period() if period is None else period
self._cash = get_default_cash_account() if cash is None else cash
self._ledger = get_default_ledger_account()\
if ledger is None else ledger
def cash(self):
return reverse(
"accounting:cash", args=(self._cash, self._period))
def cash(self) -> str:
return reverse("accounting:cash", args=(self._cash, self._period))
def cash_summary(self):
def cash_summary(self) -> str:
return reverse("accounting:cash-summary", args=(self._cash,))
def ledger(self):
return reverse(
"accounting:ledger", args=(self._ledger, self._period))
def ledger(self) -> str:
return reverse("accounting:ledger", args=(self._ledger, self._period))
def ledger_summary(self):
def ledger_summary(self) -> str:
return reverse("accounting:ledger-summary", args=(self._ledger,))
def journal(self):
def journal(self) -> str:
return reverse("accounting:journal", args=(self._period,))
def trial_balance(self):
def trial_balance(self) -> str:
return reverse("accounting:trial-balance", args=(self._period,))
def income_statement(self):
def income_statement(self) -> str:
return reverse("accounting:income-statement", args=(self._period,))
def balance_sheet(self):
def balance_sheet(self) -> str:
return reverse("accounting:balance-sheet", args=(self._period,))
@ -124,7 +128,7 @@ class Populator:
"""The helper to populate the accounting data.
Args:
user (User): The user in action.
user: The user in action.
Attributes:
user (User): The user in action.
@ -133,7 +137,7 @@ class Populator:
def __init__(self, user):
self.user = user
def add_accounts(self, accounts):
def add_accounts(self, accounts: List[AccountData]) -> None:
"""Adds accounts.
Args:
@ -152,16 +156,16 @@ class Populator:
title_zh_hans=data[3],
created_by=self.user, updated_by=self.user).save()
def add_transfer_transaction(self, date, debit, credit):
def add_transfer_transaction(self, date: Union[datetime.date, int],
debit: List[RecordData],
credit: List[RecordData]) -> None:
"""Adds a transfer transaction.
Args:
date (datetime.date|int): The date, or the number of days from
date: The date, or the number of days from
today.
debit (tuple[tuple[any]]): Tuples of (account, summary, amount)
of the debit records.
credit (tuple[tuple[any]]): Tuples of (account, summary, amount)
of the credit records.
debit: Tuples of (account, summary, amount) of the debit records.
credit: Tuples of (account, summary, amount) of the credit records.
"""
if isinstance(date, int):
date = timezone.localdate() + timezone.timedelta(days=date)
@ -196,38 +200,36 @@ class Populator:
updated_by=self.user)
order = order + 1
def add_income_transaction(self, date, credit):
def add_income_transaction(self, date: Union[datetime.date, int],
credit: List[RecordData]) -> None:
"""Adds a cash income transaction.
Args:
date (datetime.date|int): The date, or the number of days from
today.
credit (tuple[tuple[any]]): Tuples of (account, summary, amount) of
the credit records.
date: The date, or the number of days from today.
credit: Tuples of (account, summary, amount) of the credit records.
"""
amount = sum([x[2] for x in credit])
self.add_transfer_transaction(
date, ((Account.CASH, None, amount),), credit)
date, [(Account.CASH, None, amount)], credit)
def add_expense_transaction(self, date, debit):
def add_expense_transaction(self, date: Union[datetime.date, int],
debit: List[RecordData]) -> None:
"""Adds a cash income transaction.
Args:
date (datetime.date|int): The date, or the number of days from
today.
debit (tuple[tuple[any]]): Tuples of (account, summary, amount) of
the debit records.
date: The date, or the number of days from today.
debit: Tuples of (account, summary, amount) of the debit records.
"""
amount = sum([x[2] for x in debit])
self.add_transfer_transaction(
date, debit, ((Account.CASH, None, amount),))
date, debit, [(Account.CASH, None, amount)])
def get_cash_accounts():
def get_cash_accounts() -> List[Account]:
"""Returns the cash accounts.
Returns:
list[Account]: The cash accounts.
The cash accounts.
"""
accounts = list(
Account.objects
@ -247,11 +249,11 @@ def get_cash_accounts():
return accounts
def get_default_cash_account():
def get_default_cash_account() -> Account:
"""Returns the default cash account.
Returns:
Account: The default cash account.
The default cash account.
"""
try:
code = settings.ACCOUNTING["DEFAULT_CASH_ACCOUNT"]
@ -274,11 +276,11 @@ def get_default_cash_account():
return Account(code="0", title=_("current assets and liabilities"))
def get_cash_shortcut_accounts():
def get_cash_shortcut_accounts() -> List[str]:
"""Returns the codes of the shortcut cash accounts.
Returns:
list[str]: The codes of the shortcut cash accounts.
The codes of the shortcut cash accounts.
"""
try:
accounts = settings.ACCOUNTING["CASH_SHORTCUT_ACCOUNTS"]
@ -293,11 +295,11 @@ def get_cash_shortcut_accounts():
return accounts
def get_ledger_accounts():
def get_ledger_accounts() -> List[Account]:
"""Returns the accounts for the ledger.
Returns:
list[Account]: The accounts for the ledger.
The accounts for the ledger.
"""
"""
For SQL one-liner:
@ -315,18 +317,19 @@ SELECT s.*
"""
codes = {}
for code in [x.code for x in Account.objects
.annotate(Count("record")).filter(record__count__gt=0)]:
.annotate(Count("record"))
.filter(record__count__gt=0)]:
while len(code) > 0:
codes[code] = True
code = code[:-1]
return Account.objects.filter(code__in=codes).order_by("code")
def get_default_ledger_account():
def get_default_ledger_account() -> Optional[Account]:
"""Returns the default ledger account.
Returns:
Account: The default ledger account.
The default ledger account.
"""
try:
code = settings.ACCOUNTING["DEFAULT_CASH_ACCOUNT"]
@ -347,12 +350,12 @@ def get_default_ledger_account():
return None
def find_imbalanced(records):
def find_imbalanced(records: Iterable[Record]) -> None:
""""Finds the records with imbalanced transactions, and sets their
is_balanced attribute.
Args:
records (list[Record]): The accounting records.
records: The accounting records.
"""
imbalanced = [x.pk for x in Transaction.objects
.annotate(
@ -364,13 +367,13 @@ def find_imbalanced(records):
record.is_balanced = record.transaction.pk not in imbalanced
def find_order_holes(records):
def find_order_holes(records: Iterable[Record]) -> None:
""""Finds whether the order of the transactions on this day is not
1, 2, 3, 4, 5..., and should be reordered, and sets their
has_order_holes attributes.
Args:
records (list[Record]): The accounting records.
records: The accounting records.
"""
holes = [x["date"] for x in Transaction.objects
.values("date")
@ -387,12 +390,12 @@ def find_order_holes(records):
and record.transaction.date in holes
def find_payable_records(account, records):
def find_payable_records(account: Account, records: Iterable[Record]) -> None:
"""Finds and sets the whether the payable record is paid.
Args:
account (Account): The current ledger account.
records (list[Record]): The accounting records.
account: The current ledger account.
records: The accounting records.
"""
try:
payable_accounts = settings.ACCOUNTING["PAYABLE_ACCOUNTS"]
@ -418,16 +421,17 @@ def find_payable_records(account, records):
keys = ["%s-%s" % (x["account__code"], x["summary"]) for x in rows]
for x in [x for x in records
if x.pk is not None
and F"{x.account.code}-{x.summary}" in keys]:
and F"{x.account.code}-{x.summary}" in keys]:
x.is_payable = True
def find_existing_equipments(account, records):
def find_existing_equipments(account: Account,
records: Iterable[Record]) -> None:
"""Finds and sets the equipments that still exist.
Args:
account (Account): The current ledger account.
records (list[Record]): The accounting records.
account: The current ledger account.
records: The accounting records.
"""
try:
equipment_accounts = settings.ACCOUNTING["EQUIPMENT_ACCOUNTS"]
@ -453,17 +457,17 @@ def find_existing_equipments(account, records):
keys = ["%s-%s" % (x["account__code"], x["summary"]) for x in rows]
for x in [x for x in records
if x.pk is not None
and F"{x.account.code}-{x.summary}" in keys]:
and F"{x.account.code}-{x.summary}" in keys]:
x.is_existing_equipment = True
def get_summary_categories():
def get_summary_categories() -> str:
"""Finds and returns the summary categories and their corresponding account
hints.
hints as JSON.
Returns:
dict[str,str]: The summary categories and their account hints, by
their record types and category types.
The summary categories and their account hints, by their record types
and category types.
"""
rows = Record.objects\
.filter(Q(summary__contains=""),
@ -507,14 +511,15 @@ def get_summary_categories():
return json.dumps(categories)
def fill_txn_from_post(txn_type, txn, post):
def fill_txn_from_post(txn_type: str, txn: Transaction,
post: Mapping[str, str]) -> None:
"""Fills the transaction from the POSTed data. The POSTed data must be
validated and clean at this moment.
Args:
txn_type (str): The transaction type.
txn (Transaction): The transaction.
post (dict): The POSTed data.
txn_type: The transaction type.
txn: The transaction.
post: The POSTed data.
"""
m = re.match("^([0-9]{4})-([0-9]{2})-([0-9]{2})$", post["date"])
txn.date = datetime.date(
@ -565,12 +570,12 @@ def fill_txn_from_post(txn_type, txn, post):
txn.records = records
def sort_post_txn_records(post):
def sort_post_txn_records(post: Dict[str, str]) -> None:
"""Sorts the records in the form by their specified order, so that the
form can be used to populate the data to return to the user.
Args:
post (dict): The POSTed form.
post: The POSTed form.
"""
# Collects the available record numbers
record_no = {
@ -618,15 +623,16 @@ def sort_post_txn_records(post):
post[key] = new_post[key]
def make_txn_form_from_model(txn_type, txn):
def make_txn_form_from_model(txn_type: str,
txn: Transaction) -> TransactionForm:
"""Converts a transaction data model to a transaction form.
Args:
txn_type (str): The transaction type.
txn (Transaction): The transaction data model.
txn_type: The transaction type.
txn: The transaction data model.
Returns:
TransactionForm: The transaction form.
The transaction form.
"""
form = TransactionForm(
{x: str(getattr(txn, x)) for x in ["date", "notes"]
@ -658,7 +664,8 @@ def make_txn_form_from_model(txn_type, txn):
return form
def _find_max_record_no(txn_type, post):
def _find_max_record_no(txn_type: str,
post: Mapping[str, str]) -> Dict[str, int]:
"""Finds the max debit and record numbers from the POSTed form.
Args:

View File

@ -25,11 +25,11 @@ from django.utils.translation import gettext as _
from .models import Account, Record
def validate_record_id(value):
def validate_record_id(value: str) -> None:
"""Validates the record ID.
Args:
value (str): The record ID.
value: The record ID.
Raises:
ValidationError: When the validation fails.
@ -41,11 +41,11 @@ def validate_record_id(value):
code="not_exist")
def validate_record_account_code(value):
def validate_record_account_code(value: str) -> None:
"""Validates an account code.
Args:
value (str): The account code.
value: The account code.
Raises:
ValidationError: When the validation fails.

View File

@ -18,6 +18,7 @@
"""The view controllers of the accounting application.
"""
import datetime
import json
import re
@ -26,7 +27,8 @@ from django.db import transaction
from django.db.models import Sum, Case, When, F, Q, Count, BooleanField, \
ExpressionWrapper
from django.db.models.functions import TruncMonth, Coalesce
from django.http import JsonResponse, HttpResponseRedirect, Http404
from django.http import JsonResponse, HttpResponseRedirect, Http404, \
HttpRequest, HttpResponse
from django.shortcuts import render, redirect
from django.template.loader import render_to_string
from django.urls import reverse
@ -62,16 +64,17 @@ class CashDefaultView(RedirectView):
@require_GET
@login_required
def cash(request, account, period):
def cash(request: HttpRequest, account: Account,
period: Period) -> HttpResponse:
"""The cash account.
Args:
request (HttpRequest) The request.
account (Account): The account.
period (Period): The period.
request: The request.
account: The account.
period: The period.
Returns:
HttpResponse: The response.
The response.
"""
# The accounting records
if account.code == "0":
@ -180,15 +183,15 @@ class CashSummaryDefaultView(RedirectView):
@require_GET
@login_required
def cash_summary(request, account):
def cash_summary(request: HttpRequest, account: Account) -> HttpResponse:
"""The cash account summary.
Args:
request (HttpRequest) The request.
account (Account): The account.
request: The request.
account: The account.
Returns:
HttpResponse: The response.
The response.
"""
# The account
accounts = utils.get_cash_accounts()
@ -278,16 +281,17 @@ class LedgerDefaultView(RedirectView):
@require_GET
@login_required
def ledger(request, account, period):
def ledger(request: HttpRequest, account: Account,
period: Period) -> HttpResponse:
"""The ledger.
Args:
request (HttpRequest) The request.
account (Account): The account.
period (Period): The period.
request: The request.
account: The account.
period: The period.
Returns:
HttpResponse: The response.
The response.
"""
# The accounting records
records = list(
@ -354,15 +358,15 @@ class LedgerSummaryDefaultView(RedirectView):
@require_GET
@login_required
def ledger_summary(request, account):
def ledger_summary(request: HttpRequest, account: Account) -> HttpResponse:
"""The ledger summary report.
Args:
request (HttpRequest) The request.
account (Account): The account.
request: The request.
account: The account.
Returns:
HttpResponse: The response.
The response.
"""
# The month summaries
months = [utils.MonthlySummary(**x) for x in Record.objects
@ -416,15 +420,15 @@ class JournalDefaultView(RedirectView):
@require_GET
@login_required
def journal(request, period):
def journal(request: HttpRequest, period: Period) -> HttpResponse:
"""The journal.
Args:
request (HttpRequest) The request.
period (Period): The period.
request: The request.
period: The period.
Returns:
HttpResponse: The response.
The response.
"""
# The accounting records
records = Record.objects \
@ -498,15 +502,15 @@ class TrialBalanceDefaultView(RedirectView):
@require_GET
@login_required
def trial_balance(request, period):
def trial_balance(request: HttpRequest, period: Period) -> HttpResponse:
"""The trial balance.
Args:
request (HttpRequest) The request.
period (Period): The period.
request: The request.
period: The period.
Returns:
HttpResponse: The response.
The response.
"""
# The accounts
nominal = list(
@ -601,15 +605,15 @@ class IncomeStatementDefaultView(RedirectView):
@require_GET
@login_required
def income_statement(request, period):
def income_statement(request: HttpRequest, period: Period) -> HttpResponse:
"""The income statement.
Args:
request (HttpRequest) The request.
period (Period): The period.
request: The request.
period: The period.
Returns:
HttpResponse: The response.
The response.
"""
# The accounts
accounts = list(
@ -676,15 +680,15 @@ class BalanceSheetDefaultView(RedirectView):
@require_GET
@login_required
def balance_sheet(request, period):
def balance_sheet(request: HttpRequest, period: Period) -> HttpResponse:
"""The balance sheet.
Args:
request (HttpRequest) The request.
period (Period): The period.
request: The request.
period: The period.
Returns:
HttpResponse: The response.
The response.
"""
# The accounts
accounts = list(
@ -764,14 +768,14 @@ def balance_sheet(request, period):
@require_GET
@login_required
def search(request):
def search(request: HttpRequest) -> HttpResponse:
"""The search.
Args:
request (HttpRequest) The request.
request: The request.
Returns:
HttpResponse: The response.
The response.
"""
# The accounting records
query = request.GET.get("q")
@ -809,16 +813,17 @@ class TransactionView(DetailView):
@require_GET
@login_required
def txn_form(request, txn_type, txn=None):
def txn_form(request: HttpRequest, txn_type: str,
txn: Transaction = None) -> HttpResponse:
"""The view to edit an accounting transaction.
Args:
request (HttpRequest): The request.
txn_type (str): The transaction type.
txn (Transaction): The transaction.
request: The request.
txn_type: The transaction type.
txn: The transaction.
Returns:
HttpResponse: The response.
The response.
"""
previous_post = stored_post.get_previous_post(request)
if previous_post is not None:
@ -847,16 +852,17 @@ def txn_form(request, txn_type, txn=None):
@require_POST
@login_required
def txn_store(request, txn_type, txn=None):
def txn_store(request: HttpRequest, txn_type: str,
txn: Transaction = None) -> HttpResponseRedirect:
"""The view to store an accounting transaction.
Args:
request (HttpRequest): The request.
txn_type (str): The transaction type.
txn (Transaction): The transaction.
request: The request.
txn_type: The transaction type.
txn: The transaction.
Returns:
HttpResponse: The response.
The response.
"""
post = request.POST.dict()
strip_post(post)
@ -900,15 +906,15 @@ class TransactionDeleteView(DeleteView):
@login_required
def txn_sort(request, date):
def txn_sort(request: HttpRequest, date: datetime.date) -> HttpResponse:
"""The view for the form to sort the transactions in a same day.
Args:
request (HttpRequest): The request.
date (datetime.date): The day.
request: The request.
date: The day.
Returns:
HttpResponse: The response.
The response.
Raises:
Http404: When there are less than two transactions in this day.
@ -977,15 +983,16 @@ class AccountView(DetailView):
@require_GET
@login_required
def account_form(request, account=None):
def account_form(request: HttpRequest,
account: Account = None) -> HttpResponse:
"""The view to edit an accounting transaction.
Args:
request (HttpRequest): The request.
account (Account): The account.
request: The request.
account: The account.
Returns:
HttpResponse: The response.
The response.
"""
previous_post = stored_post.get_previous_post(request)
if previous_post is not None:
@ -1005,15 +1012,16 @@ def account_form(request, account=None):
@require_POST
@login_required
def account_store(request, account=None):
def account_store(request: HttpRequest,
account: Account = None) -> HttpResponseRedirect:
"""The view to store an account.
Args:
request (HttpRequest): The request.
account (Account): The account.
request: The request.
account: The account.
Returns:
HttpResponseRedirect: The response.
The response.
"""
post = request.POST.dict()
strip_post(post)
@ -1040,7 +1048,8 @@ def account_store(request, account=None):
@require_POST
@login_required
def account_delete(request, account):
def account_delete(request: HttpRequest,
account: Account) -> HttpResponseRedirect:
"""The view to delete an account.
Args:
@ -1062,28 +1071,28 @@ def account_delete(request, account):
@require_GET
@login_required
def api_account_list(request):
def api_account_list(request: HttpRequest) -> JsonResponse:
"""The API view to return all the accounts.
Args:
request (HttpRequest): The request.
request: The request.
Returns:
JsonResponse: The response.
The response.
"""
return JsonResponse({x.code: x.title for x in Account.objects.all()})
@require_GET
@login_required
def api_account_options(request):
def api_account_options(request: HttpRequest) -> JsonResponse:
"""The API view to return the account options.
Args:
request (HttpRequest): The request.
request: The request.
Returns:
JsonResponse: The response.
The response.
"""
accounts = Account.objects\
.annotate(children_count=Count("child_set"))\