21 Commits

Author SHA1 Message Date
f3c558f48a Advanced to version 1.4.1. 2023-04-22 18:22:47 +08:00
988757d30e Revised the JavaScript journal entry line item editor to only override the description with the description of the original line item when there is no existing description. 2023-04-20 00:28:28 +08:00
50cea90d1b Revised the JavaScript journal entry line item editor to allow editing the description for offsets and partially-offset original items. 2023-04-20 00:26:58 +08:00
71dfb6f003 Advanced to version 1.4.0. 2023-04-18 09:33:35 +08:00
be628b4aa1 Updated the Sphinx documentation. 2023-04-18 09:33:00 +08:00
5d444adec4 Updated the translation. 2023-04-18 09:32:38 +08:00
014d67f7b8 Removed the period filter from the unapplied original line items and unmatched offsets. It does not make sense for these two reports. 2023-04-18 09:21:42 +08:00
26b4d4388f Revised the imports in the "accounting.report.reports.unmatched" module. 2023-04-18 09:10:25 +08:00
6e2e92d0fb Removed the redundant currency from the title of the reports when the currency is the default currency. 2023-04-18 08:46:23 +08:00
b1f87cb707 Updated the icon of the unmatched offsets. 2023-04-18 08:20:51 +08:00
928dea8312 Added the account information to the original line item selector of the journal entry form. 2023-04-18 08:15:33 +08:00
b8cec8a2af Revised the account options in the report toolbar to be scrollable. 2023-04-18 08:10:33 +08:00
b6ae946f32 Removed the account code from the journal entry form for mobile screens. 2023-04-18 08:10:23 +08:00
a9acc18a6f Removed the account code from the journal entry detail for mobile screens. 2023-04-18 07:13:10 +08:00
5468010c87 Removed the account code from the account list with unmatched offsets for mobile screens. 2023-04-18 07:10:26 +08:00
b505e380df Removed the account code from the account list with unapplied original line items for mobile screens. 2023-04-18 07:10:15 +08:00
412da170e1 Added the get_net_balances function to the "accounting.report.utils.unapplied" module to replace the __get_net_balances methods of the OffsetMatcher and UnappliedOriginalLineItems classes. 2023-04-18 07:04:00 +08:00
fa237795cf Simplified the query for the unapplied original line items, replacing the offset matcher with its own query. 2023-04-18 01:26:02 +08:00
e2f854b5cc Changed the unmatched offsets from a module to a report, and to show both the unapplied original line items and the unmatched offsets instead of only the unmatched offsets, and added the accumulated balance, in order for ease of use. Removed the match information from the unapplied original line item report. Added the currency and period filters to both the unapplied original line item report and unmatched offset reports. 2023-04-18 01:12:04 +08:00
f8895e3bff Revised the documentation of the "__get_unmatched_offsets" method of the OffsetMatcher class. 2023-04-16 22:52:14 +08:00
84ad065782 Merged the "accounting.utils.unapplied" module into the "accounting.utils.offset_matcher" module as the "__get_unapplied" method of the OffsetMatcher class. It is only used in the offset matcher. 2023-04-16 22:51:46 +08:00
54 changed files with 1702 additions and 897 deletions

View File

@ -76,6 +76,22 @@ accounting.report.reports.unapplied\_accounts module
:undoc-members:
:show-inheritance:
accounting.report.reports.unmatched module
------------------------------------------
.. automodule:: accounting.report.reports.unmatched
:members:
:undoc-members:
:show-inheritance:
accounting.report.reports.unmatched\_accounts module
----------------------------------------------------
.. automodule:: accounting.report.reports.unmatched_accounts
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -28,6 +28,14 @@ accounting.report.utils.csv\_export module
:undoc-members:
:show-inheritance:
accounting.report.utils.offset\_matcher module
----------------------------------------------
.. automodule:: accounting.report.utils.offset_matcher
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.option\_link module
-------------------------------------------
@ -60,6 +68,14 @@ accounting.report.utils.unapplied module
:undoc-members:
:show-inheritance:
accounting.report.utils.unmatched module
----------------------------------------
.. automodule:: accounting.report.utils.unmatched
:members:
:undoc-members:
:show-inheritance:
accounting.report.utils.urls module
-----------------------------------

View File

@ -13,7 +13,6 @@ Subpackages
accounting.journal_entry
accounting.option
accounting.report
accounting.unmatched_offset
accounting.utils
Submodules

View File

@ -1,29 +0,0 @@
accounting.unmatched\_offset package
====================================
Submodules
----------
accounting.unmatched\_offset.queries module
-------------------------------------------
.. automodule:: accounting.unmatched_offset.queries
:members:
:undoc-members:
:show-inheritance:
accounting.unmatched\_offset.views module
-----------------------------------------
.. automodule:: accounting.unmatched_offset.views
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: accounting.unmatched_offset
:members:
:undoc-members:
:show-inheritance:

View File

@ -52,14 +52,6 @@ accounting.utils.offset\_alias module
:undoc-members:
:show-inheritance:
accounting.utils.offset\_matcher module
---------------------------------------
.. automodule:: accounting.utils.offset_matcher
:members:
:undoc-members:
:show-inheritance:
accounting.utils.options module
-------------------------------
@ -108,14 +100,6 @@ accounting.utils.strip\_text module
:undoc-members:
:show-inheritance:
accounting.utils.unapplied module
---------------------------------
.. automodule:: accounting.utils.unapplied
:members:
:undoc-members:
:show-inheritance:
accounting.utils.user module
----------------------------

View File

@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
project = 'Mia! Accounting'
copyright = '2023, imacat'
author = 'imacat'
release = '1.3.3'
release = '1.4.1'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -17,7 +17,7 @@
[project]
name = "mia-accounting"
version = "1.3.3"
version = "1.4.1"
description = "A Flask accounting module."
readme = "README.rst"
requires-python = ">=3.11"

View File

@ -91,7 +91,4 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
from . import option
option.init_app(bp)
from . import unmatched_offset
unmatched_offset.init_app(bp)
app.register_blueprint(bp, url_prefix=url_prefix)

View File

@ -267,6 +267,19 @@ class LineItemForm(FlaskForm):
self.journal_entry_form: JournalEntryForm | None = None
"""The source journal entry form."""
@property
def account_title(self) -> str:
"""Returns the title of the account.
:return: The title of the account.
"""
if self.account_code.data is None:
return ""
account: Account | None = Account.find_by_code(self.account_code.data)
if account is None:
return ""
return account.title
@property
def account_text(self) -> str:
"""Returns the text representation of the account.

View File

@ -32,6 +32,8 @@ class AccountOption:
"""The account ID."""
self.code: str = account.code
"""The account code."""
self.title: str = account.title
"""The account title."""
self.query_values: list[str] = account.query_values
"""The values to be queried."""
self.__str: str = str(account)

View File

@ -54,6 +54,14 @@ class DescriptionAccount:
"""
return str(self.__account)
@property
def title(self) -> str:
"""Returns the account title.
:return: The account title.
"""
return self.__account.title
def add_freq(self, freq: int) -> None:
"""Adds the frequency of an account.

View File

@ -742,7 +742,18 @@ class JournalEntryLineItem(db.Model):
:return: The debit amount, or None if this is not a debit line item.
"""
return self.amount if self.is_debit else None
if not hasattr(self, "__debit"):
setattr(self, "__debit", self.amount if self.is_debit else None)
return getattr(self, "__debit")
@debit.setter
def debit(self, debit: Decimal | None) -> None:
"""Sets the debit amount.
:param debit: The debit amount.
:return: None.
"""
setattr(self, "__debit", debit)
@property
def credit(self) -> Decimal | None:
@ -750,7 +761,18 @@ class JournalEntryLineItem(db.Model):
:return: The credit amount, or None if this is not a credit line item.
"""
return None if self.is_debit else self.amount
if not hasattr(self, "__credit"):
setattr(self, "__credit", None if self.is_debit else self.amount)
return getattr(self, "__credit")
@credit.setter
def credit(self, credit: Decimal | None) -> None:
"""Sets the credit amount.
:param credit: The credit amount.
:return: None.
"""
setattr(self, "__credit", credit)
@property
def net_balance(self) -> Decimal:
@ -773,6 +795,25 @@ class JournalEntryLineItem(db.Model):
"""
setattr(self, "__net_balance", net_balance)
@property
def balance(self) -> Decimal:
"""Returns the net balance.
:return: The net balance.
"""
if not hasattr(self, "__balance"):
setattr(self, "__balance", Decimal("0"))
return getattr(self, "__balance")
@balance.setter
def balance(self, balance: Decimal) -> None:
"""Sets the net balance.
:param balance: The net balance.
:return: None.
"""
setattr(self, "__balance", balance)
@property
def offsets(self) -> list[t.Self]:
"""Returns the offset items.
@ -788,6 +829,26 @@ class JournalEntryLineItem(db.Model):
setattr(self, "__offsets", offsets)
return getattr(self, "__offsets")
@property
def is_offset(self) -> bool:
"""Returns whether the line item is an offset.
:return: True if the line item is an offset, or False otherwise.
"""
if not hasattr(self, "__is_offset"):
setattr(self, "__is_offset", False)
return getattr(self, "__is_offset")
@is_offset.setter
def is_offset(self, is_offset: bool) -> None:
"""Sets whether the line item is an offset.
:param is_offset: True if the line item is an offset, or False
otherwise.
:return: None.
"""
setattr(self, "__is_offset", is_offset)
@property
def match(self) -> t.Self | None:
"""Returns the match of the line item.
@ -822,6 +883,7 @@ class JournalEntryLineItem(db.Model):
self.journal_entry.date.month,
self.journal_entry.date.day),
"" if self.description is None else self.description,
str(self.account),
format_amount(self.amount)]

View File

