Added the pseudo account for the income and expenses log to query the income and expenses log of the current assets and liabilities.

This commit is contained in:
依瑪貓 2023-03-08 00:27:40 +08:00
parent ede1160943
commit 1eed16b732
6 changed files with 158 additions and 25 deletions

View File

@ -27,8 +27,9 @@ def init_app(app: Flask, bp: Blueprint) -> None:
:param bp: The blueprint of the accounting application. :param bp: The blueprint of the accounting application.
:return: None. :return: None.
""" """
from .converters import PeriodConverter from .converters import PeriodConverter, IncomeExpensesAccountConverter
app.url_map.converters["period"] = PeriodConverter app.url_map.converters["period"] = PeriodConverter
app.url_map.converters["ioAccount"] = IncomeExpensesAccountConverter
from .views import bp as report_bp from .views import bp as report_bp
bp.register_blueprint(report_bp, url_prefix="/reports") bp.register_blueprint(report_bp, url_prefix="/reports")

View File

@ -17,9 +17,13 @@
"""The path converters for the report management. """The path converters for the report management.
""" """
import re
from flask import abort from flask import abort
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from accounting.models import Account
from .income_expense_account import IncomeExpensesAccount
from .period import Period from .period import Period
@ -45,3 +49,31 @@ class PeriodConverter(BaseConverter):
:return: Its specification. :return: Its specification.
""" """
return value.spec return value.spec
class IncomeExpensesAccountConverter(BaseConverter):
"""The supplier converter to convert the income and expenses pseudo account
code from and to the corresponding pseudo account in the routes."""
def to_python(self, value: str) -> IncomeExpensesAccount:
"""Converts an account code to an account.
:param value: The account code.
:return: The corresponding account.
"""
if value == IncomeExpensesAccount.CURRENT_AL_CODE:
return IncomeExpensesAccount.current_assets_and_liabilities()
if not re.match("^[12][12]", value):
abort(404)
account: Account | None = Account.find_by_code(value)
if account is None:
abort(404)
return IncomeExpensesAccount(account)
def to_url(self, value: IncomeExpensesAccount) -> str:
"""Converts an account to account code.
:param value: The account.
:return: Its code.
"""
return value.code

View File

