Added the search to the accounting data.
This commit is contained in:
parent
fe01d5418d
commit
9993f65627
@ -22,4 +22,5 @@ from .income_expenses import IncomeExpenses
|
||||
from .income_statement import IncomeStatement
|
||||
from .journal import Journal
|
||||
from .ledger import Ledger
|
||||
from .search import Search
|
||||
from .trial_balance import TrialBalance
|
||||
|
299
src/accounting/report/reports/search.py
Normal file
299
src/accounting/report/reports/search.py
Normal file
@ -0,0 +1,299 @@
|
||||
# The Mia! Accounting Flask Project.
|
||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
|
||||
|
||||
# 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 search.
|
||||
|
||||
"""
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import Response, render_template, request
|
||||
|
||||
from accounting.locale import gettext
|
||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
|
||||
Transaction, JournalEntry
|
||||
from accounting.utils.pagination import Pagination
|
||||
from accounting.utils.query import parse_query_keywords
|
||||
from .utils.base_report import BaseReport
|
||||
from .utils.csv_export import BaseCSVRow, csv_download
|
||||
from .utils.page_params import PageParams
|
||||
from .utils.report_chooser import ReportChooser
|
||||
from .utils.report_type import ReportType
|
||||
|
||||
|
||||
class Entry:
|
||||
"""An entry in the search result."""
|
||||
|
||||
def __init__(self, entry: JournalEntry | None = None):
|
||||
"""Constructs the entry in the search result.
|
||||
|
||||
:param entry: The journal entry.
|
||||
"""
|
||||
self.entry: JournalEntry | None = None
|
||||
"""The journal entry."""
|
||||
self.transaction: Transaction | None = None
|
||||
"""The transaction."""
|
||||
self.is_total: bool = False
|
||||
"""Whether this is the total entry."""
|
||||
self.currency: Currency | None = None
|
||||
"""The account."""
|
||||
self.account: Account | None = None
|
||||
"""The account."""
|
||||
self.summary: str | None = None
|
||||
"""The summary."""
|
||||
self.debit: Decimal | None = None
|
||||
"""The debit amount."""
|
||||
self.credit: Decimal | None = None
|
||||
"""The credit amount."""
|
||||
self.amount: Decimal | None = None
|
||||
"""The amount."""
|
||||
if entry is not None:
|
||||
self.entry = entry
|
||||
self.summary = entry.summary
|
||||
self.debit = entry.amount if entry.is_debit else None
|
||||
self.credit = None if entry.is_debit else entry.amount
|
||||
self.amount = entry.amount
|
||||
|
||||
|
||||
class CSVRow(BaseCSVRow):
|
||||
"""A row in the CSV search result."""
|
||||
|
||||
def __init__(self, txn_date: str | date,
|
||||
currency: str,
|
||||
account: str,
|
||||
summary: str | None,
|
||||
debit: str | Decimal | None,
|
||||
credit: str | Decimal | None,
|
||||
note: str | None):
|
||||
"""Constructs a row in the CSV search result.
|
||||
|
||||
:param txn_date: The transaction date.
|
||||
:param summary: The summary.
|
||||
:param debit: The debit amount.
|
||||
:param credit: The credit amount.
|
||||
:param note: The note.
|
||||
"""
|
||||
self.date: str | date = txn_date
|
||||
"""The date."""
|
||||
self.currency: str = currency
|
||||
"""The currency."""
|
||||
self.account: str = account
|
||||
"""The account."""
|
||||
self.summary: str | None = summary
|
||||
"""The summary."""
|
||||
self.debit: str | Decimal | None = debit
|
||||
"""The debit amount."""
|
||||
self.credit: str | Decimal | None = credit
|
||||
"""The credit amount."""
|
||||
self.note: str | None = note
|
||||
"""The note."""
|
||||
|
||||
@property
|
||||
def values(self) -> list[str | Decimal | None]:
|
||||
"""Returns the values of the row.
|
||||
|
||||
:return: The values of the row.
|
||||
"""
|
||||
return [self.date, self.currency, self.account, self.summary,
|
||||
self.debit, self.credit, self.note]
|
||||
|
||||
|
||||
class SearchPageParams(PageParams):
|
||||
"""The HTML parameters of the search result."""
|
||||
|
||||
def __init__(self, pagination: Pagination[Entry],
|
||||
entries: list[Entry]):
|
||||
"""Constructs the HTML parameters of the search result.
|
||||
|
||||
:param entries: The search result entries.
|
||||
"""
|
||||
self.pagination: Pagination[Entry] = pagination
|
||||
"""The pagination."""
|
||||
self.entries: list[Entry] = entries
|
||||
"""The entries."""
|
||||
|
||||
@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.entries) > 0
|
||||
|
||||
@property
|
||||
def report_chooser(self) -> ReportChooser:
|
||||
"""Returns the report chooser.
|
||||
|
||||
:return: The report chooser.
|
||||
"""
|
||||
return ReportChooser(ReportType.SEARCH)
|
||||
|
||||
|
||||
def _populate_entries(entries: list[Entry]) -> None:
|
||||
"""Populates the search result entries with relative data.
|
||||
|
||||
:param entries: The search result entries.
|
||||
:return: None.
|
||||
"""
|
||||
transactions: dict[int, Transaction] \
|
||||
= {x.id: x for x in Transaction.query.filter(
|
||||
Transaction.id.in_({x.entry.transaction_id for x in entries}))}
|
||||
accounts: dict[int, Account] \
|
||||
= {x.id: x for x in Account.query.filter(
|
||||
Account.id.in_({x.entry.account_id for x in entries}))}
|
||||
currencies: dict[int, Currency] \
|
||||
= {x.code: x for x in Currency.query.filter(
|
||||
Currency.code.in_({x.entry.currency_code for x in entries}))}
|
||||
for entry in entries:
|
||||
entry.transaction = transactions[entry.entry.transaction_id]
|
||||
entry.account = accounts[entry.entry.account_id]
|
||||
entry.currency = currencies[entry.entry.currency_code]
|
||||
|
||||
|
||||
class Search(BaseReport):
|
||||
"""The search."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs a search."""
|
||||
"""The account."""
|
||||
self.__entries: list[Entry] = self.__query_entries()
|
||||
"""The journal entries."""
|
||||
|
||||
def __query_entries(self) -> list[Entry]:
|
||||
"""Queries and returns the journal entries.
|
||||
|
||||
:return: The journal entries.
|
||||
"""
|
||||
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
||||
if len(keywords) == 0:
|
||||
return []
|
||||
conditions: list[sa.BinaryExpression] = []
|
||||
for k in keywords:
|
||||
conditions.append(sa.or_(
|
||||
JournalEntry.summary.contains(k),
|
||||
sa.cast(JournalEntry.amount, sa.String).contains(k),
|
||||
JournalEntry.account_id.in_(self.__get_account_condition(k)),
|
||||
JournalEntry.currency_code.in_(
|
||||
self.__get_currency_condition(k)),
|
||||
JournalEntry.transaction_id.in_(
|
||||
self.__get_transaction_condition(k))))
|
||||
return [Entry(x) for x in JournalEntry.query.filter(*conditions)]
|
||||
|
||||
@staticmethod
|
||||
def __get_account_condition(k: str) -> sa.Select:
|
||||
"""Composes and returns the condition to filter the account.
|
||||
|
||||
:param k: The keyword.
|
||||
:return: The condition to filter the account.
|
||||
"""
|
||||
code: sa.BinaryExpression = Account.base_code + "-" \
|
||||
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
|
||||
sa.func.char_length(sa.cast(Account.no,
|
||||
sa.String)) + 1)
|
||||
select_l10n: sa.Select = sa.select(AccountL10n.account_id)\
|
||||
.filter(AccountL10n.title.contains(k))
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [Account.base_code.contains(k),
|
||||
Account.title_l10n.contains(k),
|
||||
code.contains(k),
|
||||
Account.id.in_(select_l10n)]
|
||||
if k in gettext("Pay-off needed"):
|
||||
conditions.append(Account.is_pay_off_needed)
|
||||
return sa.select(Account.id).filter(sa.or_(*conditions))
|
||||
|
||||
@staticmethod
|
||||
def __get_currency_condition(k: str) -> sa.Select:
|
||||
"""Composes and returns the condition to filter the currency.
|
||||
|
||||
:param k: The keyword.
|
||||
:return: The condition to filter the currency.
|
||||
"""
|
||||
select_l10n: sa.Select = sa.select(CurrencyL10n.currency_code)\
|
||||
.filter(CurrencyL10n.name.contains(k))
|
||||
return sa.select(Currency.code).filter(
|
||||
sa.or_(Currency.code.contains(k),
|
||||
Currency.name_l10n.contains(k),
|
||||
Currency.code.in_(select_l10n)))
|
||||
|
||||
@staticmethod
|
||||
def __get_transaction_condition(k: str) -> sa.Select:
|
||||
"""Composes and returns the condition to filter the transaction.
|
||||
|
||||
:param k: The keyword.
|
||||
:return: The condition to filter the transaction.
|
||||
"""
|
||||
conditions: list[sa.BinaryExpression] \
|
||||
= [Transaction.note.contains(k)]
|
||||
txn_date: datetime
|
||||
try:
|
||||
txn_date = datetime.strptime(k, "%Y")
|
||||
conditions.append(
|
||||
sa.extract("year", Transaction.date) == txn_date.year)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
txn_date = datetime.strptime(k, "%Y/%m")
|
||||
conditions.append(sa.and_(
|
||||
sa.extract("year", Transaction.date) == txn_date.year,
|
||||
sa.extract("month", Transaction.date) == txn_date.month))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
txn_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
|
||||
conditions.append(sa.and_(
|
||||
sa.extract("month", Transaction.date) == txn_date.month,
|
||||
sa.extract("day", Transaction.date) == txn_date.day))
|
||||
except ValueError:
|
||||
pass
|
||||
return sa.select(Transaction.id).filter(sa.or_(*conditions))
|
||||
|
||||
def csv(self) -> Response:
|
||||
"""Returns the report as CSV for download.
|
||||
|
||||
:return: The response of the report for download.
|
||||
"""
|
||||
filename: str = "search-{q}.csv".format(q=request.args["q"])
|
||||
return csv_download(filename, self.__get_csv_rows())
|
||||
|
||||
def __get_csv_rows(self) -> list[CSVRow]:
|
||||
"""Composes and returns the CSV rows.
|
||||
|
||||
:return: The CSV rows.
|
||||
"""
|
||||
_populate_entries(self.__entries)
|
||||
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
|
||||
gettext("Account"), gettext("Summary"),
|
||||
gettext("Debit"), gettext("Credit"),
|
||||
gettext("Note"))]
|
||||
rows.extend([CSVRow(x.transaction.date, x.currency.code,
|
||||
str(x.account).title(), x.summary,
|
||||
x.debit, x.credit, x.transaction.note)
|
||||
for x in self.__entries])
|
||||
return rows
|
||||
|
||||
def html(self) -> str:
|
||||
"""Composes and returns the report as HTML.
|
||||
|
||||
:return: The report as HTML.
|
||||
"""
|
||||
pagination: Pagination[Entry] = Pagination[Entry](self.__entries)
|
||||
page_entries: list[Entry] = pagination.list
|
||||
_populate_entries(page_entries)
|
||||
params: SearchPageParams = SearchPageParams(pagination=pagination,
|
||||
entries=page_entries)
|
||||
return render_template("accounting/report/search.html",
|
||||
report=params)
|
@ -34,3 +34,5 @@ class ReportType(Enum):
|
||||
"""The income statement."""
|
||||
BALANCE_SHEET: str = "balance-sheet"
|
||||
"""The balance sheet."""
|
||||
SEARCH: str = "search"
|
||||
"""The balance sheet."""
|
||||
|
@ -24,7 +24,7 @@ from accounting.utils.permission import has_permission, can_view
|
||||
from .income_expense_account import IncomeExpensesAccount
|
||||
from .period import Period
|
||||
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
||||
IncomeStatement, BalanceSheet
|
||||
IncomeStatement, BalanceSheet, Search
|
||||
from .template_filters import format_amount
|
||||
|
||||
bp: Blueprint = Blueprint("report", __name__)
|
||||
@ -275,3 +275,17 @@ def __get_balance_sheet_list(currency: Currency, period: Period) \
|
||||
if "as" in request.args and request.args["as"] == "csv":
|
||||
return report.csv()
|
||||
return report.html()
|
||||
|
||||
|
||||
@bp.get("search", endpoint="search")
|
||||
@has_permission(can_view)
|
||||
def search() -> str | Response:
|
||||
"""Returns the search result.
|
||||
|
||||
:return: The search result.
|
||||
"""
|
||||
report: Search = Search()
|
||||
if "as" in request.args and request.args["as"] == "csv":
|
||||
return report.csv()
|
||||
return report.html()
|
||||
|
||||
|
@ -116,6 +116,8 @@ First written: 2023/3/7
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% 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">
|
||||
|
@ -29,5 +29,10 @@ First written: 2023/3/4
|
||||
{% for report in report_chooser %}
|
||||
<li><a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">{{ report.title }}</a></li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<span class="dropdown-item accounting-clickable" data-bs-toggle="modal" data-bs-target="#accounting-search-modal">
|
||||
{{ A_("Search") }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -0,0 +1,43 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
search-modal.html: The search modal
|
||||
|
||||
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/3/8
|
||||
#}
|
||||
<form action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search the Accounting Data") }}">
|
||||
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="accounting-search-modal-label">{{ A_("Search") }}</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating">
|
||||
<input id="accounting-search-modal-search" class="form-control" type="text" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label for="accounting-search-modal-search">{{ A_("Search") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||
<button type="submit" class="btn btn-primary">{{ A_("Search") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -146,6 +146,8 @@ First written: 2023/3/5
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% include "accounting/report/include/search-modal.html" %}
|
||||
|
||||
{% if report.has_data %}
|
||||
{% with pagination = report.pagination %}
|
||||
{% include "accounting/include/pagination.html" %}
|
||||
|
@ -116,6 +116,8 @@ First written: 2023/3/7
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% 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">
|
||||
|
@ -88,6 +88,8 @@ First written: 2023/3/4
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% include "accounting/report/include/search-modal.html" %}
|
||||
|
||||
{% if report.has_data %}
|
||||
{% with pagination = report.pagination %}
|
||||
{% include "accounting/include/pagination.html" %}
|
||||
|
@ -146,6 +146,8 @@ First written: 2023/3/5
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% include "accounting/report/include/search-modal.html" %}
|
||||
|
||||
{% if report.has_data %}
|
||||
{% with pagination = report.pagination %}
|
||||
{% include "accounting/include/pagination.html" %}
|
||||
|
159
src/accounting/templates/accounting/report/search.html
Normal file
159
src/accounting/templates/accounting/report/search.html
Normal file
@ -0,0 +1,159 @@
|
||||
{#
|
||||
The Mia! Accounting Flask Project
|
||||
search.html: The search result
|
||||
|
||||
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/3/8
|
||||
#}
|
||||
{% extends "accounting/base.html" %}
|
||||
|
||||
{% block accounting_scripts %}
|
||||
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
|
||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}{% block title %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="btn-group mb-2 d-none d-md-inline-flex">
|
||||
{% if accounting_can_edit() %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
{{ A_("New") }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||
{{ A_("Cash Expense") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||
{{ A_("Cash Income") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.TRANSFER)|accounting_append_next }}">
|
||||
{{ A_("Transfer") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with report_chooser = report.report_chooser %}
|
||||
{% include "accounting/report/include/report-chooser.html" %}
|
||||
{% endwith %}
|
||||
<form class="btn btn-primary d-flex input-group accounting-search-desktop-form" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search for Desktop") }}">
|
||||
<input id="accounting-search-desktop" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label for="accounting-search-desktop" class="accounting-search-label">
|
||||
<button type="submit">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
{{ A_("Search") }}
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
{{ A_("Download") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% with txn_types = report.txn_types %}
|
||||
{% include "accounting/include/add-txn-material-fab.html" %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="btn-group btn-actions mb-3 d-md-none">
|
||||
{% with report_chooser = report.report_chooser %}
|
||||
{% include "accounting/report/include/report-chooser.html" %}
|
||||
{% endwith %}
|
||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.report.search") }}" method="get" role="search" aria-label="{{ A_("Search for Mobile") }}">
|
||||
<input id="accounting-search-mobile" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label for="accounting-search-mobile" class="accounting-search-label">
|
||||
<button type="submit">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
{{ A_("Search") }}
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% include "accounting/report/include/search-modal.html" %}
|
||||
|
||||
{% if report.has_data %}
|
||||
{% with pagination = report.pagination %}
|
||||
{% include "accounting/include/pagination.html" %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="d-none d-md-block accounting-report-table accounting-journal-table">
|
||||
<div class="accounting-report-table-header">
|
||||
<div class="accounting-report-table-row">
|
||||
<div>{{ A_("Date") }}</div>
|
||||
<div>{{ A_("Currency") }}</div>
|
||||
<div>{{ A_("Account") }}</div>
|
||||
<div>{{ A_("Summary") }}</div>
|
||||
<div class="accounting-amount">{{ A_("Debit") }}</div>
|
||||
<div class="accounting-amount">{{ A_("Credit") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accounting-report-table-body">
|
||||
{% for entry in report.entries %}
|
||||
<a class="accounting-report-table-row" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||
<div>{{ entry.transaction.date|accounting_format_date }}</div>
|
||||
<div>{{ entry.currency.name }}</div>
|
||||
<div>
|
||||
<span class="d-none d-md-inline">{{ entry.account.code }}</span>
|
||||
{{ entry.account.title|title }}
|
||||
</div>
|
||||
<div>{{ entry.summary|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.debit|accounting_format_amount|accounting_default }}</div>
|
||||
<div class="accounting-amount">{{ entry.credit|accounting_format_amount|accounting_default }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group d-md-none">
|
||||
{% for entry in report.entries %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for("accounting.transaction.detail", txn=entry.transaction)|accounting_append_next }}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div {% if not entry.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||
<div class="text-muted small">
|
||||
{{ entry.transaction.date|accounting_format_date }}
|
||||
{{ entry.account.title|title }}
|
||||
{% if entry.currency.code != accounting_default_currency_code() %}
|
||||
<span class="badge rounded-pill bg-info">{{ entry.currency.code }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if entry.summary is not none %}
|
||||
<div>{{ entry.summary }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="badge rounded-pill bg-info">{{ entry.amount|accounting_format_amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ A_("There is no data.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -116,6 +116,8 @@ First written: 2023/3/5
|
||||
{% include "accounting/report/include/period-chooser.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% 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">
|
||||
|
Loading…
Reference in New Issue
Block a user