@ -21,20 +21,21 @@ from datetime import date
from decimal import Decimal
from flask import render_template, Response
from sqlalchemy.orm import selectinload
from accounting.locale import gettext
from accounting.models import Account, JournalEntryLineItem
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
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.unapplied import get_accounts_with_unapplied, \
get_net_balances
from accounting.report.utils.urls import unapplied_url
from accounting.utils.offset_matcher import OffsetMatcher
from accounting.utils.pagination import Pagination
from accounting.utils.permission import can_edit
class CSVRow(BaseCSVRow):
@ -75,25 +76,25 @@ class CSVRow(BaseCSVRow):
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, account: Account,
is_mark_matches: bool,
def __init__(self, currency: Currency,
account: Account,
pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param is_mark_matches: Whether to mark the matched offsets.
:param pagination: The pagination.
:param line_items: The line items.
"""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
self.is_mark_matches: bool = is_mark_matches
"""Whether to mark the matched offsets."""
@property
def has_data(self) -> bool:
@ -109,22 +110,32 @@ class PageParams(BasePageParams):
:return: The report chooser.
"""
return ReportChooser(ReportType.UNAPPLIED,
return ReportChooser(ReportType.UNAPPLIED, currency=self.currency,
account=self.account)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: unapplied_url(x, self.account), self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] = [OptionLink(gettext("Accounts"),
unapplied_url(None),
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unapplied_url(self.currency, None),
False)]
options.extend([OptionLink(str(x),
unapplied_url(x),
options.extend(
[OptionLink(str(x), unapplied_url(self.currency, x),
x.id == self.account.id)
for x in get_accounts_with_unapplied()])
for x in get_accounts_with_unapplied(self.currency)])
return options
@ -146,27 +157,47 @@ def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
class UnappliedOriginalLineItems(BaseReport):
"""The unapplied original line items."""
def __init__(self, account: Account):
def __init__(self, currency: Currency, account: Account):
"""Constructs the unapplied original line items.
:param currency: The currency.
:param account: The account.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
offset_matcher: OffsetMatcher = OffsetMatcher(self.__account)
self.__line_items: list[JournalEntryLineItem] \
= offset_matcher.unapplied
= self.__query_line_items()
"""The line items."""
self.__is_mark_matches: bool \
= can_edit() and len(offset_matcher.unmatched_offsets) > 0
"""Whether to mark the matched offsets."""
def __query_line_items(self) -> list[JournalEntryLineItem]:
"""Queries and returns the line items.
:return: The line items.
"""
net_balances: dict[int, Decimal | None] \
= get_net_balances(self.__currency, self.__account)
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query \
.join(Account).join(JournalEntry) \
.filter(JournalEntryLineItem.id.in_(net_balances)) \
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
for line_item in line_items:
line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \
else net_balances[line_item.id]
return line_items
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = f"unapplied-{self.__account.code}.csv"
filename: str = "unapplied-{currency}-{account}.csv"\
.format(currency=self.__currency.code, account=self.__account.code)
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
@ -177,8 +208,8 @@ class UnappliedOriginalLineItems(BaseReport):
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(account=self.__account,
is_mark_matches=self.__is_mark_matches,
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
pagination=pagination,
line_items=pagination.list)
return render_template("accounting/report/unapplied.html",

View File

@ -23,7 +23,7 @@ from decimal import Decimal
from flask import render_template, Response
from accounting.locale import gettext
from accounting.models import Account
from accounting.models import Currency, 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
@ -60,11 +60,14 @@ class CSVRow(BaseCSVRow):
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, accounts: list[Account]):
def __init__(self, currency: Currency, accounts: list[Account]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param accounts: The accounts.
"""
self.currency: Currency = currency
"""The currency."""
self.accounts: list[Account] = accounts
"""The accounts."""
@ -82,7 +85,17 @@ class PageParams(BasePageParams):
:return: The report chooser.
"""
return ReportChooser(ReportType.UNAPPLIED)
return ReportChooser(ReportType.UNAPPLIED, currency=self.currency,
account=None)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(lambda x: unapplied_url(x, None),
self.currency)
@property
def account_options(self) -> list[OptionLink]:
@ -90,12 +103,12 @@ class PageParams(BasePageParams):
:return: The account options.
"""
options: list[OptionLink] = [OptionLink(gettext("Accounts"),
unapplied_url(None),
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unapplied_url(self.currency, None),
True)]
options.extend([OptionLink(str(x),
unapplied_url(x),
False)
options.extend(
[OptionLink(str(x), unapplied_url(self.currency, x), False)
for x in self.accounts])
return options
@ -115,9 +128,14 @@ def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
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()
def __init__(self, currency: Currency):
"""Constructs the outstanding balances.
:param currency: The currency.
"""
self.__currency: Currency = currency
"""The currency."""
self.__accounts: list[Account] = get_accounts_with_unapplied(currency)
"""The accounts."""
def csv(self) -> Response:
@ -134,4 +152,5 @@ class AccountsWithUnappliedOriginalLineItems(BaseReport):
:return: The report as HTML.
"""
return render_template("accounting/report/unapplied-accounts.html",
report=PageParams(accounts=self.__accounts))
report=PageParams(currency=self.__currency,
accounts=self.__accounts))

View File

@ -0,0 +1,214 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
# 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 unmatched offsets.
"""
from datetime import date
from decimal import Decimal
from flask import render_template, Response
from flask_babel import LazyString
from accounting.locale import gettext
from accounting.models import Currency, Account, JournalEntryLineItem
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.offset_matcher import OffsetMatcher, OffsetPair
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.unmatched import get_accounts_with_unmatched
from accounting.report.utils.urls import unmatched_url
from accounting.utils.pagination import Pagination
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | date, currency: str,
description: str | None, debit: str | Decimal,
credit: str | Decimal, balance: str | Decimal):
"""Constructs a row in the CSV.
:param journal_entry_date: The journal entry date.
:param currency: The currency.
:param description: The description.
:param debit: The debit amount.
:param credit: The credit amount.
:param balance: The balance.
"""
self.date: str | date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
self.description: str | None = description
"""The description."""
self.debit: str | Decimal | None = debit
"""The debit amount."""
self.credit: str | Decimal | None = credit
"""The credit amount."""
self.balance: str | Decimal = balance
"""The balance."""
@property
def values(self) -> list[str | date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.
"""
return [self.date, self.currency, self.description, self.debit,
self.credit, self.balance]
class PageParams(BasePageParams):
"""The HTML page parameters."""
def __init__(self, currency: Currency,
account: Account,
match_status: str | LazyString,
matched_pairs: list[OffsetPair],
pagination: Pagination[JournalEntryLineItem],
line_items: list[JournalEntryLineItem]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param account: The account.
:param match_status: The match status message.
:param matched_pairs: A list of matched pairs.
:param pagination: The pagination.
:param line_items: The line items.
"""
self.currency: Currency = currency
"""The currency."""
self.account: Account = account
"""The account."""
self.match_status: str | LazyString = match_status
"""The match status message."""
self.matched_pairs: list[OffsetPair] = matched_pairs
"""A list of matched pairs."""
self.pagination: Pagination[JournalEntryLineItem] = pagination
"""The pagination."""
self.line_items: list[JournalEntryLineItem] = line_items
"""The line items."""
@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.line_items) > 0
@property
def report_chooser(self) -> ReportChooser:
"""Returns the report chooser.
:return: The report chooser.
"""
return ReportChooser(ReportType.UNMATCHED, currency=self.currency,
account=self.account)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(
lambda x: unmatched_url(x, self.account), self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unmatched_url(self.currency, None),
False)]
options.extend(
[OptionLink(str(x), unmatched_url(self.currency, x),
x.id == self.account.id)
for x in get_accounts_with_unmatched(self.currency)])
return options
def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
"""Composes and returns the CSV rows from the line items.
:param line_items: The line items.
:return: The CSV rows.
"""
rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
gettext("Description"), gettext("Debit"),
gettext("Credit"), gettext("Balance"))]
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
x.description, x.debit, x.credit, x.balance)
for x in line_items])
return rows
class UnmatchedOffsets(BaseReport):
"""The unmatched offsets."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the unmatched offsets.
:param currency: The currency.
:param account: The account.
"""
self.__currency: Currency = currency
"""The currency."""
self.__account: Account = account
"""The account."""
offset_matcher: OffsetMatcher \
= OffsetMatcher(self.__currency, self.__account)
self.__line_items: list[JournalEntryLineItem] \
= offset_matcher.line_items
"""The line items."""
self.__match_status: str | LazyString = offset_matcher.status
"""The match status message."""
self.__matched_pairs: list[OffsetPair] = offset_matcher.matched_pairs
"""A list of matched pairs."""
def csv(self) -> Response:
"""Returns the report as CSV for download.
:return: The response of the report for download.
"""
filename: str = "unmatched-{currency}-{account}.csv"\
.format(currency=self.__currency.code, account=self.__account.code)
return csv_download(filename, get_csv_rows(self.__line_items))
def html(self) -> str:
"""Composes and returns the report as HTML.
:return: The report as HTML.
"""
pagination: Pagination[JournalEntryLineItem] \
= Pagination[JournalEntryLineItem](self.__line_items,
is_reversed=True)
params: PageParams = PageParams(currency=self.__currency,
account=self.__account,
match_status=self.__match_status,
matched_pairs=self.__matched_pairs,
pagination=pagination,
line_items=pagination.list)
return render_template("accounting/report/unmatched.html",
report=params)

View File

@ -0,0 +1,157 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
# 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 unmatched offsets.
"""
from datetime import date
from decimal import Decimal
from flask import render_template, Response
from accounting.locale import gettext
from accounting.models import Currency, 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.unmatched import get_accounts_with_unmatched
from accounting.report.utils.urls import unmatched_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, currency: Currency, accounts: list[Account]):
"""Constructs the HTML page parameters.
:param currency: The currency.
:param accounts: The accounts.
"""
self.currency: Currency = currency
"""The currency."""
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.UNMATCHED, currency=self.currency,
account=None)
@property
def currency_options(self) -> list[OptionLink]:
"""Returns the currency options.
:return: The currency options.
"""
return self._get_currency_options(lambda x: unmatched_url(x, None),
self.currency)
@property
def account_options(self) -> list[OptionLink]:
"""Returns the account options.
:return: The account options.
"""
options: list[OptionLink] \
= [OptionLink(gettext("Accounts"),
unmatched_url(self.currency, None),
True)]
options.extend(
[OptionLink(str(x), unmatched_url(self.currency, 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 AccountsWithUnmatchedOffsets(BaseReport):
"""The accounts with unmatched offsets."""
def __init__(self, currency: Currency):
"""Constructs the outstanding balances.
:param currency: The currency.
"""
self.__currency: Currency = currency
"""The currency."""
self.__accounts: list[Account] \
= get_accounts_with_unmatched(currency)
"""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/unmatched-accounts.html",
report=PageParams(currency=self.__currency,
accounts=self.__accounts))

View File