@ -0,0 +1,70 @@
# The Mia! Accounting Flask Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The pseudo account for the income and expenses log.
"""
import typing as t
from accounting.locale import gettext
from accounting.models import Account
class IncomeExpensesAccount:
"""The pseudo account for the income and expenses log."""
CURRENT_AL_CODE: str = "0000-000"
"""The account code for the current assets and liabilities."""
def __init__(self, account: Account | None = None):
"""Constructs the pseudo account for the income and expenses log.
:param account: The actual account.
"""
self.account: Account | None = None
self.id: int | None = None
"""The ID."""
self.code: str | None = None
"""The code."""
self.title: str | None = None
"""The title."""
self.str: str = ""
"""The string representation of the account."""
if account is not None:
self.account = account
self.id = account.id
self.code = account.code
self.title = account.title
self.str = str(account)
def __str__(self) -> str:
"""Returns the string representation of the account.
:return: The string representation of the account.
"""
return self.str
@classmethod
def current_assets_and_liabilities(cls) -> t.Self:
"""Returns the pseudo account for current assets and liabilities.
:return: The pseudo account for current assets and liabilities.
"""
account: cls = cls()
account.id = 0
account.code = cls.CURRENT_AL_CODE
account.title = gettext("current assets and liabilities")
account.str = account.title
return account

View File

@ -26,6 +26,7 @@ from flask import url_for, render_template, Response
from accounting import db from accounting import db
from accounting.locale import gettext from accounting.locale import gettext
from accounting.models import Currency, Account, Transaction, JournalEntry from accounting.models import Currency, Account, Transaction, JournalEntry
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import Period from accounting.report.period import Period
from accounting.utils.pagination import Pagination from accounting.utils.pagination import Pagination
from .utils.csv_export import BaseCSVRow, csv_download from .utils.csv_export import BaseCSVRow, csv_download
@ -76,7 +77,8 @@ class Entry:
class EntryCollector: class EntryCollector:
"""The income and expenses log entry collector.""" """The income and expenses log entry collector."""
def __init__(self, currency: Currency, account: Account, period: Period): def __init__(self, currency: Currency, account: IncomeExpensesAccount,
period: Period):
"""Constructs the income and expenses log entry collector. """Constructs the income and expenses log entry collector.
:param currency: The currency. :param currency: The currency.
@ -85,7 +87,7 @@ class EntryCollector:
""" """
self.__currency: Currency = currency self.__currency: Currency = currency
"""The currency.""" """The currency."""
self.__account: Account = account self.__account: IncomeExpensesAccount = account
"""The account.""" """The account."""
self.__period: Period = period self.__period: Period = period
"""The period""" """The period"""
@ -111,9 +113,10 @@ class EntryCollector:
balance_func: sa.Function = sa.func.sum(sa.case( balance_func: sa.Function = sa.func.sum(sa.case(
(JournalEntry.is_debit, JournalEntry.amount), (JournalEntry.is_debit, JournalEntry.amount),
else_=-JournalEntry.amount)) else_=-JournalEntry.amount))
select: sa.Select = sa.Select(balance_func).join(Transaction)\ select: sa.Select = sa.Select(balance_func)\
.join(Transaction).join(Account)\
.filter(JournalEntry.currency_code == self.__currency.code, .filter(JournalEntry.currency_code == self.__currency.code,
JournalEntry.account_id == self.__account.id, self.__account_condition,
Transaction.date < self.__period.start) Transaction.date < self.__period.start)
balance: int | None = db.session.scalar(select) balance: int | None = db.session.scalar(select)
if balance is None: if balance is None:
@ -137,23 +140,32 @@ class EntryCollector:
""" """
conditions: list[sa.BinaryExpression] \ conditions: list[sa.BinaryExpression] \
= [JournalEntry.currency_code == self.__currency.code, = [JournalEntry.currency_code == self.__currency.code,
JournalEntry.account_id == self.__account.id] self.__account_condition]
if self.__period.start is not None: if self.__period.start is not None:
conditions.append(Transaction.date >= self.__period.start) conditions.append(Transaction.date >= self.__period.start)
if self.__period.end is not None: if self.__period.end is not None:
conditions.append(Transaction.date <= self.__period.end) conditions.append(Transaction.date <= self.__period.end)
txn_with_account: sa.Select = sa.Select(Transaction.id).\ txn_with_account: sa.Select = sa.Select(Transaction.id).\
join(JournalEntry).filter(*conditions) join(JournalEntry).join(Account).filter(*conditions)
return [Entry(x) return [Entry(x)
for x in JournalEntry.query.join(Transaction) for x in JournalEntry.query.join(Transaction).join(Account)
.filter(JournalEntry.transaction_id.in_(txn_with_account), .filter(JournalEntry.transaction_id.in_(txn_with_account),
JournalEntry.currency_code == self.__currency.code, JournalEntry.currency_code == self.__currency.code,
JournalEntry.account_id != self.__account.id) sa.not_(self.__account_condition))
.order_by(Transaction.date, .order_by(Transaction.date,
JournalEntry.is_debit, JournalEntry.is_debit,
JournalEntry.no)] JournalEntry.no)]
@property
def __account_condition(self) -> sa.BinaryExpression:
if self.__account.code == IncomeExpensesAccount.CURRENT_AL_CODE:
return sa.or_(Account.base_code.startswith("11"),
Account.base_code.startswith("12"),
Account.base_code.startswith("21"),
Account.base_code.startswith("22"))
return Account.id == self.__account.id
def __get_total_entry(self) -> Entry | None: def __get_total_entry(self) -> Entry | None:
"""Composes the total entry. """Composes the total entry.
@ -237,7 +249,7 @@ class IncomeExpensesPageParams(PageParams):
"""The HTML parameters of the income and expenses log.""" """The HTML parameters of the income and expenses log."""
def __init__(self, currency: Currency, def __init__(self, currency: Currency,
account: Account, account: IncomeExpensesAccount,
period: Period, period: Period,
has_data: bool, has_data: bool,
pagination: Pagination[Entry], pagination: Pagination[Entry],
@ -256,7 +268,7 @@ class IncomeExpensesPageParams(PageParams):
""" """
self.currency: Currency = currency self.currency: Currency = currency
"""The currency.""" """The currency."""
self.account: Account = account self.account: IncomeExpensesAccount = account
"""The account.""" """The account."""
self.period: Period = period self.period: Period = period
"""The period.""" """The period."""
@ -288,9 +300,14 @@ class IncomeExpensesPageParams(PageParams):
:return: The report chooser. :return: The report chooser.
""" """
if self.account.account is None:
return ReportChooser(ReportType.INCOME_EXPENSES, return ReportChooser(ReportType.INCOME_EXPENSES,
currency=self.currency, currency=self.currency,
account=self.account, account=Account.cash(),
period=self.period)
return ReportChooser(ReportType.INCOME_EXPENSES,
currency=self.currency,
account=self.account.account,
period=self.period) period=self.period)
@property @property
@ -320,7 +337,7 @@ class IncomeExpensesPageParams(PageParams):
:return: The account options. :return: The account options.
""" """
def get_url(account: Account): def get_url(account: IncomeExpensesAccount):
if self.period.is_default: if self.period.is_default:
return url_for("accounting.report.income-expenses-default", return url_for("accounting.report.income-expenses-default",
currency=self.currency, account=account) currency=self.currency, account=account)
@ -328,6 +345,11 @@ class IncomeExpensesPageParams(PageParams):
currency=self.currency, account=account, currency=self.currency, account=account,
period=self.period) period=self.period)
current_al: IncomeExpensesAccount \
= IncomeExpensesAccount.current_assets_and_liabilities()
options: list[OptionLink] \
= [OptionLink(str(current_al), get_url(current_al),
self.account.id == 0)]
in_use: sa.Select = sa.Select(JournalEntry.account_id)\ in_use: sa.Select = sa.Select(JournalEntry.account_id)\
.join(Account)\ .join(Account)\
.filter(JournalEntry.currency_code == self.currency.code, .filter(JournalEntry.currency_code == self.currency.code,
@ -336,9 +358,11 @@ class IncomeExpensesPageParams(PageParams):
Account.base_code.startswith("21"), Account.base_code.startswith("21"),
Account.base_code.startswith("22")))\ Account.base_code.startswith("22")))\
.group_by(JournalEntry.account_id) .group_by(JournalEntry.account_id)
return [OptionLink(str(x), get_url(x), x.id == self.account.id) options.extend([OptionLink(str(x), get_url(IncomeExpensesAccount(x)),
x.id == self.account.id)
for x in Account.query.filter(Account.id.in_(in_use)) for x in Account.query.filter(Account.id.in_(in_use))
.order_by(Account.base_code, Account.no).all()] .order_by(Account.base_code, Account.no).all()])
return options
def _populate_entries(entries: list[Entry]) -> None: def _populate_entries(entries: list[Entry]) -> None:
@ -366,7 +390,8 @@ def _populate_entries(entries: list[Entry]) -> None:
class IncomeExpenses: class IncomeExpenses:
"""The income and expenses log.""" """The income and expenses log."""
def __init__(self, currency: Currency, account: Account, period: Period): def __init__(self, currency: Currency, account: IncomeExpensesAccount,
period: Period):
"""Constructs an income and expenses log. """Constructs an income and expenses log.
:param currency: The currency. :param currency: The currency.
@ -375,7 +400,7 @@ class IncomeExpenses:
""" """
self.__currency: Currency = currency self.__currency: Currency = currency
"""The currency.""" """The currency."""
self.__account: Account = account self.__account: IncomeExpensesAccount = account
"""The account.""" """The account."""
self.__period: Period = period self.__period: Period = period
"""The period.""" """The period."""

View File

@ -27,6 +27,7 @@ from datetime import date
from flask import url_for from flask import url_for
from accounting.models import Currency, Account, Transaction from accounting.models import Currency, Account, Transaction
from accounting.report.income_expense_account import IncomeExpensesAccount
from accounting.report.period import YearPeriod, Period, ThisMonth, \ from accounting.report.period import YearPeriod, Period, ThisMonth, \
LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \ LastMonth, SinceLastMonth, ThisYear, LastYear, Today, Yesterday, \
TemplatePeriod TemplatePeriod
@ -143,11 +144,11 @@ class LedgerPeriodChooser(PeriodChooser):
class IncomeExpensesPeriodChooser(PeriodChooser): class IncomeExpensesPeriodChooser(PeriodChooser):
"""The income and expenses period chooser.""" """The income and expenses period chooser."""
def __init__(self, currency: Currency, account: Account): def __init__(self, currency: Currency, account: IncomeExpensesAccount):
"""Constructs the income and expenses period chooser.""" """Constructs the income and expenses period chooser."""
self.currency: Currency = currency self.currency: Currency = currency
"""The currency.""" """The currency."""
self.account: Account = account self.account: IncomeExpensesAccount = account
"""The account.""" """The account."""
first: Transaction | None \ first: Transaction | None \
= Transaction.query.order_by(Transaction.date).first() = Transaction.query.order_by(Transaction.date).first()

View File

@ -21,6 +21,7 @@ from flask import Blueprint, request, Response
from accounting.models import Currency, Account from accounting.models import Currency, Account
from accounting.utils.permission import has_permission, can_view from accounting.utils.permission import has_permission, can_view
from .income_expense_account import IncomeExpensesAccount
from .period import Period from .period import Period
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \ from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet IncomeStatement, BalanceSheet
@ -108,10 +109,11 @@ def __get_ledger_list(currency: Currency, account: Account, period: Period) \
return report.html() return report.html()
@bp.get("income-expenses/<currency:currency>/<account:account>", @bp.get("income-expenses/<currency:currency>/<ioAccount:account>",
endpoint="income-expenses-default") endpoint="income-expenses-default")
@has_permission(can_view) @has_permission(can_view)
def get_default_income_expenses_list(currency: Currency, account: Account) \ def get_default_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount) \
-> str | Response: -> str | Response:
"""Returns the income and expenses in the default period. """Returns the income and expenses in the default period.
@ -123,10 +125,11 @@ def get_default_income_expenses_list(currency: Currency, account: Account) \
@bp.get( @bp.get(
"income-expenses/<currency:currency>/<account:account>/<period:period>", "income-expenses/<currency:currency>/<ioAccount:account>/<period:period>",
endpoint="income-expenses") endpoint="income-expenses")
@has_permission(can_view) @has_permission(can_view)
def get_income_expenses_list(currency: Currency, account: Account, def get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses. """Returns the income and expenses.
@ -138,7 +141,8 @@ def get_income_expenses_list(currency: Currency, account: Account,
return __get_income_expenses_list(currency, account, period) return __get_income_expenses_list(currency, account, period)
def __get_income_expenses_list(currency: Currency, account: Account, def __get_income_expenses_list(currency: Currency,
account: IncomeExpensesAccount,
period: Period) -> str | Response: period: Period) -> str | Response:
"""Returns the income and expenses. """Returns the income and expenses.