Added ledger.
This commit is contained in:
parent
fe77f87110
commit
eabe80b790
34
src/accounting/report/option_link.py
Normal file
34
src/accounting/report/option_link.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# The Mia! Accounting Flask Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
|
||||||
|
|
||||||
|
# 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 option link.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OptionLink:
|
||||||
|
"""An option link."""
|
||||||
|
|
||||||
|
def __init__(self, title: str, url: str, is_active: bool):
|
||||||
|
"""Constructs an option link.
|
||||||
|
|
||||||
|
:param title: The title.
|
||||||
|
:param url: The URI.
|
||||||
|
:param is_active: True if active, or False otherwise
|
||||||
|
"""
|
||||||
|
self.title: str = title
|
||||||
|
self.url: str = url
|
||||||
|
self.is_active: bool = is_active
|
@ -26,7 +26,7 @@ from datetime import date
|
|||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from accounting.models import Transaction
|
from accounting.models import Currency, Account, Transaction
|
||||||
from .period import YearPeriod, Period, ThisMonth, LastMonth, SinceLastMonth, \
|
from .period import YearPeriod, Period, ThisMonth, LastMonth, SinceLastMonth, \
|
||||||
ThisYear, LastYear, Today, Yesterday, TemplatePeriod
|
ThisYear, LastYear, Today, Yesterday, TemplatePeriod
|
||||||
|
|
||||||
@ -116,3 +116,26 @@ class JournalPeriodChooser(PeriodChooser):
|
|||||||
if period.is_default:
|
if period.is_default:
|
||||||
return url_for("accounting.report.journal-default")
|
return url_for("accounting.report.journal-default")
|
||||||
return url_for("accounting.report.journal", period=period)
|
return url_for("accounting.report.journal", period=period)
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerPeriodChooser(PeriodChooser):
|
||||||
|
"""The ledger period chooser."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account):
|
||||||
|
"""Constructs the ledger period chooser."""
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
first: Transaction | None \
|
||||||
|
= Transaction.query.order_by(Transaction.date).first()
|
||||||
|
super(LedgerPeriodChooser, self).__init__(
|
||||||
|
None if first is None else first.date)
|
||||||
|
|
||||||
|
def _url_for(self, period: Period) -> str:
|
||||||
|
if period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.currency, account=self.account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.currency, account=self.account,
|
||||||
|
period=period)
|
||||||
|
@ -27,40 +27,26 @@ from flask_babel import LazyString
|
|||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Currency
|
from accounting.models import Currency, Account
|
||||||
from accounting.template_globals import default_currency_code
|
from accounting.template_globals import default_currency_code
|
||||||
|
from .option_link import OptionLink
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
|
|
||||||
|
|
||||||
class ReportLink:
|
|
||||||
"""A link of a report."""
|
|
||||||
|
|
||||||
def __init__(self, name: str | LazyString, url: str):
|
|
||||||
"""Constructs a report.
|
|
||||||
|
|
||||||
:param name: The report name.
|
|
||||||
:param url: The URL.
|
|
||||||
"""
|
|
||||||
self.name: str | LazyString = name
|
|
||||||
"""The report name."""
|
|
||||||
self.url: str = url
|
|
||||||
"""The URL."""
|
|
||||||
self.is_active: bool = False
|
|
||||||
"""Whether the report is the current report."""
|
|
||||||
|
|
||||||
|
|
||||||
class ReportChooser:
|
class ReportChooser:
|
||||||
"""The report chooser."""
|
"""The report chooser."""
|
||||||
|
|
||||||
def __init__(self, active_report: ReportType,
|
def __init__(self, active_report: ReportType,
|
||||||
period: Period | None = None,
|
period: Period | None = None,
|
||||||
currency: Currency | None = None):
|
currency: Currency | None = None,
|
||||||
|
account: Account | None = None):
|
||||||
"""Constructs the report chooser.
|
"""Constructs the report chooser.
|
||||||
|
|
||||||
:param active_report: The active report.
|
:param active_report: The active report.
|
||||||
:param period: The period.
|
:param period: The period.
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
"""
|
"""
|
||||||
self.__active_report: ReportType = active_report
|
self.__active_report: ReportType = active_report
|
||||||
"""The currently active report."""
|
"""The currently active report."""
|
||||||
@ -71,17 +57,21 @@ class ReportChooser:
|
|||||||
Currency, default_currency_code()) \
|
Currency, default_currency_code()) \
|
||||||
if currency is None else currency
|
if currency is None else currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__reports: list[ReportLink] = []
|
self.__account: Account = Account.find_by_code("1111-001") \
|
||||||
|
if account is None else account
|
||||||
|
"""The currency."""
|
||||||
|
self.__reports: list[OptionLink] = []
|
||||||
"""The links to the reports."""
|
"""The links to the reports."""
|
||||||
self.__reports.append(self.__journal)
|
|
||||||
self.current_report: str | LazyString = ""
|
self.current_report: str | LazyString = ""
|
||||||
"""The name of the current report."""
|
"""The title of the current report."""
|
||||||
|
self.__reports.append(self.__journal)
|
||||||
|
self.__reports.append(self.__ledger)
|
||||||
for report in self.__reports:
|
for report in self.__reports:
|
||||||
if report.is_active:
|
if report.is_active:
|
||||||
self.current_report = report.name
|
self.current_report = report.title
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __journal(self) -> ReportLink:
|
def __journal(self) -> OptionLink:
|
||||||
"""Returns the journal.
|
"""Returns the journal.
|
||||||
|
|
||||||
:return: The journal.
|
:return: The journal.
|
||||||
@ -89,12 +79,25 @@ class ReportChooser:
|
|||||||
url: str = url_for("accounting.report.journal-default") \
|
url: str = url_for("accounting.report.journal-default") \
|
||||||
if self.__period.is_default \
|
if self.__period.is_default \
|
||||||
else url_for("accounting.report.journal", period=self.__period)
|
else url_for("accounting.report.journal", period=self.__period)
|
||||||
report = ReportLink(gettext("Journal"), url)
|
return OptionLink(gettext("Journal"), url,
|
||||||
if self.__active_report == ReportType.JOURNAL:
|
self.__active_report == ReportType.JOURNAL)
|
||||||
report.is_active = True
|
|
||||||
return report
|
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[ReportLink]:
|
@property
|
||||||
|
def __ledger(self) -> OptionLink:
|
||||||
|
"""Returns the ledger.
|
||||||
|
|
||||||
|
:return: The ledger.
|
||||||
|
"""
|
||||||
|
url: str = url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.__currency, account=self.__account) \
|
||||||
|
if self.__period.is_default \
|
||||||
|
else url_for("accounting.report.ledger",
|
||||||
|
currency=self.__currency, account=self.__account,
|
||||||
|
period=self.__period)
|
||||||
|
return OptionLink(gettext("Ledger"), url,
|
||||||
|
self.__active_report == ReportType.LEDGER)
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[OptionLink]:
|
||||||
"""Returns the iteration of the reports.
|
"""Returns the iteration of the reports.
|
||||||
|
|
||||||
:return: The iteration of the reports.
|
:return: The iteration of the reports.
|
||||||
|
@ -18,8 +18,9 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import typing as t
|
||||||
from decimal import Decimal
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from accounting.models import JournalEntry, Transaction, Account, Currency
|
from accounting.models import JournalEntry, Transaction, Account, Currency
|
||||||
|
|
||||||
@ -38,27 +39,77 @@ class ReportRow(ABC):
|
|||||||
class JournalRow(ReportRow):
|
class JournalRow(ReportRow):
|
||||||
"""A row in the journal report."""
|
"""A row in the journal report."""
|
||||||
|
|
||||||
def __init__(self, entry: JournalEntry, transaction: Transaction,
|
def __init__(self, entry: JournalEntry):
|
||||||
account: Account, currency: Currency):
|
|
||||||
"""Constructs the row in the journal report.
|
"""Constructs the row in the journal report.
|
||||||
|
|
||||||
:param entry: The journal entry.
|
:param entry: The journal entry.
|
||||||
:param transaction: The transaction.
|
|
||||||
:param account: The account.
|
|
||||||
:param currency: The currency.
|
|
||||||
"""
|
"""
|
||||||
self.is_debit: bool = entry.is_debit
|
self.entry: JournalEntry = entry
|
||||||
|
"""The journal entry."""
|
||||||
self.summary: str | None = entry.summary
|
self.summary: str | None = entry.summary
|
||||||
|
"""The summary."""
|
||||||
|
self.currency_code: str = entry.currency_code
|
||||||
|
"""The currency code."""
|
||||||
|
self.is_debit: bool = entry.is_debit
|
||||||
|
"""True for a debit journal entry, or False for a credit entry."""
|
||||||
self.amount: Decimal = entry.amount
|
self.amount: Decimal = entry.amount
|
||||||
self.transaction: Account = transaction
|
"""The amount."""
|
||||||
self.account: Account = account
|
self.transaction: Transaction | None = None
|
||||||
self.currency: Currency = currency
|
"""The transaction."""
|
||||||
|
self.currency: Currency | None = None
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account | None = None
|
||||||
|
"""The account."""
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, t.Any]:
|
def as_dict(self) -> dict[str, t.Any]:
|
||||||
return {"date": self.transaction.date.isoformat(),
|
return {"date": self.transaction.date,
|
||||||
"currency": self.currency.name,
|
"currency": str(self.currency),
|
||||||
"account": str(self.account),
|
"account": str(self.account),
|
||||||
"summary": self.summary,
|
"summary": self.summary,
|
||||||
"debit": self.amount if self.is_debit else None,
|
"debit": self.amount if self.is_debit else None,
|
||||||
"credit": None if self.is_debit else self.amount,
|
"credit": None if self.is_debit else self.amount,
|
||||||
"note": self.transaction.note}
|
"note": self.transaction.note}
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerRow(ReportRow):
|
||||||
|
"""A row in the ledger report."""
|
||||||
|
|
||||||
|
def __init__(self, entry: JournalEntry | None = None):
|
||||||
|
"""Constructs the row in the journal report.
|
||||||
|
|
||||||
|
: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 row."""
|
||||||
|
self.date: date | None = None
|
||||||
|
"""The date."""
|
||||||
|
self.summary: str | None = None
|
||||||
|
"""The summary."""
|
||||||
|
self.debit: Decimal | None = None
|
||||||
|
"""The debit amount."""
|
||||||
|
self.credit: Decimal | None = None
|
||||||
|
"""The credit amount."""
|
||||||
|
self.balance: Decimal | None = None
|
||||||
|
"""The balance."""
|
||||||
|
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
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, t.Any]:
|
||||||
|
if self.is_total:
|
||||||
|
return {"date": "Total",
|
||||||
|
"summary": None,
|
||||||
|
"debit": self.debit,
|
||||||
|
"credit": self.credit,
|
||||||
|
"balance": self.balance}
|
||||||
|
return {"date": self.date,
|
||||||
|
"summary": self.summary,
|
||||||
|
"debit": self.debit,
|
||||||
|
"credit": self.credit,
|
||||||
|
"balance": self.balance}
|
||||||
|
@ -24,3 +24,5 @@ class ReportType(Enum):
|
|||||||
"""The report types."""
|
"""The report types."""
|
||||||
JOURNAL: str = "journal"
|
JOURNAL: str = "journal"
|
||||||
"""The journal."""
|
"""The journal."""
|
||||||
|
LEDGER: str = "ledger"
|
||||||
|
"""The ledger."""
|
||||||
|
@ -20,24 +20,32 @@
|
|||||||
import csv
|
import csv
|
||||||
import typing as t
|
import typing as t
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
||||||
|
urlunparse
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Response, render_template
|
from flask import Response, render_template, request, url_for
|
||||||
from flask_sqlalchemy.query import Query
|
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import JournalEntry, Transaction, Account, Currency
|
from accounting.locale import gettext
|
||||||
|
from accounting.models import Currency, Account, Transaction, JournalEntry
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.txn_types import TransactionType
|
from accounting.utils.txn_types import TransactionType
|
||||||
|
from .option_link import OptionLink
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .period_choosers import PeriodChooser, JournalPeriodChooser
|
from .period_choosers import PeriodChooser, JournalPeriodChooser, \
|
||||||
|
LedgerPeriodChooser
|
||||||
from .report_chooser import ReportChooser
|
from .report_chooser import ReportChooser
|
||||||
from .report_rows import ReportRow, JournalRow
|
from .report_rows import ReportRow, JournalRow, LedgerRow
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
|
|
||||||
|
T = t.TypeVar("T", bound=ReportRow)
|
||||||
|
"""The row class in the report."""
|
||||||
|
|
||||||
class JournalEntryReport(ABC):
|
|
||||||
|
class JournalEntryReport(t.Generic[T], ABC):
|
||||||
"""A report based on a journal entry."""
|
"""A report based on a journal entry."""
|
||||||
|
|
||||||
def __init__(self, period: Period):
|
def __init__(self, period: Period):
|
||||||
@ -47,22 +55,23 @@ class JournalEntryReport(ABC):
|
|||||||
"""
|
"""
|
||||||
self.period: Period = period
|
self.period: Period = period
|
||||||
"""The period."""
|
"""The period."""
|
||||||
self._entries: list[JournalEntry] = self.get_entries()
|
self.__rows: list[T] | None = None
|
||||||
"""The journal entries."""
|
"""The rows in the report."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_entries(self) -> list[JournalEntry]:
|
def get_rows(self) -> list[T]:
|
||||||
"""Returns the journal entries.
|
"""Returns the rows, without pagination.
|
||||||
|
|
||||||
:return: The journal entries.
|
:return: The rows.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def entries_to_rows(self, entries: list[JournalEntry]) -> list[ReportRow]:
|
def populate_rows(self, rows: list[T]) -> None:
|
||||||
"""Converts the journal entries into report rows.
|
"""Populates the transaction, currency, account, and other data to the
|
||||||
|
given rows.
|
||||||
|
|
||||||
:param entries: The journal entries.
|
:param rows: The rows.
|
||||||
:return: The report rows.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -104,6 +113,16 @@ class JournalEntryReport(ABC):
|
|||||||
:return: The report as an HTML page.
|
:return: The report as an HTML page.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rows(self) -> list[T]:
|
||||||
|
"""Returns the journal entries.
|
||||||
|
|
||||||
|
:return: The journal entries.
|
||||||
|
"""
|
||||||
|
if self.__rows is None:
|
||||||
|
self.__rows = self.get_rows()
|
||||||
|
return self.__rows
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def txn_types(self) -> t.Type[TransactionType]:
|
def txn_types(self) -> t.Type[TransactionType]:
|
||||||
"""Returns the transaction types.
|
"""Returns the transaction types.
|
||||||
@ -112,17 +131,32 @@ class JournalEntryReport(ABC):
|
|||||||
"""
|
"""
|
||||||
return TransactionType
|
return TransactionType
|
||||||
|
|
||||||
|
@property
|
||||||
|
def csv_uri(self) -> str:
|
||||||
|
"""Returns the URI to download the report as CSV.
|
||||||
|
|
||||||
|
:return: The URI to download the report as CSV.
|
||||||
|
"""
|
||||||
|
uri: str = request.full_path if request.query_string else request.path
|
||||||
|
uri_p: ParseResult = urlparse(uri)
|
||||||
|
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
|
||||||
|
params = [x for x in params if x[0] != "as"]
|
||||||
|
params.append(("as", "csv"))
|
||||||
|
parts: list[str] = list(uri_p)
|
||||||
|
parts[4] = urlencode(params)
|
||||||
|
return urlunparse(parts)
|
||||||
|
|
||||||
def as_csv_download(self) -> Response:
|
def as_csv_download(self) -> Response:
|
||||||
"""Returns the journal entries as CSV download.
|
"""Returns the report as CSV download.
|
||||||
|
|
||||||
:return: The CSV download response.
|
:return: The CSV download response.
|
||||||
"""
|
"""
|
||||||
|
self.populate_rows(self.rows)
|
||||||
with StringIO() as fp:
|
with StringIO() as fp:
|
||||||
writer: csv.DictWriter = csv.DictWriter(
|
writer: csv.DictWriter = csv.DictWriter(
|
||||||
fp, fieldnames=self.csv_field_names)
|
fp, fieldnames=self.csv_field_names)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows([x.as_dict()
|
writer.writerows([x.as_dict() for x in self.rows])
|
||||||
for x in self.entries_to_rows(self._entries)])
|
|
||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
response: Response = Response(fp.read(), mimetype="text/csv")
|
response: Response = Response(fp.read(), mimetype="text/csv")
|
||||||
response.headers["Content-Disposition"] \
|
response.headers["Content-Disposition"] \
|
||||||
@ -130,35 +164,37 @@ class JournalEntryReport(ABC):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class Journal(JournalEntryReport):
|
class Journal(JournalEntryReport[JournalRow]):
|
||||||
"""The journal."""
|
"""The journal."""
|
||||||
|
|
||||||
def get_entries(self) -> list[JournalEntry]:
|
def get_rows(self) -> list[JournalRow]:
|
||||||
conditions: list[sa.BinaryExpression] = []
|
conditions: list[sa.BinaryExpression] = []
|
||||||
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)
|
||||||
query: Query = db.session.query(JournalEntry).join(Transaction)
|
return [JournalRow(x) for x in db.session
|
||||||
if len(conditions) > 0:
|
.query(JournalEntry)
|
||||||
query = query.filter(*conditions)
|
.join(Transaction)
|
||||||
return query.order_by(Transaction.date,
|
.filter(*conditions)
|
||||||
|
.order_by(Transaction.date,
|
||||||
JournalEntry.is_debit.desc(),
|
JournalEntry.is_debit.desc(),
|
||||||
JournalEntry.no).all()
|
JournalEntry.no).all()]
|
||||||
|
|
||||||
def entries_to_rows(self, entries: list[JournalEntry]) -> list[ReportRow]:
|
def populate_rows(self, rows: list[JournalRow]) -> None:
|
||||||
transactions: dict[int, Transaction] \
|
transactions: dict[int, Transaction] \
|
||||||
= {x.id: x for x in Transaction.query.filter(
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
Transaction.id.in_({x.transaction_id for x in entries}))}
|
Transaction.id.in_({x.entry.transaction_id for x in rows}))}
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query.filter(
|
= {x.id: x for x in Account.query.filter(
|
||||||
Account.id.in_({x.account_id for x in entries}))}
|
Account.id.in_({x.entry.account_id for x in rows}))}
|
||||||
currencies: dict[int, Currency] \
|
currencies: dict[int, Currency] \
|
||||||
= {x.code: x for x in Currency.query.filter(
|
= {x.code: x for x in Currency.query.filter(
|
||||||
Currency.code.in_({x.currency_code for x in entries}))}
|
Currency.code.in_({x.entry.currency_code for x in rows}))}
|
||||||
return [JournalRow(x, transactions[x.transaction_id],
|
for row in rows:
|
||||||
accounts[x.account_id], currencies[x.currency_code])
|
row.transaction = transactions[row.entry.transaction_id]
|
||||||
for x in entries]
|
row.account = accounts[row.entry.account_id]
|
||||||
|
row.currency = currencies[row.entry.currency_code]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def csv_field_names(self) -> list[str]:
|
def csv_field_names(self) -> list[str]:
|
||||||
@ -175,10 +211,208 @@ class Journal(JournalEntryReport):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def report_chooser(self) -> ReportChooser:
|
def report_chooser(self) -> ReportChooser:
|
||||||
return ReportChooser(ReportType.JOURNAL, self.period)
|
return ReportChooser(ReportType.JOURNAL,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
def as_html_page(self) -> str:
|
def as_html_page(self) -> str:
|
||||||
pagination: Pagination = Pagination[JournalEntry](self._entries)
|
pagination: Pagination = Pagination[JournalRow](self.rows)
|
||||||
|
rows: list[JournalRow] = pagination.list
|
||||||
|
self.populate_rows(rows)
|
||||||
return render_template("accounting/report/journal.html",
|
return render_template("accounting/report/journal.html",
|
||||||
list=self.entries_to_rows(pagination.list),
|
list=rows, pagination=pagination, report=self)
|
||||||
pagination=pagination, report=self)
|
|
||||||
|
|
||||||
|
class Ledger(JournalEntryReport[LedgerRow]):
|
||||||
|
"""The ledger."""
|
||||||
|
|
||||||
|
def __init__(self, currency: Currency, account: Account, period: Period):
|
||||||
|
"""Constructs a journal.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param period: The period.
|
||||||
|
"""
|
||||||
|
super().__init__(period)
|
||||||
|
self.currency: Currency = currency
|
||||||
|
"""The currency."""
|
||||||
|
self.account: Account = account
|
||||||
|
"""The account."""
|
||||||
|
self.total_row: LedgerRow | None = None
|
||||||
|
"""The total row to show on the template."""
|
||||||
|
|
||||||
|
def get_rows(self) -> list[LedgerRow]:
|
||||||
|
brought_forward: LedgerRow | None = self.__get_brought_forward_row()
|
||||||
|
rows: list[LedgerRow] = [LedgerRow(x) for x in self.__query_entries()]
|
||||||
|
total: LedgerRow = self.__get_total_row(brought_forward, rows)
|
||||||
|
self.__populate_balance(brought_forward, rows)
|
||||||
|
if brought_forward is not None:
|
||||||
|
rows.insert(0, brought_forward)
|
||||||
|
rows.append(total)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def __get_brought_forward_row(self) -> LedgerRow | None:
|
||||||
|
"""Queries, composes and returns the brought-forward row.
|
||||||
|
|
||||||
|
:return: The brought-forward row, or None if the ledger starts from the
|
||||||
|
beginning.
|
||||||
|
"""
|
||||||
|
if self.period.start is None:
|
||||||
|
return None
|
||||||
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
|
(JournalEntry.is_debit, JournalEntry.amount),
|
||||||
|
else_=-JournalEntry.amount))
|
||||||
|
select: sa.Select = sa.Select(balance_func).join(Transaction)\
|
||||||
|
.filter(JournalEntry.currency_code == self.currency.code,
|
||||||
|
JournalEntry.account_id == self.account.id,
|
||||||
|
Transaction.date < self.period.start)
|
||||||
|
balance: int | None = db.session.scalar(select)
|
||||||
|
if balance is None:
|
||||||
|
return None
|
||||||
|
row: LedgerRow = LedgerRow()
|
||||||
|
row.date = self.period.start
|
||||||
|
row.summary = gettext("Brought forward")
|
||||||
|
if balance > 0:
|
||||||
|
row.debit = balance
|
||||||
|
elif balance < 0:
|
||||||
|
row.credit = -balance
|
||||||
|
row.balance = balance
|
||||||
|
return row
|
||||||
|
|
||||||
|
def __query_entries(self) -> list[JournalEntry]:
|
||||||
|
"""Queries and returns the journal entries.
|
||||||
|
|
||||||
|
:return: The journal entries.
|
||||||
|
"""
|
||||||
|
conditions: list[sa.BinaryExpression] \
|
||||||
|
= [JournalEntry.currency_code == self.currency.code,
|
||||||
|
JournalEntry.account_id == self.account.id]
|
||||||
|
if self.period.start is not None:
|
||||||
|
conditions.append(Transaction.date >= self.period.start)
|
||||||
|
if self.period.end is not None:
|
||||||
|
conditions.append(Transaction.date <= self.period.end)
|
||||||
|
return db.session.query(JournalEntry).join(Transaction)\
|
||||||
|
.filter(*conditions)\
|
||||||
|
.order_by(Transaction.date,
|
||||||
|
JournalEntry.is_debit.desc(),
|
||||||
|
JournalEntry.no).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_total_row(brought_forward: LedgerRow | None,
|
||||||
|
rows: list[LedgerRow]) -> LedgerRow:
|
||||||
|
"""Composes the total row.
|
||||||
|
|
||||||
|
:param brought_forward: The brought-forward row.
|
||||||
|
:param rows: The rows.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
row: LedgerRow = LedgerRow()
|
||||||
|
row.is_total = True
|
||||||
|
row.summary = gettext("Total")
|
||||||
|
row.debit = sum([x.debit for x in rows if x.debit is not None])
|
||||||
|
row.credit = sum([x.credit for x in rows if x.credit is not None])
|
||||||
|
row.balance = row.debit - row.credit
|
||||||
|
if brought_forward is not None:
|
||||||
|
row.balance = brought_forward.balance + row.balance
|
||||||
|
return row
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __populate_balance(brought_forward: LedgerRow | None,
|
||||||
|
rows: list[LedgerRow]) -> None:
|
||||||
|
"""Populates the balance of the rows.
|
||||||
|
|
||||||
|
:param brought_forward: The brought-forward row.
|
||||||
|
:param rows: The rows.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
balance: Decimal = 0 if brought_forward is None \
|
||||||
|
else brought_forward.balance
|
||||||
|
for row in rows:
|
||||||
|
if row.debit is not None:
|
||||||
|
balance = balance + row.debit
|
||||||
|
if row.credit is not None:
|
||||||
|
balance = balance - row.credit
|
||||||
|
row.balance = balance
|
||||||
|
|
||||||
|
def populate_rows(self, rows: list[LedgerRow]) -> None:
|
||||||
|
transactions: dict[int, Transaction] \
|
||||||
|
= {x.id: x for x in Transaction.query.filter(
|
||||||
|
Transaction.id.in_({x.entry.transaction_id for x in rows
|
||||||
|
if x.entry is not None}))}
|
||||||
|
for row in rows:
|
||||||
|
if row.entry is not None:
|
||||||
|
row.transaction = transactions[row.entry.transaction_id]
|
||||||
|
row.date = row.transaction.date
|
||||||
|
|
||||||
|
@property
|
||||||
|
def csv_field_names(self) -> list[str]:
|
||||||
|
return ["date", "summary", "debit", "credit", "balance"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def csv_filename(self) -> str:
|
||||||
|
return "ledger-{currency}-{account}-{period}.csv".format(
|
||||||
|
currency=self.currency.code, account=self.account.code,
|
||||||
|
period=self.period.spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def period_chooser(self) -> PeriodChooser:
|
||||||
|
return LedgerPeriodChooser(self.currency, self.account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_chooser(self) -> ReportChooser:
|
||||||
|
return ReportChooser(ReportType.LEDGER,
|
||||||
|
currency=self.currency,
|
||||||
|
account=self.account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
|
def as_html_page(self) -> str:
|
||||||
|
pagination: Pagination = Pagination[LedgerRow](self.rows)
|
||||||
|
rows: list[LedgerRow] = pagination.list
|
||||||
|
self.populate_rows(rows)
|
||||||
|
if len(rows) > 0 and rows[-1].is_total:
|
||||||
|
self.total_row = rows[-1]
|
||||||
|
rows = rows[:-1]
|
||||||
|
return render_template("accounting/report/ledger.html",
|
||||||
|
list=rows, pagination=pagination, report=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currency_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
:return: The currency options.
|
||||||
|
"""
|
||||||
|
def get_url(currency: Currency):
|
||||||
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=currency, account=self.account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=currency, account=self.account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
|
in_use: set[str] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.currency_code)
|
||||||
|
.group_by(JournalEntry.currency_code)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.code == self.currency.code)
|
||||||
|
for x in Currency.query.filter(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code).all()]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account_options(self) -> list[OptionLink]:
|
||||||
|
"""Returns the account options.
|
||||||
|
|
||||||
|
:return: The account options.
|
||||||
|
"""
|
||||||
|
def get_url(account: Account):
|
||||||
|
if self.period.is_default:
|
||||||
|
return url_for("accounting.report.ledger-default",
|
||||||
|
currency=self.currency, account=account)
|
||||||
|
return url_for("accounting.report.ledger",
|
||||||
|
currency=self.currency, account=account,
|
||||||
|
period=self.period)
|
||||||
|
|
||||||
|
in_use: set[int] = set(db.session.scalars(
|
||||||
|
sa.select(JournalEntry.account_id)
|
||||||
|
.filter(JournalEntry.currency_code == self.currency.code)
|
||||||
|
.group_by(JournalEntry.account_id)).all())
|
||||||
|
return [OptionLink(str(x), get_url(x), x.id == self.account.id)
|
||||||
|
for x in Account.query.filter(Account.id.in_(in_use))
|
||||||
|
.order_by(Account.base_code, Account.no).all()]
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, Response
|
from flask import Blueprint, request, Response
|
||||||
|
|
||||||
|
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 .period import Period
|
from .period import Period
|
||||||
from .reports import Journal
|
from .reports import Journal, Ledger
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("report", __name__)
|
bp: Blueprint = Blueprint("report", __name__)
|
||||||
"""The view blueprint for the reports."""
|
"""The view blueprint for the reports."""
|
||||||
@ -58,3 +59,47 @@ def __get_journal_list(period: Period) -> str | Response:
|
|||||||
if "as" in request.args and request.args["as"] == "csv":
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
return report.as_csv_download()
|
return report.as_csv_download()
|
||||||
return report.as_html_page()
|
return report.as_html_page()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("ledger/<currency:currency>/<account:account>",
|
||||||
|
endpoint="ledger-default")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_default_ledger_list(currency: Currency, account: Account) \
|
||||||
|
-> str | Response:
|
||||||
|
"""Returns the ledger in the default period.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:return: The ledger in the default period.
|
||||||
|
"""
|
||||||
|
return __get_ledger_list(currency, account, Period.get_instance())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
|
||||||
|
endpoint="ledger")
|
||||||
|
@has_permission(can_view)
|
||||||
|
def get_ledger_list(currency: Currency, account: Account, period: Period) \
|
||||||
|
-> str | Response:
|
||||||
|
"""Returns the ledger.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param period: The period.
|
||||||
|
:return: The ledger in the period.
|
||||||
|
"""
|
||||||
|
return __get_ledger_list(currency, account, period)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_ledger_list(currency: Currency, account: Account, period: Period) \
|
||||||
|
-> str | Response:
|
||||||
|
"""Returns the ledger.
|
||||||
|
|
||||||
|
:param currency: The currency.
|
||||||
|
:param account: The account.
|
||||||
|
:param period: The period.
|
||||||
|
:return: The ledger in the period.
|
||||||
|
"""
|
||||||
|
report: Ledger = Ledger(currency, account, period)
|
||||||
|
if "as" in request.args and request.args["as"] == "csv":
|
||||||
|
return report.as_csv_download()
|
||||||
|
return report.as_html_page()
|
||||||
|
@ -122,6 +122,11 @@ td.accounting-amount {
|
|||||||
.accounting-mobile-journal-credit {
|
.accounting-mobile-journal-credit {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
.accounting-ledger-table tfoot {
|
||||||
|
border-top: 1px double black;
|
||||||
|
font-weight: bolder;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* The Material Design text field (floating form control in Bootstrap) */
|
/* The Material Design text field (floating form control in Bootstrap) */
|
||||||
.accounting-material-text-field {
|
.accounting-material-text-field {
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
ledger-mobile-row.html: The row in the ledger for the mobile devices
|
||||||
|
|
||||||
|
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/5
|
||||||
|
#}
|
||||||
|
<div>
|
||||||
|
{% if item.date is not none %}
|
||||||
|
<div class="text-muted small">
|
||||||
|
{{ item.date|accounting_format_date }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.summary is not none %}
|
||||||
|
<div>{{ item.summary }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if item.debit is not none %}
|
||||||
|
<span class="badge rounded-pill bg-success">+{{ item.debit|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.credit is not none %}
|
||||||
|
<span class="badge rounded-pill bg-warning">-{{ item.credit|accounting_format_amount }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge rounded-pill bg-primary">{{ item.balance|accounting_format_amount }}</span>
|
||||||
|
</div>
|
@ -27,7 +27,7 @@ First written: 2023/3/4
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="accounting-report-chooser">
|
<ul class="dropdown-menu" aria-labelledby="accounting-report-chooser">
|
||||||
{% for report in report_chooser %}
|
{% for report in report_chooser %}
|
||||||
<li><a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">{{ report.name }}</a></li>
|
<li><a class="dropdown-item {% if report.is_active %} active {% endif %}" href="{{ report.url }}">{{ report.title }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,7 +41,8 @@ First written: 2023/3/4
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_EXPENSE)|accounting_append_next }}">
|
||||||
{{ A_("Cash Expense") }}</a>
|
{{ A_("Cash Expense") }}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
<a class="dropdown-item" href="{{ url_for("accounting.transaction.create", txn_type=report.txn_types.CASH_INCOME)|accounting_append_next }}">
|
||||||
@ -61,9 +62,9 @@ First written: 2023/3/4
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
<i class="fa-solid fa-calendar-day"></i>
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
{{ report.period.desc }}
|
{{ report.period.desc|title }}
|
||||||
</button>
|
</button>
|
||||||
<a class="btn btn-primary" role="button" href="?as=csv">
|
<a class="btn btn-primary" role="button" href="{{ report.csv_uri }}">
|
||||||
<i class="fa-solid fa-download"></i>
|
<i class="fa-solid fa-download"></i>
|
||||||
{{ A_("Download") }}
|
{{ A_("Download") }}
|
||||||
</a>
|
</a>
|
||||||
@ -106,7 +107,7 @@ First written: 2023/3/4
|
|||||||
<tr class="accounting-clickable accounting-table-row-link" data-href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
|
<tr class="accounting-clickable accounting-table-row-link" data-href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
|
||||||
<td>{{ item.transaction.date|accounting_format_date }}</td>
|
<td>{{ item.transaction.date|accounting_format_date }}</td>
|
||||||
<td>{{ item.currency.name }}</td>
|
<td>{{ item.currency.name }}</td>
|
||||||
<td>{{ item.account.title }}</td>
|
<td>{{ item.account }}</td>
|
||||||
<td>{{ "" if item.summary is none else item.summary }}</td>
|
<td>{{ "" if item.summary is none else item.summary }}</td>
|
||||||
<td class="accounting-amount">{{ "" if not item.is_debit else item.amount|accounting_format_amount }}</td>
|
<td class="accounting-amount">{{ "" if not item.is_debit else item.amount|accounting_format_amount }}</td>
|
||||||
<td class="accounting-amount">{{ "" if item.is_debit else item.amount|accounting_format_amount }}</td>
|
<td class="accounting-amount">{{ "" if item.is_debit else item.amount|accounting_format_amount }}</td>
|
||||||
@ -124,7 +125,7 @@ First written: 2023/3/4
|
|||||||
{{ item.transaction.date|accounting_format_date }}
|
{{ item.transaction.date|accounting_format_date }}
|
||||||
{{ item.account.title }}
|
{{ item.account.title }}
|
||||||
{% if item.currency_code != accounting_default_currency_code() %}
|
{% if item.currency_code != accounting_default_currency_code() %}
|
||||||
<span class="badge rounded-pill bg-info">{{ item.currency.code }}</span>
|
<span class="badge rounded-pill bg-info">{{ item.currency_code }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if item.summary is not none %}
|
{% if item.summary is not none %}
|
||||||
|
211
src/accounting/templates/accounting/report/ledger.html
Normal file
211
src/accounting/templates/accounting/report/ledger.html
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
{#
|
||||||
|
The Mia! Accounting Flask Project
|
||||||
|
ledger.html: The ledger
|
||||||
|
|
||||||
|
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/5
|
||||||
|
#}
|
||||||
|
{% 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>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/table-row-link.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% block title %}{{ _("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency, account=report.account|title, period=report.period.desc|title) }}{% 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 %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ report.currency.name|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-book"></i>
|
||||||
|
{{ report.account.title|title }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ report.period.desc|title }}
|
||||||
|
</button>
|
||||||
|
<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 %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-money-bill-wave"></i>
|
||||||
|
{{ A_("Currency") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for currency in report.currency_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if currency.is_active %} active {% endif %}" href="{{ currency.url }}">
|
||||||
|
{{ currency.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fa-solid fa-book"></i>
|
||||||
|
{{ A_("Account") }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for account in report.account_options %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
|
{{ account.title|title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-period-chooser-modal">
|
||||||
|
<i class="fa-solid fa-calendar-day"></i>
|
||||||
|
{{ A_("Period") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with period = report.period, period_chooser = report.period_chooser %}
|
||||||
|
{% include "accounting/report/include/period-chooser.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if list %}
|
||||||
|
{% include "accounting/include/pagination.html" %}
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover d-none d-md-table accounting-ledger-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{ A_("Date") }}</th>
|
||||||
|
<th scope="col">{{ A_("Summary") }}</th>
|
||||||
|
<th class="accounting-amount" scope="col">{{ A_("Debit") }}</th>
|
||||||
|
<th class="accounting-amount" scope="col">{{ A_("Credit") }}</th>
|
||||||
|
<th class="accounting-amount" scope="col">{{ A_("Balance") }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in list %}
|
||||||
|
<tr {% if item.transaction is not none %} class="accounting-clickable accounting-table-row-link" data-href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}" {% endif %}>
|
||||||
|
<td>{{ item.date|accounting_format_date }}</td>
|
||||||
|
<td>{{ "" if item.summary is none else item.summary }}</td>
|
||||||
|
<td class="accounting-amount">{{ "" if item.debit is none else item.debit|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ "" if item.credit is none else item.credit|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ item.balance|accounting_format_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% if report.total_row is not none %}
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">{{ A_("Total") }}</td>
|
||||||
|
<td class="accounting-amount">{{ report.total_row.debit|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ report.total_row.credit|accounting_format_amount }}</td>
|
||||||
|
<td class="accounting-amount">{{ report.total_row.balance|accounting_format_amount }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="list-group d-md-none">
|
||||||
|
{% for item in list %}
|
||||||
|
{% if item.transaction is not none %}
|
||||||
|
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.transaction.detail", txn=item.transaction)|accounting_append_next }}">
|
||||||
|
{% include "accounting/report/include/ledger-mobile-row.html" %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
|
{% include "accounting/report/include/ledger-mobile-row.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if report.total_row is not none %}
|
||||||
|
{% with item = report.total_row %}
|
||||||
|
<div class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||||
|
{% include "accounting/report/include/ledger-mobile-row.html" %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ A_("There is no data.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user