@ -0,0 +1,180 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 forms for the unmatched offset management.
"""
from decimal import Decimal
import sqlalchemy as sa
from flask_babel import LazyString
from sqlalchemy.orm import selectinload
from accounting.locale import lazy_gettext
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.report.utils.unapplied import get_net_balances
class OffsetPair:
"""A pair of an original line item and its offset."""
def __init__(self, original_line_item: JournalEntryLineItem,
offset: JournalEntryLineItem):
"""Constructs a pair of an original line item and its offset.
:param original_line_item: The original line item.
:param offset: The offset.
"""
self.original_line_item: JournalEntryLineItem = original_line_item
"""The original line item."""
self.offset: JournalEntryLineItem = offset
"""The offset."""
class OffsetMatcher:
"""The offset matcher."""
def __init__(self, currency: Currency, account: Account):
"""Constructs the offset matcher.
:param currency: The currency.
:param account: The account.
"""
self.__currency: Account = currency
"""The currency."""
self.__account: Account = account
"""The account."""
self.matched_pairs: list[OffsetPair] = []
"""A list of matched pairs."""
self.line_items: list[JournalEntryLineItem] = []
"""The unapplied debits or credits and unmatched offsets."""
self.unapplied: list[JournalEntryLineItem] = []
"""The unapplied debits or credits."""
self.unmatched: list[JournalEntryLineItem] = []
"""The unmatched offsets."""
self.__find_matches()
def __find_matches(self) -> None:
"""Finds the matched original line items and their offsets.
:return: None.
"""
self.__get_line_items()
if len(self.unapplied) == 0 or len(self.unmatched) == 0:
return
remains: list[JournalEntryLineItem] = self.unmatched.copy()
for original_item in self.unapplied:
offset_candidates: list[JournalEntryLineItem] \
= [x for x in remains
if (x.journal_entry.date > original_item.journal_entry.date
or (x.journal_entry.date
== original_item.journal_entry.date
and x.journal_entry.no
> original_item.journal_entry.no))
and x.currency_code == original_item.currency_code
and x.description == original_item.description
and x.amount == original_item.net_balance]
if len(offset_candidates) == 0:
continue
self.matched_pairs.append(
OffsetPair(original_item, offset_candidates[0]))
original_item.match = offset_candidates[0]
offset_candidates[0].match = original_item
remains.remove(offset_candidates[0])
def __get_line_items(self) -> None:
"""Returns the unapplied original line items and unmatched offsets of
the account.
:return: The unapplied original line items and unmatched offsets of the
account.
"""
net_balances: dict[int, Decimal | None] \
= get_net_balances(self.__currency, self.__account)
unmatched_offset_condition: sa.BinaryExpression \
= sa.and_(Account.id == self.__account.id,
JournalEntryLineItem.currency_code
== self.__currency.code,
JournalEntryLineItem.original_line_item_id.is_(None),
sa.or_(sa.and_(Account.base_code.startswith("2"),
JournalEntryLineItem.is_debit),
sa.and_(Account.base_code.startswith("1"),
sa.not_(JournalEntryLineItem.is_debit))))
self.line_items = JournalEntryLineItem.query \
.join(Account).join(JournalEntry) \
.filter(sa.or_(JournalEntryLineItem.id.in_(net_balances),
unmatched_offset_condition)) \
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
for line_item in self.line_items:
line_item.is_offset = line_item.id in net_balances
self.unapplied = [x for x in self.line_items
if x.is_offset]
for line_item in self.unapplied:
line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \
else net_balances[line_item.id]
self.unmatched = [x for x in self.line_items
if not x.is_offset]
self.__populate_accumulated_balances()
def __populate_accumulated_balances(self) -> None:
"""Populates the accumulated balances of the line items.
:return: None.
"""
balance: Decimal = Decimal("0")
for line_item in self.line_items:
amount: Decimal = line_item.amount if line_item.is_offset \
else line_item.net_balance
if line_item.is_debit:
line_item.debit = amount
line_item.credit = None
balance = balance + amount
else:
line_item.debit = None
line_item.credit = amount
balance = balance - amount
line_item.balance = balance
@property
def status(self) -> str | LazyString:
"""Returns the match status message.
:return: The match status message.
"""
if len(self.unmatched) == 0:
return lazy_gettext("There is no unmatched offset.")
if len(self.matched_pairs) == 0:
return lazy_gettext(
"%(total)s unmatched offsets without original items.",
total=len(self.unmatched))
return lazy_gettext(
"%(matches)s unmatched offsets out of %(total)s"
" can match with their original items.",
matches=len(self.matched_pairs),
total=len(self.unmatched))
def match(self) -> None:
"""Matches the original line items with offsets.
:return: None.
"""
for pair in self.matched_pairs:
pair.offset.original_line_item_id = pair.original_line_item.id

View File

@ -31,10 +31,12 @@ from accounting.models import Currency, Account
from accounting.report.period import Period, get_period
from accounting.template_globals import default_currency_code
from accounting.utils.current_account import CurrentAccount
from accounting.utils.permission import can_edit
from .option_link import OptionLink
from .report_type import ReportType
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, unmatched_url
class ReportChooser:
@ -75,6 +77,8 @@ class ReportChooser:
self.__reports.append(self.__income_statement)
self.__reports.append(self.__balance_sheet)
self.__reports.append(self.__unapplied)
if can_edit():
self.__reports.append(self.__unmatched)
for report in self.__reports:
if report.is_active:
self.current_report = report.title
@ -160,15 +164,32 @@ class ReportChooser:
"""
account: Account = self.__account
if not account.is_need_offset:
return OptionLink(gettext("Unapplied Original Line Items"),
unapplied_url(None),
return OptionLink(gettext("Unapplied Items"),
unapplied_url(self.__currency, None),
self.__active_report == ReportType.UNAPPLIED,
fa_icon="fa-solid fa-link-slash")
return OptionLink(gettext("Unapplied Original Line Items"),
unapplied_url(account),
return OptionLink(gettext("Unapplied Items"),
unapplied_url(self.__currency, self.__account),
self.__active_report == ReportType.UNAPPLIED,
fa_icon="fa-solid fa-link-slash")
@property
def __unmatched(self) -> OptionLink:
"""Returns the unmatched offsets.
:return: The unmatched offsets.
"""
account: Account = self.__account
if not account.is_need_offset:
return OptionLink(gettext("Unmatched Offsets"),
unmatched_url(self.__currency, None),
self.__active_report == ReportType.UNMATCHED,
fa_icon="fa-solid fa-file-circle-question")
return OptionLink(gettext("Unmatched Offsets"),
unmatched_url(self.__currency, self.__account),
self.__active_report == ReportType.UNMATCHED,
fa_icon="fa-solid fa-file-circle-question")
def __iter__(self) -> t.Iterator[OptionLink]:
"""Returns the iteration of the reports.

View File

@ -36,5 +36,7 @@ class ReportType(Enum):
"""The balance sheet."""
UNAPPLIED: str = "unapplied"
"""The unapplied original line items."""
UNMATCHED: str = "unmatched"
"""The unmatched offsets."""
SEARCH: str = "search"
"""The search."""

View File

@ -17,17 +17,21 @@
"""The unapplied original line item utilities.
"""
from decimal import Decimal
import sqlalchemy as sa
from accounting import db
from accounting.models import Account, JournalEntryLineItem
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.utils.cast import be
from accounting.utils.offset_alias import offset_alias
def get_accounts_with_unapplied() -> list[Account]:
def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
"""Returns the accounts with unapplied original line items.
:param currency: The currency.
:return: The accounts with unapplied original line items.
"""
offset: sa.Alias = offset_alias()
@ -39,11 +43,12 @@ def get_accounts_with_unapplied() -> list[Account]:
else_=-offset.c.amount))).label("net_balance")
select_unapplied: sa.Select \
= sa.select(JournalEntryLineItem.id)\
.join(Account)\
.join(JournalEntry).join(Account)\
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
isouter=True)\
.filter(Account.is_need_offset,
be(JournalEntryLineItem.currency_code == currency.code),
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),
@ -65,3 +70,36 @@ def get_accounts_with_unapplied() -> list[Account]:
for account in accounts:
account.count = counts[account.id]
return accounts
def get_net_balances(currency: Currency, account: Account) \
-> dict[int, Decimal | None]:
"""Returns the net balances of the unapplied line items of the account.
:param currency: The currency.
:param account: The account.
:return: The net balances of the unapplied line items of the account.
"""
offset: sa.Alias = offset_alias()
net_balance: sa.Label \
= (JournalEntryLineItem.amount
+ sa.func.sum(sa.case(
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
select_net_balances: sa.Select \
= sa.select(JournalEntryLineItem.id, net_balance) \
.join(JournalEntry).join(Account) \
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
isouter=True) \
.filter(be(Account.id == account.id),
be(JournalEntryLineItem.currency_code == currency.code),
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),
JournalEntryLineItem.is_debit))) \
.group_by(JournalEntryLineItem.id) \
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
return {x.id: x.net_balance
for x in db.session.execute(select_net_balances).all()}

View File

@ -14,26 +14,31 @@
# 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 queries for the unmatched offset management.
"""The unmatched offset utilities.
"""
import sqlalchemy as sa
from accounting import db
from accounting.models import Account, JournalEntryLineItem
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.utils.cast import be
def get_accounts_with_unmatched_offsets() -> list[Account]:
def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
"""Returns the accounts with unmatched offsets.
:param currency: The currency.
:return: The accounts with unmatched offsets, with the "count" property set
to the number of unmatched offsets.
"""
count_func: sa.Label \
= sa.func.count(JournalEntryLineItem.id).label("count")
select: sa.Select = sa.select(Account.id, count_func)\
.select_from(Account).join(JournalEntryLineItem, isouter=True)\
.select_from(Account)\
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
.filter(Account.is_need_offset,
be(JournalEntryLineItem.currency_code == currency.code),
JournalEntryLineItem.original_line_item_id.is_(None),
sa.or_(sa.and_(Account.base_code.startswith("2"),
JournalEntryLineItem.is_debit),

View File

@ -113,13 +113,35 @@ def balance_sheet_url(currency: Currency, period: Period) -> str:
currency=currency, period=period)
def unapplied_url(account: Account | None) -> str:
def unapplied_url(currency: Currency, account: Account | None) -> str:
"""Returns the URL of the unapplied original line items.
:param currency: The currency.
:param account: The account, or None to list the accounts with 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)
if currency.code == default_currency_code():
return url_for("accounting-report.unapplied-accounts-default")
return url_for("accounting-report.unapplied-accounts",
currency=currency)
return url_for("accounting-report.unapplied",
currency=currency, account=account)
def unmatched_url(currency: Currency, account: Account | None) -> str:
"""Returns the URL of the unmatched offset line items.
:param currency: The currency.
:param account: The account, or None to list the accounts with unmatched
offset line items.
:return: The URL of the unmatched offset line items.
"""
if account is None:
if currency.code == default_currency_code():
return url_for("accounting-report.unmatched-accounts-default")
return url_for("accounting-report.unmatched-accounts",
currency=currency)
return url_for("accounting-report.unmatched",
currency=currency, account=account)

View File

@ -17,20 +17,27 @@
"""The views for the report management.
"""
from flask import Blueprint, request, Response
from flask import Blueprint, request, Response, redirect, flash
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import Currency, Account
from accounting.report.period import Period, get_period
from accounting.template_globals import default_currency_code
from accounting.utils.cast import s
from accounting.utils.current_account import CurrentAccount
from accounting.utils.next_uri import or_next
from accounting.utils.options import options
from accounting.utils.permission import has_permission, can_view
from accounting.utils.permission import has_permission, can_view, can_edit
from .period import Period, get_period
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
IncomeStatement, BalanceSheet, Search
from .reports.unapplied import UnappliedOriginalLineItems
from .reports.unapplied_accounts import AccountsWithUnappliedOriginalLineItems
from .reports.unmatched import UnmatchedOffsets
from .reports.unmatched_accounts import AccountsWithUnmatchedOffsets
from .template_filters import format_amount
from .utils.offset_matcher import OffsetMatcher
from .utils.urls import unmatched_url
bp: Blueprint = Blueprint("accounting-report", __name__)
"""The view blueprint for the reports."""
@ -278,34 +285,130 @@ def __get_balance_sheet(currency: Currency, period: Period) \
return report.html()
@bp.get("unapplied", endpoint="unapplied-default")
@bp.get("unapplied", endpoint="unapplied-accounts-default")
@has_permission(can_view)
def get_default_unapplied() -> str | Response:
def get_default_unapplied_accounts() -> str | Response:
"""Returns the accounts with unapplied original line items.
:return: The accounts with unapplied original line items.
"""
return __get_unapplied_accounts(
db.session.get(Currency, default_currency_code()))
@bp.get("unapplied/<currency:currency>", endpoint="unapplied-accounts")
@has_permission(can_view)
def get_unapplied_accounts(currency: Currency) -> str | Response:
"""Returns the accounts with unapplied original line items.
:param currency: The currency.
:return: The accounts with unapplied original line items.
"""
return __get_unapplied_accounts(currency)
def __get_unapplied_accounts(currency: Currency) -> str | Response:
"""Returns the accounts with unapplied original line items.
:param currency: The currency.
:return: The accounts with unapplied original line items.
"""
report: AccountsWithUnappliedOriginalLineItems \
= AccountsWithUnappliedOriginalLineItems()
= AccountsWithUnappliedOriginalLineItems(currency)
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()
@bp.get("unapplied/<needOffsetAccount:account>", endpoint="unapplied")
@bp.get("unapplied/<currency:currency>/<needOffsetAccount:account>",
endpoint="unapplied")
@has_permission(can_view)
def get_unapplied(account: Account) -> str | Response:
def get_unapplied(currency: Currency, account: Account) -> str | Response:
"""Returns the unapplied original line items.
:param currency: The currency.
:param account: The Account.
:return: The unapplied original line items.
:return: The unapplied original line items in the period.
"""
report: UnappliedOriginalLineItems = UnappliedOriginalLineItems(account)
report: UnappliedOriginalLineItems \
= UnappliedOriginalLineItems(currency, account)
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()
@bp.get("unmatched", endpoint="unmatched-accounts-default")
@has_permission(can_edit)
def get_default_unmatched_accounts() -> str | Response:
"""Returns the accounts with unmatched offsets.
:return: The accounts with unmatched offsets.
"""
return __get_unmatched_accounts(
db.session.get(Currency, default_currency_code()))
@bp.get("unmatched/<currency:currency>", endpoint="unmatched-accounts")
@has_permission(can_edit)
def get_unmatched_accounts(currency: Currency) -> str | Response:
"""Returns the accounts with unmatched offsets.
:param currency: The currency.
:return: The accounts with unmatched offsets.
"""
return __get_unmatched_accounts(currency)
def __get_unmatched_accounts(currency: Currency) -> str | Response:
"""Returns the accounts with unmatched offsets.
:param currency: The currency.
:return: The accounts with unmatched offsets.
"""
report: AccountsWithUnmatchedOffsets \
= AccountsWithUnmatchedOffsets(currency)
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()
@bp.get("unmatched/<currency:currency>/<needOffsetAccount:account>",
endpoint="unmatched")
@has_permission(can_edit)
def get_unmatched(currency: Currency, account: Account) -> str | Response:
"""Returns the unmatched offsets.
:param currency: The currency.
:param account: The Account.
:return: The unmatched offsets in the period.
"""
report: UnmatchedOffsets = UnmatchedOffsets(currency, account)
if "as" in request.args and request.args["as"] == "csv":
return report.csv()
return report.html()
@bp.post("match-offsets/<currency:currency>/<needOffsetAccount:account>",
endpoint="match-offsets")
@has_permission(can_edit)
def match_offsets(currency: Currency, account: Account) -> redirect:
"""Matches the original line items with their offsets.
:return: Redirection to the view of the unmatched offsets.
"""
matcher: OffsetMatcher = OffsetMatcher(currency, account)
if len(matcher.matched_pairs) == 0:
flash(s(lazy_gettext("No more offset to match automatically.")),
"success")
return redirect(or_next(
unmatched_url(currency, account)))
matcher.match()
db.session.commit()
flash(s(lazy_gettext("Matched %(matches)s offsets.",
matches=len(matcher.matched_pairs))), "success")
return redirect(or_next(unmatched_url(currency, account)))
@bp.get("search", endpoint="search")
@has_permission(can_view)
def search() -> str | Response:

View File

@ -39,6 +39,10 @@
.accounting-toolbar {
display: flex;
}
.accounting-toolbar-accounts {
max-height: 20rem;
overflow-y: scroll;
}
.accounting-toolbar .input-group > .input-group-text {
padding: 0;
background-color: transparent;
@ -322,7 +326,7 @@ a.accounting-report-table-row {
font-style: italic;
}
.accounting-unapplied-table .accounting-report-table-row {
grid-template-columns: 1fr 1fr 5fr 1fr 1fr;
grid-template-columns: 1fr 5fr 1fr 1fr;
}
.accounting-unapplied-account-table .accounting-report-table-row {
display: flex;
@ -331,6 +335,16 @@ a.accounting-report-table-row {
.accounting-unapplied-account-table .accounting-report-table-header .accounting-report-table-row {
display: block;
}
.accounting-unmatched-table .accounting-report-table-row {
grid-template-columns: 1fr 5fr 1fr 1fr 1fr;
}
.accounting-unmatched-account-table .accounting-report-table-row {
display: flex;
justify-content: space-between;
}
.accounting-unmatched-account-table .accounting-report-table-header .accounting-report-table-row {
display: block;
}
/* The accounting report */
.accounting-mobile-journal-credit {

View File

@ -364,12 +364,13 @@ class DescriptionEditorAccount extends JournalEntryAccount {
*
* @param editor {DescriptionEditor} the description editor
* @param code {string} the account code
* @param title {string} the account title
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, code, text, isNeedOffset, button) {
super(code, text, isNeedOffset);
constructor(editor, code, title, text, isNeedOffset, button) {
super(code, title, text, isNeedOffset);
this.#element = button;
this.#element.onclick = () => editor.selectAccount(this);
}
@ -424,7 +425,7 @@ class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, button) {
super(editor, button.dataset.code, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
super(editor, button.dataset.code, button.dataset.title, button.dataset.text, button.classList.contains("accounting-account-is-need-offset"), button);
}
}
@ -441,7 +442,7 @@ class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
* @param button {HTMLButtonElement} the account button
*/
constructor(editor, button) {
super(editor, "", "", false, button);
super(editor, "", "", "", false, button);
this.isConfirmedAccount = true;
}

View File

@ -202,6 +202,12 @@ class JournalEntryAccountOption {
*/
code;
/**
* The account title
* @type {string}
*/
title;
/**
* The account text
* @type {string}
@ -235,6 +241,7 @@ class JournalEntryAccountOption {
constructor(selector, element) {
this.#element = element;
this.code = element.dataset.code;
this.title = element.dataset.title;
this.text = element.dataset.text;
this.#isInUse = element.classList.contains("accounting-account-is-in-use");
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");

View File

@ -791,6 +791,12 @@ class JournalEntryAccount {
*/
code;
/**
* The account title
* @type {string}
*/
title;
/**
* The account text
* @type {string}
@ -807,11 +813,13 @@ class JournalEntryAccount {
* Constructs a journal entry account.
*
* @param code {string} the account code
* @param title {string} the account title
* @param text {string} the account text
* @param isNeedOffset {boolean} true if the line items in the account needs offset, or false otherwise
*/
constructor(code, text, isNeedOffset) {
constructor(code, title, text, isNeedOffset) {
this.code = code;
this.title = title;
this.text = text;
this.isNeedOffset = isNeedOffset;
}
@ -822,7 +830,7 @@ class JournalEntryAccount {
* @return {JournalEntryAccount} the copy of the account
*/
copy() {
return new JournalEntryAccount(this.code, this.text, this.isNeedOffset);
return new JournalEntryAccount(this.code, this.title, this.text, this.isNeedOffset);
}
}
@ -887,10 +895,16 @@ class LineItemSubForm {
#accountCode;
/**
* The text display of the account
* @type {HTMLDivElement}
* The code part of the text display of the account
* @type {HTMLSpanElement}
*/
#accountText;
#accountTextCode;
/**
* The title part of the text display of the account
* @type {HTMLSpanElement}
*/
#accountTextTitle;
/**
* The description
@ -957,7 +971,8 @@ class LineItemSubForm {
this.#error = document.getElementById(`${prefix}-error`);
this.#no = document.getElementById(`${prefix}-no`);
this.#accountCode = document.getElementById(`${prefix}-account-code`);
this.#accountText = document.getElementById(`${prefix}-account-text`);
this.#accountTextCode = document.getElementById(`${prefix}-account-text-code`);
this.#accountTextTitle = document.getElementById(`${prefix}-account-text-title`);
this.#description = document.getElementById(`${prefix}-description`);
this.#descriptionText = document.getElementById(`${prefix}-description-text`);
this.#originalLineItemId = document.getElementById(`${prefix}-original-line-item-id`);
@ -1024,7 +1039,7 @@ class LineItemSubForm {
* @return {JournalEntryAccount|null} the account
*/
get account() {
return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
return this.#accountCode.value === null? null: new JournalEntryAccount(this.#accountCode.value, this.#accountCode.dataset.title, this.#accountCode.dataset.text, this.#accountCode.classList.contains("accounting-account-is-need-offset"));
}
/**
@ -1092,13 +1107,15 @@ class LineItemSubForm {
this.#originalLineItemText.innerText = A_("Offset %(item)s", {item: editor.originalLineItemText});
}
this.#accountCode.value = editor.account.code;
this.#accountCode.dataset.title = editor.account.title;
this.#accountCode.dataset.text = editor.account.text;
if (editor.account.isNeedOffset) {
this.#accountCode.classList.add("accounting-account-is-need-offset");
} else {
this.#accountCode.classList.remove("accounting-account-is-need-offset");
}
this.#accountText.innerText = editor.account.text;
this.#accountTextCode.innerText = editor.account.code
this.#accountTextTitle.innerText = editor.account.title
this.#description.value = editor.description === null? "": editor.description;
this.#descriptionText.innerText = editor.description === null? "": editor.description;
this.#amount.value = editor.amount;

View File

@ -277,6 +277,7 @@ class JournalEntryLineItemEditor {
this.originalLineItemText = originalLineItem.text;
this.#originalLineItemText.innerText = originalLineItem.text;
this.#setEnableDescriptionAccount(false);
if (this.description === null) {
if (originalLineItem.description === "") {
this.#descriptionControl.classList.remove("accounting-not-empty");
} else {
@ -284,6 +285,8 @@ class JournalEntryLineItemEditor {
}
this.description = originalLineItem.description === ""? null: originalLineItem.description;
this.#descriptionText.innerText = originalLineItem.description;
}
this.#setEnableAccount(false);
this.#accountControl.classList.add("accounting-not-empty");
this.account = originalLineItem.account.copy();
this.isAccountConfirmed = false;
@ -305,7 +308,7 @@ class JournalEntryLineItemEditor {
this.originalLineItemDate = null;
this.originalLineItemText = null;
this.#originalLineItemText.innerText = "";
this.#setEnableDescriptionAccount(true);
this.#setEnableAccount(true);
this.#accountControl.classList.remove("accounting-not-empty");
this.account = null;
this.isAccountConfirmed = false;
@ -356,7 +359,7 @@ class JournalEntryLineItemEditor {
*/
saveAccount(account) {
this.#accountControl.classList.add("accounting-not-empty");
this.account = new JournalEntryAccount(account.code, account.text, account.isNeedOffset);
this.account = new JournalEntryAccount(account.code, account.title, account.text, account.isNeedOffset);
this.isAccountConfirmed = true;
this.#accountText.innerText = account.text;
this.#validateAccount();
@ -472,12 +475,13 @@ class JournalEntryLineItemEditor {
this.originalLineItemDate = null;
this.originalLineItemText = null;
this.#originalLineItemText.innerText = "";
this.#setEnableDescriptionAccount(true);
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#descriptionControl.classList.remove("accounting-not-empty");
this.#descriptionControl.classList.remove("is-invalid");
this.description = null;
this.#descriptionText.innerText = ""
this.#descriptionError.innerText = ""
this.#setEnableAccount(true);
this.#accountControl.classList.remove("accounting-not-empty");
this.#accountControl.classList.remove("is-invalid");
this.account = null;
@ -511,7 +515,7 @@ class JournalEntryLineItemEditor {
this.#originalLineItemContainer.classList.remove("d-none");
this.#originalLineItemControl.classList.add("accounting-not-empty");
}
this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.description = lineItem.description;
if (this.description === null) {
this.#descriptionControl.classList.remove("accounting-not-empty");
@ -519,6 +523,7 @@ class JournalEntryLineItemEditor {
this.#descriptionControl.classList.add("accounting-not-empty");
}
this.#descriptionText.innerText = this.description === null? "": this.description;
this.#setEnableAccount(!lineItem.isMatched && this.originalLineItemId === null);
this.account = lineItem.account;
this.isAccountConfirmed = true;
if (this.account === null) {
@ -547,25 +552,17 @@ class JournalEntryLineItemEditor {
}
/**
* Sets the enable status of the description and account.
* Sets the enable status of the account.
*
* @param isEnabled {boolean} true to enable, or false otherwise
*/
#setEnableDescriptionAccount(isEnabled) {
#setEnableAccount(isEnabled) {
if (isEnabled) {
this.#descriptionControl.dataset.bsToggle = "modal";
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#descriptionControl.classList.remove("accounting-disabled");
this.#descriptionControl.classList.add("accounting-clickable");
this.#accountControl.dataset.bsToggle = "modal";
this.#accountControl.dataset.bsTarget = `#accounting-account-selector-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#accountControl.classList.remove("accounting-disabled");
this.#accountControl.classList.add("accounting-clickable");
} else {
this.#descriptionControl.dataset.bsToggle = "";
this.#descriptionControl.dataset.bsTarget = "";
this.#descriptionControl.classList.add("accounting-disabled");
this.#descriptionControl.classList.remove("accounting-clickable");
this.#accountControl.dataset.bsToggle = "";
this.#accountControl.dataset.bsTarget = "";
this.#accountControl.classList.add("accounting-disabled");

View File

@ -284,7 +284,7 @@ class OriginalLineItem {
this.date = element.dataset.date;
this.#debitCredit = element.dataset.debitCredit;
this.#currencyCode = element.dataset.currencyCode;
this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountText, false);
this.account = new JournalEntryAccount(element.dataset.accountCode, element.dataset.accountTitle, element.dataset.accountText, false);
this.description = element.dataset.description;
this.bareNetBalance = new Decimal(element.dataset.netBalance);
this.netBalance = this.bareNetBalance;

View File

@ -51,14 +51,6 @@ First written: 2023/1/26
{{ A_("Currencies") }}
</a>
</li>
{% if accounting_can_edit() %}
<li>
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.unmatched-offset.") %} active {% endif %}" href="{{ url_for("accounting.unmatched-offset.dashboard") }}">
<i class="fa-solid fa-link-slash"></i>
{{ A_("Unmatched Offsets") }}
</a>
</li>
{% endif %}
{% if accounting_can_admin() %}
<li>
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.option.") %} active {% endif %}" href="{{ url_for("accounting.option.detail") }}">

View File

@ -37,7 +37,7 @@ First written: 2023/2/25
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
{% for account in account_options %}
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
{{ account }}
</li>
{% endfor %}

View File

@ -184,7 +184,7 @@ First written: 2023/2/28
<div class="mt-3 accounting-description-editor-buttons">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-account-confirmed" class="btn btn-primary mb-1 d-none" type="button"></button>
{% for account in description_editor.accounts %}
<button class="btn btn-outline-primary d-none accounting-description-editor-{{ description_editor.debit_credit }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-text="{{ account }}">
<button class="btn btn-outline-primary d-none accounting-description-editor-{{ description_editor.debit_credit }}-account {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="button" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}">
{{ account }}
</button>
{% endfor %}

View File

@ -24,7 +24,10 @@ First written: 2023/3/14
<li class="list-group-item accounting-journal-entry-line-item">
<div class="d-flex justify-content-between">
<div>
<div class="small">{{ line_item.account }}</div>
<div class="small">
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
{{ line_item.account.title|title }}
</div>
{% if line_item.description is not none %}
<div>{{ line_item.description }}</div>
{% endif %}

View File

@ -26,13 +26,16 @@ First written: 2023/2/25
{% endif %}
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-no" value="{{ line_item_index }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-id" class="accounting-original-line-item-id" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original_line_item_id" value="{{ form.original_line_item_id.data|accounting_default }}" data-date="{{ form.original_line_item_date|accounting_default }}" data-text="{{ form.original_line_item_text|accounting_default }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" class="{% if form.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-text="{{ form.account_text }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-code" class="{% if form.is_need_offset %} accounting-account-is-need-offset {% endif %}" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account_code" value="{{ form.account_code.data|accounting_default }}" data-title="{{ form.account_title }}" data-text="{{ form.account_text }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
<div class="accounting-line-item-content">
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<div>
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text" class="small">{{ form.account_text }}</div>
<div class="small">
<span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text-code" class="d-none d-md-inline">{{ form.account_code.data|accounting_default }}</span>
<span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text-title">{{ form.account_title }}</span>
</div>
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description-text">{{ form.description.data|accounting_default }}</div>
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-original-line-item-text" class="fst-italic small accounting-original-line-item {% if not form.original_line_item_id.data %} d-none {% endif %}">
{% if form.original_line_item_id.data %}{{ A_("Offset %(item)s", item=form.original_line_item_text|accounting_default) }}{% endif %}

View File

@ -37,8 +37,15 @@ First written: 2023/2/25
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
{% for line_item in form.original_line_item_options %}
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<div>{{ line_item.journal_entry.date|accounting_format_date }} {{ line_item.description|accounting_default }}</div>
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-title="{{ line_item.account.title }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
<div>
<div class="small">
{{ line_item.journal_entry.date|accounting_format_date }}
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
{{ line_item.account.title|title }}
</div>
{{ line_item.description|accounting_default }}
</div>
<div>
<span class="badge bg-primary rounded-pill">
<span id="accounting-original-line-item-selector-option-{{ line_item.id }}-net-balance">{{ line_item.net_balance|accounting_format_amount }}</span>

View File

@ -26,7 +26,7 @@ First written: 2023/3/7
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Balance Sheet %(period)s", period=report.period.desc|title) }}{% else %}{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
@ -46,7 +46,13 @@ First written: 2023/3/7
{% 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_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
<h2 class="text-center">
{% if report.currency.code == accounting_default_currency_code() %}
{{ A_("Balance Sheet %(period)s", period=report.period.desc|title) }}
{% else %}
{{ A_("Balance Sheet of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}
{% endif %}
</h2>
</div>
<div class="row accounting-report-table accounting-balance-sheet-table">

View File

@ -89,7 +89,7 @@ First written: 2023/3/8
<i class="fa-solid fa-clipboard"></i>
<span class="d-none d-md-inline">{{ A_("Account") }}</span>
</button>
<ul class="dropdown-menu" aria-labelledby="accounting-choose-account">
<ul class="dropdown-menu accounting-toolbar-accounts" aria-labelledby="accounting-choose-account">
{% for account in report.account_options %}
<li>
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">

View File

@ -26,7 +26,7 @@ First written: 2023/3/5
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Income and Expenses Log of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Income and Expenses Log of %(account)s %(period)s", account=report.account.title|title, period=report.period.desc|title) }}{% else %}{{ A_("Income and Expenses Log of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}

View File

@ -26,7 +26,7 @@ First written: 2023/3/7
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Income Statement %(period)s", period=report.period.desc|title) }}{% else %}{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
@ -46,7 +46,13 @@ First written: 2023/3/7
{% 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_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
<h2 class="text-center">
{% if report.currency.code == accounting_default_currency_code() %}
{{ A_("Income Statement %(period)s", period=report.period.desc|title) }}
{% else %}
{{ A_("Income Statement of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}
{% endif %}
</h2>
</div>
<div class="accounting-report-table accounting-income-statement-table">

View File

@ -26,7 +26,7 @@ First written: 2023/3/5
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Ledger of %(account)s %(period)s", account=report.account.title|title, period=report.period.desc|title) }}{% else %}{{ A_("Ledger of %(account)s in %(currency)s %(period)s", currency=report.currency.name|title, account=report.account.title|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}

View File

@ -26,7 +26,7 @@ First written: 2023/3/5
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %}
{% block header %}{% block title %}{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Trial Balance %(period)s", period=report.period.desc|title) }}{% else %}{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
@ -46,7 +46,13 @@ First written: 2023/3/5
{% 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_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}</h2>
<h2 class="text-center">
{% if report.currency.code == accounting_default_currency_code() %}
{{ A_("Trial Balance %(period)s", period=report.period.desc|title) }}
{% else %}
{{ A_("Trial Balance of %(currency)s %(period)s", currency=report.currency.name|title, period=report.period.desc|title) }}
{% endif %}
</h2>
</div>
<div class="accounting-report-table accounting-trial-balance-table">

View File

@ -21,12 +21,18 @@ First written: 2023/4/8
#}
{% extends "accounting/base.html" %}
{% block header %}{% block title %}{{ A_("Accounts with Unapplied Original Line Items") }}{% endblock %}{% endblock %}
{% 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 %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Accounts with Unapplied Items") }}{% else %}{{ A_("Accounts with Unapplied Items in %(currency)s", currency=report.currency.name|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
<div class="mb-3 accounting-toolbar">
{% with use_account_chooser = true %}
{% with use_currency_chooser = true,
use_account_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
{% endwith %}
</div>
@ -38,7 +44,13 @@ First written: 2023/4/8
{% 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>
<h2 class="text-center">
{% if report.currency.code == accounting_default_currency_code() %}
{{ A_("Accounts with Unapplied Items") }}
{% else %}
{{ A_("Accounts with Unapplied Items in %(currency)s", currency=report.currency.name|title) }}
{% endif %}
</h2>
</div>
<div class="accounting-report-table accounting-unapplied-account-table">
@ -49,8 +61,11 @@ First written: 2023/4/8
</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>
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unapplied", currency=report.currency, account=account, period=report.period) }}">
<div>
<span class="d-none d-md-inline">{{ account.code }}</span>
{{ account.title|title }}
</div>
<div class="accounting-amount">{{ account.count }}</div>
</a>
{% endfor %}

View File

@ -23,14 +23,16 @@ First written: 2023/4/7
{% 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_("Unapplied Original Line Items of %(account)s", account=report.account.title|title) }}{% endblock %}{% endblock %}
{% block header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Unapplied Items of %(account)s", account=report.account.title|title) }}{% else %}{{ A_("Unapplied Items of %(account)s in %(currency)s", currency=report.currency.name|title, account=report.account.title|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
<div class="mb-3 accounting-toolbar">
{% with use_account_chooser = true %}
{% with use_currency_chooser = true,
use_account_chooser = true %}
{% include "accounting/report/include/toolbar-buttons.html" %}
{% endwith %}
</div>
@ -48,7 +50,6 @@ First written: 2023/4/7
<div class="accounting-report-table-header">
<div class="accounting-report-table-row">
<div>{{ A_("Date") }}</div>
<div>{{ A_("Currency") }}</div>
<div>{{ A_("Description") }}</div>
<div class="accounting-amount">{{ A_("Amount") }}</div>
<div class="accounting-amount">{{ A_("Net Balance") }}</div>
@ -56,15 +57,9 @@ First written: 2023/4/7
</div>
<div class="accounting-report-table-body">
{% for line_item in report.line_items %}
<a class="accounting-report-table-row {% if report.is_mark_matches and not line_item.match %} accounting-report-table-row-danger {% endif %}" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
<a class="accounting-report-table-row" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
<div>{{ line_item.journal_entry.date|accounting_format_date }}</div>
<div>{{ line_item.currency.name }}</div>
<div>
{{ line_item.description|accounting_default }}
{% if report.is_mark_matches and line_item.match %}
<div>{{ A_("Can match %(offset)s", offset=line_item.match) }}</div>
{% endif %}
</div>
<div>{{ line_item.description|accounting_default }}</div>
<div class="accounting-amount">{{ line_item.amount|accounting_format_amount }}</div>
<div class="accounting-amount">{{ line_item.net_balance|accounting_format_amount }}</div>
</a>
@ -78,9 +73,6 @@ First written: 2023/4/7
<div>
<div class="text-muted small">
{{ line_item.journal_entry.date|accounting_format_date }}
{% if line_item.currency.code != accounting_default_currency_code() %}
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
{% endif %}
</div>
{% if line_item.description is not none %}
<div>{{ line_item.description }}</div>

View File

@ -0,0 +1,79 @@
{#
The Mia! Accounting Project
unmatched-accounts.html: The account list with unmatched offsets
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/17
#}
{% 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 %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Accounts with Unmatched Offsets") }}{% else %}{{ A_("Accounts with Unmatched Offsets in %(currency)s", currency=report.currency.name|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
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">
{% if report.currency.code == accounting_default_currency_code() %}
{{ A_("Accounts with Unmatched Offsets") }}
{% else %}
{{ A_("Accounts with Unmatched Offsets in %(currency)s", currency=report.currency.name|title) }}
{% endif %}
</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.unmatched", currency=report.currency, account=account, period=report.period) }}">
<div>
<span class="d-none d-md-inline">{{ account.code }}</span>
{{ account.title|title }}
</div>
<div class="accounting-amount">{{ account.count }}</div>
</a>
{% endfor %}
</div>
</div>
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,150 @@
{#
The Mia! Accounting Project
unmatched.html: The unmatched offsets
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/17
#}
{% 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 %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Unmatched Offsets of %(account)s", account=report.account.title|title) }}{% else %}{{ A_("Unmatched Offsets of %(account)s in %(currency)s", currency=report.currency.name|title, account=report.account.title|title) }}{% endif %}{% endblock %}{% endblock %}
{% block content %}
<div class="mb-3 accounting-toolbar">
{% with use_currency_chooser = true,
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.matched_pairs %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-match-modal">
<i class="fa-solid fa-link"></i>
{{ A_("Match") }}
</button>
<form action="{{ url_for("accounting-report.match-offsets", currency=report.currency, account=report.account) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}">
<div class="modal fade" id="accounting-match-modal" tabindex="-1" aria-labelledby="accounting-match-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-match-modal-label">{{ A_("Confirm Match Offsets") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
</div>
<div class="modal-body">
<p>{{ A_("Do you really want to match the following original line items with their offsets? This cannot be undone. Please backup your database first, and review before you confirm.") }}</p>
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover accounting-unmatched-offset-pair-list">
{% for pair in report.matched_pairs %}
<li class="list-group-item">
{{ pair.offset.description|accounting_default }}
<span class="badge bg-info">{{ pair.offset.amount|accounting_format_amount }}</span>
{{ pair.original_line_item.journal_entry.date|accounting_format_date }} &rarr; {{ pair.offset.journal_entry.date|accounting_format_date }}
</li>
{% endfor %}
</ul>
</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-danger">{{ A_("Confirm") }}</button>
</div>
</div>
</div>
</div>
</form>
{% endif %}
<p>{{ report.match_status }}</p>
{% 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-unmatched-table">
<div class="accounting-report-table-header">
<div class="accounting-report-table-row">
<div>{{ A_("Date") }}</div>
<div>{{ A_("Description") }}</div>
<div class="accounting-amount">{{ A_("Debit") }}</div>
<div class="accounting-amount">{{ A_("Credit") }}</div>
<div class="accounting-amount">{{ A_("Balance") }}</div>
</div>
</div>
<div class="accounting-report-table-body">
{% for line_item in report.line_items %}
<a class="accounting-report-table-row {% if not line_item.match %} accounting-report-table-row-danger {% endif %}" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
<div>{{ line_item.journal_entry.date|accounting_format_date }}</div>
<div>
{{ line_item.description|accounting_default }}
{% if line_item.match %}
<div class="small">{{ A_("Can match %(item)s", item=line_item.match) }}</div>
{% endif %}
</div>
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount">{{ line_item.credit|accounting_format_amount|accounting_default }}</div>
<div class="accounting-amount {% if line_item.balance < 0 %} text-danger {% endif %}">{{ line_item.balance|accounting_report_format_amount }}</div>
</a>
{% endfor %}
</div>
</div>
<div class="list-group d-md-none">
{% for line_item in report.line_items %}
<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for("accounting.journal-entry.detail", journal_entry=line_item.journal_entry)|accounting_append_next }}">
<div>
<div class="text-muted small">
{{ line_item.journal_entry.date|accounting_format_date }}
</div>
{% if line_item.description is not none %}
<div>{{ line_item.description }}</div>
{% endif %}
</div>
<div>
{% if line_item.debit %}
<span class="badge rounded-pill bg-success">+{{ line_item.debit|accounting_format_amount }}</span>
{% endif %}
{% if line_item.credit %}
<span class="badge rounded-pill bg-warning">-{{ line_item.credit|accounting_format_amount }}</span>
{% endif %}
{% if line_item.balance < 0 %}
<span class="badge rounded-pill bg-danger">{{ line_item.balance|accounting_format_amount }}</span>
{% else %}
<span class="badge rounded-pill bg-primary">{{ line_item.balance|accounting_format_amount }}</span>
{% endif %}
</div>
</a>
{% endfor %}
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

@ -1,40 +0,0 @@
{#
The Mia! Accounting Project
dashboard.html: The account list with unmatched offsets
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_("Unmatched Offsets") }}{% endblock %}{% endblock %}
{% block content %}
{% if list %}
<div>
{% for account in list %}
<a class="btn btn-primary mb-1" role="button" href="{{ url_for("accounting.unmatched-offset.list", account=account) }}">
{{ account }} ({{ account.count }})
</a>
{% endfor %}
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

@ -1,107 +0,0 @@
{#
The Mia! Accounting Project
list.html: The unmatched offset list
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_("Unmatched Offsets in %(account)s", account=matcher.account.title|title) }}{% endblock %}{% endblock %}
{% block content %}
<div class="btn-group mb-3" role="group" aria-label="{{ A_("Toolbar") }}">
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.unmatched-offset.dashboard") }}">
<i class="fa-solid fa-circle-chevron-left"></i>
{{ A_("Back") }}
</a>
{% if matcher.is_having_matches %}
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#accounting-match-modal">
<i class="fa-solid fa-link"></i>
{{ A_("Match") }}
</button>
{% else %}
<button class="btn btn-secondary" type="button" disabled="disabled">
<i class="fa-solid fa-link"></i>
{{ A_("Match") }}
</button>
{% endif %}
</div>
{% if matcher.is_having_matches %}
<form action="{{ url_for("accounting.unmatched-offset.match", account=matcher.account) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal fade" id="accounting-match-modal" tabindex="-1" aria-labelledby="accounting-match-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-match-modal-label">{{ A_("Confirm Match Offsets") }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
</div>
<div class="modal-body">
<p>{{ A_("Do you really want to match the following original line items with their offsets? This cannot be undone. Please backup your database first, and review before you confirm.") }}</p>
<ul class="list-group accounting-list-group-stripped accounting-list-group-hover accounting-unmatched-offset-pair-list">
{% for pair in matcher.matched_pairs %}
<li class="list-group-item">
{{ pair.offset.description|accounting_default }}
<span class="badge bg-info">{{ pair.offset.amount|accounting_format_amount }}</span>
{{ pair.original_line_item.journal_entry.date|accounting_format_date }} &rarr; {{ pair.offset.journal_entry.date|accounting_format_date }}
</li>
{% endfor %}
</ul>
</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-danger">{{ A_("Confirm") }}</button>
</div>
</div>
</div>
</div>
</form>
{% endif %}
{% if matcher.total %}
{% if matcher.is_having_matches %}
<p>{{ A_("%(matches)s unapplied original line items out of %(total)s can match with their offsets.", matches=matcher.matches, total=matcher.total) }}</p>
{% else %}
<p>{{ A_("%(total)s unapplied original line items without matching offsets.", total=matcher.total) }}</p>
{% endif %}
<p><a href="{{ url_for("accounting-report.unapplied", account=matcher.account) }}">{{ A_("Go to unapplied original line items.") }}</a></p>
{% else %}
<p>{{ A_("All original line items are fully offset.") }}</p>
{% endif %}
{% if list %}
{% include "accounting/include/pagination.html" %}
<div class="list-group">
{% for item in list %}
<a class="list-group-item list-group-item-action {% if not item.match %} list-group-item-danger {% endif %}" href="{{ url_for("accounting.journal-entry.detail", journal_entry=item.journal_entry)|accounting_append_next }}">
{{ item }}
{% if item.match %}
<div class="small">{{ A_("Can match %(item)s", item=item.match) }}</div>
{% endif %}
</a>
{% endfor %}
</div>
{% else %}
<p>{{ A_("There is no data.") }}</p>
{% endif %}
{% endblock %}

View File

@ -6,10 +6,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: mia-accounting 1.1.1\n"
"Project-Id-Version: mia-accounting 1.4.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-04-09 01:41+0800\n"
"PO-Revision-Date: 2023-04-09 01:41+0800\n"
"POT-Creation-Date: 2023-04-18 09:32+0800\n"
"PO-Revision-Date: 2023-04-18 09:32+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
@ -20,7 +20,7 @@ msgstr ""
"Generated-By: Babel 2.12.1\n"
#: src/accounting/forms.py:33
#: src/accounting/static/js/journal-entry-form.js:1065
#: src/accounting/static/js/journal-entry-form.js:1080
#: src/accounting/static/js/journal-entry-line-item-editor.js:411
#: src/accounting/static/js/option-form.js:537
#: src/accounting/static/js/option-form.js:803
@ -302,11 +302,11 @@ msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
msgid "The amount must not be less than the offset total %(total)s."
msgstr "金額不可低於抵銷總額 %(total)s 。"
#: src/accounting/journal_entry/forms/line_item.py:413
#: src/accounting/journal_entry/forms/line_item.py:426
msgid "This account is not for debit line items."
msgstr "科目不是借方科目。"
#: src/accounting/journal_entry/forms/line_item.py:465
#: src/accounting/journal_entry/forms/line_item.py:478
msgid "This account is not for credit line items."
msgstr "科目不是貸方科目。"
@ -354,6 +354,15 @@ msgstr "設定未異動。"
msgid "The settings are saved successfully."
msgstr "設定存好了。"
#: src/accounting/report/views.py:401
msgid "No more offset to match automatically."
msgstr "無法自動配對抵銷。"
#: src/accounting/report/views.py:408
#, python-format
msgid "Matched %(matches)s offsets."
msgstr "抵銷了 %(matches)s 筆。"
#: src/accounting/report/period/description.py:33
msgid "for all time"
msgstr "全部"
@ -423,16 +432,16 @@ msgstr "全部"
#: src/accounting/templates/accounting/journal-entry/receipt/detail.html:43
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:39
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:55
#: src/accounting/templates/accounting/report/balance-sheet.html:59
#: src/accounting/templates/accounting/report/balance-sheet.html:71
#: src/accounting/templates/accounting/report/balance-sheet.html:81
#: src/accounting/templates/accounting/report/balance-sheet.html:65
#: src/accounting/templates/accounting/report/balance-sheet.html:77
#: src/accounting/templates/accounting/report/balance-sheet.html:87
#: src/accounting/templates/accounting/report/balance-sheet.html:96
#: src/accounting/templates/accounting/report/balance-sheet.html:103
#: src/accounting/templates/accounting/report/balance-sheet.html:93
#: src/accounting/templates/accounting/report/balance-sheet.html:102
#: src/accounting/templates/accounting/report/balance-sheet.html:109
#: src/accounting/templates/accounting/report/income-expenses.html:81
#: src/accounting/templates/accounting/report/income-statement.html:83
#: src/accounting/templates/accounting/report/income-statement.html:89
#: src/accounting/templates/accounting/report/ledger.html:82
#: src/accounting/templates/accounting/report/trial-balance.html:74
#: src/accounting/templates/accounting/report/trial-balance.html:80
msgid "Total"
msgstr "合計"
@ -444,42 +453,47 @@ msgstr "前期轉入"
#: src/accounting/report/reports/income_expenses.py:407
#: src/accounting/report/reports/journal.py:158
#: src/accounting/report/reports/ledger.py:366
#: src/accounting/report/reports/unapplied.py:137
#: src/accounting/report/reports/unapplied.py:148
#: src/accounting/report/reports/unmatched.py:158
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
#: src/accounting/templates/accounting/report/include/period-chooser.html:111
#: src/accounting/templates/accounting/report/income-expenses.html:55
#: src/accounting/templates/accounting/report/journal.html:53
#: src/accounting/templates/accounting/report/ledger.html:55
#: src/accounting/templates/accounting/report/search.html:50
#: src/accounting/templates/accounting/report/unapplied.html:50
#: src/accounting/templates/accounting/report/unapplied.html:52
#: src/accounting/templates/accounting/report/unmatched.html:93
msgid "Date"
msgstr "日期"
#: src/accounting/report/reports/income_expenses.py:407
#: src/accounting/report/reports/journal.py:159
#: src/accounting/report/reports/trial_balance.py:225
#: src/accounting/report/reports/unapplied_accounts.py:109
#: src/accounting/report/reports/unapplied_accounts.py:122
#: src/accounting/report/reports/unmatched_accounts.py:122
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:57
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:39
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
#: src/accounting/templates/accounting/report/income-expenses.html:56
#: src/accounting/templates/accounting/report/journal.html:55
#: src/accounting/templates/accounting/report/search.html:52
#: src/accounting/templates/accounting/report/trial-balance.html:55
#: src/accounting/templates/accounting/report/trial-balance.html:61
msgid "Account"
msgstr "科目"
#: src/accounting/report/reports/income_expenses.py:408
#: src/accounting/report/reports/journal.py:159
#: src/accounting/report/reports/ledger.py:366
#: src/accounting/report/reports/unapplied.py:138
#: src/accounting/report/reports/unapplied.py:149
#: src/accounting/report/reports/unmatched.py:159
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:28
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:49
#: src/accounting/templates/accounting/report/income-expenses.html:57
#: src/accounting/templates/accounting/report/journal.html:56
#: src/accounting/templates/accounting/report/ledger.html:56
#: src/accounting/templates/accounting/report/search.html:53
#: src/accounting/templates/accounting/report/unapplied.html:52
#: src/accounting/templates/accounting/report/unapplied.html:53
#: src/accounting/templates/accounting/report/unmatched.html:94
msgid "Description"
msgstr "摘要"
@ -495,8 +509,10 @@ msgstr "支出"
#: src/accounting/report/reports/income_expenses.py:409
#: src/accounting/report/reports/ledger.py:368
#: src/accounting/report/reports/unmatched.py:160
#: src/accounting/templates/accounting/report/income-expenses.html:60
#: src/accounting/templates/accounting/report/ledger.html:60
#: src/accounting/templates/accounting/report/unmatched.html:97
msgid "Balance"
msgstr "餘額"
@ -533,64 +549,88 @@ msgid "net income or loss for current period"
msgstr "本期損益"
#: src/accounting/report/reports/income_statement.py:301
#: src/accounting/report/reports/unapplied.py:138
#: src/accounting/report/reports/unapplied.py:149
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:65
#: src/accounting/templates/accounting/report/income-statement.html:55
#: src/accounting/templates/accounting/report/unapplied.html:53
#: src/accounting/templates/accounting/report/income-statement.html:61
#: src/accounting/templates/accounting/report/unapplied.html:54
msgid "Amount"
msgstr "金額"
#: src/accounting/report/reports/journal.py:158
#: src/accounting/report/reports/unapplied.py:137
#: src/accounting/report/reports/unapplied.py:148
#: src/accounting/report/reports/unmatched.py:158
#: src/accounting/templates/accounting/journal-entry/include/form-currency.html:33
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:73
#: src/accounting/templates/accounting/report/journal.html:54
#: src/accounting/templates/accounting/report/search.html:51
#: src/accounting/templates/accounting/report/unapplied.html:51
msgid "Currency"
msgstr "貨幣"
#: src/accounting/report/reports/journal.py:160
#: src/accounting/report/reports/ledger.py:367
#: src/accounting/report/reports/trial_balance.py:225
#: src/accounting/report/reports/unmatched.py:159
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:30
#: src/accounting/templates/accounting/report/journal.html:57
#: src/accounting/templates/accounting/report/ledger.html:57
#: src/accounting/templates/accounting/report/search.html:54
#: src/accounting/templates/accounting/report/trial-balance.html:56
#: src/accounting/templates/accounting/report/trial-balance.html:62
#: src/accounting/templates/accounting/report/unmatched.html:95
msgid "Debit"
msgstr "借方"
#: src/accounting/report/reports/journal.py:160
#: src/accounting/report/reports/ledger.py:367
#: src/accounting/report/reports/trial_balance.py:226
#: src/accounting/report/reports/unmatched.py:160
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
#: src/accounting/templates/accounting/journal-entry/transfer/include/form-currency.html:41
#: src/accounting/templates/accounting/report/journal.html:58
#: src/accounting/templates/accounting/report/ledger.html:58
#: src/accounting/templates/accounting/report/search.html:55
#: src/accounting/templates/accounting/report/trial-balance.html:57
#: src/accounting/templates/accounting/report/trial-balance.html:63
#: src/accounting/templates/accounting/report/unmatched.html:96
msgid "Credit"
msgstr "貸方"
#: src/accounting/report/reports/unapplied.py:121
#: src/accounting/report/reports/unapplied_accounts.py:93
#: src/accounting/report/reports/unapplied.py:132
#: src/accounting/report/reports/unapplied_accounts.py:107
#: src/accounting/report/reports/unmatched.py:142
#: src/accounting/report/reports/unmatched_accounts.py:107
#: src/accounting/templates/accounting/include/nav.html:39
msgid "Accounts"
msgstr "科目"
#: src/accounting/report/reports/unapplied.py:139
#: src/accounting/templates/accounting/report/unapplied.html:54
#: src/accounting/report/reports/unapplied.py:150
#: src/accounting/templates/accounting/report/unapplied.html:55
msgid "Net Balance"
msgstr "淨額"
#: src/accounting/report/reports/unapplied_accounts.py:109
#: src/accounting/templates/accounting/report/unapplied-accounts.html:47
#: src/accounting/report/reports/unapplied_accounts.py:122
#: src/accounting/report/reports/unmatched_accounts.py:122
#: src/accounting/templates/accounting/report/unapplied-accounts.html:59
#: src/accounting/templates/accounting/report/unmatched-accounts.html:59
msgid "Count"
msgstr "數量"
#: src/accounting/report/utils/report_chooser.py:82
#: src/accounting/report/utils/offset_matcher.py:163
msgid "There is no unmatched offset."
msgstr "沒有遺漏的抵銷分錄"
#: src/accounting/report/utils/offset_matcher.py:167
#, python-format
msgid "%(total)s unmatched offsets without original items."
msgstr "%(total)s 筆遺漏的抵銷分錄無法自動抵銷。"
#: src/accounting/report/utils/offset_matcher.py:172
#, python-format
msgid ""
"%(matches)s unmatched offsets out of %(total)s can match with their "
"original items."
msgstr "%(total)s 筆遺漏的抵銷分錄中,可配對抵銷掉 %(matches)s 筆。"
#: src/accounting/report/utils/report_chooser.py:86
#: src/accounting/templates/accounting/account/include/form.html:98
#: src/accounting/templates/accounting/account/list.html:40
#: src/accounting/templates/accounting/base-account/list.html:34
@ -606,114 +646,119 @@ msgstr "數量"
msgid "Search"
msgstr "搜尋"
#: src/accounting/report/utils/report_chooser.py:93
#: src/accounting/report/utils/report_chooser.py:97
msgid "Income and Expenses Log"
msgstr "收支帳"
#: src/accounting/report/utils/report_chooser.py:106
#: src/accounting/report/utils/report_chooser.py:110
msgid "Ledger"
msgstr "分類帳"
#: src/accounting/report/utils/report_chooser.py:118
#: src/accounting/report/utils/report_chooser.py:122
msgid "Journal"
msgstr "日記簿"
#: src/accounting/report/utils/report_chooser.py:128
#: src/accounting/report/utils/report_chooser.py:132
msgid "Trial Balance"
msgstr "試算表"
#: src/accounting/report/utils/report_chooser.py:139
#: src/accounting/report/utils/report_chooser.py:143
msgid "Income Statement"
msgstr "損益表"
#: src/accounting/report/utils/report_chooser.py:150
#: src/accounting/report/utils/report_chooser.py:154
msgid "Balance Sheet"
msgstr "資產負債表"
#: src/accounting/report/utils/report_chooser.py:163
#: src/accounting/report/utils/report_chooser.py:167
msgid "Unapplied Original Line Items"
msgstr "未抵銷原始分錄"
#: src/accounting/report/utils/report_chooser.py:171
msgid "Unapplied Items"
msgstr "未抵銷項目"
#: src/accounting/report/utils/report_chooser.py:184
#: src/accounting/report/utils/report_chooser.py:188
msgid "Unmatched Offsets"
msgstr "遺漏的抵銷項目"
#: src/accounting/static/js/account-form.js:206
msgid "Please fill in the title."
msgstr "請填上標題。"
#: src/accounting/static/js/description-editor.js:951
#: src/accounting/static/js/description-editor.js:1129
#: src/accounting/static/js/description-editor.js:952
#: src/accounting/static/js/description-editor.js:1130
msgid "Please fill in the tag."
msgstr "請填上標籤。"
#: src/accounting/static/js/description-editor.js:961
#: src/accounting/static/js/description-editor.js:1149
#: src/accounting/static/js/description-editor.js:962
#: src/accounting/static/js/description-editor.js:1150
msgid "Please fill in the origin."
msgstr "請填上起點。"
#: src/accounting/static/js/description-editor.js:971
#: src/accounting/static/js/description-editor.js:1159
#: src/accounting/static/js/description-editor.js:972
#: src/accounting/static/js/description-editor.js:1160
msgid "Please fill in the destination."
msgstr "請填上終點。"
#: src/accounting/static/js/description-editor.js:1139
#: src/accounting/static/js/description-editor.js:1140
msgid "Please fill in the route."
msgstr "請填上路線名稱。"
#: src/accounting/static/js/description-editor.js:1192
#: src/accounting/static/js/description-editor.js:1193
msgid "January"
msgstr "一月"
#: src/accounting/static/js/description-editor.js:1192
#: src/accounting/static/js/description-editor.js:1193
msgid "February"
msgstr "二月"
#: src/accounting/static/js/description-editor.js:1192
#: src/accounting/static/js/description-editor.js:1193
msgid "March"
msgstr "三月"
#: src/accounting/static/js/description-editor.js:1192
#: src/accounting/static/js/description-editor.js:1193
msgid "April"
msgstr "四月"
#: src/accounting/static/js/description-editor.js:1193
#: src/accounting/static/js/description-editor.js:1194
msgid "May"
msgstr "五月"
#: src/accounting/static/js/description-editor.js:1193
#: src/accounting/static/js/description-editor.js:1194
msgid "June"
msgstr "六月"
#: src/accounting/static/js/description-editor.js:1193
#: src/accounting/static/js/description-editor.js:1194
msgid "July"
msgstr "七月"
#: src/accounting/static/js/description-editor.js:1193
#: src/accounting/static/js/description-editor.js:1194
msgid "August"
msgstr "八月"
#: src/accounting/static/js/description-editor.js:1194
#: src/accounting/static/js/description-editor.js:1195
msgid "September"
msgstr "九月"
#: src/accounting/static/js/description-editor.js:1194
#: src/accounting/static/js/description-editor.js:1195
msgid "October"
msgstr "十月"
#: src/accounting/static/js/description-editor.js:1194
#: src/accounting/static/js/description-editor.js:1195
msgid "November"
msgstr "十一月"
#: src/accounting/static/js/description-editor.js:1194
#: src/accounting/static/js/description-editor.js:1195
msgid "December"
msgstr "十二月"
#: src/accounting/static/js/journal-entry-form.js:1070
#: src/accounting/static/js/journal-entry-form.js:1085
#: src/accounting/static/js/journal-entry-line-item-editor.js:430
msgid "Please fill in the amount."
msgstr "請填上金額。"
#: src/accounting/static/js/journal-entry-form.js:1092
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:34
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:38
#: src/accounting/static/js/journal-entry-form.js:1107
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:37
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:41
#, python-format
msgid "Offset %(item)s"
msgstr "抵銷 %(item)s"
@ -756,7 +801,6 @@ msgstr "新增科目"
#: src/accounting/templates/accounting/journal-entry/include/form.html:38
#: src/accounting/templates/accounting/journal-entry/order.html:36
#: src/accounting/templates/accounting/option/form.html:36
#: src/accounting/templates/accounting/unmatched-offset/list.html:31
msgid "Back"
msgstr "回上頁"
@ -797,7 +841,7 @@ msgstr "確認刪除科目"
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:28
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
#: src/accounting/templates/accounting/report/include/search-modal.html:28
#: src/accounting/templates/accounting/unmatched-offset/list.html:54
#: src/accounting/templates/accounting/report/unmatched.html:58
msgid "Close"
msgstr "關閉"
@ -815,7 +859,7 @@ msgstr "你確定要刪掉這個科目嗎?"
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:48
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:65
#: src/accounting/templates/accounting/report/include/search-modal.html:37
#: src/accounting/templates/accounting/unmatched-offset/list.html:70
#: src/accounting/templates/accounting/report/unmatched.html:74
msgid "Cancel"
msgstr "取消"
@ -823,7 +867,7 @@ msgstr "取消"
#: src/accounting/templates/accounting/currency/detail.html:80
#: src/accounting/templates/accounting/journal-entry/include/detail.html:85
#: src/accounting/templates/accounting/report/include/period-chooser.html:141
#: src/accounting/templates/accounting/unmatched-offset/list.html:71
#: src/accounting/templates/accounting/report/unmatched.html:75
msgid "Confirm"
msgstr "確定"
@ -871,22 +915,22 @@ msgstr "新增"
#: src/accounting/templates/accounting/base-account/list.html:51
#: src/accounting/templates/accounting/currency/list.html:65
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:46
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:51
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:58
#: src/accounting/templates/accounting/journal-entry/order.html:82
#: src/accounting/templates/accounting/option/detail.html:67
#: src/accounting/templates/accounting/option/detail.html:83
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:45
#: src/accounting/templates/accounting/report/balance-sheet.html:110
#: src/accounting/templates/accounting/report/balance-sheet.html:116
#: src/accounting/templates/accounting/report/income-expenses.html:113
#: src/accounting/templates/accounting/report/income-statement.html:96
#: src/accounting/templates/accounting/report/income-statement.html:102
#: src/accounting/templates/accounting/report/journal.html:103
#: src/accounting/templates/accounting/report/ledger.html:116
#: src/accounting/templates/accounting/report/search.html:100
#: src/accounting/templates/accounting/report/trial-balance.html:82
#: src/accounting/templates/accounting/report/unapplied-accounts.html:61
#: src/accounting/templates/accounting/report/unapplied.html:98
#: src/accounting/templates/accounting/unmatched-offset/dashboard.html:37
#: src/accounting/templates/accounting/unmatched-offset/list.html:104
#: src/accounting/templates/accounting/report/trial-balance.html:88
#: src/accounting/templates/accounting/report/unapplied-accounts.html:76
#: src/accounting/templates/accounting/report/unapplied.html:90
#: src/accounting/templates/accounting/report/unmatched-accounts.html:76
#: src/accounting/templates/accounting/report/unmatched.html:147
msgid "There is no data."
msgstr "沒有資料。"
@ -984,12 +1028,7 @@ msgstr "基本科目"
msgid "Currencies"
msgstr "貨幣"
#: src/accounting/templates/accounting/include/nav.html:57
#: src/accounting/templates/accounting/unmatched-offset/dashboard.html:24
msgid "Unmatched Offsets"
msgstr "遺漏的抵銷分錄"
#: src/accounting/templates/accounting/include/nav.html:64
#: src/accounting/templates/accounting/include/nav.html:58
#: src/accounting/templates/accounting/option/detail.html:24
#: src/accounting/templates/accounting/option/detail.html:41
#: src/accounting/templates/accounting/option/form.html:29
@ -1088,23 +1127,23 @@ msgstr "路線"
msgid "The Number of Items"
msgstr "數量"
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:42
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:43
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:45
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:46
msgid "Offsets"
msgstr "抵銷"
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:55
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:54
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:58
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:57
msgid "Net balance"
msgstr "淨額"
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:60
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:51
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:63
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:54
msgid "Fully offset"
msgstr "全部抵銷"
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:65
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:59
#: src/accounting/templates/accounting/journal-entry/include/detail-line-items.html:68
#: src/accounting/templates/accounting/journal-entry/include/form-line-item.html:62
msgid "Unmatched"
msgstr "未抵銷"
@ -1217,18 +1256,35 @@ msgid "Water bill for {last_bimonthly_name}"
msgstr "水費{last_bimonthly_number}月"
#: src/accounting/templates/accounting/report/balance-sheet.html:29
#: src/accounting/templates/accounting/report/balance-sheet.html:49
#: src/accounting/templates/accounting/report/balance-sheet.html:51
#, python-format
msgid "Balance Sheet %(period)s"
msgstr "%(period)s資產負債表"
#: src/accounting/templates/accounting/report/balance-sheet.html:29
#: src/accounting/templates/accounting/report/balance-sheet.html:53
#, python-format
msgid "Balance Sheet of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s資產負債表"
#: src/accounting/templates/accounting/report/income-expenses.html:29
#, python-format
msgid "Income and Expenses Log of %(account)s %(period)s"
msgstr "%(period)s%(account)s收支帳"
#: src/accounting/templates/accounting/report/income-expenses.html:29
#, python-format
msgid "Income and Expenses Log of %(account)s in %(currency)s %(period)s"
msgstr "%(period)s%(currency)s%(account)s收支帳"
#: src/accounting/templates/accounting/report/income-statement.html:29
#: src/accounting/templates/accounting/report/income-statement.html:49
#: src/accounting/templates/accounting/report/income-statement.html:51
#, python-format
msgid "Income Statement %(period)s"
msgstr "%(period)s損益表"
#: src/accounting/templates/accounting/report/income-statement.html:29
#: src/accounting/templates/accounting/report/income-statement.html:53
#, python-format
msgid "Income Statement of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s損益表"
@ -1238,31 +1294,89 @@ msgstr "%(period)s%(currency)s損益表"
msgid "Journal %(period)s"
msgstr "%(period)s日記簿"
#: src/accounting/templates/accounting/report/ledger.html:29
#, python-format
msgid "Ledger of %(account)s %(period)s"
msgstr "%(period)s%(account)s分類帳"
#: src/accounting/templates/accounting/report/ledger.html:29
#, python-format
msgid "Ledger of %(account)s in %(currency)s %(period)s"
msgstr "%(period)s%(currency)s%(account)s分類帳"
#: src/accounting/templates/accounting/report/trial-balance.html:29
#: src/accounting/templates/accounting/report/trial-balance.html:49
#: src/accounting/templates/accounting/report/trial-balance.html:51
#, python-format
msgid "Trial Balance %(period)s"
msgstr "%(period)s試算表"
#: src/accounting/templates/accounting/report/trial-balance.html:29
#: src/accounting/templates/accounting/report/trial-balance.html:53
#, python-format
msgid "Trial Balance of %(currency)s %(period)s"
msgstr "%(period)s%(currency)s試算表"
#: src/accounting/templates/accounting/report/unapplied-accounts.html:24
#: src/accounting/templates/accounting/report/unapplied-accounts.html:41
msgid "Accounts with Unapplied Original Line Items"
msgstr "未抵銷原始分錄的科目"
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
#: src/accounting/templates/accounting/report/unapplied-accounts.html:49
msgid "Accounts with Unapplied Items"
msgstr "未抵銷項目的科目"
#: src/accounting/templates/accounting/report/unapplied.html:28
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
#: src/accounting/templates/accounting/report/unapplied-accounts.html:51
#, python-format
msgid "Unapplied Original Line Items of %(account)s"
msgstr "%(account)s未抵銷原始分錄"
msgid "Accounts with Unapplied Items in %(currency)s"
msgstr "%(currency)s未抵銷項目的科目"
#: src/accounting/templates/accounting/report/unapplied.html:65
#: src/accounting/templates/accounting/report/unapplied.html:29
#, python-format
msgid "Can match %(offset)s"
msgstr "可抵銷 %(offset)s"
msgid "Unapplied Items of %(account)s"
msgstr "%(account)s未抵銷項目"
#: src/accounting/templates/accounting/report/unapplied.html:29
#, python-format
msgid "Unapplied Items of %(account)s in %(currency)s"
msgstr "%(currency)s%(account)s未抵銷項目"
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
#: src/accounting/templates/accounting/report/unmatched-accounts.html:49
msgid "Accounts with Unmatched Offsets"
msgstr "含遺漏抵銷項目的科目"
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
#: src/accounting/templates/accounting/report/unmatched-accounts.html:51
#, python-format
msgid "Accounts with Unmatched Offsets in %(currency)s"
msgstr "%(currency)s含遺漏抵銷項目的科目"
#: src/accounting/templates/accounting/report/unmatched.html:29
#, python-format
msgid "Unmatched Offsets of %(account)s"
msgstr "%(account)s遺漏的抵銷項目"
#: src/accounting/templates/accounting/report/unmatched.html:29
#, python-format
msgid "Unmatched Offsets of %(account)s in %(currency)s"
msgstr "%(currency)s%(account)s遺漏的抵銷項目"
#: src/accounting/templates/accounting/report/unmatched.html:47
msgid "Match"
msgstr "抵銷"
#: src/accounting/templates/accounting/report/unmatched.html:57
msgid "Confirm Match Offsets"
msgstr "確認抵銷"
#: src/accounting/templates/accounting/report/unmatched.html:61
msgid ""
"Do you really want to match the following original line items with their "
"offsets? This cannot be undone. Please backup your database first, and "
"review before you confirm."
msgstr "你確定要抵銷下列原始分錄與抵銷分錄嗎?結果無法復原。請先備份資料庫,並仔細核對抵銷分錄是否正確。"
#: src/accounting/templates/accounting/report/unmatched.html:107
#, python-format
msgid "Can match %(item)s"
msgstr "可抵銷 %(item)s"
#: src/accounting/templates/accounting/report/include/period-chooser.html:26
msgid "Period Chooser"
@ -1297,65 +1411,6 @@ msgstr "期間"
msgid "Download"
msgstr "下載"
#: src/accounting/templates/accounting/unmatched-offset/list.html:24
#, python-format
msgid "Unmatched Offsets in %(account)s"
msgstr "%(account)s遺漏的抵銷分錄"
#: src/accounting/templates/accounting/unmatched-offset/list.html:28
msgid "Toolbar"
msgstr "工具列"
#: src/accounting/templates/accounting/unmatched-offset/list.html:36
#: src/accounting/templates/accounting/unmatched-offset/list.html:41
msgid "Match"
msgstr "抵銷"
#: src/accounting/templates/accounting/unmatched-offset/list.html:53
msgid "Confirm Match Offsets"
msgstr "確認抵銷"
#: src/accounting/templates/accounting/unmatched-offset/list.html:57
msgid ""
"Do you really want to match the following original line items with their "
"offsets? This cannot be undone. Please backup your database first, and "
"review before you confirm."
msgstr "你確定要抵銷下列原始分錄與抵銷分錄嗎?結果無法復原。請先備份資料庫,並仔細核對抵銷分錄是否正確。"
#: src/accounting/templates/accounting/unmatched-offset/list.html:81
#, python-format
msgid ""
"%(matches)s unapplied original line items out of %(total)s can match with"
" their offsets."
msgstr "%(total)s 筆未抵銷原始分錄中,可配對抵銷掉 %(matches)s 筆。"
#: src/accounting/templates/accounting/unmatched-offset/list.html:83
#, python-format
msgid "%(total)s unapplied original line items without matching offsets."
msgstr "%(total)s 筆未抵銷原始分錄,無法自動配對抵銷。"
#: src/accounting/templates/accounting/unmatched-offset/list.html:85
msgid "Go to unapplied original line items."
msgstr "查閱未抵銷原始分錄。"
#: src/accounting/templates/accounting/unmatched-offset/list.html:87
msgid "All original line items are fully offset."
msgstr "原始分錄已全部抵銷。"
#: src/accounting/templates/accounting/unmatched-offset/list.html:98
#, python-format
msgid "Can match %(item)s"
msgstr "可抵銷 %(item)s"
#: src/accounting/unmatched_offset/views.py:71
msgid "No more offset to match automatically."
msgstr "無法自動配對抵銷。"
#: src/accounting/unmatched_offset/views.py:77
#, python-format
msgid "Matches %(matches)s from %(total)s unapplied line items."
msgstr "%(total)s 筆未抵銷原始分錄中,配對抵銷掉 %(matches)s 筆。"
#: src/accounting/utils/current_account.py:65
msgid "current assets and liabilities"
msgstr "流動資產與負債"

View File

@ -1,30 +0,0 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 unmatched offset management.
"""
from flask import Blueprint
def init_app(bp: Blueprint) -> None:
"""Initialize the application.
:param bp: The blueprint of the accounting application.
:return: None.
"""
from .views import bp as unmatched_offset_bp
bp.register_blueprint(unmatched_offset_bp, url_prefix="/unmatched-offsets")

View File

@ -1,81 +0,0 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 views for the unmatched offset management.
"""
from flask import Blueprint, render_template, redirect, url_for, flash
from accounting import db
from accounting.locale import lazy_gettext
from accounting.models import JournalEntryLineItem, Account
from accounting.utils.cast import s
from accounting.utils.offset_matcher import OffsetMatcher
from accounting.utils.pagination import Pagination
from accounting.utils.permission import has_permission, can_edit
from .queries import get_accounts_with_unmatched_offsets
bp: Blueprint = Blueprint("unmatched-offset", __name__)
"""The view blueprint for the unmatched offset management."""
@bp.get("", endpoint="dashboard")
@has_permission(can_edit)
def show_offset_dashboard() -> str:
"""Shows the dashboard about offsets.
:return: The dashboard about offsets.
"""
return render_template("accounting/unmatched-offset/dashboard.html",
list=get_accounts_with_unmatched_offsets())
@bp.get("<needOffsetAccount:account>", endpoint="list")
@has_permission(can_edit)
def show_unmatched_offsets(account: Account) -> str:
"""Shows the unmatched offsets in an account.
:return: The unmatched offsets in an account.
"""
matcher: OffsetMatcher = OffsetMatcher(account)
pagination: Pagination \
= Pagination[JournalEntryLineItem](matcher.unmatched_offsets,
is_reversed=True)
return render_template("accounting/unmatched-offset/list.html",
matcher=matcher,
list=pagination.list, pagination=pagination)
@bp.post("<needOffsetAccount:account>", endpoint="match")
@has_permission(can_edit)
def match_offsets(account: Account) -> redirect:
"""Matches the original line items with their offsets.
:return: Redirection to the view of the unmatched offsets.
"""
matcher: OffsetMatcher = OffsetMatcher(account)
if not matcher.is_having_matches:
flash(s(lazy_gettext("No more offset to match automatically.")),
"success")
return redirect(url_for("accounting.unmatched-offset.list",
account=account))
matcher.match()
db.session.commit()
flash(s(lazy_gettext(
"Matches %(matches)s from %(total)s unapplied line items.",
matches=matcher.matches, total=matcher.total)), "success")
return redirect(url_for("accounting.unmatched-offset.list",
account=account))

View File

@ -1,129 +0,0 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 forms for the unmatched offset management.
"""
import sqlalchemy as sa
from sqlalchemy.orm import selectinload
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.utils.unapplied import get_unapplied_original_line_items
class OffsetPair:
"""A pair of an original line item and its offset."""
def __init__(self, original_line_item: JournalEntryLineItem,
offset: JournalEntryLineItem):
"""Constructs a pair of an original line item and its offset.
:param original_line_item: The original line item.
:param offset: The offset.
"""
self.original_line_item: JournalEntryLineItem = original_line_item
"""The original line item."""
self.offset: JournalEntryLineItem = offset
"""The offset."""
class OffsetMatcher:
"""The offset matcher."""
def __init__(self, account: Account):
"""Constructs the offset matcher.
:param account: The account.
"""
self.account: Account = account
"""The account."""
self.matched_pairs: list[OffsetPair] = []
"""A list of matched pairs."""
self.is_having_matches: bool = False
"""Whether there is any matches."""
self.total: int = 0
"""The total number of unapplied debits or credits."""
self.unapplied: list[JournalEntryLineItem] = []
"""The unapplied debits or credits."""
self.unmatched_offsets: list[JournalEntryLineItem] = []
"""The unmatched offsets."""
self.__find_matches()
def __find_matches(self) -> None:
"""Finds the matched original line items and their offsets.
:return: None.
"""
self.unapplied: list[JournalEntryLineItem] \
= get_unapplied_original_line_items(self.account)
self.total = len(self.unapplied)
if self.total == 0:
self.is_having_matches = False
return
self.unmatched_offsets = self.__get_unmatched_offsets()
remains: list[JournalEntryLineItem] = self.unmatched_offsets.copy()
for original_item in self.unapplied:
offset_candidates: list[JournalEntryLineItem] \
= [x for x in remains
if (x.journal_entry.date > original_item.journal_entry.date
or (x.journal_entry.date
== original_item.journal_entry.date
and x.journal_entry.no
> original_item.journal_entry.no))
and x.currency_code == original_item.currency_code
and x.description == original_item.description
and x.amount == original_item.net_balance]
if len(offset_candidates) == 0:
continue
self.matched_pairs.append(
OffsetPair(original_item, offset_candidates[0]))
original_item.match = offset_candidates[0]
offset_candidates[0].match = original_item
remains.remove(offset_candidates[0])
self.is_having_matches = len(self.matched_pairs) > 0
def __get_unmatched_offsets(self) -> list[JournalEntryLineItem]:
"""Returns the unmatched offsets of an account.
:return: The unmatched offsets of the account.
"""
return JournalEntryLineItem.query.join(Account).join(JournalEntry)\
.filter(Account.id == self.account.id,
JournalEntryLineItem.original_line_item_id.is_(None),
sa.or_(sa.and_(Account.base_code.startswith("2"),
JournalEntryLineItem.is_debit),
sa.and_(Account.base_code.startswith("1"),
sa.not_(JournalEntryLineItem.is_debit))))\
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)\
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
@property
def matches(self) -> int:
"""Returns the number of matches.
:return: The number of matches.
"""
return len(self.matched_pairs)
def match(self) -> None:
"""Matches the original line items with offsets.
:return: None.
"""
for pair in self.matched_pairs:
pair.offset.original_line_item_id = pair.original_line_item.id

View File

@ -1,72 +0,0 @@
# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/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 unapplied original line item utilities.
"""
from decimal import Decimal
import sqlalchemy as sa
from sqlalchemy.orm import selectinload
from accounting import db
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.utils.cast import be
from accounting.utils.offset_alias import offset_alias
def get_unapplied_original_line_items(account: Account) \
-> list[JournalEntryLineItem]:
"""Queries and returns the unapplied original line items in an account.
:param account: The account.
:return: The unapplied original line items in the account.
"""
offset: sa.Alias = offset_alias()
net_balance: sa.Label \
= (JournalEntryLineItem.amount
+ sa.func.sum(sa.case(
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
select_net_balances: sa.Select \
= sa.select(JournalEntryLineItem.id, net_balance) \
.join(Account) \
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
isouter=True) \
.filter(be(Account.id == account.id),
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),
JournalEntryLineItem.is_debit))) \
.group_by(JournalEntryLineItem.id) \
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
net_balances: dict[int, Decimal] \
= {x.id: x.net_balance
for x in db.session.execute(select_net_balances).all()}
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query \
.filter(JournalEntryLineItem.id.in_({x for x in net_balances})) \
.join(JournalEntry) \
.order_by(JournalEntry.date, JournalEntry.no,
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
.options(selectinload(JournalEntryLineItem.currency),
selectinload(JournalEntryLineItem.journal_entry)).all()
for line_item in line_items:
line_item.net_balance = line_item.amount \
if net_balances[line_item.id] is None \
else net_balances[line_item.id]
return line_items

View File

@ -107,10 +107,26 @@ class ReportTestCase(unittest.TestCase):
response = client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
response = client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
response = client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unmatched")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unmatched?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/search?q=Salary")
@ -190,13 +206,29 @@ class ReportTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
response = client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
response = client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = client.get(f"{PREFIX}/unmatched")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/unmatched?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 200)
@ -275,11 +307,28 @@ class ReportTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
response = self.client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.get(
f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unmatched")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unmatched?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
@ -360,11 +409,28 @@ class ReportTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied/{Accounts.PAYABLE}")
response = self.client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.get(
f"{PREFIX}/unapplied/{Accounts.PAYABLE}?as=csv")
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unmatched")
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unmatched?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME)

View File

@ -25,9 +25,9 @@ from flask import Flask
from test_site import db
from test_site.lib import JournalEntryCurrencyData, JournalEntryData, \
BaseTestData
from testlib import create_test_app, get_client, Accounts
from testlib import NEXT_URI, create_test_app, get_client, Accounts
PREFIX: str = "/accounting/unmatched-offsets"
PREFIX: str = "/accounting/match-offsets/USD"
"""The URL prefix for the unmatched offset management."""
@ -58,14 +58,9 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
DifferentTestData(self.app, "nobody").populate()
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token})
data={"csrf_token": csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None:
@ -77,14 +72,9 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
DifferentTestData(self.app, "viewer").populate()
response: httpx.Response
response = client.get(PREFIX)
self.assertEqual(response.status_code, 403)
response = client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 403)
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token})
data={"csrf_token": csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 403)
def test_editor(self) -> None:
@ -95,17 +85,11 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
DifferentTestData(self.app, "editor").populate()
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token})
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.headers["Location"], NEXT_URI)
def test_empty_db(self) -> None:
"""Test the empty database.
@ -114,43 +98,42 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
"""
response: httpx.Response
response = self.client.get(PREFIX)
self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token})
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"],
f"{PREFIX}/{Accounts.PAYABLE}")
self.assertEqual(response.headers["Location"], NEXT_URI)
def test_different(self) -> None:
"""Tests to match against different descriptions and amounts.
:return: None.
"""
from accounting.models import Account, JournalEntryLineItem
from accounting.utils.offset_matcher import OffsetMatcher
from accounting.models import Currency, Account, JournalEntryLineItem
from accounting.report.utils.offset_matcher import OffsetMatcher
from accounting.template_globals import default_currency_code
data: DifferentTestData = DifferentTestData(self.app, "editor")
data.populate()
account: Account | None
line_item: JournalEntryLineItem | None
matcher: OffsetMatcher
list_uri: str
match_uri: str
response: httpx.Response
with self.app.app_context():
currency: Currency | None \
= db.session.get(Currency, default_currency_code())
assert currency is not None
# The receivables
with self.app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_r_or1d.id, data.l_r_or2d.id,
data.l_r_or3d.id, data.l_r_or4d.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_r_of1c.id, data.l_r_of2c.id,
data.l_r_of3c.id, data.l_r_of4c.id,
data.l_r_of5c.id})
@ -164,24 +147,24 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item)
self.assertIsNone(line_item.original_line_item_id)
list_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token})
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri)
self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_r_or1d.id, data.l_r_or2d.id,
data.l_r_or3d.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_r_of1c.id, data.l_r_of2c.id,
data.l_r_of3c.id, data.l_r_of4c.id})
self.assertEqual(matcher.matches, 0)
self.assertEqual(len(matcher.matched_pairs), 0)
for line_item_id in {data.l_r_of1c.id, data.l_r_of2c.id,
data.l_r_of3c.id, data.l_r_of4c.id}:
line_item = db.session.get(JournalEntryLineItem, line_item_id)
@ -196,11 +179,11 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
with self.app.app_context():
account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_p_or1c.id, data.l_p_or2c.id,
data.l_p_or3c.id, data.l_p_or4c.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_p_of1d.id, data.l_p_of2d.id,
data.l_p_of3d.id, data.l_p_of4d.id,
data.l_p_of5d.id})
@ -214,24 +197,24 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item)
self.assertIsNone(line_item.original_line_item_id)
list_uri = f"{PREFIX}/{Accounts.PAYABLE}"
match_uri = f"{PREFIX}/{Accounts.PAYABLE}"
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token})
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri)
self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context():
account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_p_or1c.id, data.l_p_or2c.id,
data.l_p_or3c.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_p_of1d.id, data.l_p_of2d.id,
data.l_p_of3d.id, data.l_p_of4d.id})
self.assertEqual(matcher.matches, 0)
self.assertEqual(len(matcher.matched_pairs), 0)
for line_item_id in {data.l_p_of1d.id, data.l_p_of2d.id,
data.l_p_of3d.id, data.l_p_of4d.id}:
line_item = db.session.get(JournalEntryLineItem, line_item_id)
@ -247,27 +230,32 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
:return: None.
"""
from accounting.models import Account, JournalEntryLineItem
from accounting.utils.offset_matcher import OffsetMatcher
from accounting.models import Currency, Account, JournalEntryLineItem
from accounting.report.utils.offset_matcher import OffsetMatcher
from accounting.template_globals import default_currency_code
data: SameTestData = SameTestData(self.app, "editor")
data.populate()
account: Account | None
line_item: JournalEntryLineItem | None
matcher: OffsetMatcher
list_uri: str
match_uri: str
response: httpx.Response
with self.app.app_context():
currency: Currency | None \
= db.session.get(Currency, default_currency_code())
assert currency is not None
# The receivables
with self.app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_r_or1d.id, data.l_r_or3d.id,
data.l_r_or4d.id, data.l_r_or5d.id,
data.l_r_or6d.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_r_of1c.id, data.l_r_of2c.id,
data.l_r_of4c.id, data.l_r_of5c.id,
data.l_r_of6c.id})
@ -287,22 +275,22 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item.original_line_item_id)
self.assertEqual(line_item.original_line_item_id, data.l_r_or2d.id)
list_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token})
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri)
self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_r_or5d.id, data.l_r_or6d.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_r_of1c.id, data.l_r_of5c.id})
self.assertEqual(matcher.matches, 0)
self.assertEqual(len(matcher.matched_pairs), 0)
for line_item_id in {data.l_r_of1c.id, data.l_r_of5c.id}:
line_item = db.session.get(JournalEntryLineItem, line_item_id)
self.assertIsNotNone(line_item)
@ -328,12 +316,12 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
with self.app.app_context():
account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_p_or1c.id, data.l_p_or3c.id,
data.l_p_or4c.id, data.l_p_or5c.id,
data.l_p_or6c.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_p_of1d.id, data.l_p_of2d.id,
data.l_p_of4d.id, data.l_p_of5d.id,
data.l_p_of6d.id})
@ -353,22 +341,22 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNotNone(line_item.original_line_item_id)
self.assertEqual(line_item.original_line_item_id, data.l_p_or2c.id)
list_uri = f"{PREFIX}/{Accounts.PAYABLE}"
match_uri = f"{PREFIX}/{Accounts.PAYABLE}"
response = self.client.post(match_uri,
data={"csrf_token": self.csrf_token})
data={"csrf_token": self.csrf_token,
"next": NEXT_URI})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri)
self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context():
account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None
matcher = OffsetMatcher(account)
matcher = OffsetMatcher(currency, account)
self.assertEqual({x.id for x in matcher.unapplied},
{data.l_p_or5c.id, data.l_p_or6c.id})
self.assertEqual({x.id for x in matcher.unmatched_offsets},
self.assertEqual({x.id for x in matcher.unmatched},
{data.l_p_of1d.id, data.l_p_of5d.id})
self.assertEqual(matcher.matches, 0)
self.assertEqual(len(matcher.matched_pairs), 0)
for line_item_id in {data.l_p_of1d.id, data.l_p_of5d.id}:
line_item = db.session.get(JournalEntryLineItem, line_item_id)
self.assertIsNotNone(line_item)