Added the account list as the default page for the unapplied original line items.

This commit is contained in:
依瑪貓 2023-04-08 10:01:03 +08:00
parent e5cc2b5a2f
commit 6eee17d44f
8 changed files with 250 additions and 12 deletions

View File

@ -118,10 +118,14 @@ class PageParams(BasePageParams):
:return: The account options. :return: The account options.
""" """
return [OptionLink(str(x), options: list[OptionLink] = [OptionLink(gettext("Accounts"),
unapplied_url(x), unapplied_url(None),
x.id == self.account.id) False)]
for x in get_accounts_with_unapplied()] options.extend([OptionLink(str(x),
unapplied_url(x),
x.id == self.account.id)
for x in get_accounts_with_unapplied()])
return options
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]: def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:

View File

@ -0,0 +1,137 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 accounts with unapplied original line items.
"""
from datetime import date
from decimal import Decimal
from flask import render_template, Response
from accounting.locale import gettext
from accounting.models import Account
from accounting.report.utils.base_page_params import BasePageParams
from accounting.report.utils.base_report import BaseReport
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
from accounting.report.utils.option_link import OptionLink
from accounting.report.utils.report_chooser import ReportChooser
from accounting.report.utils.report_type import ReportType
from accounting.report.utils.unapplied import get_accounts_with_unapplied
from accounting.report.utils.urls import unapplied_url
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, account: str, count: int | str):
"""Constructs a row in the CSV.
:param account: The account.
:param count: The number of unapplied original line items.
"""
self.account: str = account
"""The currency."""
self.count: int | str = count
"""The number of unapplied original line items."""
@property
def values(self) -> list[str | date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.account, self.count]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, accounts: list[Account]):
"""Constructs the HTML page parameters.
:param accounts: The accounts.
"""
self.accounts: list[Account] = accounts
"""The accounts."""
@property
def has_data(self) -> bool:
"""Returns whether there is any data on the page.
:return: True if there is any data, or False otherwise.
"""
return len(self.accounts) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.UNAPPLIED)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] = [OptionLink(gettext("Accounts"),
unapplied_url(None),
True)]
options.extend([OptionLink(str(x),
unapplied_url(x),
False)
for x in self.accounts])
return options
def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param accounts: The accounts.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
rows.extend([CSVRow(str(x).title(), x.count)
for x in accounts])
return rows
class AccountsWithUnappliedOriginalLineItems(BaseReport):
"""The accounts with unapplied original line items."""
def __init__(self):
"""Constructs the outstanding balances."""
self.__accounts: list[Account] = get_accounts_with_unapplied()
"""The accounts."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = f"unapplied-accounts.csv"
return csv_download(filename, get_csv_rows(self.__accounts))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
return render_template("accounting/report/unapplied-accounts.html",
report=PageParams(accounts=self.__accounts))

View File

@ -33,7 +33,6 @@ from accounting.template_globals import default_currency_code
from accounting.utils.current_account import CurrentAccount from accounting.utils.current_account import CurrentAccount
from .option_link import OptionLink from .option_link import OptionLink
from .report_type import ReportType from .report_type import ReportType
from .unapplied import get_accounts_with_unapplied
from .urls import journal_url, ledger_url, income_expenses_url, \ from .urls import journal_url, ledger_url, income_expenses_url, \
trial_balance_url, income_statement_url, balance_sheet_url, unapplied_url trial_balance_url, income_statement_url, balance_sheet_url, unapplied_url
@ -161,7 +160,10 @@ class ReportChooser:
""" """
account: Account = self.__account account: Account = self.__account
if not account.is_need_offset: if not account.is_need_offset:
account = get_accounts_with_unapplied()[0] return OptionLink(gettext("Unapplied Original Line Items"),
unapplied_url(None),
self.__active_report == ReportType.UNAPPLIED,
fa_icon="fa-solid fa-link-slash")
return OptionLink(gettext("Unapplied Original Line Items"), return OptionLink(gettext("Unapplied Original Line Items"),
unapplied_url(account), unapplied_url(account),
self.__active_report == ReportType.UNAPPLIED, self.__active_report == ReportType.UNAPPLIED,

View File

@ -19,6 +19,7 @@
""" """
import sqlalchemy as sa import sqlalchemy as sa
from accounting import db
from accounting.models import Account, JournalEntryLineItem from accounting.models import Account, JournalEntryLineItem
from accounting.utils.cast import be from accounting.utils.cast import be
from accounting.utils.offset_alias import offset_alias from accounting.utils.offset_alias import offset_alias
@ -50,12 +51,17 @@ def get_accounts_with_unapplied() -> list[Account]:
.group_by(JournalEntryLineItem.id)\ .group_by(JournalEntryLineItem.id)\
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0)) .having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
count_func: sa.Function \ count_func: sa.Label \
= sa.func.count(JournalEntryLineItem.id) = sa.func.count(JournalEntryLineItem.id).label("count")
select: sa.Select = sa.select(Account.id)\ select: sa.Select = sa.select(Account.id, count_func)\
.join(JournalEntryLineItem, isouter=True)\ .join(JournalEntryLineItem, isouter=True)\
.filter(JournalEntryLineItem.id.in_(select_unapplied))\ .filter(JournalEntryLineItem.id.in_(select_unapplied))\
.group_by(Account.id)\ .group_by(Account.id)\
.having(count_func > 0) .having(count_func > 0)
return Account.query.filter(Account.id.in_(select))\ counts: dict[int, int] \
= {x.id: x.count for x in db.session.execute(select)}
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
.order_by(Account.base_code, Account.no).all() .order_by(Account.base_code, Account.no).all()
for account in accounts:
account.count = counts[account.id]
return accounts

View File

@ -118,10 +118,13 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
currency=currency, period=period) currency=currency, period=period)
def unapplied_url(account: Account) -> str: def unapplied_url(account: Account | None) -> str:
"""Returns the URL of the unapplied original line items. """Returns the URL of the unapplied original line items.
:param account: The account. :param account: The account, or None to list the accounts with unapplied
original line items.
:return: The URL of the unapplied original line items. :return: The URL of the unapplied original line items.
""" """
if account is None:
return url_for("accounting-report.unapplied-default")
return url_for("accounting-report.unapplied", account=account) return url_for("accounting-report.unapplied", account=account)

View File

@ -29,6 +29,7 @@ from accounting.utils.permission import has_permission, can_view
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \ from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet, Search IncomeStatement, BalanceSheet, Search
from .reports.unapplied import UnappliedOriginalLineItems from .reports.unapplied import UnappliedOriginalLineItems
from .reports.unapplied_accounts import AccountsWithUnappliedOriginalLineItems
from .template_filters import format_amount from .template_filters import format_amount
bp: Blueprint = Blueprint("accounting-report", __name__) bp: Blueprint = Blueprint("accounting-report", __name__)
@ -286,6 +287,20 @@ def __get_balance_sheet(currency: Currency, period: Period) \
return report.html() return report.html()
@bp.get("unapplied", endpoint="unapplied-default")
@has_permission(can_view)
def get_default_unapplied() -> str | Response:
"""Returns the accounts with unapplied original line items.
:return: The accounts with unapplied original line items.
"""
report: AccountsWithUnappliedOriginalLineItems \
= AccountsWithUnappliedOriginalLineItems()
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()
@bp.get("unapplied/<unappliedAccount:account>", endpoint="unapplied") @bp.get("unapplied/<unappliedAccount:account>", endpoint="unapplied")
@has_permission(can_view) @has_permission(can_view)
def get_unapplied(account: Account) -> str | Response: def get_unapplied(account: Account) -> str | Response:

View File

@ -312,6 +312,13 @@ a.accounting-report-table-row {
.accounting-unapplied-table .accounting-report-table-row { .accounting-unapplied-table .accounting-report-table-row {
grid-template-columns: 1fr 1fr 5fr 1fr 1fr; grid-template-columns: 1fr 1fr 5fr 1fr 1fr;
} }
.accounting-unapplied-account-table .accounting-report-table-row {
display: flex;
justify-content: space-between;
}
.accounting-unapplied-account-table .accounting-report-table-header .accounting-report-table-row {
display: block;
}
/* The accounting report */ /* The accounting report */
.accounting-mobile-journal-credit { .accounting-mobile-journal-credit {

View File

@ -0,0 +1,64 @@
{#
The Mia! Accounting Project
unapplied-accounts.html: The account list with unapplied original line items
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.
Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/4/8
#}
{% extends "accounting/base.html" %}
{% block header %}{% block title %}{{ A_("Accounts with Unapplied Original Line Items") }}{% endblock %}{% endblock %}
{% block content %}
<div class="mb-3 accounting-toolbar">
{% with use_account_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
{% endwith %}
</div>
{% include "accounting/report/include/add-journal-entry-material-fab.html" %}
{% include "accounting/report/include/search-modal.html" %}
{% if report.has_data %}
<div class="accounting-sheet">
<div class="d-none d-sm-flex justify-content-center mb-3">
<h2 class="text-center">{{ A_("Accounts with Unapplied Original Line Items") }}</h2>
</div>
<div class="accounting-report-table accounting-unapplied-account-table">
<div class="accounting-report-table-header">
<div class="accounting-report-table-row">
<div class="accounting-amount">{{ A_("Count") }}</div>
</div>
</div>
<div class="accounting-report-table-body">
{% for account in report.accounts %}
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unapplied", account=account) }}">
<div>{{ account }}</div>
<div class="accounting-amount">{{ account.count }}</div>
</a>
{% endfor %}
</div>
</div>
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}