Compare commits
99 Commits
Author | SHA1 | Date | |
---|---|---|---|
50d4526e0b | |||
3f0a0b4227 | |||
dcc9626b23 | |||
79eb077129 | |||
d5719ad223 | |||
eb3fa8f414 | |||
937908717b | |||
0104fa4c21 | |||
14365ca255 | |||
cd86651606 | |||
9147744ff7 | |||
1a212a5330 | |||
0614457b7b | |||
545f49043b | |||
cac0d66ca1 | |||
5ffd37c859 | |||
9ae8c1bce9 | |||
ec0ff3e2e6 | |||
40a8080751 | |||
736a4086ee | |||
6723077b72 | |||
0ae00bce79 | |||
356d2010cc | |||
501c4b1d22 | |||
64b9c8c11f | |||
9072de82d4 | |||
30fd9c2164 | |||
7cb01b4cee | |||
9a4e04c41f | |||
a9c4fa9de0 | |||
3a676e0b5a | |||
9cc7b64bb3 | |||
352867797d | |||
09a344d749 | |||
818c357613 | |||
822c8fc49b | |||
3b8a2e3bb1 | |||
9e4927ee0b | |||
3b030c577c | |||
60b33f2a3b | |||
08fdf59844 | |||
b397515457 | |||
abe90d3483 | |||
65e7dcdf6d | |||
74e414badf | |||
69175979ff | |||
2f69e0f215 | |||
961385c389 | |||
a691cfd2da | |||
482a0faa23 | |||
0ecf7b6617 | |||
4408bbfc82 | |||
433110f486 | |||
0b1dd4f4fc | |||
46bd27e126 | |||
b718d19450 | |||
2969e83afe | |||
a732656746 | |||
1daed940b6 | |||
f29cb00aec | |||
693f07a49c | |||
8c899776f2 | |||
f9aa226bf9 | |||
c9bb4197be | |||
9ae8d587d8 | |||
158058dcfb | |||
0bc9947234 | |||
8c58a9083a | |||
f45663754c | |||
cda9e4e3c6 | |||
ee5b447c23 | |||
25bfcf4aa4 | |||
5956d2cd4c | |||
833285d924 | |||
dee4f5e83f | |||
f0d1cae32d | |||
5dc71697b3 | |||
1bb1e03c08 | |||
914ff92e0f | |||
8a1cf463b1 | |||
d4cf224d6b | |||
8d412ec00a | |||
2986c518ce | |||
f1351243a6 | |||
969e8c76a6 | |||
10f5e75752 | |||
169b3c292a | |||
3eb3aef2f2 | |||
6c455a615c | |||
4f3339bf68 | |||
b5aa7e923f | |||
359c335662 | |||
c11ae23885 | |||
e083b11394 | |||
167990fc4c | |||
d5c1be3d80 | |||
f6567794e0 | |||
ded85d88f7 | |||
6d780e9296 |
@ -38,3 +38,4 @@ python:
|
|||||||
install:
|
install:
|
||||||
- method: pip
|
- method: pip
|
||||||
path: .
|
path: .
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
|
1
docs/requirements.txt
Normal file
1
docs/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
sphinx_rtd_theme
|
@ -100,6 +100,14 @@ accounting.utils.strip\_text module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
accounting.utils.title\_case module
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: accounting.utils.title_case
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
accounting.utils.user module
|
accounting.utils.user module
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
@ -1,5 +1,127 @@
|
|||||||
Changes
|
Change Log
|
||||||
=======
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.10
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2023/11/28
|
||||||
|
|
||||||
|
Bug fix.
|
||||||
|
|
||||||
|
* Fixed the form validator to enable the selection of Accumulated Profit or
|
||||||
|
Loss accounts other than 3351-001.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.9
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/11/28
|
||||||
|
|
||||||
|
Bug fix.
|
||||||
|
|
||||||
|
* Refined to enable the selection of Accumulated Profit or Loss accounts other
|
||||||
|
than 3351-001, facilitating the consolidation of existing balances.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.8
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/10/24
|
||||||
|
|
||||||
|
Bug fix.
|
||||||
|
|
||||||
|
* Fixed an icon in the detail of the cash receipt journal entry.
|
||||||
|
|
||||||
|
Released at Jaipur, India on vacation.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.7
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/7/29
|
||||||
|
|
||||||
|
Revised account title capitalization to capitalize account titles
|
||||||
|
upon initialization of base accounts, rather than when displaying
|
||||||
|
the accounts. This prevents the system from incorrectly
|
||||||
|
capitalizing titles of user-added accounts.
|
||||||
|
|
||||||
|
For existing installation, run the ``accounting-titleize`` console
|
||||||
|
command to capitalize the existing account titles that were already
|
||||||
|
initialized.
|
||||||
|
|
||||||
|
Other fixes:
|
||||||
|
|
||||||
|
* Added missing documentation to the global variables, class
|
||||||
|
properties, and object properties.
|
||||||
|
* Various minor fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.6
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/5/23
|
||||||
|
|
||||||
|
Bug fixes.
|
||||||
|
|
||||||
|
* Fixed the return URI of the creation forms to decode the next URI.
|
||||||
|
* Fixed the unmatched offset list to use the encoded next URI.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/5/23
|
||||||
|
|
||||||
|
Security fixes.
|
||||||
|
|
||||||
|
* Revised the next URI utilities to encode and decode the next URI
|
||||||
|
preventing tampering with the next URI.
|
||||||
|
* Added the integrity value of the CDN stylesheet links.
|
||||||
|
* Various fixes.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/5/18
|
||||||
|
|
||||||
|
Security fixes.
|
||||||
|
|
||||||
|
* Added safeguard to the next URI utilities, to prevent Cross-Site
|
||||||
|
Scripting (XSS) attacks.
|
||||||
|
* Applied the safe next URI utilities to the test site.
|
||||||
|
* Added the ``SameSite`` and ``Secure`` flags to the session cookie
|
||||||
|
of the test site.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error of the net balance in the unmatched offset list.
|
||||||
|
* Revised the original line item editor not to override the existing
|
||||||
|
amount when the existing amount is less or equal to the net
|
||||||
|
balance.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error of the net balance in the unmatched offset list.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2023/4/30
|
||||||
|
|
||||||
|
* Fixed the error calling the old ``setEnableDescriptionAccount``
|
||||||
|
method in the ``saveOriginalLineItem`` method of the JavaScript
|
||||||
|
``JournalEntryLineItemEditor`` class.
|
||||||
|
|
||||||
|
|
||||||
Version 1.5.0
|
Version 1.5.0
|
||||||
|
@ -50,9 +50,9 @@ The following front-end JavaScript libraries must be loaded. You may
|
|||||||
download it locally or use CDN_.
|
download it locally or use CDN_.
|
||||||
|
|
||||||
* Bootstrap_ 5.2.3 or above
|
* Bootstrap_ 5.2.3 or above
|
||||||
* FontAwesome_ 6.2.1 or above
|
* FontAwesome_ 6.4.0 or above
|
||||||
* `Decimal.js`_ 6.4.3 or above
|
* `decimal.js`_ 10.4.3 or above, or `decimal.js-light`_ 2.5.1 or above.
|
||||||
* `Tempus-Dominus`_ 6.4.3 or above
|
* `Tempus-Dominus`_ 6.7.7 or above
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
@ -114,6 +114,7 @@ Check your Flask application and see how it works.
|
|||||||
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
|
||||||
.. _Bootstrap: https://getbootstrap.com
|
.. _Bootstrap: https://getbootstrap.com
|
||||||
.. _FontAwesome: https://fontawesome.com
|
.. _FontAwesome: https://fontawesome.com
|
||||||
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
.. _decimal.js: https://mikemcl.github.io/decimal.js
|
||||||
|
.. _decimal.js-light: https://mikemcl.github.io/decimal.js-light
|
||||||
.. _Tempus-Dominus: https://getdatepicker.com
|
.. _Tempus-Dominus: https://getdatepicker.com
|
||||||
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
||||||
|
@ -24,7 +24,7 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
|
|
||||||
from accounting.utils.user import UserUtilityInterface
|
from accounting.utils.user import UserUtilityInterface
|
||||||
|
|
||||||
VERSION: str = "1.5.0"
|
VERSION: str = "1.5.10"
|
||||||
"""The package version."""
|
"""The package version."""
|
||||||
db: SQLAlchemy = SQLAlchemy()
|
db: SQLAlchemy = SQLAlchemy()
|
||||||
"""The database instance."""
|
"""The database instance."""
|
||||||
@ -63,8 +63,9 @@ def init_app(app: Flask, user_utils: UserUtilityInterface,
|
|||||||
bp.add_app_template_global(default_currency_code,
|
bp.add_app_template_global(default_currency_code,
|
||||||
"accounting_default_currency_code")
|
"accounting_default_currency_code")
|
||||||
|
|
||||||
from .commands import init_db_command
|
from .commands import init_db_command, titleize_command
|
||||||
app.cli.add_command(init_db_command)
|
app.cli.add_command(init_db_command)
|
||||||
|
app.cli.add_command(titleize_command)
|
||||||
|
|
||||||
from . import locale
|
from . import locale
|
||||||
locale.init_app(app, bp)
|
locale.init_app(app, bp)
|
||||||
|
@ -17,15 +17,15 @@
|
|||||||
"""The console commands for the account management.
|
"""The console commands for the account management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from secrets import randbelow
|
from secrets import randbelow
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import BaseAccount, Account, AccountL10n
|
from accounting.models import BaseAccount, Account, AccountL10n
|
||||||
from accounting.utils.user import get_user_pk
|
from accounting.utils.user import get_user_pk
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
AccountData = tuple[int, str, int, str, str, str, bool]
|
AccountData = tuple[int, str, int, str, str, str, bool]
|
||||||
"""The format of the account data, as a list of (ID, base account code, number,
|
"""The format of the account data, as a list of (ID, base account code, number,
|
||||||
@ -63,8 +63,8 @@ def init_accounts_command(username: str) -> None:
|
|||||||
existing_id.add(new_id)
|
existing_id.add(new_id)
|
||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
data: list[dict[str, t.Any]] = []
|
data: list[dict[str, Any]] = []
|
||||||
l10n_data: list[dict[str, t.Any]] = []
|
l10n_data: list[dict[str, Any]] = []
|
||||||
for base in bases_to_add:
|
for base in bases_to_add:
|
||||||
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
|
l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
|
||||||
account_id: int = get_new_id()
|
account_id: int = get_new_id()
|
||||||
|
@ -168,7 +168,9 @@ class AccountReorderForm:
|
|||||||
:param base: The base account.
|
:param base: The base account.
|
||||||
"""
|
"""
|
||||||
self.base: BaseAccount = base
|
self.base: BaseAccount = base
|
||||||
|
"""The base account."""
|
||||||
self.is_modified: bool = False
|
self.is_modified: bool = False
|
||||||
|
"""Whether the order is modified."""
|
||||||
|
|
||||||
def save_order(self) -> None:
|
def save_order(self) -> None:
|
||||||
"""Saves the order of the account.
|
"""Saves the order of the account.
|
||||||
|
@ -24,6 +24,7 @@ import sqlalchemy as sa
|
|||||||
from accounting import data_dir
|
from accounting import data_dir
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import BaseAccount, BaseAccountL10n
|
from accounting.models import BaseAccount, BaseAccountL10n
|
||||||
|
from accounting.utils.title_case import title_case
|
||||||
|
|
||||||
|
|
||||||
def init_base_accounts_command() -> None:
|
def init_base_accounts_command() -> None:
|
||||||
@ -34,7 +35,7 @@ def init_base_accounts_command() -> None:
|
|||||||
with open(data_dir / "base_accounts.csv") as fp:
|
with open(data_dir / "base_accounts.csv") as fp:
|
||||||
data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
|
data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
|
||||||
account_data: list[dict[str, str]] = [{"code": x["code"],
|
account_data: list[dict[str, str]] = [{"code": x["code"],
|
||||||
"title_l10n": x["title"]}
|
"title_l10n": title_case(x["title"])}
|
||||||
for x in data]
|
for x in data]
|
||||||
locales: list[str] = [x[5:] for x in data[0] if x.startswith("l10n-")]
|
locales: list[str] = [x[5:] for x in data[0] if x.startswith("l10n-")]
|
||||||
l10n_data: list[dict[str, str]] = [{"account_code": x["code"],
|
l10n_data: list[dict[str, str]] = [{"account_code": x["code"],
|
||||||
|
@ -26,7 +26,10 @@ from accounting import db
|
|||||||
from accounting.account import init_accounts_command
|
from accounting.account import init_accounts_command
|
||||||
from accounting.base_account import init_base_accounts_command
|
from accounting.base_account import init_base_accounts_command
|
||||||
from accounting.currency import init_currencies_command
|
from accounting.currency import init_currencies_command
|
||||||
from accounting.utils.user import has_user
|
from accounting.models import BaseAccount, Account
|
||||||
|
from accounting.utils.title_case import title_case
|
||||||
|
from accounting.utils.user import has_user, get_user_pk
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
def __validate_username(ctx: click.core.Context, param: click.core.Option,
|
def __validate_username(ctx: click.core.Context, param: click.core.Option,
|
||||||
@ -60,3 +63,32 @@ def init_db_command(username: str) -> None:
|
|||||||
init_currencies_command(username)
|
init_currencies_command(username)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
click.echo("Accounting database initialized.")
|
click.echo("Accounting database initialized.")
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("accounting-titleize")
|
||||||
|
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
|
||||||
|
help="The username.", callback=__validate_username,
|
||||||
|
default=lambda: os.getlogin())
|
||||||
|
@with_appcontext
|
||||||
|
def titleize_command(username: str) -> None:
|
||||||
|
"""Capitalize the account titles."""
|
||||||
|
updater_pk: int = get_user_pk(username)
|
||||||
|
updated: int = 0
|
||||||
|
for base in BaseAccount.query:
|
||||||
|
new_title: str = title_case(base.title_l10n)
|
||||||
|
if base.title_l10n != new_title:
|
||||||
|
base.title_l10n = new_title
|
||||||
|
updated = updated + 1
|
||||||
|
for account in Account.query:
|
||||||
|
if account.title_l10n.lower() == account.base.title_l10n.lower():
|
||||||
|
new_title: str = title_case(account.title_l10n)
|
||||||
|
if account.title_l10n != new_title:
|
||||||
|
account.title_l10n = new_title
|
||||||
|
account.updated_at = sa.func.now()
|
||||||
|
account.updated_by_id = updater_pk
|
||||||
|
updated = updated + 1
|
||||||
|
if updated == 0:
|
||||||
|
click.echo("All account titles were already capitalized.")
|
||||||
|
return
|
||||||
|
db.session.commit()
|
||||||
|
click.echo(f"{updated} account titles capitalized.")
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@ -39,11 +39,11 @@ def init_currencies_command(username: str) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
creator_pk: int = get_user_pk(username)
|
creator_pk: int = get_user_pk(username)
|
||||||
currency_data: list[dict[str, t.Any]] = [{"code": x["code"],
|
currency_data: list[dict[str, Any]] = [{"code": x["code"],
|
||||||
"name_l10n": x["name"],
|
"name_l10n": x["name"],
|
||||||
"created_by_id": creator_pk,
|
"created_by_id": creator_pk,
|
||||||
"updated_by_id": creator_pk}
|
"updated_by_id": creator_pk}
|
||||||
for x in to_add]
|
for x in to_add]
|
||||||
locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
|
locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
|
||||||
l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
|
l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
|
||||||
"locale": y,
|
"locale": y,
|
||||||
|
@ -65,12 +65,13 @@ class IsDebitAccount:
|
|||||||
:param message: The error message.
|
:param message: The error message.
|
||||||
"""
|
"""
|
||||||
self.__message: str | LazyString = message
|
self.__message: str | LazyString = message
|
||||||
|
"""The error message."""
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
if field.data is None:
|
if field.data is None:
|
||||||
return
|
return
|
||||||
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
|
if re.match(r"^(?:[1235689]|7[5678])", field.data) \
|
||||||
and not field.data.startswith("3351-") \
|
and field.data != "3351-001" \
|
||||||
and not field.data.startswith("3353-"):
|
and not field.data.startswith("3353-"):
|
||||||
return
|
return
|
||||||
raise ValidationError(self.__message)
|
raise ValidationError(self.__message)
|
||||||
@ -85,12 +86,13 @@ class IsCreditAccount:
|
|||||||
:param message: The error message.
|
:param message: The error message.
|
||||||
"""
|
"""
|
||||||
self.__message: str | LazyString = message
|
self.__message: str | LazyString = message
|
||||||
|
"""The error message."""
|
||||||
|
|
||||||
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
def __call__(self, form: FlaskForm, field: StringField) -> None:
|
||||||
if field.data is None:
|
if field.data is None:
|
||||||
return
|
return
|
||||||
if re.match(r"^(?:[123489]|7[1234])", field.data) \
|
if re.match(r"^(?:[123489]|7[1234])", field.data) \
|
||||||
and not field.data.startswith("3351-") \
|
and field.data != "3351-001" \
|
||||||
and not field.data.startswith("3353-"):
|
and not field.data.startswith("3353-"):
|
||||||
return
|
return
|
||||||
raise ValidationError(self.__message)
|
raise ValidationError(self.__message)
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The path converters for the journal entry management.
|
"""The path converters for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
|
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
@ -82,18 +82,18 @@ class DateConverter(BaseConverter):
|
|||||||
"""The date converter to convert the ISO date from and to the
|
"""The date converter to convert the ISO date from and to the
|
||||||
corresponding date in the routes."""
|
corresponding date in the routes."""
|
||||||
|
|
||||||
def to_python(self, value: str) -> date:
|
def to_python(self, value: str) -> dt.date:
|
||||||
"""Converts an ISO date to a date.
|
"""Converts an ISO date to a date.
|
||||||
|
|
||||||
:param value: The ISO date.
|
:param value: The ISO date.
|
||||||
:return: The corresponding date.
|
:return: The corresponding date.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return date.fromisoformat(value)
|
return dt.date.fromisoformat(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def to_url(self, value: date) -> str:
|
def to_url(self, value: dt.date) -> str:
|
||||||
"""Converts a date to its ISO date.
|
"""Converts a date to its ISO date.
|
||||||
|
|
||||||
:param value: The date.
|
:param value: The date.
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TypeVar, Generic, Type
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
@ -29,13 +29,13 @@ from wtforms import DateField, FieldList, FormField, TextAreaField, \
|
|||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
|
from accounting.journal_entry.utils.account_option import AccountOption
|
||||||
|
from accounting.journal_entry.utils.description_editor import DescriptionEditor
|
||||||
|
from accounting.journal_entry.utils.original_line_items import \
|
||||||
|
get_selectable_original_line_items
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
|
from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
|
||||||
JournalEntryCurrency
|
JournalEntryCurrency
|
||||||
from accounting.journal_entry.utils.account_option import AccountOption
|
|
||||||
from accounting.journal_entry.utils.original_line_items import \
|
|
||||||
get_selectable_original_line_items
|
|
||||||
from accounting.journal_entry.utils.description_editor import DescriptionEditor
|
|
||||||
from accounting.utils.random_id import new_id
|
from accounting.utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_multiline_text
|
from accounting.utils.strip_text import strip_multiline_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
@ -123,7 +123,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.is_modified: bool = False
|
self.is_modified: bool = False
|
||||||
"""Whether the journal entry is modified during populate_obj()."""
|
"""Whether the journal entry is modified during populate_obj()."""
|
||||||
self.collector: t.Type[LineItemCollector] = LineItemCollector
|
self.collector: Type[LineItemCollector] = LineItemCollector
|
||||||
"""The line item collector. The default is the base abstract
|
"""The line item collector. The default is the base abstract
|
||||||
collector only to provide the correct type. The subclass forms should
|
collector only to provide the correct type. The subclass forms should
|
||||||
provide their own collectors."""
|
provide their own collectors."""
|
||||||
@ -151,11 +151,10 @@ class JournalEntryForm(FlaskForm):
|
|||||||
is_new: bool = obj.id is None
|
is_new: bool = obj.id is None
|
||||||
if is_new:
|
if is_new:
|
||||||
obj.id = new_id(JournalEntry)
|
obj.id = new_id(JournalEntry)
|
||||||
self.date: DateField
|
|
||||||
self.__set_date(obj, self.date.data)
|
self.__set_date(obj, self.date.data)
|
||||||
obj.note = self.note.data
|
obj.note = self.note.data
|
||||||
|
|
||||||
collector_cls: t.Type[LineItemCollector] = self.collector
|
collector_cls: Type[LineItemCollector] = self.collector
|
||||||
collector: collector_cls = collector_cls(self, obj)
|
collector: collector_cls = collector_cls(self, obj)
|
||||||
collector.collect()
|
collector.collect()
|
||||||
|
|
||||||
@ -309,11 +308,11 @@ class JournalEntryForm(FlaskForm):
|
|||||||
return db.session.scalar(select)
|
return db.session.scalar(select)
|
||||||
|
|
||||||
|
|
||||||
T = t.TypeVar("T", bound=JournalEntryForm)
|
T = TypeVar("T", bound=JournalEntryForm)
|
||||||
"""A journal entry form variant."""
|
"""A journal entry form variant."""
|
||||||
|
|
||||||
|
|
||||||
class LineItemCollector(t.Generic[T], ABC):
|
class LineItemCollector(Generic[T], ABC):
|
||||||
"""The line item collector."""
|
"""The line item collector."""
|
||||||
|
|
||||||
def __init__(self, form: T, obj: JournalEntry):
|
def __init__(self, form: T, obj: JournalEntry):
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The line item sub-forms for the journal entry management.
|
"""The line item sub-forms for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -307,7 +307,7 @@ class LineItemForm(FlaskForm):
|
|||||||
return getattr(self, "____original_line_item")
|
return getattr(self, "____original_line_item")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def original_line_item_date(self) -> date | None:
|
def original_line_item_date(self) -> dt.date | None:
|
||||||
"""Returns the text representation of the original line item.
|
"""Returns the text representation of the original line item.
|
||||||
|
|
||||||
:return: The text representation of the original line item.
|
:return: The text representation of the original line item.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The reorder forms for the journal entry management.
|
"""The reorder forms for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -26,17 +26,15 @@ from accounting import db
|
|||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
|
|
||||||
|
|
||||||
def sort_journal_entries_in(journal_entry_date: date,
|
def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
|
||||||
exclude: int | None = None) -> None:
|
|
||||||
"""Sorts the journal entries under a date after changing the date or
|
"""Sorts the journal entries under a date after changing the date or
|
||||||
deleting a journal entry.
|
deleting a journal entry.
|
||||||
|
|
||||||
:param journal_entry_date: The date of the journal entry.
|
:param date: The date of the journal entry.
|
||||||
:param exclude: The journal entry ID to exclude.
|
:param exclude: The journal entry ID to exclude.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.BinaryExpression] = [JournalEntry.date == date]
|
||||||
= [JournalEntry.date == journal_entry_date]
|
|
||||||
if exclude is not None:
|
if exclude is not None:
|
||||||
conditions.append(JournalEntry.id != exclude)
|
conditions.append(JournalEntry.id != exclude)
|
||||||
journal_entries: list[JournalEntry] = JournalEntry.query\
|
journal_entries: list[JournalEntry] = JournalEntry.query\
|
||||||
@ -50,13 +48,15 @@ def sort_journal_entries_in(journal_entry_date: date,
|
|||||||
class JournalEntryReorderForm:
|
class JournalEntryReorderForm:
|
||||||
"""The form to reorder the journal entries."""
|
"""The form to reorder the journal entries."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: date):
|
def __init__(self, date: dt.date):
|
||||||
"""Constructs the form to reorder the journal entries in a day.
|
"""Constructs the form to reorder the journal entries in a day.
|
||||||
|
|
||||||
:param journal_entry_date: The date.
|
:param date: The date.
|
||||||
"""
|
"""
|
||||||
self.date: date = journal_entry_date
|
self.date: dt.date = date
|
||||||
|
"""The date."""
|
||||||
self.is_modified: bool = False
|
self.is_modified: bool = False
|
||||||
|
"""Whether the order is modified."""
|
||||||
|
|
||||||
def save_order(self) -> None:
|
def save_order(self) -> None:
|
||||||
"""Saves the order of the account.
|
"""Saves the order of the account.
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import typing as t
|
from typing import Literal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@ -124,12 +124,12 @@ class DescriptionTag:
|
|||||||
class DescriptionType:
|
class DescriptionType:
|
||||||
"""A description type"""
|
"""A description type"""
|
||||||
|
|
||||||
def __init__(self, type_id: t.Literal["general", "travel", "bus"]):
|
def __init__(self, type_id: Literal["general", "travel", "bus"]):
|
||||||
"""Constructs a description type.
|
"""Constructs a description type.
|
||||||
|
|
||||||
:param type_id: The type ID, either "general", "travel", or "bus".
|
:param type_id: The type ID, either "general", "travel", or "bus".
|
||||||
"""
|
"""
|
||||||
self.id: t.Literal["general", "travel", "bus"] = type_id
|
self.id: Literal["general", "travel", "bus"] = type_id
|
||||||
"""The type ID."""
|
"""The type ID."""
|
||||||
self.__tag_dict: dict[str, DescriptionTag] = {}
|
self.__tag_dict: dict[str, DescriptionTag] = {}
|
||||||
"""A dictionary from the tag name to their corresponding tag."""
|
"""A dictionary from the tag name to their corresponding tag."""
|
||||||
@ -166,8 +166,11 @@ class DescriptionRecurring:
|
|||||||
:param account: The account.
|
:param account: The account.
|
||||||
"""
|
"""
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
"""The name."""
|
||||||
self.account: DescriptionAccount = DescriptionAccount(account, 0)
|
self.account: DescriptionAccount = DescriptionAccount(account, 0)
|
||||||
|
"""The account."""
|
||||||
self.description_template: str = description_template
|
self.description_template: str = description_template
|
||||||
|
"""The description template."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account_codes(self) -> list[str]:
|
def account_codes(self) -> list[str]:
|
||||||
@ -181,12 +184,12 @@ class DescriptionRecurring:
|
|||||||
class DescriptionDebitCredit:
|
class DescriptionDebitCredit:
|
||||||
"""The description on debit or credit."""
|
"""The description on debit or credit."""
|
||||||
|
|
||||||
def __init__(self, debit_credit: t.Literal["debit", "credit"]):
|
def __init__(self, debit_credit: Literal["debit", "credit"]):
|
||||||
"""Constructs the description on debit or credit.
|
"""Constructs the description on debit or credit.
|
||||||
|
|
||||||
:param debit_credit: Either "debit" or "credit".
|
:param debit_credit: Either "debit" or "credit".
|
||||||
"""
|
"""
|
||||||
self.debit_credit: t.Literal["debit", "credit"] = debit_credit
|
self.debit_credit: Literal["debit", "credit"] = debit_credit
|
||||||
"""Either debit or credit."""
|
"""Either debit or credit."""
|
||||||
self.general: DescriptionType = DescriptionType("general")
|
self.general: DescriptionType = DescriptionType("general")
|
||||||
"""The general tags."""
|
"""The general tags."""
|
||||||
@ -194,14 +197,14 @@ class DescriptionDebitCredit:
|
|||||||
"""The travel tags."""
|
"""The travel tags."""
|
||||||
self.bus: DescriptionType = DescriptionType("bus")
|
self.bus: DescriptionType = DescriptionType("bus")
|
||||||
"""The bus tags."""
|
"""The bus tags."""
|
||||||
self.__type_dict: dict[t.Literal["general", "travel", "bus"],
|
self.__type_dict: dict[Literal["general", "travel", "bus"],
|
||||||
DescriptionType] \
|
DescriptionType] \
|
||||||
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
= {x.id: x for x in {self.general, self.travel, self.bus}}
|
||||||
"""A dictionary from the type ID to the corresponding tags."""
|
"""A dictionary from the type ID to the corresponding tags."""
|
||||||
self.recurring: list[DescriptionRecurring] = []
|
self.recurring: list[DescriptionRecurring] = []
|
||||||
"""The recurring transactions."""
|
"""The recurring transactions."""
|
||||||
|
|
||||||
def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
|
def add_tag(self, tag_type: Literal["general", "travel", "bus"],
|
||||||
name: str, account: Account, freq: int) -> None:
|
name: str, account: Account, freq: int) -> None:
|
||||||
"""Adds a tag.
|
"""Adds a tag.
|
||||||
|
|
||||||
@ -278,7 +281,7 @@ class DescriptionEditor:
|
|||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in Account.query
|
||||||
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
||||||
debit_credit_dict: dict[t.Literal["debit", "credit"],
|
debit_credit_dict: dict[Literal["debit", "credit"],
|
||||||
DescriptionDebitCredit] \
|
DescriptionDebitCredit] \
|
||||||
= {x.debit_credit: x for x in {self.debit, self.credit}}
|
= {x.debit_credit: x for x in {self.debit, self.credit}}
|
||||||
for row in result:
|
for row in result:
|
||||||
|
@ -17,19 +17,19 @@
|
|||||||
"""The operators for different journal entry types.
|
"""The operators for different journal entry types.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from flask import render_template, request, abort
|
from flask import render_template, request, abort
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
|
|
||||||
from accounting.models import JournalEntry
|
|
||||||
from accounting.template_globals import default_currency_code
|
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
|
||||||
from accounting.journal_entry.forms import JournalEntryForm, \
|
from accounting.journal_entry.forms import JournalEntryForm, \
|
||||||
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
|
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
|
||||||
TransferJournalEntryForm
|
TransferJournalEntryForm
|
||||||
from accounting.journal_entry.forms.line_item import LineItemForm
|
from accounting.journal_entry.forms.line_item import LineItemForm
|
||||||
|
from accounting.models import JournalEntry
|
||||||
|
from accounting.template_globals import default_currency_code
|
||||||
|
from accounting.utils.journal_entry_types import JournalEntryType
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryOperator(ABC):
|
class JournalEntryOperator(ABC):
|
||||||
@ -39,7 +39,7 @@ class JournalEntryOperator(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@ -100,7 +100,7 @@ class CashReceiptJournalEntry(JournalEntryOperator):
|
|||||||
"""The order when checking the journal entry operator."""
|
"""The order when checking the journal entry operator."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@ -170,7 +170,7 @@ class CashDisbursementJournalEntry(JournalEntryOperator):
|
|||||||
"""The order when checking the journal entry operator."""
|
"""The order when checking the journal entry operator."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@ -243,7 +243,7 @@ class TransferJournalEntry(JournalEntryOperator):
|
|||||||
"""The order when checking the journal entry operator."""
|
"""The order when checking the journal entry operator."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> t.Type[JournalEntryForm]:
|
def form(self) -> Type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The views for the journal entry management.
|
"""The views for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from urllib.parse import parse_qsl, urlencode
|
from urllib.parse import parse_qsl, urlencode
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -30,9 +30,9 @@ from accounting.locale import lazy_gettext
|
|||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
from accounting.utils.cast import s
|
from accounting.utils.cast import s
|
||||||
from accounting.utils.flash_errors import flash_form_errors
|
from accounting.utils.flash_errors import flash_form_errors
|
||||||
|
from accounting.utils.journal_entry_types import JournalEntryType
|
||||||
from accounting.utils.next_uri import inherit_next, or_next
|
from accounting.utils.next_uri import inherit_next, or_next
|
||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
from accounting.utils.permission import has_permission, can_view, can_edit
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
from .forms import sort_journal_entries_in, JournalEntryReorderForm
|
from .forms import sort_journal_entries_in, JournalEntryReorderForm
|
||||||
from .template_filters import with_type, to_transfer, format_amount_input, \
|
from .template_filters import with_type, to_transfer, format_amount_input, \
|
||||||
@ -67,7 +67,7 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
|
|||||||
form.validate()
|
form.validate()
|
||||||
else:
|
else:
|
||||||
form = journal_entry_op.form()
|
form = journal_entry_op.form()
|
||||||
form.date.data = date.today()
|
form.date.data = dt.date.today()
|
||||||
return journal_entry_op.render_create_template(form)
|
return journal_entry_op.render_create_template(form)
|
||||||
|
|
||||||
|
|
||||||
@ -186,31 +186,31 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
|||||||
return redirect(or_next(__get_default_page_uri()))
|
return redirect(or_next(__get_default_page_uri()))
|
||||||
|
|
||||||
|
|
||||||
@bp.get("dates/<date:journal_entry_date>", endpoint="order")
|
@bp.get("dates/<date:date>", endpoint="order")
|
||||||
@has_permission(can_view)
|
@has_permission(can_view)
|
||||||
def show_journal_entry_order(journal_entry_date: date) -> str:
|
def show_journal_entry_order(date: dt.date) -> str:
|
||||||
"""Shows the order of the journal entries in a same date.
|
"""Shows the order of the journal entries in a same date.
|
||||||
|
|
||||||
:param journal_entry_date: The date.
|
:param date: The date.
|
||||||
:return: The order of the journal entries in the date.
|
:return: The order of the journal entries in the date.
|
||||||
"""
|
"""
|
||||||
journal_entries: list[JournalEntry] = JournalEntry.query \
|
journal_entries: list[JournalEntry] = JournalEntry.query \
|
||||||
.filter(JournalEntry.date == journal_entry_date) \
|
.filter(JournalEntry.date == date) \
|
||||||
.order_by(JournalEntry.no).all()
|
.order_by(JournalEntry.no).all()
|
||||||
return render_template("accounting/journal-entry/order.html",
|
return render_template("accounting/journal-entry/order.html",
|
||||||
date=journal_entry_date, list=journal_entries)
|
date=date, list=journal_entries)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("dates/<date:journal_entry_date>", endpoint="sort")
|
@bp.post("dates/<date:date>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_journal_entries(journal_entry_date: date) -> redirect:
|
def sort_journal_entries(date: dt.date) -> redirect:
|
||||||
"""Reorders the journal entries in a date.
|
"""Reorders the journal entries in a date.
|
||||||
|
|
||||||
:param journal_entry_date: The date.
|
:param date: The date.
|
||||||
:return: The redirection to the incoming account or the account list. The
|
:return: The redirection to the incoming account or the account list. The
|
||||||
reordering operation does not fail.
|
reordering operation does not fail.
|
||||||
"""
|
"""
|
||||||
form: JournalEntryReorderForm = JournalEntryReorderForm(journal_entry_date)
|
form: JournalEntryReorderForm = JournalEntryReorderForm(date)
|
||||||
form.save_order()
|
form.save_order()
|
||||||
if not form.is_modified:
|
if not form.is_modified:
|
||||||
flash(s(lazy_gettext("The order was not modified.")), "success")
|
flash(s(lazy_gettext("The order was not modified.")), "success")
|
||||||
|
@ -25,8 +25,10 @@ from flask_babel import LazyString, Domain
|
|||||||
from flask_babel_js import JAVASCRIPT, c2js
|
from flask_babel_js import JAVASCRIPT, c2js
|
||||||
|
|
||||||
translation_dir: Path = Path(__file__).parent / "translations"
|
translation_dir: Path = Path(__file__).parent / "translations"
|
||||||
|
"""The directory of the translation files."""
|
||||||
domain: Domain = Domain(translation_directories=[translation_dir],
|
domain: Domain = Domain(translation_directories=[translation_dir],
|
||||||
domain="accounting")
|
domain="accounting")
|
||||||
|
"""The message domain."""
|
||||||
|
|
||||||
|
|
||||||
def gettext(string, **variables) -> str:
|
def gettext(string, **variables) -> str:
|
||||||
@ -120,6 +122,5 @@ def init_app(app: Flask, bp: Blueprint) -> None:
|
|||||||
:param bp: The blueprint of the accounting application.
|
:param bp: The blueprint of the accounting application.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
bp.add_url_rule("/_jstrans.js", "babel_catalog",
|
bp.add_url_rule("/_jstrans.js", "babel_catalog", __babel_js_catalog_view)
|
||||||
__babel_js_catalog_view)
|
|
||||||
app.jinja_env.globals["A_"] = domain.gettext
|
app.jinja_env.globals["A_"] = domain.gettext
|
||||||
|
@ -21,8 +21,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
import typing as t
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Type, Self
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
@ -40,7 +40,7 @@ class BaseAccount(db.Model):
|
|||||||
__tablename__ = "accounting_base_accounts"
|
__tablename__ = "accounting_base_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
code: Mapped[str] = mapped_column(primary_key=True)
|
code: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The code."""
|
"""The account code."""
|
||||||
title_l10n: Mapped[str] = mapped_column("title")
|
title_l10n: Mapped[str] = mapped_column("title")
|
||||||
"""The title."""
|
"""The title."""
|
||||||
l10n: Mapped[list[BaseAccountL10n]] \
|
l10n: Mapped[list[BaseAccountL10n]] \
|
||||||
@ -54,7 +54,7 @@ class BaseAccount(db.Model):
|
|||||||
|
|
||||||
:return: The string representation of the base account.
|
:return: The string representation of the base account.
|
||||||
"""
|
"""
|
||||||
return f"{self.code} {self.title.title()}"
|
return f"{self.code} {self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> str:
|
def title(self) -> str:
|
||||||
@ -87,7 +87,7 @@ class BaseAccountL10n(db.Model):
|
|||||||
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
"""The code of the account."""
|
"""The account code."""
|
||||||
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
|
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale: Mapped[str] = mapped_column(primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
@ -117,21 +117,21 @@ class Account(db.Model):
|
|||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The date and time when this record was created."""
|
||||||
created_by_id: Mapped[int] \
|
created_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the creator."""
|
"""The ID of the user who created the record."""
|
||||||
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
"""The creator."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id: Mapped[int] \
|
updated_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the updator."""
|
"""The ID of the last user who updated the record."""
|
||||||
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The updator."""
|
"""The last user who updated the record."""
|
||||||
l10n: Mapped[list[AccountL10n]] \
|
l10n: Mapped[list[AccountL10n]] \
|
||||||
= db.relationship(back_populates="account", lazy=False)
|
= db.relationship(back_populates="account", lazy=False)
|
||||||
"""The localized titles."""
|
"""The localized titles."""
|
||||||
@ -151,7 +151,7 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The string representation of this account.
|
:return: The string representation of this account.
|
||||||
"""
|
"""
|
||||||
return f"{self.base_code}-{self.no:03d} {self.title.title()}"
|
return f"{self.base_code}-{self.no:03d} {self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code(self) -> str:
|
def code(self) -> str:
|
||||||
@ -182,6 +182,8 @@ class Account(db.Model):
|
|||||||
:param value: The new title.
|
:param value: The new title.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
if self.title == value:
|
||||||
|
return
|
||||||
if self.title_l10n is None:
|
if self.title_l10n is None:
|
||||||
self.title_l10n = value
|
self.title_l10n = value
|
||||||
return
|
return
|
||||||
@ -222,13 +224,13 @@ class Account(db.Model):
|
|||||||
return getattr(self, "__count")
|
return getattr(self, "__count")
|
||||||
|
|
||||||
@count.setter
|
@count.setter
|
||||||
def count(self, count: int) -> None:
|
def count(self, value: int) -> None:
|
||||||
"""Sets the number of items in the account.
|
"""Sets the number of items in the account.
|
||||||
|
|
||||||
:param count: The number of items in the account.
|
:param value: The number of items in the account.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__count", count)
|
setattr(self, "__count", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query_values(self) -> list[str]:
|
def query_values(self) -> list[str]:
|
||||||
@ -267,11 +269,11 @@ class Account(db.Model):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
AccountL10n.query.filter(AccountL10n.account == self).delete()
|
AccountL10n.query.filter(AccountL10n.account == self).delete()
|
||||||
cls: t.Type[t.Self] = self.__class__
|
cls: Type[Self] = self.__class__
|
||||||
cls.query.filter(cls.id == self.id).delete()
|
cls.query.filter(cls.id == self.id).delete()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_by_code(cls, code: str) -> t.Self | None:
|
def find_by_code(cls, code: str) -> Self | None:
|
||||||
"""Finds an account by its code.
|
"""Finds an account by its code.
|
||||||
|
|
||||||
:param code: The code.
|
:param code: The code.
|
||||||
@ -284,7 +286,7 @@ class Account(db.Model):
|
|||||||
cls.no == int(m.group(2))).first()
|
cls.no == int(m.group(2))).first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectable_debit(cls) -> list[t.Self]:
|
def selectable_debit(cls) -> list[Self]:
|
||||||
"""Returns the selectable debit accounts.
|
"""Returns the selectable debit accounts.
|
||||||
Payable line items can not start from debit.
|
Payable line items can not start from debit.
|
||||||
|
|
||||||
@ -302,12 +304,13 @@ class Account(db.Model):
|
|||||||
cls.base_code.startswith("78"),
|
cls.base_code.startswith("78"),
|
||||||
cls.base_code.startswith("8"),
|
cls.base_code.startswith("8"),
|
||||||
cls.base_code.startswith("9")),
|
cls.base_code.startswith("9")),
|
||||||
cls.base_code != "3351",
|
sa.not_(sa.and_(cls.base_code == "3351",
|
||||||
|
cls.no == 1)),
|
||||||
cls.base_code != "3353")\
|
cls.base_code != "3353")\
|
||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectable_credit(cls) -> list[t.Self]:
|
def selectable_credit(cls) -> list[Self]:
|
||||||
"""Returns the selectable debit accounts.
|
"""Returns the selectable debit accounts.
|
||||||
Receivable line items can not start from credit.
|
Receivable line items can not start from credit.
|
||||||
|
|
||||||
@ -324,12 +327,13 @@ class Account(db.Model):
|
|||||||
cls.base_code.startswith("74"),
|
cls.base_code.startswith("74"),
|
||||||
cls.base_code.startswith("8"),
|
cls.base_code.startswith("8"),
|
||||||
cls.base_code.startswith("9")),
|
cls.base_code.startswith("9")),
|
||||||
cls.base_code != "3351",
|
sa.not_(sa.and_(cls.base_code == "3351",
|
||||||
|
cls.no == 1)),
|
||||||
cls.base_code != "3353")\
|
cls.base_code != "3353")\
|
||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cash(cls) -> t.Self:
|
def cash(cls) -> Self:
|
||||||
"""Returns the cash account.
|
"""Returns the cash account.
|
||||||
|
|
||||||
:return: The cash account
|
:return: The cash account
|
||||||
@ -337,7 +341,7 @@ class Account(db.Model):
|
|||||||
return cls.find_by_code(cls.CASH_CODE)
|
return cls.find_by_code(cls.CASH_CODE)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accumulated_change(cls) -> t.Self:
|
def accumulated_change(cls) -> Self:
|
||||||
"""Returns the accumulated-change account.
|
"""Returns the accumulated-change account.
|
||||||
|
|
||||||
:return: The accumulated-change account
|
:return: The accumulated-change account
|
||||||
@ -367,28 +371,28 @@ class Currency(db.Model):
|
|||||||
__tablename__ = "accounting_currencies"
|
__tablename__ = "accounting_currencies"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
code: Mapped[str] = mapped_column(primary_key=True)
|
code: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The code."""
|
"""The currency code."""
|
||||||
name_l10n: Mapped[str] = mapped_column("name")
|
name_l10n: Mapped[str] = mapped_column("name")
|
||||||
"""The name."""
|
"""The currency name."""
|
||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The date and time when this record was created."""
|
||||||
created_by_id: Mapped[int] \
|
created_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the creator."""
|
"""The ID of the user who created the record."""
|
||||||
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
"""The creator."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id: Mapped[int] \
|
updated_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the updator."""
|
"""The ID of the last user who updated the record."""
|
||||||
updated_by: Mapped[user_cls] \
|
updated_by: Mapped[user_cls] \
|
||||||
= db.relationship(foreign_keys=updated_by_id)
|
= db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The updator."""
|
"""The last user who updated the record."""
|
||||||
l10n: Mapped[list[CurrencyL10n]] \
|
l10n: Mapped[list[CurrencyL10n]] \
|
||||||
= db.relationship(back_populates="currency", lazy=False)
|
= db.relationship(back_populates="currency", lazy=False)
|
||||||
"""The localized names."""
|
"""The localized names."""
|
||||||
@ -424,6 +428,8 @@ class Currency(db.Model):
|
|||||||
:param value: The new name.
|
:param value: The new name.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
if self.name == value:
|
||||||
|
return
|
||||||
if self.name_l10n is None:
|
if self.name_l10n is None:
|
||||||
self.name_l10n = value
|
self.name_l10n = value
|
||||||
return
|
return
|
||||||
@ -467,7 +473,7 @@ class Currency(db.Model):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
|
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
|
||||||
cls: t.Type[t.Self] = self.__class__
|
cls: Type[Self] = self.__class__
|
||||||
cls.query.filter(cls.code == self.code).delete()
|
cls.query.filter(cls.code == self.code).delete()
|
||||||
|
|
||||||
|
|
||||||
@ -540,27 +546,27 @@ class JournalEntry(db.Model):
|
|||||||
date: Mapped[dt.date]
|
date: Mapped[dt.date]
|
||||||
"""The date."""
|
"""The date."""
|
||||||
no: Mapped[int] = mapped_column(default=text("1"))
|
no: Mapped[int] = mapped_column(default=text("1"))
|
||||||
"""The account number under the date."""
|
"""The journal entry number under the date."""
|
||||||
note: Mapped[str | None]
|
note: Mapped[str | None]
|
||||||
"""The note."""
|
"""The note."""
|
||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The date and time when this record was created."""
|
||||||
created_by_id: Mapped[int] \
|
created_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the creator."""
|
"""The ID of the user who created the record."""
|
||||||
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
"""The creator."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id: Mapped[int] \
|
updated_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the updator."""
|
"""The ID of the last user who updated the record."""
|
||||||
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The updator."""
|
"""The last user who updated the record."""
|
||||||
line_items: Mapped[list[JournalEntryLineItem]] \
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
= db.relationship(back_populates="journal_entry")
|
= db.relationship(back_populates="journal_entry")
|
||||||
"""The line items."""
|
"""The line items."""
|
||||||
@ -735,13 +741,13 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return getattr(self, "__debit")
|
return getattr(self, "__debit")
|
||||||
|
|
||||||
@debit.setter
|
@debit.setter
|
||||||
def debit(self, debit: Decimal | None) -> None:
|
def debit(self, value: Decimal | None) -> None:
|
||||||
"""Sets the debit amount.
|
"""Sets the debit amount.
|
||||||
|
|
||||||
:param debit: The debit amount.
|
:param value: The debit amount.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__debit", debit)
|
setattr(self, "__debit", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def credit(self) -> Decimal | None:
|
def credit(self) -> Decimal | None:
|
||||||
@ -754,13 +760,13 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return getattr(self, "__credit")
|
return getattr(self, "__credit")
|
||||||
|
|
||||||
@credit.setter
|
@credit.setter
|
||||||
def credit(self, credit: Decimal | None) -> None:
|
def credit(self, value: Decimal | None) -> None:
|
||||||
"""Sets the credit amount.
|
"""Sets the credit amount.
|
||||||
|
|
||||||
:param credit: The credit amount.
|
:param value: The credit amount.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__credit", credit)
|
setattr(self, "__credit", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def net_balance(self) -> Decimal:
|
def net_balance(self) -> Decimal:
|
||||||
@ -775,42 +781,42 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return getattr(self, "__net_balance")
|
return getattr(self, "__net_balance")
|
||||||
|
|
||||||
@net_balance.setter
|
@net_balance.setter
|
||||||
def net_balance(self, net_balance: Decimal) -> None:
|
def net_balance(self, value: Decimal) -> None:
|
||||||
"""Sets the net balance.
|
"""Sets the net balance.
|
||||||
|
|
||||||
:param net_balance: The net balance.
|
:param value: The net balance.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__net_balance", net_balance)
|
setattr(self, "__net_balance", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self) -> Decimal:
|
def balance(self) -> Decimal:
|
||||||
"""Returns the net balance.
|
"""Returns the balance.
|
||||||
|
|
||||||
:return: The net balance.
|
:return: The balance.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "__balance"):
|
if not hasattr(self, "__balance"):
|
||||||
setattr(self, "__balance", Decimal("0"))
|
setattr(self, "__balance", Decimal("0"))
|
||||||
return getattr(self, "__balance")
|
return getattr(self, "__balance")
|
||||||
|
|
||||||
@balance.setter
|
@balance.setter
|
||||||
def balance(self, balance: Decimal) -> None:
|
def balance(self, value: Decimal) -> None:
|
||||||
"""Sets the net balance.
|
"""Sets the balance.
|
||||||
|
|
||||||
:param balance: The net balance.
|
:param value: The balance.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__balance", balance)
|
setattr(self, "__balance", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offsets(self) -> list[t.Self]:
|
def offsets(self) -> list[Self]:
|
||||||
"""Returns the offset items.
|
"""Returns the offset items.
|
||||||
|
|
||||||
:return: The offset items.
|
:return: The offset items.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "__offsets"):
|
if not hasattr(self, "__offsets"):
|
||||||
cls: t.Type[t.Self] = self.__class__
|
cls: Type[Self] = self.__class__
|
||||||
offsets: list[t.Self] = cls.query.join(JournalEntry)\
|
offsets: list[Self] = cls.query.join(JournalEntry)\
|
||||||
.filter(JournalEntryLineItem.original_line_item_id == self.id)\
|
.filter(JournalEntryLineItem.original_line_item_id == self.id)\
|
||||||
.order_by(JournalEntry.date, JournalEntry.no,
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
cls.is_debit, cls.no).all()
|
cls.is_debit, cls.no).all()
|
||||||
@ -828,17 +834,16 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return getattr(self, "__is_offset")
|
return getattr(self, "__is_offset")
|
||||||
|
|
||||||
@is_offset.setter
|
@is_offset.setter
|
||||||
def is_offset(self, is_offset: bool) -> None:
|
def is_offset(self, value: bool) -> None:
|
||||||
"""Sets whether the line item is an offset.
|
"""Sets whether the line item is an offset.
|
||||||
|
|
||||||
:param is_offset: True if the line item is an offset, or False
|
:param value: True if the line item is an offset, or False otherwise.
|
||||||
otherwise.
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__is_offset", is_offset)
|
setattr(self, "__is_offset", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def match(self) -> t.Self | None:
|
def match(self) -> Self | None:
|
||||||
"""Returns the match of the line item.
|
"""Returns the match of the line item.
|
||||||
|
|
||||||
:return: The match of the line item.
|
:return: The match of the line item.
|
||||||
@ -848,13 +853,13 @@ class JournalEntryLineItem(db.Model):
|
|||||||
return getattr(self, "__match")
|
return getattr(self, "__match")
|
||||||
|
|
||||||
@match.setter
|
@match.setter
|
||||||
def match(self, match: t.Self) -> None:
|
def match(self, value: Self) -> None:
|
||||||
"""Sets the match of the line item.
|
"""Sets the match of the line item.
|
||||||
|
|
||||||
:param match: The matcho of the line item.
|
:param value: The matcho of the line item.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
setattr(self, "__match", match)
|
setattr(self, "__match", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query_values(self) -> list[str]:
|
def query_values(self) -> list[str]:
|
||||||
@ -886,18 +891,18 @@ class Option(db.Model):
|
|||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The date and time when this record was created."""
|
||||||
created_by_id: Mapped[int] \
|
created_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the creator."""
|
"""The ID of the user who created the record."""
|
||||||
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
|
||||||
"""The creator."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The date and time when this record was last updated."""
|
||||||
updated_by_id: Mapped[int] \
|
updated_by_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the updator."""
|
"""The ID of the last user who updated the record."""
|
||||||
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
|
||||||
"""The updator."""
|
"""The last user who updated the record."""
|
||||||
|
@ -20,8 +20,8 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
2021/9/16 by imacat (imacat@nanoparma.com).
|
2021/9/16 by imacat (imacat@nanoparma.com).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import datetime as dt
|
||||||
from datetime import date
|
from collections.abc import Callable
|
||||||
|
|
||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
from .period import Period
|
from .period import Period
|
||||||
@ -32,13 +32,13 @@ from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
|||||||
class PeriodChooser:
|
class PeriodChooser:
|
||||||
"""The period chooser."""
|
"""The period chooser."""
|
||||||
|
|
||||||
def __init__(self, get_url: t.Callable[[Period], str]):
|
def __init__(self, get_url: Callable[[Period], str]):
|
||||||
"""Constructs a period chooser.
|
"""Constructs a period chooser.
|
||||||
|
|
||||||
:param get_url: The callback to return the URL of the current report in
|
:param get_url: The callback to return the URL of the current report in
|
||||||
a period.
|
a period.
|
||||||
"""
|
"""
|
||||||
self.__get_url: t.Callable[[Period], str] = get_url
|
self.__get_url: Callable[[Period], str] = get_url
|
||||||
"""The callback to return the URL of the current report in a period."""
|
"""The callback to return the URL of the current report in a period."""
|
||||||
|
|
||||||
# Shortcut periods
|
# Shortcut periods
|
||||||
@ -63,10 +63,10 @@ class PeriodChooser:
|
|||||||
|
|
||||||
first: JournalEntry | None \
|
first: JournalEntry | None \
|
||||||
= JournalEntry.query.order_by(JournalEntry.date).first()
|
= JournalEntry.query.order_by(JournalEntry.date).first()
|
||||||
start: date | None = None if first is None else first.date
|
start: dt.date | None = None if first is None else first.date
|
||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
self.data_start: date | None = start
|
self.data_start: dt.date | None = start
|
||||||
"""The start of the data."""
|
"""The start of the data."""
|
||||||
self.has_data: bool = start is not None
|
self.has_data: bool = start is not None
|
||||||
"""Whether there is any data."""
|
"""Whether there is any data."""
|
||||||
@ -80,8 +80,8 @@ class PeriodChooser:
|
|||||||
"""The available years."""
|
"""The available years."""
|
||||||
|
|
||||||
if self.has_data:
|
if self.has_data:
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
self.has_last_month = start < date(today.year, today.month, 1)
|
self.has_last_month = start < dt.date(today.year, today.month, 1)
|
||||||
self.has_last_year = start.year < today.year
|
self.has_last_year = start.year < today.year
|
||||||
self.has_yesterday = start < today
|
self.has_yesterday = start < today
|
||||||
if start.year < today.year - 1:
|
if start.year < today.year - 1:
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
"""The period description composer.
|
"""The period description composer.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date, timedelta
|
import datetime as dt
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
|
|
||||||
|
|
||||||
def get_desc(start: date | None, end: date | None) -> str:
|
def get_desc(start: dt.date | None, end: dt.date | None) -> str:
|
||||||
"""Returns the period description.
|
"""Returns the period description.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -46,7 +46,7 @@ def get_desc(start: date | None, end: date | None) -> str:
|
|||||||
return __get_day_desc(start, end)
|
return __get_day_desc(start, end)
|
||||||
|
|
||||||
|
|
||||||
def __get_since_desc(start: date) -> str:
|
def __get_since_desc(start: dt.date) -> str:
|
||||||
"""Returns the description without the end day.
|
"""Returns the description without the end day.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -67,7 +67,7 @@ def __get_since_desc(start: date) -> str:
|
|||||||
return gettext("since %(start)s", start=get_start_desc())
|
return gettext("since %(start)s", start=get_start_desc())
|
||||||
|
|
||||||
|
|
||||||
def __get_until_desc(end: date) -> str:
|
def __get_until_desc(end: dt.date) -> str:
|
||||||
"""Returns the description without the start day.
|
"""Returns the description without the start day.
|
||||||
|
|
||||||
:param end: The end of the period.
|
:param end: The end of the period.
|
||||||
@ -81,14 +81,14 @@ def __get_until_desc(end: date) -> str:
|
|||||||
"""
|
"""
|
||||||
if end.month == 12 and end.day == 31:
|
if end.month == 12 and end.day == 31:
|
||||||
return str(end.year)
|
return str(end.year)
|
||||||
if (end + timedelta(days=1)).day == 1:
|
if (end + dt.timedelta(days=1)).day == 1:
|
||||||
return __format_month(end)
|
return __format_month(end)
|
||||||
return __format_day(end)
|
return __format_day(end)
|
||||||
|
|
||||||
return gettext("until %(end)s", end=get_end_desc())
|
return gettext("until %(end)s", end=get_end_desc())
|
||||||
|
|
||||||
|
|
||||||
def __get_year_desc(start: date, end: date) -> str:
|
def __get_year_desc(start: dt.date, end: dt.date) -> str:
|
||||||
"""Returns the description as a year range.
|
"""Returns the description as a year range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -105,7 +105,7 @@ def __get_year_desc(start: date, end: date) -> str:
|
|||||||
return __get_from_to_desc(start_text, str(end.year))
|
return __get_from_to_desc(start_text, str(end.year))
|
||||||
|
|
||||||
|
|
||||||
def __get_month_desc(start: date, end: date) -> str:
|
def __get_month_desc(start: dt.date, end: dt.date) -> str:
|
||||||
"""Returns the description as a month range.
|
"""Returns the description as a month range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -113,7 +113,7 @@ def __get_month_desc(start: date, end: date) -> str:
|
|||||||
:return: The description as a month range.
|
:return: The description as a month range.
|
||||||
:raise ValueError: The period is not a month range.
|
:raise ValueError: The period is not a month range.
|
||||||
"""
|
"""
|
||||||
if start.day != 1 or (end + timedelta(days=1)).day != 1:
|
if start.day != 1 or (end + dt.timedelta(days=1)).day != 1:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
start_text: str = __format_month(start)
|
start_text: str = __format_month(start)
|
||||||
if start.year == end.year and start.month == end.month:
|
if start.year == end.year and start.month == end.month:
|
||||||
@ -123,7 +123,7 @@ def __get_month_desc(start: date, end: date) -> str:
|
|||||||
return __get_from_to_desc(start_text, __format_month(end))
|
return __get_from_to_desc(start_text, __format_month(end))
|
||||||
|
|
||||||
|
|
||||||
def __get_day_desc(start: date, end: date) -> str:
|
def __get_day_desc(start: dt.date, end: dt.date) -> str:
|
||||||
"""Returns the description as a day range.
|
"""Returns the description as a day range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -142,7 +142,7 @@ def __get_day_desc(start: date, end: date) -> str:
|
|||||||
return __get_from_to_desc(start_text, __format_day(end))
|
return __get_from_to_desc(start_text, __format_day(end))
|
||||||
|
|
||||||
|
|
||||||
def __format_month(month: date) -> str:
|
def __format_month(month: dt.date) -> str:
|
||||||
"""Formats a month.
|
"""Formats a month.
|
||||||
|
|
||||||
:param month: The month.
|
:param month: The month.
|
||||||
@ -151,7 +151,7 @@ def __format_month(month: date) -> str:
|
|||||||
return f"{month.year}/{month.month}"
|
return f"{month.year}/{month.month}"
|
||||||
|
|
||||||
|
|
||||||
def __format_day(day: date) -> str:
|
def __format_day(day: dt.date) -> str:
|
||||||
"""Formats a day.
|
"""Formats a day.
|
||||||
|
|
||||||
:param day: The day.
|
:param day: The day.
|
||||||
|
@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import calendar
|
import calendar
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
|
|
||||||
|
|
||||||
def month_end(day: date) -> date:
|
def month_end(day: dt.date) -> dt.date:
|
||||||
"""Returns the end day of month for a date.
|
"""Returns the end day of month for a date.
|
||||||
|
|
||||||
:param day: The date.
|
:param day: The date.
|
||||||
:return: The end day of the month of that day.
|
:return: The end day of the month of that day.
|
||||||
"""
|
"""
|
||||||
last_day: int = calendar.monthrange(day.year, day.month)[1]
|
last_day: int = calendar.monthrange(day.year, day.month)[1]
|
||||||
return date(day.year, day.month, last_day)
|
return dt.date(day.year, day.month, last_day)
|
||||||
|
@ -18,9 +18,10 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import calendar
|
import calendar
|
||||||
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
import typing as t
|
from collections.abc import Callable
|
||||||
from datetime import date
|
from typing import Type
|
||||||
|
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
||||||
@ -39,7 +40,7 @@ def get_period(spec: str | None = None) -> Period:
|
|||||||
"""
|
"""
|
||||||
if spec is None:
|
if spec is None:
|
||||||
return ThisMonth()
|
return ThisMonth()
|
||||||
named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
|
named_periods: dict[str, Type[Callable[[], Period]]] = {
|
||||||
"this-month": lambda: ThisMonth(),
|
"this-month": lambda: ThisMonth(),
|
||||||
"last-month": lambda: LastMonth(),
|
"last-month": lambda: LastMonth(),
|
||||||
"since-last-month": lambda: SinceLastMonth(),
|
"since-last-month": lambda: SinceLastMonth(),
|
||||||
@ -57,7 +58,7 @@ def get_period(spec: str | None = None) -> Period:
|
|||||||
return Period(start, end)
|
return Period(start, end)
|
||||||
|
|
||||||
|
|
||||||
def __parse_spec(text: str) -> tuple[date | None, date | None]:
|
def __parse_spec(text: str) -> tuple[dt.date | None, dt.date | None]:
|
||||||
"""Parses the period specification.
|
"""Parses the period specification.
|
||||||
|
|
||||||
:param text: The period specification.
|
:param text: The period specification.
|
||||||
@ -84,7 +85,7 @@ def __parse_spec(text: str) -> tuple[date | None, date | None]:
|
|||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
def __get_start(year: str, month: str | None, day: str | None) -> date:
|
def __get_start(year: str, month: str | None, day: str | None) -> dt.date:
|
||||||
"""Returns the start of the period from the date representation.
|
"""Returns the start of the period from the date representation.
|
||||||
|
|
||||||
:param year: The year.
|
:param year: The year.
|
||||||
@ -94,13 +95,13 @@ def __get_start(year: str, month: str | None, day: str | None) -> date:
|
|||||||
:raise ValueError: When the date is invalid.
|
:raise ValueError: When the date is invalid.
|
||||||
"""
|
"""
|
||||||
if day is not None:
|
if day is not None:
|
||||||
return date(int(year), int(month), int(day))
|
return dt.date(int(year), int(month), int(day))
|
||||||
if month is not None:
|
if month is not None:
|
||||||
return date(int(year), int(month), 1)
|
return dt.date(int(year), int(month), 1)
|
||||||
return date(int(year), 1, 1)
|
return dt.date(int(year), 1, 1)
|
||||||
|
|
||||||
|
|
||||||
def __get_end(year: str, month: str | None, day: str | None) -> date:
|
def __get_end(year: str, month: str | None, day: str | None) -> dt.date:
|
||||||
"""Returns the end of the period from the date representation.
|
"""Returns the end of the period from the date representation.
|
||||||
|
|
||||||
:param year: The year.
|
:param year: The year.
|
||||||
@ -110,10 +111,10 @@ def __get_end(year: str, month: str | None, day: str | None) -> date:
|
|||||||
:raise ValueError: When the date is invalid.
|
:raise ValueError: When the date is invalid.
|
||||||
"""
|
"""
|
||||||
if day is not None:
|
if day is not None:
|
||||||
return date(int(year), int(month), int(day))
|
return dt.date(int(year), int(month), int(day))
|
||||||
if month is not None:
|
if month is not None:
|
||||||
year_n: int = int(year)
|
year_n: int = int(year)
|
||||||
month_n: int = int(month)
|
month_n: int = int(month)
|
||||||
day_n: int = calendar.monthrange(year_n, month_n)[1]
|
day_n: int = calendar.monthrange(year_n, month_n)[1]
|
||||||
return date(year_n, month_n, day_n)
|
return dt.date(year_n, month_n, day_n)
|
||||||
return date(int(year), 12, 31)
|
return dt.date(int(year), 12, 31)
|
||||||
|
@ -20,8 +20,8 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
2021/9/16 by imacat (imacat@nanoparma.com).
|
2021/9/16 by imacat (imacat@nanoparma.com).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import datetime as dt
|
||||||
from datetime import date, timedelta
|
from typing import Self
|
||||||
|
|
||||||
from .description import get_desc
|
from .description import get_desc
|
||||||
from .month_end import month_end
|
from .month_end import month_end
|
||||||
@ -31,18 +31,18 @@ from .specification import get_spec
|
|||||||
class Period:
|
class Period:
|
||||||
"""A date period."""
|
"""A date period."""
|
||||||
|
|
||||||
def __init__(self, start: date | None, end: date | None):
|
def __init__(self, start: dt.date | None, end: dt.date | None):
|
||||||
"""Constructs a new date period.
|
"""Constructs a new date period.
|
||||||
|
|
||||||
:param start: The start date, or None from the very beginning.
|
:param start: The start date, or None from the very beginning.
|
||||||
:param end: The end date, or None till no end.
|
:param end: The end date, or None till no end.
|
||||||
"""
|
"""
|
||||||
self.start: date | None = start
|
self.start: dt.date | None = start
|
||||||
"""The start of the period."""
|
"""The start of the period."""
|
||||||
self.end: date | None = end
|
self.end: dt.date | None = end
|
||||||
"""The end of the period."""
|
"""The end of the period."""
|
||||||
self.is_default: bool = False
|
self.is_default: bool = False
|
||||||
"""Whether the is the default period."""
|
"""Whether this is the default period."""
|
||||||
self.is_this_month: bool = False
|
self.is_this_month: bool = False
|
||||||
"""Whether the period is this month."""
|
"""Whether the period is this month."""
|
||||||
self.is_last_month: bool = False
|
self.is_last_month: bool = False
|
||||||
@ -95,8 +95,8 @@ class Period:
|
|||||||
self.is_a_month = self.start.day == 1 \
|
self.is_a_month = self.start.day == 1 \
|
||||||
and self.end == month_end(self.start)
|
and self.end == month_end(self.start)
|
||||||
self.is_type_month = self.is_a_month
|
self.is_type_month = self.is_a_month
|
||||||
self.is_a_year = self.start == date(self.start.year, 1, 1) \
|
self.is_a_year = self.start == dt.date(self.start.year, 1, 1) \
|
||||||
and self.end == date(self.start.year, 12, 31)
|
and self.end == dt.date(self.start.year, 12, 31)
|
||||||
self.is_a_day = self.start == self.end
|
self.is_a_day = self.start == self.end
|
||||||
|
|
||||||
def is_year(self, year: int) -> bool:
|
def is_year(self, year: int) -> bool:
|
||||||
@ -119,11 +119,11 @@ class Period:
|
|||||||
and not self.is_a_day
|
and not self.is_a_day
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def before(self) -> t.Self | None:
|
def before(self) -> Self | None:
|
||||||
"""Returns the period before this period.
|
"""Returns the period before this period.
|
||||||
|
|
||||||
:return: The period before this period.
|
:return: The period before this period.
|
||||||
"""
|
"""
|
||||||
if self.start is None:
|
if self.start is None:
|
||||||
return None
|
return None
|
||||||
return Period(None, self.start - timedelta(days=1))
|
return Period(None, self.start - dt.timedelta(days=1))
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The named shortcut periods.
|
"""The named shortcut periods.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date, timedelta
|
import datetime as dt
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from .month_end import month_end
|
from .month_end import month_end
|
||||||
@ -27,8 +27,8 @@ from .period import Period
|
|||||||
class ThisMonth(Period):
|
class ThisMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
this_month_start: date = date(today.year, today.month, 1)
|
this_month_start: dt.date = dt.date(today.year, today.month, 1)
|
||||||
super().__init__(this_month_start, month_end(today))
|
super().__init__(this_month_start, month_end(today))
|
||||||
self.is_default = True
|
self.is_default = True
|
||||||
self.is_this_month = True
|
self.is_this_month = True
|
||||||
@ -43,13 +43,13 @@ class ThisMonth(Period):
|
|||||||
class LastMonth(Period):
|
class LastMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
year: int = today.year
|
year: int = today.year
|
||||||
month: int = today.month - 1
|
month: int = today.month - 1
|
||||||
if month < 1:
|
if month < 1:
|
||||||
year = year - 1
|
year = year - 1
|
||||||
month = 12
|
month = 12
|
||||||
start: date = date(year, month, 1)
|
start: dt.date = dt.date(year, month, 1)
|
||||||
super().__init__(start, month_end(start))
|
super().__init__(start, month_end(start))
|
||||||
self.is_last_month = True
|
self.is_last_month = True
|
||||||
|
|
||||||
@ -63,13 +63,13 @@ class LastMonth(Period):
|
|||||||
class SinceLastMonth(Period):
|
class SinceLastMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
year: int = today.year
|
year: int = today.year
|
||||||
month: int = today.month - 1
|
month: int = today.month - 1
|
||||||
if month < 1:
|
if month < 1:
|
||||||
year = year - 1
|
year = year - 1
|
||||||
month = 12
|
month = 12
|
||||||
start: date = date(year, month, 1)
|
start: dt.date = dt.date(year, month, 1)
|
||||||
super().__init__(start, None)
|
super().__init__(start, None)
|
||||||
self.is_since_last_month = True
|
self.is_since_last_month = True
|
||||||
|
|
||||||
@ -82,9 +82,9 @@ class SinceLastMonth(Period):
|
|||||||
class ThisYear(Period):
|
class ThisYear(Period):
|
||||||
"""The period of this year."""
|
"""The period of this year."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
year: int = date.today().year
|
year: int = dt.date.today().year
|
||||||
start: date = date(year, 1, 1)
|
start: dt.date = dt.date(year, 1, 1)
|
||||||
end: date = date(year, 12, 31)
|
end: dt.date = dt.date(year, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
self.is_this_year = True
|
self.is_this_year = True
|
||||||
|
|
||||||
@ -97,9 +97,9 @@ class ThisYear(Period):
|
|||||||
class LastYear(Period):
|
class LastYear(Period):
|
||||||
"""The period of last year."""
|
"""The period of last year."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
year: int = date.today().year
|
year: int = dt.date.today().year
|
||||||
start: date = date(year - 1, 1, 1)
|
start: dt.date = dt.date(year - 1, 1, 1)
|
||||||
end: date = date(year - 1, 12, 31)
|
end: dt.date = dt.date(year - 1, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
self.is_last_year = True
|
self.is_last_year = True
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ class LastYear(Period):
|
|||||||
class Today(Period):
|
class Today(Period):
|
||||||
"""The period of today."""
|
"""The period of today."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
super().__init__(today, today)
|
super().__init__(today, today)
|
||||||
self.is_today = True
|
self.is_today = True
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ class Today(Period):
|
|||||||
class Yesterday(Period):
|
class Yesterday(Period):
|
||||||
"""The period of yesterday."""
|
"""The period of yesterday."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
yesterday: date = date.today() - timedelta(days=1)
|
yesterday: dt.date = dt.date.today() - dt.timedelta(days=1)
|
||||||
super().__init__(yesterday, yesterday)
|
super().__init__(yesterday, yesterday)
|
||||||
self.is_yesterday = True
|
self.is_yesterday = True
|
||||||
|
|
||||||
@ -163,6 +163,6 @@ class YearPeriod(Period):
|
|||||||
|
|
||||||
:param year: The year.
|
:param year: The year.
|
||||||
"""
|
"""
|
||||||
start: date = date(year, 1, 1)
|
start: dt.date = dt.date(year, 1, 1)
|
||||||
end: date = date(year, 12, 31)
|
end: dt.date = dt.date(year, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
"""The period specification composer.
|
"""The period specification composer.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date, timedelta
|
import datetime as dt
|
||||||
|
|
||||||
|
|
||||||
def get_spec(start: date | None, end: date | None) -> str:
|
def get_spec(start: dt.date | None, end: dt.date | None) -> str:
|
||||||
"""Returns the period specification.
|
"""Returns the period specification.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -44,7 +44,7 @@ def get_spec(start: date | None, end: date | None) -> str:
|
|||||||
return __get_day_spec(start, end)
|
return __get_day_spec(start, end)
|
||||||
|
|
||||||
|
|
||||||
def __get_since_spec(start: date) -> str:
|
def __get_since_spec(start: dt.date) -> str:
|
||||||
"""Returns the period specification without the end day.
|
"""Returns the period specification without the end day.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -57,7 +57,7 @@ def __get_since_spec(start: date) -> str:
|
|||||||
return start.strftime("%Y-%m-%d-")
|
return start.strftime("%Y-%m-%d-")
|
||||||
|
|
||||||
|
|
||||||
def __get_until_spec(end: date) -> str:
|
def __get_until_spec(end: dt.date) -> str:
|
||||||
"""Returns the period specification without the start day.
|
"""Returns the period specification without the start day.
|
||||||
|
|
||||||
:param end: The end of the period.
|
:param end: The end of the period.
|
||||||
@ -65,12 +65,12 @@ def __get_until_spec(end: date) -> str:
|
|||||||
"""
|
"""
|
||||||
if end.month == 12 and end.day == 31:
|
if end.month == 12 and end.day == 31:
|
||||||
return end.strftime("-%Y")
|
return end.strftime("-%Y")
|
||||||
if (end + timedelta(days=1)).day == 1:
|
if (end + dt.timedelta(days=1)).day == 1:
|
||||||
return end.strftime("-%Y-%m")
|
return end.strftime("-%Y-%m")
|
||||||
return end.strftime("-%Y-%m-%d")
|
return end.strftime("-%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
def __get_year_spec(start: date, end: date) -> str:
|
def __get_year_spec(start: dt.date, end: dt.date) -> str:
|
||||||
"""Returns the period specification as a year range.
|
"""Returns the period specification as a year range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -88,7 +88,7 @@ def __get_year_spec(start: date, end: date) -> str:
|
|||||||
return f"{start_spec}-{end_spec}"
|
return f"{start_spec}-{end_spec}"
|
||||||
|
|
||||||
|
|
||||||
def __get_month_spec(start: date, end: date) -> str:
|
def __get_month_spec(start: dt.date, end: dt.date) -> str:
|
||||||
"""Returns the period specification as a month range.
|
"""Returns the period specification as a month range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
@ -96,7 +96,7 @@ def __get_month_spec(start: date, end: date) -> str:
|
|||||||
:return: The period specification as a month range.
|
:return: The period specification as a month range.
|
||||||
:raise ValueError: The period is not a month range.
|
:raise ValueError: The period is not a month range.
|
||||||
"""
|
"""
|
||||||
if start.day != 1 or (end + timedelta(days=1)).day != 1:
|
if start.day != 1 or (end + dt.timedelta(days=1)).day != 1:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
start_spec: str = start.strftime("%Y-%m")
|
start_spec: str = start.strftime("%Y-%m")
|
||||||
if start.year == end.year and start.month == end.month:
|
if start.year == end.year and start.month == end.month:
|
||||||
@ -105,7 +105,7 @@ def __get_month_spec(start: date, end: date) -> str:
|
|||||||
return f"{start_spec}-{end_spec}"
|
return f"{start_spec}-{end_spec}"
|
||||||
|
|
||||||
|
|
||||||
def __get_day_spec(start: date, end: date) -> str:
|
def __get_day_spec(start: dt.date, end: dt.date) -> str:
|
||||||
"""Returns the period specification as a day range.
|
"""Returns the period specification as a day range.
|
||||||
|
|
||||||
:param start: The start of the period.
|
:param start: The start of the period.
|
||||||
|
@ -145,6 +145,7 @@ class AccountCollector:
|
|||||||
.filter(sa.or_(Account.id.in_({x.id for x in account_balances}),
|
.filter(sa.or_(Account.id.in_({x.id for x in account_balances}),
|
||||||
Account.base_code == "3351",
|
Account.base_code == "3351",
|
||||||
Account.base_code == "3353")).all()
|
Account.base_code == "3353")).all()
|
||||||
|
"""The accounts."""
|
||||||
account_by_id: dict[int, Account] \
|
account_by_id: dict[int, Account] \
|
||||||
= {x.id: x for x in self.__all_accounts}
|
= {x.id: x for x in self.__all_accounts}
|
||||||
self.accounts: list[ReportAccount] \
|
self.accounts: list[ReportAccount] \
|
||||||
@ -154,6 +155,7 @@ class AccountCollector:
|
|||||||
account_by_id[x.id],
|
account_by_id[x.id],
|
||||||
self.__period))
|
self.__period))
|
||||||
for x in account_balances]
|
for x in account_balances]
|
||||||
|
"""The accounts on the balance sheet."""
|
||||||
self.__add_accumulated()
|
self.__add_accumulated()
|
||||||
self.__add_current_period()
|
self.__add_current_period()
|
||||||
self.accounts.sort(key=lambda x: (x.account.base_code, x.account.no))
|
self.accounts.sort(key=lambda x: (x.account.base_code, x.account.no))
|
||||||
@ -452,11 +454,11 @@ class BalanceSheet(BaseReport):
|
|||||||
:return: The CSV rows for the section.
|
:return: The CSV rows for the section.
|
||||||
"""
|
"""
|
||||||
rows: list[CSVHalfRow] \
|
rows: list[CSVHalfRow] \
|
||||||
= [CSVHalfRow(section.title.title.title(), None)]
|
= [CSVHalfRow(section.title.title, None)]
|
||||||
for subsection in section.subsections:
|
for subsection in section.subsections:
|
||||||
rows.append(CSVHalfRow(f" {subsection.title.title.title()}", None))
|
rows.append(CSVHalfRow(f" {subsection.title.title}", None))
|
||||||
for account in subsection.accounts:
|
for account in subsection.accounts:
|
||||||
rows.append(CSVHalfRow(f" {str(account.account).title()}",
|
rows.append(CSVHalfRow(f" {str(account.account)}",
|
||||||
account.amount))
|
account.amount))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The income and expenses log.
|
"""The income and expenses log.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -53,7 +53,7 @@ class ReportLineItem:
|
|||||||
"""Whether this is the brought-forward line item."""
|
"""Whether this is the brought-forward line item."""
|
||||||
self.is_total: bool = False
|
self.is_total: bool = False
|
||||||
"""Whether this is the total line item."""
|
"""Whether this is the total line item."""
|
||||||
self.date: date | None = None
|
self.date: dt.date | None = None
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.account: Account | None = None
|
self.account: Account | None = None
|
||||||
"""The account."""
|
"""The account."""
|
||||||
@ -213,7 +213,7 @@ class LineItemCollector:
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: date | str | None,
|
def __init__(self, date: dt.date | str | None,
|
||||||
account: str | None,
|
account: str | None,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
income: str | Decimal | None,
|
income: str | Decimal | None,
|
||||||
@ -222,7 +222,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
:param journal_entry_date: The journal entry date.
|
:param date: The journal entry date.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
:param description: The description.
|
:param description: The description.
|
||||||
:param income: The income.
|
:param income: The income.
|
||||||
@ -230,7 +230,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
:param balance: The balance.
|
:param balance: The balance.
|
||||||
:param note: The note.
|
:param note: The note.
|
||||||
"""
|
"""
|
||||||
self.date: date | str | None = journal_entry_date
|
self.date: dt.date | str | None = date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.account: str | None = account
|
self.account: str | None = account
|
||||||
"""The account."""
|
"""The account."""
|
||||||
@ -407,13 +407,13 @@ class IncomeExpenses(BaseReport):
|
|||||||
gettext("Note"))]
|
gettext("Note"))]
|
||||||
if self.__brought_forward is not None:
|
if self.__brought_forward is not None:
|
||||||
rows.append(CSVRow(self.__brought_forward.date,
|
rows.append(CSVRow(self.__brought_forward.date,
|
||||||
str(self.__brought_forward.account).title(),
|
str(self.__brought_forward.account),
|
||||||
self.__brought_forward.description,
|
self.__brought_forward.description,
|
||||||
self.__brought_forward.income,
|
self.__brought_forward.income,
|
||||||
self.__brought_forward.expense,
|
self.__brought_forward.expense,
|
||||||
self.__brought_forward.balance,
|
self.__brought_forward.balance,
|
||||||
None))
|
None))
|
||||||
rows.extend([CSVRow(x.date, str(x.account).title(), x.description,
|
rows.extend([CSVRow(x.date, str(x.account), x.description,
|
||||||
x.income, x.expense, x.balance, x.note)
|
x.income, x.expense, x.balance, x.note)
|
||||||
for x in self.__line_items])
|
for x in self.__line_items])
|
||||||
if self.__total is not None:
|
if self.__total is not None:
|
||||||
|
@ -106,6 +106,7 @@ class Section:
|
|||||||
"""The subsections in the section."""
|
"""The subsections in the section."""
|
||||||
self.accumulated: AccumulatedTotal \
|
self.accumulated: AccumulatedTotal \
|
||||||
= AccumulatedTotal(accumulated_title)
|
= AccumulatedTotal(accumulated_title)
|
||||||
|
"""The accumulated total."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self) -> Decimal:
|
def total(self) -> Decimal:
|
||||||
@ -225,12 +226,12 @@ class IncomeStatement(BaseReport):
|
|||||||
for x in balances})).all()
|
for x in balances})).all()
|
||||||
|
|
||||||
total_titles: dict[str, str] \
|
total_titles: dict[str, str] \
|
||||||
= {"4": gettext("total operating revenue"),
|
= {"4": gettext("Total Operating Revenue"),
|
||||||
"5": gettext("gross income"),
|
"5": gettext("Gross Income"),
|
||||||
"6": gettext("operating income"),
|
"6": gettext("Operating Income"),
|
||||||
"7": gettext("before tax income"),
|
"7": gettext("Before Tax Income"),
|
||||||
"8": gettext("after tax income"),
|
"8": gettext("After Tax Income"),
|
||||||
"9": gettext("net income or loss for current period")}
|
"9": gettext("Net Income or Loss for Current Period")}
|
||||||
|
|
||||||
sections: dict[str, Section] \
|
sections: dict[str, Section] \
|
||||||
= {x.code: Section(x, total_titles[x.code]) for x in titles}
|
= {x.code: Section(x, total_titles[x.code]) for x in titles}
|
||||||
@ -300,14 +301,14 @@ class IncomeStatement(BaseReport):
|
|||||||
total_str: str = gettext("Total")
|
total_str: str = gettext("Total")
|
||||||
rows: list[CSVRow] = [CSVRow(None, gettext("Amount"))]
|
rows: list[CSVRow] = [CSVRow(None, gettext("Amount"))]
|
||||||
for section in self.__sections:
|
for section in self.__sections:
|
||||||
rows.append(CSVRow(str(section.title).title(), None))
|
rows.append(CSVRow(str(section.title), None))
|
||||||
for subsection in section.subsections:
|
for subsection in section.subsections:
|
||||||
rows.append(CSVRow(f" {str(subsection.title).title()}", None))
|
rows.append(CSVRow(f" {str(subsection.title)}", None))
|
||||||
for account in subsection.accounts:
|
for account in subsection.accounts:
|
||||||
rows.append(CSVRow(f" {str(account.account).title()}",
|
rows.append(CSVRow(f" {str(account.account)}",
|
||||||
account.amount))
|
account.amount))
|
||||||
rows.append(CSVRow(f" {total_str}", subsection.total))
|
rows.append(CSVRow(f" {total_str}", subsection.total))
|
||||||
rows.append(CSVRow(section.accumulated.title.title(),
|
rows.append(CSVRow(section.accumulated.title,
|
||||||
section.accumulated.amount))
|
section.accumulated.amount))
|
||||||
rows.append(CSVRow(None, None))
|
rows.append(CSVRow(None, None))
|
||||||
rows = rows[:-1]
|
rows = rows[:-1]
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The journal.
|
"""The journal.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -67,7 +67,7 @@ class ReportLineItem:
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: str | date,
|
def __init__(self, journal_entry_date: str | dt.date,
|
||||||
currency: str,
|
currency: str,
|
||||||
account: str,
|
account: str,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
@ -84,7 +84,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
:param credit: The credit amount.
|
:param credit: The credit amount.
|
||||||
:param note: The note.
|
:param note: The note.
|
||||||
"""
|
"""
|
||||||
self.date: str | date = journal_entry_date
|
self.date: str | dt.date = journal_entry_date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.currency: str = currency
|
self.currency: str = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
@ -160,7 +160,7 @@ def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
|
|||||||
gettext("Debit"), gettext("Credit"),
|
gettext("Debit"), gettext("Credit"),
|
||||||
gettext("Note"))]
|
gettext("Note"))]
|
||||||
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
|
rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
|
||||||
str(x.account).title(), x.description,
|
str(x.account), x.description,
|
||||||
x.debit, x.credit, x.journal_entry.note)
|
x.debit, x.credit, x.journal_entry.note)
|
||||||
for x in line_items])
|
for x in line_items])
|
||||||
return rows
|
return rows
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The ledger.
|
"""The ledger.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -52,7 +52,7 @@ class ReportLineItem:
|
|||||||
"""Whether this is the brought-forward line item."""
|
"""Whether this is the brought-forward line item."""
|
||||||
self.is_total: bool = False
|
self.is_total: bool = False
|
||||||
"""Whether this is the total line item."""
|
"""Whether this is the total line item."""
|
||||||
self.date: date | None = None
|
self.date: dt.date | None = None
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.description: str | None = None
|
self.description: str | None = None
|
||||||
"""The description."""
|
"""The description."""
|
||||||
@ -196,7 +196,7 @@ class LineItemCollector:
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: date | str | None,
|
def __init__(self, date: dt.date | str | None,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
debit: str | Decimal | None,
|
debit: str | Decimal | None,
|
||||||
credit: str | Decimal | None,
|
credit: str | Decimal | None,
|
||||||
@ -204,14 +204,14 @@ class CSVRow(BaseCSVRow):
|
|||||||
note: str | None):
|
note: str | None):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV.
|
||||||
|
|
||||||
:param journal_entry_date: The journal entry date.
|
:param date: The journal entry date.
|
||||||
:param description: The description.
|
:param description: The description.
|
||||||
:param debit: The debit amount.
|
:param debit: The debit amount.
|
||||||
:param credit: The credit amount.
|
:param credit: The credit amount.
|
||||||
:param balance: The balance.
|
:param balance: The balance.
|
||||||
:param note: The note.
|
:param note: The note.
|
||||||
"""
|
"""
|
||||||
self.date: date | str | None = journal_entry_date
|
self.date: dt.date | str | None = date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.description: str | None = description
|
self.description: str | None = description
|
||||||
"""The description."""
|
"""The description."""
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The search.
|
"""The search.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
@ -124,40 +124,33 @@ class LineItemCollector:
|
|||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.BinaryExpression] \
|
||||||
= [JournalEntry.note.icontains(k)]
|
= [JournalEntry.note.icontains(k)]
|
||||||
journal_entry_date: datetime
|
date: dt.datetime
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(k, "%Y")
|
date = dt.datetime.strptime(k, "%Y")
|
||||||
conditions.append(sa.extract("year", JournalEntry.date)
|
conditions.append(
|
||||||
== journal_entry_date.year)
|
sa.extract("year", JournalEntry.date) == date.year)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(k, "%Y/%m")
|
date = dt.datetime.strptime(k, "%Y/%m")
|
||||||
conditions.append(sa.and_(
|
conditions.append(sa.and_(
|
||||||
sa.extract("year", JournalEntry.date)
|
sa.extract("year", JournalEntry.date) == date.year,
|
||||||
== journal_entry_date.year,
|
sa.extract("month", JournalEntry.date) == date.month))
|
||||||
sa.extract("month", JournalEntry.date)
|
|
||||||
== journal_entry_date.month))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
|
date = dt.datetime.strptime(f"2000/{k}", "%Y/%m/%d")
|
||||||
conditions.append(sa.and_(
|
conditions.append(sa.and_(
|
||||||
sa.extract("month", JournalEntry.date)
|
sa.extract("month", JournalEntry.date) == date.month,
|
||||||
== journal_entry_date.month,
|
sa.extract("day", JournalEntry.date) == date.day))
|
||||||
sa.extract("day", JournalEntry.date)
|
|
||||||
== journal_entry_date.day))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(k, "%Y/%m/%d")
|
date = dt.datetime.strptime(k, "%Y/%m/%d")
|
||||||
conditions.append(sa.and_(
|
conditions.append(sa.and_(
|
||||||
sa.extract("year", JournalEntry.date)
|
sa.extract("year", JournalEntry.date) == date.year,
|
||||||
== journal_entry_date.year,
|
sa.extract("month", JournalEntry.date) == date.month,
|
||||||
sa.extract("month", JournalEntry.date)
|
sa.extract("day", JournalEntry.date) == date.day))
|
||||||
== journal_entry_date.month,
|
|
||||||
sa.extract("day", JournalEntry.date)
|
|
||||||
== journal_entry_date.day))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
|
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
|
||||||
|
@ -224,7 +224,7 @@ class TrialBalance(BaseReport):
|
|||||||
"""
|
"""
|
||||||
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Debit"),
|
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Debit"),
|
||||||
gettext("Credit"))]
|
gettext("Credit"))]
|
||||||
rows.extend([CSVRow(str(x.account).title(), x.debit, x.credit)
|
rows.extend([CSVRow(str(x.account), x.debit, x.credit)
|
||||||
for x in self.__accounts])
|
for x in self.__accounts])
|
||||||
rows.append(CSVRow(gettext("Total"), self.__total.debit,
|
rows.append(CSVRow(gettext("Total"), self.__total.debit,
|
||||||
self.__total.credit))
|
self.__total.credit))
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The unapplied original line items.
|
"""The unapplied original line items.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
@ -41,7 +41,7 @@ from accounting.utils.pagination import Pagination
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: str | date, currency: str,
|
def __init__(self, journal_entry_date: str | dt.date, currency: str,
|
||||||
description: str | None, amount: str | Decimal,
|
description: str | None, amount: str | Decimal,
|
||||||
net_balance: str | Decimal):
|
net_balance: str | Decimal):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV.
|
||||||
@ -52,7 +52,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
:param amount: The amount.
|
:param amount: The amount.
|
||||||
:param net_balance: The net balance.
|
:param net_balance: The net balance.
|
||||||
"""
|
"""
|
||||||
self.date: str | date = journal_entry_date
|
self.date: str | dt.date = journal_entry_date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.currency: str = currency
|
self.currency: str = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
@ -64,7 +64,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
"""The net balance."""
|
"""The net balance."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self) -> list[str | date | Decimal | None]:
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
"""Returns the values of the row.
|
"""Returns the values of the row.
|
||||||
|
|
||||||
:return: The values of the row.
|
:return: The values of the row.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The accounts with unapplied original line items.
|
"""The accounts with unapplied original line items.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
@ -49,7 +49,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
"""The number of unapplied original line items."""
|
"""The number of unapplied original line items."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self) -> list[str | date | Decimal | None]:
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
"""Returns the values of the row.
|
"""Returns the values of the row.
|
||||||
|
|
||||||
:return: The values of the row.
|
:return: The values of the row.
|
||||||
@ -120,8 +120,7 @@ def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
|
|||||||
:return: The CSV rows.
|
:return: The CSV rows.
|
||||||
"""
|
"""
|
||||||
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
||||||
rows.extend([CSVRow(str(x).title(), x.count)
|
rows.extend([CSVRow(str(x), x.count) for x in accounts])
|
||||||
for x in accounts])
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@ -143,7 +142,7 @@ class AccountsWithUnappliedOriginalLineItems(BaseReport):
|
|||||||
|
|
||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = f"unapplied-accounts.csv"
|
filename: str = "unapplied-accounts.csv"
|
||||||
return csv_download(filename, get_csv_rows(self.__accounts))
|
return csv_download(filename, get_csv_rows(self.__accounts))
|
||||||
|
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The unmatched offsets.
|
"""The unmatched offsets.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
@ -40,7 +40,7 @@ from accounting.utils.pagination import Pagination
|
|||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
"""A row in the CSV."""
|
"""A row in the CSV."""
|
||||||
|
|
||||||
def __init__(self, journal_entry_date: str | date, currency: str,
|
def __init__(self, journal_entry_date: str | dt.date, currency: str,
|
||||||
description: str | None, debit: str | Decimal,
|
description: str | None, debit: str | Decimal,
|
||||||
credit: str | Decimal, balance: str | Decimal):
|
credit: str | Decimal, balance: str | Decimal):
|
||||||
"""Constructs a row in the CSV.
|
"""Constructs a row in the CSV.
|
||||||
@ -52,7 +52,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
:param credit: The credit amount.
|
:param credit: The credit amount.
|
||||||
:param balance: The balance.
|
:param balance: The balance.
|
||||||
"""
|
"""
|
||||||
self.date: str | date = journal_entry_date
|
self.date: str | dt.date = journal_entry_date
|
||||||
"""The date."""
|
"""The date."""
|
||||||
self.currency: str = currency
|
self.currency: str = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
@ -66,7 +66,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
"""The balance."""
|
"""The balance."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self) -> list[str | date | Decimal | None]:
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
"""Returns the values of the row.
|
"""Returns the values of the row.
|
||||||
|
|
||||||
:return: The values of the row.
|
:return: The values of the row.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The accounts with unmatched offsets.
|
"""The accounts with unmatched offsets.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
@ -49,7 +49,7 @@ class CSVRow(BaseCSVRow):
|
|||||||
"""The number of unapplied original line items."""
|
"""The number of unapplied original line items."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self) -> list[str | date | Decimal | None]:
|
def values(self) -> list[str | dt.date | Decimal | None]:
|
||||||
"""Returns the values of the row.
|
"""Returns the values of the row.
|
||||||
|
|
||||||
:return: The values of the row.
|
:return: The values of the row.
|
||||||
@ -120,8 +120,7 @@ def get_csv_rows(accounts: list[Account]) -> list[CSVRow]:
|
|||||||
:return: The CSV rows.
|
:return: The CSV rows.
|
||||||
"""
|
"""
|
||||||
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Count"))]
|
||||||
rows.extend([CSVRow(str(x).title(), x.count)
|
rows.extend([CSVRow(str(x), x.count) for x in accounts])
|
||||||
for x in accounts])
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@ -144,7 +143,7 @@ class AccountsWithUnmatchedOffsets(BaseReport):
|
|||||||
|
|
||||||
:return: The response of the report for download.
|
:return: The response of the report for download.
|
||||||
"""
|
"""
|
||||||
filename: str = f"unapplied-accounts.csv"
|
filename: str = "unmatched-accounts.csv"
|
||||||
return csv_download(filename, get_csv_rows(self.__accounts))
|
return csv_download(filename, get_csv_rows(self.__accounts))
|
||||||
|
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
|
@ -17,8 +17,9 @@
|
|||||||
"""The page parameters of a report.
|
"""The page parameters of a report.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Type
|
||||||
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
|
||||||
urlunparse
|
urlunparse
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class BasePageParams(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def journal_entry_types(self) -> t.Type[JournalEntryType]:
|
def journal_entry_types(self) -> Type[JournalEntryType]:
|
||||||
"""Returns the journal entry types.
|
"""Returns the journal entry types.
|
||||||
|
|
||||||
:return: The journal entry types.
|
:return: The journal entry types.
|
||||||
@ -72,7 +73,7 @@ class BasePageParams(ABC):
|
|||||||
return urlunparse(parts)
|
return urlunparse(parts)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_currency_options(get_url: t.Callable[[Currency], str],
|
def _get_currency_options(get_url: Callable[[Currency], str],
|
||||||
active_currency: Currency) -> list[OptionLink]:
|
active_currency: Currency) -> list[OptionLink]:
|
||||||
"""Returns the currency options.
|
"""Returns the currency options.
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
|
import datetime as dt
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import timedelta, date
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@ -77,7 +77,7 @@ def period_spec(period: Period) -> str:
|
|||||||
return f"{start}-{end}"
|
return f"{start}-{end}"
|
||||||
|
|
||||||
|
|
||||||
def __get_start_str(start: date | None) -> str | None:
|
def __get_start_str(start: dt.date | None) -> str | None:
|
||||||
"""Returns the string representation of the start date.
|
"""Returns the string representation of the start date.
|
||||||
|
|
||||||
:param start: The start date.
|
:param start: The start date.
|
||||||
@ -93,7 +93,7 @@ def __get_start_str(start: date | None) -> str | None:
|
|||||||
return start.strftime("%Y%m%d")
|
return start.strftime("%Y%m%d")
|
||||||
|
|
||||||
|
|
||||||
def __get_end_str(end: date | None) -> str | None:
|
def __get_end_str(end: dt.date | None) -> str | None:
|
||||||
"""Returns the string representation of the end date.
|
"""Returns the string representation of the end date.
|
||||||
|
|
||||||
:param end: The end date.
|
:param end: The end date.
|
||||||
@ -104,6 +104,6 @@ def __get_end_str(end: date | None) -> str | None:
|
|||||||
return None
|
return None
|
||||||
if end.month == 12 and end.day == 31:
|
if end.month == 12 and end.day == 31:
|
||||||
return str(end.year)
|
return str(end.year)
|
||||||
if (end + timedelta(days=1)).day == 1:
|
if (end + dt.timedelta(days=1)).day == 1:
|
||||||
return end.strftime("%Y%m")
|
return end.strftime("%Y%m")
|
||||||
return end.strftime("%Y%m%d")
|
return end.strftime("%Y%m%d")
|
||||||
|
@ -123,15 +123,13 @@ class OffsetMatcher:
|
|||||||
.options(selectinload(JournalEntryLineItem.currency),
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry)).all()
|
||||||
for line_item in self.line_items:
|
for line_item in self.line_items:
|
||||||
line_item.is_offset = line_item.id in net_balances
|
line_item.is_offset = line_item.id not in net_balances
|
||||||
self.unapplied = [x for x in self.line_items
|
self.unapplied = [x for x in self.line_items if not x.is_offset]
|
||||||
if x.is_offset]
|
|
||||||
for line_item in self.unapplied:
|
for line_item in self.unapplied:
|
||||||
line_item.net_balance = line_item.amount \
|
line_item.net_balance = line_item.amount \
|
||||||
if net_balances[line_item.id] is None \
|
if net_balances[line_item.id] is None \
|
||||||
else net_balances[line_item.id]
|
else net_balances[line_item.id]
|
||||||
self.unmatched = [x for x in self.line_items
|
self.unmatched = [x for x in self.line_items if x.is_offset]
|
||||||
if not x.is_offset]
|
|
||||||
self.__populate_accumulated_balances()
|
self.__populate_accumulated_balances()
|
||||||
|
|
||||||
def __populate_accumulated_balances(self) -> None:
|
def __populate_accumulated_balances(self) -> None:
|
||||||
|
@ -21,7 +21,7 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import typing as t
|
from collections.abc import Iterator
|
||||||
|
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ class ReportChooser:
|
|||||||
self.__active_report == ReportType.UNMATCHED,
|
self.__active_report == ReportType.UNMATCHED,
|
||||||
fa_icon="fa-solid fa-file-circle-question")
|
fa_icon="fa-solid fa-file-circle-question")
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[OptionLink]:
|
def __iter__(self) -> Iterator[OptionLink]:
|
||||||
"""Returns the iteration of the reports.
|
"""Returns the iteration of the reports.
|
||||||
|
|
||||||
:return: The iteration of the reports.
|
:return: The iteration of the reports.
|
||||||
|
@ -276,7 +276,6 @@ class JournalEntryLineItemEditor {
|
|||||||
this.originalLineItemDate = originalLineItem.date;
|
this.originalLineItemDate = originalLineItem.date;
|
||||||
this.originalLineItemText = originalLineItem.text;
|
this.originalLineItemText = originalLineItem.text;
|
||||||
this.#originalLineItemText.innerText = originalLineItem.text;
|
this.#originalLineItemText.innerText = originalLineItem.text;
|
||||||
this.#setEnableDescriptionAccount(false);
|
|
||||||
if (this.description === null) {
|
if (this.description === null) {
|
||||||
if (originalLineItem.description === "") {
|
if (originalLineItem.description === "") {
|
||||||
this.#descriptionControl.classList.remove("accounting-not-empty");
|
this.#descriptionControl.classList.remove("accounting-not-empty");
|
||||||
@ -291,7 +290,9 @@ class JournalEntryLineItemEditor {
|
|||||||
this.account = originalLineItem.account.copy();
|
this.account = originalLineItem.account.copy();
|
||||||
this.isAccountConfirmed = false;
|
this.isAccountConfirmed = false;
|
||||||
this.#accountText.innerText = this.account.text;
|
this.#accountText.innerText = this.account.text;
|
||||||
this.#amountInput.value = String(originalLineItem.netBalance);
|
if (this.#amountInput.value === "" || new Decimal(this.#amountInput.value).greaterThan(originalLineItem.netBalance)) {
|
||||||
|
this.#amountInput.value = String(originalLineItem.netBalance);
|
||||||
|
}
|
||||||
this.#amountInput.max = String(originalLineItem.netBalance);
|
this.#amountInput.max = String(originalLineItem.netBalance);
|
||||||
this.#amountInput.min = "0";
|
this.#amountInput.min = "0";
|
||||||
this.#validate();
|
this.#validate();
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
"""The template filters.
|
"""The template filters.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
import datetime as dt
|
||||||
from datetime import date, timedelta
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
|
|
||||||
@ -41,24 +41,24 @@ def format_amount(value: Decimal | None) -> str | None:
|
|||||||
return "{:,}".format(whole) + str(abs(frac))[1:]
|
return "{:,}".format(whole) + str(abs(frac))[1:]
|
||||||
|
|
||||||
|
|
||||||
def format_date(value: date) -> str:
|
def format_date(value: dt.date) -> str:
|
||||||
"""Formats a date to be human-friendly.
|
"""Formats a date to be human-friendly.
|
||||||
|
|
||||||
:param value: The date.
|
:param value: The date.
|
||||||
:return: The human-friendly date text.
|
:return: The human-friendly date text.
|
||||||
"""
|
"""
|
||||||
today: date = date.today()
|
today: dt.date = dt.date.today()
|
||||||
if value == today:
|
if value == today:
|
||||||
return gettext("Today")
|
return gettext("Today")
|
||||||
if value == today - timedelta(days=1):
|
if value == today - dt.timedelta(days=1):
|
||||||
return gettext("Yesterday")
|
return gettext("Yesterday")
|
||||||
if value == today + timedelta(days=1):
|
if value == today + dt.timedelta(days=1):
|
||||||
return gettext("Tomorrow")
|
return gettext("Tomorrow")
|
||||||
locale = str(get_locale())
|
locale = str(get_locale())
|
||||||
if locale == "zh" or locale.startswith("zh_"):
|
if locale == "zh" or locale.startswith("zh_"):
|
||||||
if value == today - timedelta(days=2):
|
if value == today - dt.timedelta(days=2):
|
||||||
return gettext("The day before yesterday")
|
return gettext("The day before yesterday")
|
||||||
if value == today + timedelta(days=2):
|
if value == today + dt.timedelta(days=2):
|
||||||
return gettext("The day after tomorrow")
|
return gettext("The day after tomorrow")
|
||||||
if locale == "zh" or locale.startswith("zh_"):
|
if locale == "zh" or locale.startswith("zh_"):
|
||||||
weekdays = ["一", "二", "三", "四", "五", "六", "日"]
|
weekdays = ["一", "二", "三", "四", "五", "六", "日"]
|
||||||
@ -71,7 +71,7 @@ def format_date(value: date) -> str:
|
|||||||
return "{}/{}({})".format(value.month, value.day, weekday)
|
return "{}/{}({})".format(value.month, value.day, weekday)
|
||||||
|
|
||||||
|
|
||||||
def default(value: t.Any, default_value: t.Any = "") -> t.Any:
|
def default(value: Any, default_value: Any = "") -> Any:
|
||||||
"""Returns the default value if the given value is None.
|
"""Returns the default value if the given value is None.
|
||||||
|
|
||||||
:param value: The value.
|
:param value: The value.
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Account") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Account") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.account.list") }}{% endblock %}
|
{% block back_url %}{{ url_for("accounting.account.list")|accounting_or_next }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.account.store") }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.account.store") }}{% endblock %}
|
||||||
|
@ -90,7 +90,7 @@ First written: 2023/1/31
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="accounting-card col-sm-6">
|
<div class="accounting-card col-sm-6">
|
||||||
<div class="accounting-card-title">{{ obj.title|title }}</div>
|
<div class="accounting-card-title">{{ obj.title }}</div>
|
||||||
<div class="accounting-card-code">{{ obj.code }}</div>
|
<div class="accounting-card-code">{{ obj.code }}</div>
|
||||||
{% if obj.is_need_offset %}
|
{% if obj.is_need_offset %}
|
||||||
<div>
|
<div>
|
||||||
|
@ -32,7 +32,7 @@ First written: 2023/1/30
|
|||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -33,7 +33,7 @@ First written: 2023/2/1
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-card col-sm-6">
|
<div class="accounting-card col-sm-6">
|
||||||
<div class="accounting-card-title">{{ obj.title|title }}</div>
|
<div class="accounting-card-title">{{ obj.title }}</div>
|
||||||
<div class="accounting-card-code">{{ obj.code }}</div>
|
<div class="accounting-card-code">{{ obj.code }}</div>
|
||||||
{% if obj.accounts %}
|
{% if obj.accounts %}
|
||||||
<div>
|
<div>
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/1/26
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="mb-2 accounting-toolbar">
|
<div class="mb-2 accounting-toolbar">
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Currency") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Currency") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting.currency.list") }}{% endblock %}
|
{% block back_url %}{{ url_for("accounting.currency.list")|accounting_or_next }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.currency.store") }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.currency.store") }}{% endblock %}
|
||||||
|
@ -32,7 +32,7 @@ First written: 2023/2/6
|
|||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Journal Entry") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Cash Disbursement Journal Entry") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting-report.default") }}{% endblock %}
|
{% block back_url %}{{ url_for("accounting-report.default")|accounting_or_next }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
|
||||||
|
@ -20,6 +20,7 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
|||||||
First written: 2023/2/28
|
First written: 2023/2/28
|
||||||
#}
|
#}
|
||||||
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" data-debit-credit="{{ description_editor.debit_credit }}">
|
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" data-debit-credit="{{ description_editor.debit_credit }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/14
|
|||||||
<div>
|
<div>
|
||||||
<div class="small">
|
<div class="small">
|
||||||
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
</div>
|
</div>
|
||||||
{% if line_item.description is not none %}
|
{% if line_item.description is not none %}
|
||||||
<div>{{ line_item.description }}</div>
|
<div>{{ line_item.description }}</div>
|
||||||
|
@ -36,7 +36,7 @@ First written: 2023/2/26
|
|||||||
{{ A_("Edit") }}
|
{{ A_("Edit") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.order", date=obj.date)|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-bars-staggered"></i>
|
<i class="fa-solid fa-bars-staggered"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -20,6 +20,7 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
|||||||
First written: 2023/2/25
|
First written: 2023/2/25
|
||||||
#}
|
#}
|
||||||
<form id="accounting-line-item-editor">
|
<form id="accounting-line-item-editor">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
|
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -42,7 +42,7 @@ First written: 2023/2/25
|
|||||||
<div class="small">
|
<div class="small">
|
||||||
{{ line_item.journal_entry.date|accounting_format_date }}
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
</div>
|
</div>
|
||||||
{{ line_item.description|accounting_default }}
|
{{ line_item.description|accounting_default }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,7 +38,7 @@ First written: 2023/2/26
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if list|length > 1 and accounting_can_edit() %}
|
{% if list|length > 1 and accounting_can_edit() %}
|
||||||
<form action="{{ url_for("accounting.journal-entry.sort", journal_entry_date=date) }}" method="post">
|
<form action="{{ url_for("accounting.journal-entry.sort", date=date) }}" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
{% if request.args.next %}
|
{% if request.args.next %}
|
||||||
<input type="hidden" name="next" value="{{ request.args.next }}">
|
<input type="hidden" name="next" value="{{ request.args.next }}">
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Journal Entry") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Cash Receipt Journal Entry") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting-report.default") }}{% endblock %}
|
{% block back_url %}{{ url_for("accounting-report.default")|accounting_or_next }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
|
||||||
|
@ -23,7 +23,7 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
{% block as_trasfer %}
|
{% block as_trasfer %}
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_journal_entry_to_transfer|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-bars-staggered"></i>
|
<i class="fa-solid fa-table-columns"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
|
<span class="d-none d-md-inline">{{ A_("As Transfer") }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -23,6 +23,6 @@ First written: 2023/2/25
|
|||||||
|
|
||||||
{% block header %}{% block title %}{{ A_("Add a New Transfer Journal Entry") }}{% endblock %}{% endblock %}
|
{% block header %}{% block title %}{{ A_("Add a New Transfer Journal Entry") }}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block back_url %}{{ request.args.get("next") or url_for("accounting-report.default") }}{% endblock %}
|
{% block back_url %}{{ url_for("accounting-report.default")|accounting_or_next }}{% endblock %}
|
||||||
|
|
||||||
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
|
{% block action_url %}{{ url_for("accounting.journal-entry.store", journal_entry_type=journal_entry_type) }}{% endblock %}
|
||||||
|
@ -20,6 +20,7 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
|||||||
First written: 2023/3/22
|
First written: 2023/3/22
|
||||||
#}
|
#}
|
||||||
<form id="accounting-recurring-item-editor-{{ expense_income }}">
|
<form id="accounting-recurring-item-editor-{{ expense_income }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
|
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -20,21 +20,21 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
|||||||
First written: 2023/3/8
|
First written: 2023/3/8
|
||||||
#}
|
#}
|
||||||
<div class="accounting-report-table-row accounting-balance-sheet-section">
|
<div class="accounting-report-table-row accounting-balance-sheet-section">
|
||||||
<div>{{ section.title.title|title }}</div>
|
<div>{{ section.title.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accounting-report-table-body">
|
<div class="accounting-report-table-body">
|
||||||
{% for subsection in section.subsections %}
|
{% for subsection in section.subsections %}
|
||||||
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
|
<div class="accounting-report-table-row accounting-balance-sheet-subsection">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
||||||
{{ subsection.title.title|title }}
|
{{ subsection.title.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for account in subsection.accounts %}
|
{% for account in subsection.accounts %}
|
||||||
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
|
<a class="d-flex justify-content-between accounting-report-table-row accounting-balance-sheet-account" href="{{ account.url }}">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
||||||
{{ account.account.title|title }}
|
{{ account.account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -20,7 +20,7 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
|||||||
First written: 2023/3/8
|
First written: 2023/3/8
|
||||||
#}
|
#}
|
||||||
<div>{{ line_item.date|accounting_format_date }}</div>
|
<div>{{ line_item.date|accounting_format_date }}</div>
|
||||||
<div>{{ line_item.account.title|title }}</div>
|
<div>{{ line_item.account.title }}</div>
|
||||||
<div>{{ line_item.description|accounting_default }}</div>
|
<div>{{ line_item.description|accounting_default }}</div>
|
||||||
<div class="accounting-amount">{{ line_item.income|accounting_format_amount|accounting_default }}</div>
|
<div class="accounting-amount">{{ line_item.income|accounting_format_amount|accounting_default }}</div>
|
||||||
<div class="accounting-amount">{{ line_item.expense|accounting_format_amount|accounting_default }}</div>
|
<div class="accounting-amount">{{ line_item.expense|accounting_format_amount|accounting_default }}</div>
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/5
|
|||||||
{{ line_item.date|accounting_format_date }}
|
{{ line_item.date|accounting_format_date }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if line_item.account %}
|
{% if line_item.account %}
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -19,7 +19,7 @@ search-modal.html: The search modal
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/3/8
|
First written: 2023/3/8
|
||||||
#}
|
#}
|
||||||
<form action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label">
|
<form action="{{ url_for("accounting-report.search") }}" name="accounting-search-form" method="get" role="search" aria-labelledby="accounting-search-modal-label">
|
||||||
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
|
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -93,7 +93,7 @@ First written: 2023/3/8
|
|||||||
{% for account in report.account_options %}
|
{% for account in report.account_options %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
<a class="dropdown-item {% if account.is_active %} active {% endif %}" href="{{ account.url }}">
|
||||||
{{ account.title|title }}
|
{{ account.title }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -118,7 +118,7 @@ First written: 2023/3/8
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if use_search %}
|
{% if use_search %}
|
||||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/5
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% 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 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, 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, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -66,21 +66,21 @@ First written: 2023/3/7
|
|||||||
<div class="accounting-report-table-row accounting-income-statement-section">
|
<div class="accounting-report-table-row accounting-income-statement-section">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ section.title.code }}</span>
|
<span class="d-none d-md-inline">{{ section.title.code }}</span>
|
||||||
{{ section.title.title|title }}
|
{{ section.title.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for subsection in section.subsections %}
|
{% for subsection in section.subsections %}
|
||||||
<div class="accounting-report-table-row accounting-income-statement-subsection">
|
<div class="accounting-report-table-row accounting-income-statement-subsection">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
<span class="d-none d-md-inline">{{ subsection.title.code }}</span>
|
||||||
{{ subsection.title.title|title }}
|
{{ subsection.title.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for account in subsection.accounts %}
|
{% for account in subsection.accounts %}
|
||||||
<a class="accounting-report-table-row accounting-income-statement-account" href="{{ account.url }}">
|
<a class="accounting-report-table-row accounting-income-statement-account" href="{{ account.url }}">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
||||||
{{ account.account.title|title }}
|
{{ account.account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
<div class="accounting-amount {% if account.amount < 0 %} text-danger {% endif %}">{{ account.amount|accounting_report_format_amount }}</div>
|
||||||
</a>
|
</a>
|
||||||
@ -91,7 +91,7 @@ First written: 2023/3/7
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="accounting-report-table-row accounting-income-statement-total">
|
<div class="accounting-report-table-row accounting-income-statement-total">
|
||||||
<div>{{ section.accumulated.title|title }}</div>
|
<div>{{ section.accumulated.title }}</div>
|
||||||
<div class="accounting-amount {% if section.accumulated.amount < 0 %} text-danger {% endif %}">{{ section.accumulated.amount|accounting_report_format_amount }}</div>
|
<div class="accounting-amount {% if section.accumulated.amount < 0 %} text-danger {% endif %}">{{ section.accumulated.amount|accounting_report_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -65,7 +65,7 @@ First written: 2023/3/4
|
|||||||
<div>{{ line_item.currency.name }}</div>
|
<div>{{ line_item.currency.name }}</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ line_item.description|accounting_default }}</div>
|
<div>{{ line_item.description|accounting_default }}</div>
|
||||||
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||||
@ -82,7 +82,7 @@ First written: 2023/3/4
|
|||||||
<div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
<div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||||
<div class="text-muted small">
|
<div class="text-muted small">
|
||||||
{{ line_item.journal_entry.date|accounting_format_date }}
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
{% if line_item.currency.code != accounting_default_currency_code() %}
|
{% if line_item.currency.code != accounting_default_currency_code() %}
|
||||||
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
|
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/3/5
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% 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 header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Ledger of %(account)s %(period)s", account=report.account.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, period=report.period.desc|title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ First written: 2023/3/8
|
|||||||
<div>{{ line_item.currency.name }}</div>
|
<div>{{ line_item.currency.name }}</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
<span class="d-none d-md-inline">{{ line_item.account.code }}</span>
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ line_item.description|accounting_default }}</div>
|
<div>{{ line_item.description|accounting_default }}</div>
|
||||||
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
<div class="accounting-amount">{{ line_item.debit|accounting_format_amount|accounting_default }}</div>
|
||||||
@ -79,7 +79,7 @@ First written: 2023/3/8
|
|||||||
<div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
<div {% if not line_item.is_debit %} class="accounting-mobile-journal-credit" {% endif %}>
|
||||||
<div class="text-muted small">
|
<div class="text-muted small">
|
||||||
{{ line_item.journal_entry.date|accounting_format_date }}
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
{{ line_item.account.title|title }}
|
{{ line_item.account.title }}
|
||||||
{% if line_item.currency.code != accounting_default_currency_code() %}
|
{% if line_item.currency.code != accounting_default_currency_code() %}
|
||||||
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
|
<span class="badge rounded-pill bg-info">{{ line_item.currency.code }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -68,7 +68,7 @@ First written: 2023/3/5
|
|||||||
<a class="accounting-report-table-row" href="{{ account.url }}">
|
<a class="accounting-report-table-row" href="{{ account.url }}">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
<span class="d-none d-md-inline">{{ account.account.code }}</span>
|
||||||
{{ account.account.title|title }}
|
{{ account.account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="accounting-amount">{{ account.debit|accounting_format_amount|accounting_default }}</div>
|
<div class="accounting-amount">{{ account.debit|accounting_format_amount|accounting_default }}</div>
|
||||||
<div class="accounting-amount">{{ account.credit|accounting_format_amount|accounting_default }}</div>
|
<div class="accounting-amount">{{ account.credit|accounting_format_amount|accounting_default }}</div>
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/4/8
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% 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 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 %}
|
{% block content %}
|
||||||
|
|
||||||
@ -46,9 +46,9 @@ First written: 2023/4/8
|
|||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">
|
<h2 class="text-center">
|
||||||
{% if report.currency.code == accounting_default_currency_code() %}
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
{{ A_("Accounts with Unapplied Items") }}
|
{{ A_("Accounts With Unapplied Items") }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ A_("Accounts with Unapplied Items in %(currency)s", currency=report.currency.name|title) }}
|
{{ A_("Accounts With Unapplied Items in %(currency)s", currency=report.currency.name|title) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -64,7 +64,7 @@ First written: 2023/4/8
|
|||||||
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unapplied", currency=report.currency, account=account, period=report.period) }}">
|
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unapplied", currency=report.currency, account=account, period=report.period) }}">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ account.code }}</span>
|
<span class="d-none d-md-inline">{{ account.code }}</span>
|
||||||
{{ account.title|title }}
|
{{ account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="accounting-amount">{{ account.count }}</div>
|
<div class="accounting-amount">{{ account.count }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/4/7
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% 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 header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Unapplied Items of %(account)s", account=report.account.title) }}{% else %}{{ A_("Unapplied Items of %(account)s in %(currency)s", currency=report.currency.name|title, account=report.account.title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/4/17
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% 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 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 %}
|
{% block content %}
|
||||||
|
|
||||||
@ -46,9 +46,9 @@ First written: 2023/4/17
|
|||||||
<div class="d-none d-sm-flex justify-content-center mb-3">
|
<div class="d-none d-sm-flex justify-content-center mb-3">
|
||||||
<h2 class="text-center">
|
<h2 class="text-center">
|
||||||
{% if report.currency.code == accounting_default_currency_code() %}
|
{% if report.currency.code == accounting_default_currency_code() %}
|
||||||
{{ A_("Accounts with Unmatched Offsets") }}
|
{{ A_("Accounts With Unmatched Offsets") }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ A_("Accounts with Unmatched Offsets in %(currency)s", currency=report.currency.name|title) }}
|
{{ A_("Accounts With Unmatched Offsets in %(currency)s", currency=report.currency.name|title) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -64,7 +64,7 @@ First written: 2023/4/17
|
|||||||
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unmatched", currency=report.currency, account=account, period=report.period) }}">
|
<a class="accounting-report-table-row" href="{{ url_for("accounting-report.unmatched", currency=report.currency, account=account, period=report.period) }}">
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline">{{ account.code }}</span>
|
<span class="d-none d-md-inline">{{ account.code }}</span>
|
||||||
{{ account.title|title }}
|
{{ account.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="accounting-amount">{{ account.count }}</div>
|
<div class="accounting-amount">{{ account.count }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -26,7 +26,7 @@ First written: 2023/4/17
|
|||||||
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
|
||||||
{% endblock %}
|
{% 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 header %}{% block title %}{% if report.currency.code == accounting_default_currency_code() %}{{ A_("Unmatched Offsets of %(account)s", account=report.account.title) }}{% else %}{{ A_("Unmatched Offsets of %(account)s in %(currency)s", currency=report.currency.name|title, account=report.account.title) }}{% endif %}{% endblock %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ First written: 2023/4/17
|
|||||||
|
|
||||||
<form action="{{ url_for("accounting-report.match-offsets", currency=report.currency, account=report.account) }}" method="post">
|
<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="csrf_token" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" name="next" value="{{ request.full_path if request.query_string else request.path }}">
|
<input type="hidden" name="next" value="{{ accounting_as_next() }}">
|
||||||
<div class="modal fade" id="accounting-match-modal" tabindex="-1" aria-labelledby="accounting-match-modal-label" aria-hidden="true">
|
<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-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -8,8 +8,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: mia-accounting 1.4.0\n"
|
"Project-Id-Version: mia-accounting 1.4.0\n"
|
||||||
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
|
||||||
"POT-Creation-Date: 2023-04-18 09:32+0800\n"
|
"POT-Creation-Date: 2023-07-29 08:55+0800\n"
|
||||||
"PO-Revision-Date: 2023-04-18 09:32+0800\n"
|
"PO-Revision-Date: 2023-07-29 08:56+0800\n"
|
||||||
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
|
||||||
"Language: zh_Hant\n"
|
"Language: zh_Hant\n"
|
||||||
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
|
||||||
@ -21,7 +21,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/accounting/forms.py:33
|
#: src/accounting/forms.py:33
|
||||||
#: src/accounting/static/js/journal-entry-form.js:1080
|
#: src/accounting/static/js/journal-entry-form.js:1080
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:411
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:415
|
||||||
#: src/accounting/static/js/option-form.js:537
|
#: src/accounting/static/js/option-form.js:537
|
||||||
#: src/accounting/static/js/option-form.js:803
|
#: src/accounting/static/js/option-form.js:803
|
||||||
msgid "Please select the account."
|
msgid "Please select the account."
|
||||||
@ -35,22 +35,22 @@ msgstr "沒有這個貨幣。"
|
|||||||
msgid "The account does not exist."
|
msgid "The account does not exist."
|
||||||
msgstr "沒有這個科目。"
|
msgstr "沒有這個科目。"
|
||||||
|
|
||||||
#: src/accounting/models.py:581
|
#: src/accounting/models.py:578
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cash Disbursement Journal Entry#%(id)s"
|
msgid "Cash Disbursement Journal Entry#%(id)s"
|
||||||
msgstr "現金支出傳票#%(id)s"
|
msgstr "現金支出傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:584
|
#: src/accounting/models.py:581
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cash Receipt Journal Entry#%(id)s"
|
msgid "Cash Receipt Journal Entry#%(id)s"
|
||||||
msgstr "現金收入傳票#%(id)s"
|
msgstr "現金收入傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:585
|
#: src/accounting/models.py:582
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Transfer Journal Entry#%(id)s"
|
msgid "Transfer Journal Entry#%(id)s"
|
||||||
msgstr "轉帳傳票#%(id)s"
|
msgstr "轉帳傳票#%(id)s"
|
||||||
|
|
||||||
#: src/accounting/models.py:714
|
#: src/accounting/models.py:706
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(date)s %(description)s %(amount)s"
|
msgid "%(date)s %(description)s %(amount)s"
|
||||||
msgstr "%(date)s %(description)s %(amount)s"
|
msgstr "%(date)s %(description)s %(amount)s"
|
||||||
@ -101,7 +101,7 @@ msgid "Please fill in the title"
|
|||||||
msgstr "請填上標題。"
|
msgstr "請填上標題。"
|
||||||
|
|
||||||
#: src/accounting/account/queries.py:50
|
#: src/accounting/account/queries.py:50
|
||||||
#: src/accounting/report/reports/search.py:101
|
#: src/accounting/report/reports/search.py:100
|
||||||
#: src/accounting/templates/accounting/account/detail.html:97
|
#: src/accounting/templates/accounting/account/detail.html:97
|
||||||
#: src/accounting/templates/accounting/account/list.html:62
|
#: src/accounting/templates/accounting/account/list.html:62
|
||||||
msgid "Needs Offset"
|
msgid "Needs Offset"
|
||||||
@ -205,24 +205,24 @@ msgstr "傳票不可刪除。"
|
|||||||
msgid "The journal entry is deleted successfully."
|
msgid "The journal entry is deleted successfully."
|
||||||
msgstr "傳票刪掉了"
|
msgstr "傳票刪掉了"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:39
|
#: src/accounting/journal_entry/forms/currency.py:38
|
||||||
msgid "Please select the currency."
|
msgid "Please select the currency."
|
||||||
msgstr "請選擇貨幣。"
|
msgstr "請選擇貨幣。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:62
|
#: src/accounting/journal_entry/forms/currency.py:61
|
||||||
msgid "The currency must be the same as the original line item."
|
msgid "The currency must be the same as the original line item."
|
||||||
msgstr "貨幣需和原始分錄相同。"
|
msgstr "貨幣需和原始分錄相同。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:89
|
#: src/accounting/journal_entry/forms/currency.py:88
|
||||||
msgid "The currency must not be changed when there is offset."
|
msgid "The currency must not be changed when there is offset."
|
||||||
msgstr "抵銷過不可變更貨幣。"
|
msgstr "抵銷過不可變更貨幣。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:98
|
#: src/accounting/journal_entry/forms/currency.py:97
|
||||||
#: src/accounting/static/js/journal-entry-form.js:773
|
#: src/accounting/static/js/journal-entry-form.js:773
|
||||||
msgid "Please add some line items."
|
msgid "Please add some line items."
|
||||||
msgstr "請加上分錄。"
|
msgstr "請加上分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/currency.py:111
|
#: src/accounting/journal_entry/forms/currency.py:110
|
||||||
#: src/accounting/static/js/journal-entry-form.js:522
|
#: src/accounting/static/js/journal-entry-form.js:522
|
||||||
msgid "The totals of the debit and credit amounts do not match."
|
msgid "The totals of the debit and credit amounts do not match."
|
||||||
msgstr "借方貸方合計不符。 "
|
msgstr "借方貸方合計不符。 "
|
||||||
@ -251,62 +251,62 @@ msgstr "請加上貨幣。"
|
|||||||
msgid "Line items with offset cannot be deleted."
|
msgid "Line items with offset cannot be deleted."
|
||||||
msgstr "無法刪除抵銷過的分錄。"
|
msgstr "無法刪除抵銷過的分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:49
|
#: src/accounting/journal_entry/forms/line_item.py:48
|
||||||
msgid "The original line item does not exist."
|
msgid "The original line item does not exist."
|
||||||
msgstr "沒有這筆原始分錄。"
|
msgstr "沒有這筆原始分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:70
|
#: src/accounting/journal_entry/forms/line_item.py:69
|
||||||
msgid "The original line item is on the same debit or credit."
|
msgid "The original line item is on the same debit or credit."
|
||||||
msgstr "原始分錄在借貸同一邊。"
|
msgstr "原始分錄在借貸同一邊。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:85
|
#: src/accounting/journal_entry/forms/line_item.py:84
|
||||||
msgid "The original line item does not need offset."
|
msgid "The original line item does not need offset."
|
||||||
msgstr "這筆原始分錄不需抵銷。"
|
msgstr "這筆原始分錄不需抵銷。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:101
|
#: src/accounting/journal_entry/forms/line_item.py:100
|
||||||
msgid "The original line item cannot be an offset item."
|
msgid "The original line item cannot be an offset item."
|
||||||
msgstr "原始分錄不可以是抵銷分錄。"
|
msgstr "原始分錄不可以是抵銷分錄。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:119
|
#: src/accounting/journal_entry/forms/line_item.py:118
|
||||||
msgid "The account must be the same as the original line item."
|
msgid "The account must be the same as the original line item."
|
||||||
msgstr "科目需和原始分錄相同。"
|
msgstr "科目需和原始分錄相同。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:135
|
#: src/accounting/journal_entry/forms/line_item.py:134
|
||||||
msgid "The account must not be changed when there is offset."
|
msgid "The account must not be changed when there is offset."
|
||||||
msgstr "抵銷過不可變更科目。"
|
msgstr "抵銷過不可變更科目。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:151
|
#: src/accounting/journal_entry/forms/line_item.py:150
|
||||||
msgid "A payable line item cannot start from debit."
|
msgid "A payable line item cannot start from debit."
|
||||||
msgstr "不可由借方新建應付款。"
|
msgstr "不可由借方新建應付款。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:167
|
#: src/accounting/journal_entry/forms/line_item.py:166
|
||||||
msgid "A receivable line item cannot start from credit."
|
msgid "A receivable line item cannot start from credit."
|
||||||
msgstr "不可由貸方新建應收款。"
|
msgstr "不可由貸方新建應收款。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:178
|
#: src/accounting/journal_entry/forms/line_item.py:177
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:436
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:440
|
||||||
msgid "Please fill in a positive amount."
|
msgid "Please fill in a positive amount."
|
||||||
msgstr "金額請填正數。"
|
msgstr "金額請填正數。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:220
|
#: src/accounting/journal_entry/forms/line_item.py:219
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:442
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:446
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The amount must not exceed the net balance %(balance)s of the original "
|
"The amount must not exceed the net balance %(balance)s of the original "
|
||||||
"line item."
|
"line item."
|
||||||
msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
|
msgstr "金額不可超過原始分錄凈額 %(balance)s 。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:241
|
#: src/accounting/journal_entry/forms/line_item.py:239
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:450
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:454
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The amount must not be less than the offset total %(total)s."
|
msgid "The amount must not be less than the offset total %(total)s."
|
||||||
msgstr "金額不可低於抵銷總額 %(total)s 。"
|
msgstr "金額不可低於抵銷總額 %(total)s 。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:426
|
#: src/accounting/journal_entry/forms/line_item.py:424
|
||||||
msgid "This account is not for debit line items."
|
msgid "This account is not for debit line items."
|
||||||
msgstr "科目不是借方科目。"
|
msgstr "科目不是借方科目。"
|
||||||
|
|
||||||
#: src/accounting/journal_entry/forms/line_item.py:478
|
#: src/accounting/journal_entry/forms/line_item.py:476
|
||||||
msgid "This account is not for credit line items."
|
msgid "This account is not for credit line items."
|
||||||
msgstr "科目不是貸方科目。"
|
msgstr "科目不是貸方科目。"
|
||||||
|
|
||||||
@ -417,15 +417,15 @@ msgstr "去年"
|
|||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "全部"
|
msgstr "全部"
|
||||||
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:423
|
#: src/accounting/report/reports/balance_sheet.py:425
|
||||||
#: src/accounting/report/reports/balance_sheet.py:427
|
#: src/accounting/report/reports/balance_sheet.py:429
|
||||||
#: src/accounting/report/reports/balance_sheet.py:439
|
|
||||||
#: src/accounting/report/reports/balance_sheet.py:441
|
#: src/accounting/report/reports/balance_sheet.py:441
|
||||||
#: src/accounting/report/reports/income_expenses.py:189
|
#: src/accounting/report/reports/balance_sheet.py:443
|
||||||
#: src/accounting/report/reports/income_expenses.py:423
|
#: src/accounting/report/reports/income_expenses.py:187
|
||||||
#: src/accounting/report/reports/income_statement.py:300
|
#: src/accounting/report/reports/income_expenses.py:420
|
||||||
#: src/accounting/report/reports/ledger.py:171
|
#: src/accounting/report/reports/income_statement.py:301
|
||||||
#: src/accounting/report/reports/ledger.py:380
|
#: src/accounting/report/reports/ledger.py:168
|
||||||
|
#: src/accounting/report/reports/ledger.py:376
|
||||||
#: src/accounting/report/reports/trial_balance.py:229
|
#: src/accounting/report/reports/trial_balance.py:229
|
||||||
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
|
#: src/accounting/templates/accounting/journal-entry/disbursement/detail.html:43
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:38
|
#: src/accounting/templates/accounting/journal-entry/include/form-debit-credit.html:38
|
||||||
@ -445,14 +445,14 @@ msgstr "全部"
|
|||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr "合計"
|
msgstr "合計"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:136
|
#: src/accounting/report/reports/income_expenses.py:134
|
||||||
#: src/accounting/report/reports/ledger.py:132
|
#: src/accounting/report/reports/ledger.py:129
|
||||||
msgid "Brought forward"
|
msgid "Brought forward"
|
||||||
msgstr "前期轉入"
|
msgstr "前期轉入"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:407
|
#: src/accounting/report/reports/income_expenses.py:404
|
||||||
#: src/accounting/report/reports/journal.py:158
|
#: src/accounting/report/reports/journal.py:158
|
||||||
#: src/accounting/report/reports/ledger.py:366
|
#: src/accounting/report/reports/ledger.py:362
|
||||||
#: src/accounting/report/reports/unapplied.py:148
|
#: src/accounting/report/reports/unapplied.py:148
|
||||||
#: src/accounting/report/reports/unmatched.py:158
|
#: src/accounting/report/reports/unmatched.py:158
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:50
|
||||||
@ -466,13 +466,13 @@ msgstr "前期轉入"
|
|||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "日期"
|
msgstr "日期"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:407
|
#: src/accounting/report/reports/income_expenses.py:404
|
||||||
#: src/accounting/report/reports/journal.py:159
|
#: src/accounting/report/reports/journal.py:159
|
||||||
#: src/accounting/report/reports/trial_balance.py:225
|
#: src/accounting/report/reports/trial_balance.py:225
|
||||||
#: src/accounting/report/reports/unapplied_accounts.py:122
|
#: src/accounting/report/reports/unapplied_accounts.py:122
|
||||||
#: src/accounting/report/reports/unmatched_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/journal-entry/include/journal-entry-line-item-editor-modal.html:58
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:39
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:40
|
||||||
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
|
#: src/accounting/templates/accounting/report/include/toolbar-buttons.html:90
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:56
|
#: src/accounting/templates/accounting/report/income-expenses.html:56
|
||||||
#: src/accounting/templates/accounting/report/journal.html:55
|
#: src/accounting/templates/accounting/report/journal.html:55
|
||||||
@ -481,13 +481,13 @@ msgstr "日期"
|
|||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr "科目"
|
msgstr "科目"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:408
|
#: src/accounting/report/reports/income_expenses.py:405
|
||||||
#: src/accounting/report/reports/journal.py:159
|
#: src/accounting/report/reports/journal.py:159
|
||||||
#: src/accounting/report/reports/ledger.py:366
|
#: src/accounting/report/reports/ledger.py:362
|
||||||
#: src/accounting/report/reports/unapplied.py:149
|
#: src/accounting/report/reports/unapplied.py:149
|
||||||
#: src/accounting/report/reports/unmatched.py:159
|
#: 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/description-editor-modal.html:29
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:49
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:50
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:57
|
#: src/accounting/templates/accounting/report/income-expenses.html:57
|
||||||
#: src/accounting/templates/accounting/report/journal.html:56
|
#: src/accounting/templates/accounting/report/journal.html:56
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:56
|
#: src/accounting/templates/accounting/report/ledger.html:56
|
||||||
@ -497,18 +497,18 @@ msgstr "科目"
|
|||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "摘要"
|
msgstr "摘要"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:408
|
#: src/accounting/report/reports/income_expenses.py:405
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:58
|
#: src/accounting/templates/accounting/report/income-expenses.html:58
|
||||||
msgid "Income"
|
msgid "Income"
|
||||||
msgstr "收入"
|
msgstr "收入"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:409
|
#: src/accounting/report/reports/income_expenses.py:406
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:59
|
#: src/accounting/templates/accounting/report/income-expenses.html:59
|
||||||
msgid "Expense"
|
msgid "Expense"
|
||||||
msgstr "支出"
|
msgstr "支出"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:409
|
#: src/accounting/report/reports/income_expenses.py:406
|
||||||
#: src/accounting/report/reports/ledger.py:368
|
#: src/accounting/report/reports/ledger.py:364
|
||||||
#: src/accounting/report/reports/unmatched.py:160
|
#: src/accounting/report/reports/unmatched.py:160
|
||||||
#: src/accounting/templates/accounting/report/income-expenses.html:60
|
#: src/accounting/templates/accounting/report/income-expenses.html:60
|
||||||
#: src/accounting/templates/accounting/report/ledger.html:60
|
#: src/accounting/templates/accounting/report/ledger.html:60
|
||||||
@ -516,41 +516,41 @@ msgstr "支出"
|
|||||||
msgid "Balance"
|
msgid "Balance"
|
||||||
msgstr "餘額"
|
msgstr "餘額"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_expenses.py:410
|
#: src/accounting/report/reports/income_expenses.py:407
|
||||||
#: src/accounting/report/reports/journal.py:161
|
#: src/accounting/report/reports/journal.py:161
|
||||||
#: src/accounting/report/reports/ledger.py:368
|
#: src/accounting/report/reports/ledger.py:364
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:178
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:179
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:73
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:73
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
msgstr "備註"
|
msgstr "備註"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:228
|
#: src/accounting/report/reports/income_statement.py:229
|
||||||
msgid "total operating revenue"
|
msgid "Total Operating Revenue"
|
||||||
msgstr "營業收入總額"
|
msgstr "營業收入總額"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:229
|
#: src/accounting/report/reports/income_statement.py:230
|
||||||
msgid "gross income"
|
msgid "Gross Income"
|
||||||
msgstr "營業毛利"
|
msgstr "營業毛利"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:230
|
#: src/accounting/report/reports/income_statement.py:231
|
||||||
msgid "operating income"
|
msgid "Operating Income"
|
||||||
msgstr "營業淨利"
|
msgstr "營業淨利"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:231
|
#: src/accounting/report/reports/income_statement.py:232
|
||||||
msgid "before tax income"
|
msgid "Before Tax Income"
|
||||||
msgstr "稅前淨利"
|
msgstr "稅前淨利"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:232
|
#: src/accounting/report/reports/income_statement.py:233
|
||||||
msgid "after tax income"
|
msgid "After Tax Income"
|
||||||
msgstr "稅後淨利"
|
msgstr "稅後淨利"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:233
|
#: src/accounting/report/reports/income_statement.py:234
|
||||||
msgid "net income or loss for current period"
|
msgid "Net Income or Loss for Current Period"
|
||||||
msgstr "本期損益"
|
msgstr "本期損益"
|
||||||
|
|
||||||
#: src/accounting/report/reports/income_statement.py:301
|
#: src/accounting/report/reports/income_statement.py:302
|
||||||
#: src/accounting/report/reports/unapplied.py:149
|
#: 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/journal-entry/include/journal-entry-line-item-editor-modal.html:66
|
||||||
#: src/accounting/templates/accounting/report/income-statement.html:61
|
#: src/accounting/templates/accounting/report/income-statement.html:61
|
||||||
#: src/accounting/templates/accounting/report/unapplied.html:54
|
#: src/accounting/templates/accounting/report/unapplied.html:54
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
@ -567,7 +567,7 @@ msgid "Currency"
|
|||||||
msgstr "貨幣"
|
msgstr "貨幣"
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:160
|
#: src/accounting/report/reports/journal.py:160
|
||||||
#: src/accounting/report/reports/ledger.py:367
|
#: src/accounting/report/reports/ledger.py:363
|
||||||
#: src/accounting/report/reports/trial_balance.py:225
|
#: src/accounting/report/reports/trial_balance.py:225
|
||||||
#: src/accounting/report/reports/unmatched.py:159
|
#: src/accounting/report/reports/unmatched.py:159
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
|
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:33
|
||||||
@ -581,7 +581,7 @@ msgid "Debit"
|
|||||||
msgstr "借方"
|
msgstr "借方"
|
||||||
|
|
||||||
#: src/accounting/report/reports/journal.py:160
|
#: src/accounting/report/reports/journal.py:160
|
||||||
#: src/accounting/report/reports/ledger.py:367
|
#: src/accounting/report/reports/ledger.py:363
|
||||||
#: src/accounting/report/reports/trial_balance.py:226
|
#: src/accounting/report/reports/trial_balance.py:226
|
||||||
#: src/accounting/report/reports/unmatched.py:160
|
#: src/accounting/report/reports/unmatched.py:160
|
||||||
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
|
#: src/accounting/templates/accounting/journal-entry/transfer/detail.html:49
|
||||||
@ -614,16 +614,16 @@ msgstr "淨額"
|
|||||||
msgid "Count"
|
msgid "Count"
|
||||||
msgstr "數量"
|
msgstr "數量"
|
||||||
|
|
||||||
#: src/accounting/report/utils/offset_matcher.py:163
|
#: src/accounting/report/utils/offset_matcher.py:161
|
||||||
msgid "There is no unmatched offset."
|
msgid "There is no unmatched offset."
|
||||||
msgstr "沒有遺漏的抵銷分錄"
|
msgstr "沒有遺漏的抵銷分錄"
|
||||||
|
|
||||||
#: src/accounting/report/utils/offset_matcher.py:167
|
#: src/accounting/report/utils/offset_matcher.py:165
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(total)s unmatched offsets without original items."
|
msgid "%(total)s unmatched offsets without original items."
|
||||||
msgstr "%(total)s 筆遺漏的抵銷分錄無法自動抵銷。"
|
msgstr "%(total)s 筆遺漏的抵銷分錄無法自動抵銷。"
|
||||||
|
|
||||||
#: src/accounting/report/utils/offset_matcher.py:172
|
#: src/accounting/report/utils/offset_matcher.py:170
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"%(matches)s unmatched offsets out of %(total)s can match with their "
|
"%(matches)s unmatched offsets out of %(total)s can match with their "
|
||||||
@ -752,7 +752,7 @@ msgid "December"
|
|||||||
msgstr "十二月"
|
msgstr "十二月"
|
||||||
|
|
||||||
#: src/accounting/static/js/journal-entry-form.js:1085
|
#: src/accounting/static/js/journal-entry-form.js:1085
|
||||||
#: src/accounting/static/js/journal-entry-line-item-editor.js:430
|
#: src/accounting/static/js/journal-entry-line-item-editor.js:434
|
||||||
msgid "Please fill in the amount."
|
msgid "Please fill in the amount."
|
||||||
msgstr "請填上金額。"
|
msgstr "請填上金額。"
|
||||||
|
|
||||||
@ -833,12 +833,12 @@ msgstr "確認刪除科目"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:91
|
#: src/accounting/templates/accounting/account/include/form.html:91
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:73
|
#: src/accounting/templates/accounting/currency/detail.html:73
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:27
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:27
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:30
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:31
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:78
|
#: src/accounting/templates/accounting/journal-entry/include/detail.html:78
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:28
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:29
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:27
|
#: src/accounting/templates/accounting/journal-entry/include/original-line-item-selector-modal.html:27
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:27
|
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:27
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:28
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:29
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:27
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
#: src/accounting/templates/accounting/report/include/search-modal.html:28
|
||||||
#: src/accounting/templates/accounting/report/unmatched.html:58
|
#: src/accounting/templates/accounting/report/unmatched.html:58
|
||||||
@ -853,11 +853,11 @@ msgstr "你確定要刪掉這個科目嗎?"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:112
|
#: src/accounting/templates/accounting/account/include/form.html:112
|
||||||
#: src/accounting/templates/accounting/currency/detail.html:79
|
#: src/accounting/templates/accounting/currency/detail.html:79
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
|
#: src/accounting/templates/accounting/journal-entry/include/account-selector-modal.html:49
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:194
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:195
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/detail.html:84
|
#: src/accounting/templates/accounting/journal-entry/include/detail.html:84
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:70
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-account-selector-modal.html:48
|
#: 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/option/include/recurring-item-editor-modal.html:66
|
||||||
#: src/accounting/templates/accounting/report/include/search-modal.html:37
|
#: src/accounting/templates/accounting/report/include/search-modal.html:37
|
||||||
#: src/accounting/templates/accounting/report/unmatched.html:74
|
#: src/accounting/templates/accounting/report/unmatched.html:74
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
@ -942,12 +942,12 @@ msgstr "%(base)s下的科目"
|
|||||||
#: src/accounting/templates/accounting/account/include/form.html:75
|
#: src/accounting/templates/accounting/account/include/form.html:75
|
||||||
#: src/accounting/templates/accounting/account/order.html:62
|
#: src/accounting/templates/accounting/account/order.html:62
|
||||||
#: src/accounting/templates/accounting/currency/include/form.html:57
|
#: src/accounting/templates/accounting/currency/include/form.html:57
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:195
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:196
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/form.html:80
|
#: src/accounting/templates/accounting/journal-entry/include/form.html:80
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:71
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:72
|
||||||
#: src/accounting/templates/accounting/journal-entry/order.html:61
|
#: src/accounting/templates/accounting/journal-entry/order.html:61
|
||||||
#: src/accounting/templates/accounting/option/form.html:80
|
#: src/accounting/templates/accounting/option/form.html:80
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:66
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:67
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "儲存"
|
msgstr "儲存"
|
||||||
|
|
||||||
@ -1008,7 +1008,7 @@ msgid "Code"
|
|||||||
msgstr "代碼"
|
msgstr "代碼"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/currency/include/form.html:50
|
#: src/accounting/templates/accounting/currency/include/form.html:50
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:33
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:34
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "名稱"
|
msgstr "名稱"
|
||||||
|
|
||||||
@ -1077,53 +1077,53 @@ msgstr "選擇科目"
|
|||||||
msgid "More…"
|
msgid "More…"
|
||||||
msgstr "更多…"
|
msgstr "更多…"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:36
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:37
|
||||||
msgid "Offset..."
|
msgid "Offset..."
|
||||||
msgstr "抵銷…"
|
msgstr "抵銷…"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:44
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:45
|
||||||
msgid "General"
|
msgid "General"
|
||||||
msgstr "一般"
|
msgstr "一般"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:49
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:50
|
||||||
msgid "Travel"
|
msgid "Travel"
|
||||||
msgstr "差旅"
|
msgstr "差旅"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:54
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:55
|
||||||
msgid "Bus"
|
msgid "Bus"
|
||||||
msgstr "公車"
|
msgstr "公車"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:59
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:60
|
||||||
msgid "Recurring"
|
msgid "Recurring"
|
||||||
msgstr "常用"
|
msgstr "常用"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:64
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:65
|
||||||
msgid "Annotation"
|
msgid "Annotation"
|
||||||
msgstr "註記"
|
msgstr "註記"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:73
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:74
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:90
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:91
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:125
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:126
|
||||||
msgid "Tag"
|
msgid "Tag"
|
||||||
msgstr "標籤"
|
msgstr "標籤"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:105
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:106
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:146
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:147
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:129
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:129
|
||||||
msgid "From"
|
msgid "From"
|
||||||
msgstr "從"
|
msgstr "從"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:114
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:115
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:151
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:152
|
||||||
#: src/accounting/templates/accounting/report/include/period-chooser.html:135
|
#: src/accounting/templates/accounting/report/include/period-chooser.html:135
|
||||||
msgid "To"
|
msgid "To"
|
||||||
msgstr "至"
|
msgstr "至"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:130
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:131
|
||||||
msgid "Route"
|
msgid "Route"
|
||||||
msgstr "路線"
|
msgstr "路線"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:172
|
#: src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html:173
|
||||||
msgid "The Number of Items"
|
msgid "The Number of Items"
|
||||||
msgstr "數量"
|
msgstr "數量"
|
||||||
|
|
||||||
@ -1155,11 +1155,11 @@ msgstr "確認刪除傳票"
|
|||||||
msgid "Do you really want to delete this journal entry?"
|
msgid "Do you really want to delete this journal entry?"
|
||||||
msgstr "你確定要刪掉這張傳票嗎?"
|
msgstr "你確定要刪掉這張傳票嗎?"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:27
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:28
|
||||||
msgid "Line Item Content"
|
msgid "Line Item Content"
|
||||||
msgstr "分錄內容"
|
msgstr "分錄內容"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:34
|
#: src/accounting/templates/accounting/journal-entry/include/journal-entry-line-item-editor-modal.html:35
|
||||||
msgid "Original Line Item"
|
msgid "Original Line Item"
|
||||||
msgstr "原始分錄"
|
msgstr "原始分錄"
|
||||||
|
|
||||||
@ -1215,43 +1215,43 @@ msgstr "常用支出"
|
|||||||
msgid "Recurring Income"
|
msgid "Recurring Income"
|
||||||
msgstr "常用收入"
|
msgstr "常用收入"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:47
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:48
|
||||||
msgid "Description Template"
|
msgid "Description Template"
|
||||||
msgstr "摘要範本"
|
msgstr "摘要範本"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:52
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:53
|
||||||
msgid "Available template variables:"
|
msgid "Available template variables:"
|
||||||
msgstr "範本變數說明:"
|
msgstr "範本變數說明:"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:54
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:55
|
||||||
msgid "This month, as a number."
|
msgid "This month, as a number."
|
||||||
msgstr "這個月的數字。"
|
msgstr "這個月的數字。"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:55
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:56
|
||||||
msgid "This month, in its name."
|
msgid "This month, in its name."
|
||||||
msgstr "這個月的名稱。"
|
msgstr "這個月的名稱。"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:56
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:57
|
||||||
msgid "Last month, as a number."
|
msgid "Last month, as a number."
|
||||||
msgstr "上個月的數字。"
|
msgstr "上個月的數字。"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:57
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:58
|
||||||
msgid "Last month, in its name."
|
msgid "Last month, in its name."
|
||||||
msgstr "上個月的名稱。"
|
msgstr "上個月的名稱。"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:58
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:59
|
||||||
msgid "The previous bimonthly period, as numbers."
|
msgid "The previous bimonthly period, as numbers."
|
||||||
msgstr "前個雙月期的數字。"
|
msgstr "前個雙月期的數字。"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:59
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:60
|
||||||
msgid "The previous bimonthly period, as their names."
|
msgid "The previous bimonthly period, as their names."
|
||||||
msgstr "前個雙月期的名稱。"
|
msgstr "前個雙月期的名稱。"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:61
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:62
|
||||||
msgid "Example:"
|
msgid "Example:"
|
||||||
msgstr "範例:"
|
msgstr "範例:"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:61
|
#: src/accounting/templates/accounting/option/include/recurring-item-editor-modal.html:62
|
||||||
msgid "Water bill for {last_bimonthly_name}"
|
msgid "Water bill for {last_bimonthly_name}"
|
||||||
msgstr "水費{last_bimonthly_number}月"
|
msgstr "水費{last_bimonthly_number}月"
|
||||||
|
|
||||||
@ -1318,13 +1318,13 @@ msgstr "%(period)s%(currency)s試算表"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
|
||||||
#: src/accounting/templates/accounting/report/unapplied-accounts.html:49
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:49
|
||||||
msgid "Accounts with Unapplied Items"
|
msgid "Accounts With Unapplied Items"
|
||||||
msgstr "含未抵銷項目的科目"
|
msgstr "含未抵銷項目的科目"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:29
|
||||||
#: src/accounting/templates/accounting/report/unapplied-accounts.html:51
|
#: src/accounting/templates/accounting/report/unapplied-accounts.html:51
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Accounts with Unapplied Items in %(currency)s"
|
msgid "Accounts With Unapplied Items in %(currency)s"
|
||||||
msgstr "%(currency)s含未抵銷項目的科目"
|
msgstr "%(currency)s含未抵銷項目的科目"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/unapplied.html:29
|
#: src/accounting/templates/accounting/report/unapplied.html:29
|
||||||
@ -1339,13 +1339,13 @@ msgstr "%(currency)s%(account)s未抵銷項目"
|
|||||||
|
|
||||||
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
|
||||||
#: src/accounting/templates/accounting/report/unmatched-accounts.html:49
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:49
|
||||||
msgid "Accounts with Unmatched Offsets"
|
msgid "Accounts With Unmatched Offsets"
|
||||||
msgstr "含遺漏抵銷項目的科目"
|
msgstr "含遺漏抵銷項目的科目"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:29
|
||||||
#: src/accounting/templates/accounting/report/unmatched-accounts.html:51
|
#: src/accounting/templates/accounting/report/unmatched-accounts.html:51
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Accounts with Unmatched Offsets in %(currency)s"
|
msgid "Accounts With Unmatched Offsets in %(currency)s"
|
||||||
msgstr "%(currency)s含遺漏抵銷項目的科目"
|
msgstr "%(currency)s含遺漏抵銷項目的科目"
|
||||||
|
|
||||||
#: src/accounting/templates/accounting/report/unmatched.html:29
|
#: src/accounting/templates/accounting/report/unmatched.html:29
|
||||||
@ -1415,12 +1415,12 @@ msgstr "下載"
|
|||||||
msgid "current assets and liabilities"
|
msgid "current assets and liabilities"
|
||||||
msgstr "流動資產與負債"
|
msgstr "流動資產與負債"
|
||||||
|
|
||||||
#: src/accounting/utils/pagination.py:206
|
#: src/accounting/utils/pagination.py:207
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
msgstr "上一頁"
|
msgstr "上一頁"
|
||||||
|
|
||||||
#: src/accounting/utils/pagination.py:255
|
#: src/accounting/utils/pagination.py:256
|
||||||
msgctxt "Pagination|"
|
msgctxt "Pagination|"
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "下一頁"
|
msgstr "下一頁"
|
||||||
|
@ -14,18 +14,15 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The utility to cast a SQLAlchemy column into the column type, to avoid
|
"""The utilities to cast values into desired types, to avoid IDE warnings.
|
||||||
warnings from the IDE.
|
|
||||||
|
|
||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
def s(message: t.Any) -> str:
|
def s(message: Any) -> str:
|
||||||
"""Casts the LazyString message to the string type.
|
"""Casts the LazyString message to the string type.
|
||||||
|
|
||||||
:param message: The message.
|
:param message: The message.
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
"""The current assets and liabilities account.
|
"""The current assets and liabilities account.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Self
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
class CurrentAccount:
|
class CurrentAccount:
|
||||||
@ -54,7 +54,7 @@ class CurrentAccount:
|
|||||||
return self.str
|
return self.str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def current_assets_and_liabilities(cls) -> t.Self:
|
def current_assets_and_liabilities(cls) -> Self:
|
||||||
"""Returns the pseudo account for all current assets and liabilities.
|
"""Returns the pseudo account for all current assets and liabilities.
|
||||||
|
|
||||||
:return: The pseudo account for all current assets and liabilities.
|
:return: The pseudo account for all current assets and liabilities.
|
||||||
@ -67,14 +67,14 @@ class CurrentAccount:
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accounts(cls) -> list[t.Self]:
|
def accounts(cls) -> list[Self]:
|
||||||
"""Returns the current assets and liabilities accounts.
|
"""Returns the current assets and liabilities accounts.
|
||||||
|
|
||||||
:return: The current assets and liabilities accounts.
|
:return: The current assets and liabilities accounts.
|
||||||
"""
|
"""
|
||||||
accounts: list[cls] = [cls.current_assets_and_liabilities()]
|
accounts: list[cls] = [cls.current_assets_and_liabilities()]
|
||||||
accounts.extend([CurrentAccount(x)
|
accounts.extend([CurrentAccount(x)
|
||||||
for x in db.session.query(Account)
|
for x in Account.query
|
||||||
.filter(cls.sql_condition())
|
.filter(cls.sql_condition())
|
||||||
.order_by(Account.base_code, Account.no)])
|
.order_by(Account.base_code, Account.no)])
|
||||||
return accounts
|
return accounts
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
@ -34,7 +34,7 @@ def flash_form_errors(form: FlaskForm) -> None:
|
|||||||
__flash_errors(form.errors)
|
__flash_errors(form.errors)
|
||||||
|
|
||||||
|
|
||||||
def __flash_errors(error: t.Any) -> None:
|
def __flash_errors(error: Any) -> None:
|
||||||
"""Flash all errors recursively.
|
"""Flash all errors recursively.
|
||||||
|
|
||||||
:param error: The errors.
|
:param error: The errors.
|
||||||
|
@ -22,7 +22,17 @@ This module should not import any other module from the application.
|
|||||||
from urllib.parse import urlparse, parse_qsl, ParseResult, urlencode, \
|
from urllib.parse import urlparse, parse_qsl, ParseResult, urlencode, \
|
||||||
urlunparse
|
urlunparse
|
||||||
|
|
||||||
from flask import request, Blueprint
|
from flask import request, Blueprint, current_app
|
||||||
|
from itsdangerous import URLSafeSerializer, BadData
|
||||||
|
|
||||||
|
|
||||||
|
def __as_next() -> str:
|
||||||
|
"""Encodes the current request URI as value for the next URI.
|
||||||
|
|
||||||
|
:return: The current request URI as value for the next URI.
|
||||||
|
"""
|
||||||
|
return encode_next(
|
||||||
|
request.full_path if request.query_string else request.path)
|
||||||
|
|
||||||
|
|
||||||
def append_next(uri: str) -> str:
|
def append_next(uri: str) -> str:
|
||||||
@ -41,11 +51,8 @@ def inherit_next(uri: str) -> str:
|
|||||||
:param uri: The URI.
|
:param uri: The URI.
|
||||||
:return: The URI with the current next URI added at the query argument.
|
:return: The URI with the current next URI added at the query argument.
|
||||||
"""
|
"""
|
||||||
next_uri: str | None = request.form.get("next") \
|
next_uri: str | None = __get_next()
|
||||||
if request.method == "POST" else request.args.get("next")
|
return uri if next_uri is None else __set_next(uri, next_uri)
|
||||||
if next_uri is None:
|
|
||||||
return uri
|
|
||||||
return __set_next(uri, next_uri)
|
|
||||||
|
|
||||||
|
|
||||||
def or_next(uri: str) -> str:
|
def or_next(uri: str) -> str:
|
||||||
@ -54,9 +61,23 @@ def or_next(uri: str) -> str:
|
|||||||
:param uri: The URI.
|
:param uri: The URI.
|
||||||
:return: The next URI or the supplied URI.
|
:return: The next URI or the supplied URI.
|
||||||
"""
|
"""
|
||||||
|
next_uri: str | None = __get_next()
|
||||||
|
return uri if next_uri is None else next_uri
|
||||||
|
|
||||||
|
|
||||||
|
def __get_next() -> str | None:
|
||||||
|
"""Returns the valid next URI.
|
||||||
|
|
||||||
|
:return: The valid next URI.
|
||||||
|
"""
|
||||||
next_uri: str | None = request.form.get("next") \
|
next_uri: str | None = request.form.get("next") \
|
||||||
if request.method == "POST" else request.args.get("next")
|
if request.method == "POST" else request.args.get("next")
|
||||||
return uri if next_uri is None else next_uri
|
if next_uri is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return decode_next(next_uri)
|
||||||
|
except BadData:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def __set_next(uri: str, next_uri: str) -> str:
|
def __set_next(uri: str, next_uri: str) -> str:
|
||||||
@ -69,18 +90,39 @@ def __set_next(uri: str, next_uri: str) -> str:
|
|||||||
uri_p: ParseResult = urlparse(uri)
|
uri_p: ParseResult = urlparse(uri)
|
||||||
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
|
params: list[tuple[str, str]] = parse_qsl(uri_p.query)
|
||||||
params = [x for x in params if x[0] != "next"]
|
params = [x for x in params if x[0] != "next"]
|
||||||
params.append(("next", next_uri))
|
params.append(("next", encode_next(next_uri)))
|
||||||
parts: list[str] = list(uri_p)
|
parts: list[str] = list(uri_p)
|
||||||
parts[4] = urlencode(params)
|
parts[4] = urlencode(params)
|
||||||
return urlunparse(parts)
|
return urlunparse(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_next(uri: str) -> str:
|
||||||
|
"""Encodes the next URI.
|
||||||
|
|
||||||
|
:param uri: The next URI.
|
||||||
|
:return: The encoded next URI.
|
||||||
|
"""
|
||||||
|
return URLSafeSerializer(current_app.config["SECRET_KEY"])\
|
||||||
|
.dumps(uri, "next")
|
||||||
|
|
||||||
|
|
||||||
|
def decode_next(uri: str) -> str:
|
||||||
|
"""Decodes the encoded next URI.
|
||||||
|
|
||||||
|
:param uri: The encoded next URI.
|
||||||
|
:return: The next URI.
|
||||||
|
"""
|
||||||
|
return URLSafeSerializer(current_app.config["SECRET_KEY"])\
|
||||||
|
.loads(uri, "next")
|
||||||
|
|
||||||
|
|
||||||
def init_app(bp: Blueprint) -> None:
|
def init_app(bp: Blueprint) -> None:
|
||||||
"""Initializes the application.
|
"""Initializes the application.
|
||||||
|
|
||||||
:param bp: The blueprint of the accounting application.
|
:param bp: The blueprint of the accounting application.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
bp.add_app_template_global(__as_next, "accounting_as_next")
|
||||||
bp.add_app_template_filter(append_next, "accounting_append_next")
|
bp.add_app_template_filter(append_next, "accounting_append_next")
|
||||||
bp.add_app_template_filter(inherit_next, "accounting_inherit_next")
|
bp.add_app_template_filter(inherit_next, "accounting_inherit_next")
|
||||||
bp.add_app_template_filter(or_next, "accounting_or_next")
|
bp.add_app_template_filter(or_next, "accounting_or_next")
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"""The SQLAlchemy alias for the offset items.
|
"""The SQLAlchemy alias for the offset items.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@ -30,10 +30,10 @@ def offset_alias() -> sa.Alias:
|
|||||||
:return: The SQLAlchemy alias for the offset items.
|
:return: The SQLAlchemy alias for the offset items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def as_from(model_cls: t.Any) -> sa.FromClause:
|
def as_from(model_cls: Any) -> sa.FromClause:
|
||||||
return model_cls
|
return model_cls
|
||||||
|
|
||||||
def as_alias(alias: t.Any) -> sa.Alias:
|
def as_alias(alias: Any) -> sa.Alias:
|
||||||
return alias
|
return alias
|
||||||
|
|
||||||
return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))
|
return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))
|
||||||
|
@ -39,8 +39,11 @@ class RecurringItem:
|
|||||||
:param description_template: The description template.
|
:param description_template: The description template.
|
||||||
"""
|
"""
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
"""The name."""
|
||||||
self.account_code: str = account_code
|
self.account_code: str = account_code
|
||||||
|
"""The account code."""
|
||||||
self.description_template: str = description_template
|
self.description_template: str = description_template
|
||||||
|
"""The description template."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account_text(self) -> str:
|
def account_text(self) -> str:
|
||||||
@ -61,8 +64,10 @@ class Recurring:
|
|||||||
"""
|
"""
|
||||||
self.expenses: list[RecurringItem] \
|
self.expenses: list[RecurringItem] \
|
||||||
= [RecurringItem(x[0], x[1], x[2]) for x in data["expense"]]
|
= [RecurringItem(x[0], x[1], x[2]) for x in data["expense"]]
|
||||||
|
"""The recurring expenses."""
|
||||||
self.incomes: list[RecurringItem] \
|
self.incomes: list[RecurringItem] \
|
||||||
= [RecurringItem(x[0], x[1], x[2]) for x in data["income"]]
|
= [RecurringItem(x[0], x[1], x[2]) for x in data["income"]]
|
||||||
|
"""The recurring incomes."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def codes(self) -> set[str]:
|
def codes(self) -> set[str]:
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from typing import TypeVar, Generic
|
||||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
|
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, \
|
||||||
ParseResult
|
ParseResult
|
||||||
|
|
||||||
@ -62,10 +62,11 @@ class Redirection(RequestRedirect):
|
|||||||
DEFAULT_PAGE_SIZE: int = 10
|
DEFAULT_PAGE_SIZE: int = 10
|
||||||
"""The default page size."""
|
"""The default page size."""
|
||||||
|
|
||||||
T = t.TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
"""The pagination item type."""
|
||||||
|
|
||||||
|
|
||||||
class Pagination(t.Generic[T]):
|
class Pagination(Generic[T]):
|
||||||
"""The pagination utility."""
|
"""The pagination utility."""
|
||||||
|
|
||||||
def __init__(self, items: list[T], is_reversed: bool = False):
|
def __init__(self, items: list[T], is_reversed: bool = False):
|
||||||
@ -91,7 +92,7 @@ class Pagination(t.Generic[T]):
|
|||||||
"""The options to the number of items in a page."""
|
"""The options to the number of items in a page."""
|
||||||
|
|
||||||
|
|
||||||
class AbstractPagination(t.Generic[T]):
|
class AbstractPagination(Generic[T]):
|
||||||
"""An abstract pagination."""
|
"""An abstract pagination."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -19,21 +19,21 @@
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
from collections.abc import Callable
|
||||||
|
|
||||||
from flask import abort, Blueprint, Response
|
from flask import abort, Blueprint, Response
|
||||||
|
|
||||||
from accounting.utils.user import get_current_user, UserUtilityInterface
|
from accounting.utils.user import get_current_user, UserUtilityInterface
|
||||||
|
|
||||||
|
|
||||||
def has_permission(rule: t.Callable[[], bool]) -> t.Callable:
|
def has_permission(rule: Callable[[], bool]) -> Callable:
|
||||||
"""The permission decorator to check whether the current user is allowed.
|
"""The permission decorator to check whether the current user is allowed.
|
||||||
|
|
||||||
:param rule: The permission rule.
|
:param rule: The permission rule.
|
||||||
:return: The view decorator.
|
:return: The view decorator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(view: t.Callable) -> t.Callable:
|
def decorator(view: Callable) -> Callable:
|
||||||
"""The view decorator to decorate a view with permission tests.
|
"""The view decorator to decorate a view with permission tests.
|
||||||
|
|
||||||
:param view: The view.
|
:param view: The view.
|
||||||
@ -61,16 +61,16 @@ def has_permission(rule: t.Callable[[], bool]) -> t.Callable:
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
__can_view_func: t.Callable[[], bool] = lambda: True
|
__can_view_func: Callable[[], bool] = lambda: True
|
||||||
"""The callback that returns whether the current user can view the accounting
|
"""The callback that returns whether the current user can view the accounting
|
||||||
data."""
|
data."""
|
||||||
__can_edit_func: t.Callable[[], bool] = lambda: True
|
__can_edit_func: Callable[[], bool] = lambda: True
|
||||||
"""The callback that returns whether the current user can edit the accounting
|
"""The callback that returns whether the current user can edit the accounting
|
||||||
data."""
|
data."""
|
||||||
__can_admin_func: t.Callable[[], bool] = lambda: True
|
__can_admin_func: Callable[[], bool] = lambda: True
|
||||||
"""The callback that returns whether the current user can administrate the
|
"""The callback that returns whether the current user can administrate the
|
||||||
accounting settings."""
|
accounting settings."""
|
||||||
_unauthorized_func: t.Callable[[], Response | None] \
|
_unauthorized_func: Callable[[], Response | None] \
|
||||||
= lambda: Response(status=403)
|
= lambda: Response(status=403)
|
||||||
"""The callback that returns the response to require the user to log in."""
|
"""The callback that returns the response to require the user to log in."""
|
||||||
|
|
||||||
|
@ -14,22 +14,22 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""The random ID mixin for the data models.
|
"""The random ID utility for the data models.
|
||||||
|
|
||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from secrets import randbelow
|
from secrets import randbelow
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
|
|
||||||
|
|
||||||
def new_id(cls: t.Type):
|
def new_id(cls: Type[db.Model]):
|
||||||
"""Returns a new random ID for the data model.
|
"""Generates and returns a new, unused random ID for the data model.
|
||||||
|
|
||||||
:param cls: The data model.
|
:param cls: The data model.
|
||||||
:return: The generated new random ID.
|
:return: The newly-generated, unused random ID.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
obj_id: int = 100000000 + randbelow(900000000)
|
obj_id: int = 100000000 + randbelow(900000000)
|
||||||
|
59
src/accounting/utils/title_case.py
Normal file
59
src/accounting/utils/title_case.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# The Mia! Accounting Project.
|
||||||
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/7/29
|
||||||
|
|
||||||
|
# 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 title case capitalization for the base account titles.
|
||||||
|
This follows the APA style title case capitalization. See
|
||||||
|
https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case .
|
||||||
|
|
||||||
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
CONJUNCTIONS: set[str] = {"and", "as", "but", "for", "if", "nor", "or", "so",
|
||||||
|
"yet"}
|
||||||
|
"""Short conjunctions."""
|
||||||
|
ARTICLES: set[str] = {"a", "an", "the"}
|
||||||
|
"""Articles."""
|
||||||
|
PREPOSITIONS: set[str] = {"as", "at", "by", "for", "in", "of", "on", "per",
|
||||||
|
"to", "up", "via"}
|
||||||
|
"""Short prepositions."""
|
||||||
|
MINOR_WORDS: set[str] \
|
||||||
|
= CONJUNCTIONS.copy().union(ARTICLES).union(PREPOSITIONS)
|
||||||
|
"""Minor words that should be in lowercase."""
|
||||||
|
# Excludes "by" as in "1223 by-products"
|
||||||
|
MINOR_WORDS.remove("by")
|
||||||
|
|
||||||
|
|
||||||
|
def title_case(s: str) -> str:
|
||||||
|
"""Capitalize a title string for the base account titles. Do not use it
|
||||||
|
in other places. This excludes "by" as in "1223 by-products".
|
||||||
|
|
||||||
|
:param s: The title string.
|
||||||
|
:return: The capitalized title string.
|
||||||
|
"""
|
||||||
|
return re.sub(r"\w+", __cap_word, s)
|
||||||
|
|
||||||
|
|
||||||
|
def __cap_word(m: re.Match) -> str:
|
||||||
|
"""Capitalize a matched title word.
|
||||||
|
|
||||||
|
:param m: The matched title word.
|
||||||
|
:return: The capitalized title word.
|
||||||
|
"""
|
||||||
|
if m.group(0).lower() in MINOR_WORDS:
|
||||||
|
return m.group(0)
|
||||||
|
return m.group(0).title()
|
@ -19,17 +19,18 @@
|
|||||||
This module should not import any other module from the application.
|
This module should not import any other module from the application.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import typing as t
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TypeVar, Generic, Type
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import g, Response
|
from flask import g, Response
|
||||||
from flask_sqlalchemy.model import Model
|
from flask_sqlalchemy.model import Model
|
||||||
|
|
||||||
T = t.TypeVar("T", bound=Model)
|
T = TypeVar("T", bound=Model)
|
||||||
|
"""The user data model data type."""
|
||||||
|
|
||||||
|
|
||||||
class UserUtilityInterface(t.Generic[T], ABC):
|
class UserUtilityInterface(Generic[T], ABC):
|
||||||
"""The interface for the user utilities."""
|
"""The interface for the user utilities."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -72,7 +73,7 @@ class UserUtilityInterface(t.Generic[T], ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cls(self) -> t.Type[T]:
|
def cls(self) -> Type[T]:
|
||||||
"""Returns the class of the user data model.
|
"""Returns the class of the user data model.
|
||||||
|
|
||||||
:return: The class of the user data model.
|
:return: The class of the user data model.
|
||||||
@ -112,7 +113,7 @@ class UserUtilityInterface(t.Generic[T], ABC):
|
|||||||
|
|
||||||
__user_utils: UserUtilityInterface
|
__user_utils: UserUtilityInterface
|
||||||
"""The user utilities."""
|
"""The user utilities."""
|
||||||
user_cls: t.Type[Model] = Model
|
user_cls: Type[Model] = Model
|
||||||
"""The user class."""
|
"""The user class."""
|
||||||
user_pk_column: sa.Column = sa.Column(sa.Integer)
|
user_pk_column: sa.Column = sa.Column(sa.Integer)
|
||||||
"""The primary key column of the user class."""
|
"""The primary key column of the user class."""
|
||||||
|
@ -28,8 +28,11 @@ from babel.messages.frontend import CommandLineInterface
|
|||||||
from opencc import OpenCC
|
from opencc import OpenCC
|
||||||
|
|
||||||
root_dir: Path = Path(__file__).parent.parent
|
root_dir: Path = Path(__file__).parent.parent
|
||||||
|
"""The project root directory."""
|
||||||
translation_dir: Path = root_dir / "tests" / "test_site" / "translations"
|
translation_dir: Path = root_dir / "tests" / "test_site" / "translations"
|
||||||
|
"""The directory of the translation files."""
|
||||||
domain: str = "messages"
|
domain: str = "messages"
|
||||||
|
"""The message domain."""
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
@ -28,8 +28,11 @@ from babel.messages.frontend import CommandLineInterface
|
|||||||
from opencc import OpenCC
|
from opencc import OpenCC
|
||||||
|
|
||||||
root_dir: Path = Path(__file__).parent.parent
|
root_dir: Path = Path(__file__).parent.parent
|
||||||
|
"""The project root directory."""
|
||||||
translation_dir: Path = root_dir / "src" / "accounting" / "translations"
|
translation_dir: Path = root_dir / "src" / "accounting" / "translations"
|
||||||
|
"""The directory of the translation files."""
|
||||||
domain: str = "accounting"
|
domain: str = "accounting"
|
||||||
|
"""The message domain."""
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
@ -17,15 +17,16 @@
|
|||||||
"""The test for the account management.
|
"""The test for the account management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime as dt
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import timedelta, date
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from accounting.utils.next_uri import encode_next
|
||||||
from test_site import db
|
from test_site import db
|
||||||
from testlib import NEXT_URI, create_test_app, get_client, set_locale, \
|
from testlib import NEXT_URI, create_test_app, get_client, get_csrf_token, \
|
||||||
add_journal_entry
|
set_locale, add_journal_entry
|
||||||
|
|
||||||
|
|
||||||
class AccountData:
|
class AccountData:
|
||||||
@ -71,29 +72,35 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
from accounting.models import Account, AccountL10n
|
from accounting.models import Account, AccountL10n
|
||||||
AccountL10n.query.delete()
|
AccountL10n.query.delete()
|
||||||
Account.query.delete()
|
Account.query.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
self.__encoded_next_uri: str = encode_next(NEXT_URI)
|
||||||
|
"""The encoded next URI."""
|
||||||
|
|
||||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
self.__client: httpx.Client = get_client(self.__app, "editor")
|
||||||
|
"""The user client."""
|
||||||
|
self.__csrf_token: str = get_csrf_token(self.__client)
|
||||||
|
"""The CSRF token."""
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": CASH.base_code,
|
"base_code": CASH.base_code,
|
||||||
"title": CASH.title})
|
"title": CASH.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{CASH.code}")
|
f"{PREFIX}/{CASH.code}")
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": BANK.base_code,
|
"base_code": BANK.base_code,
|
||||||
"title": BANK.title})
|
"title": BANK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{BANK.code}")
|
f"{PREFIX}/{BANK.code}")
|
||||||
@ -104,7 +111,8 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client: httpx.Client = get_client(self.__app, "nobody")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(PREFIX)
|
response = client.get(PREFIX)
|
||||||
@ -138,12 +146,12 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
|
response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
cash_id: int = Account.find_by_code(CASH.code).id
|
cash_id: int = Account.find_by_code(CASH.code).id
|
||||||
|
|
||||||
response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
|
response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
|
||||||
data={"csrf_token": csrf_token,
|
data={"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
f"{cash_id}-no": "5"})
|
f"{cash_id}-no": "5"})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -153,7 +161,8 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client: httpx.Client = get_client(self.__app, "viewer")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(PREFIX)
|
response = client.get(PREFIX)
|
||||||
@ -187,12 +196,12 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
|
response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
cash_id: int = Account.find_by_code(CASH.code).id
|
cash_id: int = Account.find_by_code(CASH.code).id
|
||||||
|
|
||||||
response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
|
response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
|
||||||
data={"csrf_token": csrf_token,
|
data={"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
f"{cash_id}-no": "5"})
|
f"{cash_id}-no": "5"})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -204,48 +213,48 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.get(PREFIX)
|
response = self.__client.get(PREFIX)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{CASH.code}")
|
response = self.__client.get(f"{PREFIX}/{CASH.code}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/create")
|
response = self.__client.get(f"{PREFIX}/create")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": STOCK.base_code,
|
"base_code": STOCK.base_code,
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{STOCK.code}")
|
f"{PREFIX}/{STOCK.code}")
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{CASH.code}/edit")
|
response = self.__client.get(f"{PREFIX}/{CASH.code}/edit")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/{CASH.code}/update",
|
response = self.__client.post(f"{PREFIX}/{CASH.code}/update",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": CASH.base_code,
|
"base_code": CASH.base_code,
|
||||||
"title": f"{CASH.title}-2"})
|
"title": f"{CASH.title}-2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/{BANK.code}/delete",
|
response = self.__client.post(f"{PREFIX}/{BANK.code}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], PREFIX)
|
self.assertEqual(response.headers["Location"], PREFIX)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/bases/{CASH.base_code}")
|
response = self.__client.get(f"{PREFIX}/bases/{CASH.base_code}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
cash_id: int = Account.find_by_code(CASH.code).id
|
cash_id: int = Account.find_by_code(CASH.code).id
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/bases/{CASH.base_code}",
|
response = self.__client.post(f"{PREFIX}/bases/{CASH.base_code}",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
f"{cash_id}-no": "5"})
|
f"{cash_id}-no": "5"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], NEXT_URI)
|
self.assertEqual(response.headers["Location"], NEXT_URI)
|
||||||
|
|
||||||
@ -260,96 +269,97 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
detail_uri: str = f"{PREFIX}/{STOCK.code}"
|
detail_uri: str = f"{PREFIX}/{STOCK.code}"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Account.query.all()},
|
self.assertEqual({x.code for x in Account.query.all()},
|
||||||
{CASH.code, BANK.code})
|
{CASH.code, BANK.code})
|
||||||
|
|
||||||
# Missing CSRF token
|
# Missing CSRF token
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"base_code": STOCK.base_code,
|
data={"base_code": STOCK.base_code,
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
# CSRF token mismatch
|
# CSRF token mismatch
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": f"{self.csrf_token}-2",
|
data={"csrf_token":
|
||||||
"base_code": STOCK.base_code,
|
f"{self.__csrf_token}-2",
|
||||||
"title": STOCK.title})
|
"base_code": STOCK.base_code,
|
||||||
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
# Empty base account code
|
# Empty base account code
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": " ",
|
"base_code": " ",
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Non-existing base account
|
# Non-existing base account
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "9999",
|
"base_code": "9999",
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Unavailable base account
|
# Unavailable base account
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "1",
|
"base_code": "1",
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Empty name
|
# Empty name
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": STOCK.base_code,
|
"base_code": STOCK.base_code,
|
||||||
"title": " "})
|
"title": " "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# A nominal account that needs offset
|
# A nominal account that needs offset
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "6172",
|
"base_code": "6172",
|
||||||
"title": STOCK.title,
|
"title": STOCK.title,
|
||||||
"is_need_offset": "yes"})
|
"is_need_offset": "yes"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Success, with spaces to be stripped
|
# Success, with spaces to be stripped
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": f" {STOCK.base_code} ",
|
"base_code": f" {STOCK.base_code} ",
|
||||||
"title": f" {STOCK.title} "})
|
"title": f" {STOCK.title} "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
# Success under the same base
|
# Success under the same base
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": STOCK.base_code,
|
"base_code": STOCK.base_code,
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{STOCK.base_code}-002")
|
f"{PREFIX}/{STOCK.base_code}-002")
|
||||||
|
|
||||||
# Success under the same base, with order in a mess.
|
# Success under the same base, with order in a mess.
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002")
|
stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002")
|
||||||
stock_2.no = 66
|
stock_2.no = 66
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": STOCK.base_code,
|
"base_code": STOCK.base_code,
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{STOCK.base_code}-003")
|
f"{PREFIX}/{STOCK.base_code}-003")
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Account.query.all()},
|
self.assertEqual({x.code for x in Account.query.all()},
|
||||||
{CASH.code, BANK.code, STOCK.code,
|
{CASH.code, BANK.code, STOCK.code,
|
||||||
f"{STOCK.base_code}-002",
|
f"{STOCK.base_code}-002",
|
||||||
@ -372,71 +382,71 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
# Success, with spaces to be stripped
|
# Success, with spaces to be stripped
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": f" {CASH.base_code} ",
|
"base_code": f" {CASH.base_code} ",
|
||||||
"title": f" {CASH.title}-1 "})
|
"title": f" {CASH.title}-1 "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account: Account = Account.find_by_code(CASH.code)
|
account: Account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.base_code, CASH.base_code)
|
self.assertEqual(account.base_code, CASH.base_code)
|
||||||
self.assertEqual(account.title_l10n, f"{CASH.title}-1")
|
self.assertEqual(account.title_l10n, f"{CASH.title}-1")
|
||||||
|
|
||||||
# Empty base account code
|
# Empty base account code
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": " ",
|
"base_code": " ",
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Non-existing base account
|
# Non-existing base account
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "9999",
|
"base_code": "9999",
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Unavailable base account
|
# Unavailable base account
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "1",
|
"base_code": "1",
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Empty name
|
# Empty name
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": STOCK.base_code,
|
"base_code": STOCK.base_code,
|
||||||
"title": " "})
|
"title": " "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# A nominal account that needs offset
|
# A nominal account that needs offset
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "6172",
|
"base_code": "6172",
|
||||||
"title": STOCK.title,
|
"title": STOCK.title,
|
||||||
"is_need_offset": "yes"})
|
"is_need_offset": "yes"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Change the base account
|
# Change the base account
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": STOCK.base_code,
|
"base_code": STOCK.base_code,
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_c_uri)
|
self.assertEqual(response.headers["Location"], detail_c_uri)
|
||||||
|
|
||||||
response = self.client.get(detail_uri)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
response = self.client.get(detail_c_uri)
|
response = self.__client.get(detail_c_uri)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_update_not_modified(self) -> None:
|
def test_update_not_modified(self) -> None:
|
||||||
@ -450,29 +460,29 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
account: Account
|
account: Account
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": f" {CASH.base_code} ",
|
"base_code": f" {CASH.base_code} ",
|
||||||
"title": f" {CASH.title} "})
|
"title": f" {CASH.title} "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertIsNotNone(account)
|
self.assertIsNotNone(account)
|
||||||
account.created_at \
|
account.created_at \
|
||||||
= account.created_at - timedelta(seconds=5)
|
= account.created_at - dt.timedelta(seconds=5)
|
||||||
account.updated_at = account.created_at
|
account.updated_at = account.created_at
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": CASH.base_code,
|
"base_code": CASH.base_code,
|
||||||
"title": STOCK.title})
|
"title": STOCK.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertIsNotNone(account)
|
self.assertIsNotNone(account)
|
||||||
self.assertLess(account.created_at,
|
self.assertLess(account.created_at,
|
||||||
@ -485,13 +495,14 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
editor_username, admin_username = "editor", "admin"
|
editor_username, admin_username = "editor", "admin"
|
||||||
client, csrf_token = get_client(self.app, admin_username)
|
client: httpx.Client = get_client(self.__app, admin_username)
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
detail_uri: str = f"{PREFIX}/{CASH.code}"
|
detail_uri: str = f"{PREFIX}/{CASH.code}"
|
||||||
update_uri: str = f"{PREFIX}/{CASH.code}/update"
|
update_uri: str = f"{PREFIX}/{CASH.code}/update"
|
||||||
account: Account
|
account: Account
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.created_by.username, editor_username)
|
self.assertEqual(account.created_by.username, editor_username)
|
||||||
self.assertEqual(account.updated_by.username, editor_username)
|
self.assertEqual(account.updated_by.username, editor_username)
|
||||||
@ -503,7 +514,7 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.created_by.username,
|
self.assertEqual(account.created_by.username,
|
||||||
editor_username)
|
editor_username)
|
||||||
@ -521,51 +532,51 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
account: Account
|
account: Account
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.title_l10n, CASH.title)
|
self.assertEqual(account.title_l10n, CASH.title)
|
||||||
self.assertEqual(account.l10n, [])
|
self.assertEqual(account.l10n, [])
|
||||||
|
|
||||||
set_locale(self.client, self.csrf_token, "zh_Hant")
|
set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": CASH.base_code,
|
"base_code": CASH.base_code,
|
||||||
"title": f"{CASH.title}-zh_Hant"})
|
"title": f"{CASH.title}-zh_Hant"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.title_l10n, CASH.title)
|
self.assertEqual(account.title_l10n, CASH.title)
|
||||||
self.assertEqual({(x.locale, x.title) for x in account.l10n},
|
self.assertEqual({(x.locale, x.title) for x in account.l10n},
|
||||||
{("zh_Hant", f"{CASH.title}-zh_Hant")})
|
{("zh_Hant", f"{CASH.title}-zh_Hant")})
|
||||||
|
|
||||||
set_locale(self.client, self.csrf_token, "en")
|
set_locale(self.__app, self.__client, self.__csrf_token, "en")
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": CASH.base_code,
|
"base_code": CASH.base_code,
|
||||||
"title": f"{CASH.title}-2"})
|
"title": f"{CASH.title}-2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.title_l10n, f"{CASH.title}-2")
|
self.assertEqual(account.title_l10n, f"{CASH.title}-2")
|
||||||
self.assertEqual({(x.locale, x.title) for x in account.l10n},
|
self.assertEqual({(x.locale, x.title) for x in account.l10n},
|
||||||
{("zh_Hant", f"{CASH.title}-zh_Hant")})
|
{("zh_Hant", f"{CASH.title}-zh_Hant")})
|
||||||
|
|
||||||
set_locale(self.client, self.csrf_token, "zh_Hant")
|
set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": CASH.base_code,
|
"base_code": CASH.base_code,
|
||||||
"title": f"{CASH.title}-zh_Hant-2"})
|
"title": f"{CASH.title}-zh_Hant-2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(CASH.code)
|
account = Account.find_by_code(CASH.code)
|
||||||
self.assertEqual(account.title_l10n, f"{CASH.title}-2")
|
self.assertEqual(account.title_l10n, f"{CASH.title}-2")
|
||||||
self.assertEqual({(x.locale, x.title) for x in account.l10n},
|
self.assertEqual({(x.locale, x.title) for x in account.l10n},
|
||||||
@ -582,53 +593,53 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
list_uri: str = PREFIX
|
list_uri: str = PREFIX
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": PETTY.base_code,
|
"base_code": PETTY.base_code,
|
||||||
"title": PETTY.title})
|
"title": PETTY.title})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
add_journal_entry(self.client,
|
add_journal_entry(self.__client,
|
||||||
form={"csrf_token": self.csrf_token,
|
form={"csrf_token": self.__csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
"date": date.today().isoformat(),
|
"date": dt.date.today().isoformat(),
|
||||||
"currency-1-code": "USD",
|
"currency-1-code": "USD",
|
||||||
"currency-1-credit-1-account_code": BANK.code,
|
"currency-1-credit-1-account_code": BANK.code,
|
||||||
"currency-1-credit-1-amount": "20"})
|
"currency-1-credit-1-amount": "20"})
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Account.query.all()},
|
self.assertEqual({x.code for x in Account.query.all()},
|
||||||
{CASH.code, PETTY.code, BANK.code})
|
{CASH.code, PETTY.code, BANK.code})
|
||||||
|
|
||||||
# Cannot delete the cash account
|
# Cannot delete the cash account
|
||||||
response = self.client.post(f"{PREFIX}/{CASH.code}/delete",
|
response = self.__client.post(f"{PREFIX}/{CASH.code}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
|
||||||
|
|
||||||
# Cannot delete the account that is in use
|
# Cannot delete the account that is in use
|
||||||
response = self.client.post(f"{PREFIX}/{BANK.code}/delete",
|
response = self.__client.post(f"{PREFIX}/{BANK.code}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{BANK.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{BANK.code}")
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
response = self.client.get(detail_uri)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
response = self.client.post(delete_uri,
|
response = self.__client.post(delete_uri,
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], list_uri)
|
self.assertEqual(response.headers["Location"], list_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Account.query.all()},
|
self.assertEqual({x.code for x in Account.query.all()},
|
||||||
{CASH.code, BANK.code})
|
{CASH.code, BANK.code})
|
||||||
|
|
||||||
response = self.client.get(detail_uri)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
response = self.client.post(delete_uri,
|
response = self.__client.post(delete_uri,
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def test_change_base_code(self) -> None:
|
def test_change_base_code(self) -> None:
|
||||||
@ -640,15 +651,15 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
for i in range(2, 6):
|
for i in range(2, 6):
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "1111",
|
"base_code": "1111",
|
||||||
"title": "Title"})
|
"title": "Title"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/1111-00{i}")
|
f"{PREFIX}/1111-00{i}")
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account_1: Account = Account.find_by_code("1111-001")
|
account_1: Account = Account.find_by_code("1111-001")
|
||||||
id_1: int = account_1.id
|
id_1: int = account_1.id
|
||||||
account_2: Account = Account.find_by_code("1111-002")
|
account_2: Account = Account.find_by_code("1111-002")
|
||||||
@ -668,14 +679,14 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
account_5.no = 6
|
account_5.no = 6
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/1111-005/update",
|
response = self.__client.post(f"{PREFIX}/1111-005/update",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "1112",
|
"base_code": "1112",
|
||||||
"title": "Title"})
|
"title": "Title"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/1112-003")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/1112-003")
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual(db.session.get(Account, id_1).no, 1)
|
self.assertEqual(db.session.get(Account, id_1).no, 1)
|
||||||
self.assertEqual(db.session.get(Account, id_2).no, 3)
|
self.assertEqual(db.session.get(Account, id_2).no, 3)
|
||||||
self.assertEqual(db.session.get(Account, id_3).no, 2)
|
self.assertEqual(db.session.get(Account, id_3).no, 2)
|
||||||
@ -691,34 +702,34 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
for i in range(2, 6):
|
for i in range(2, 6):
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"base_code": "1111",
|
"base_code": "1111",
|
||||||
"title": "Title"})
|
"title": "Title"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/1111-00{i}")
|
f"{PREFIX}/1111-00{i}")
|
||||||
|
|
||||||
# Normal reorder
|
# Normal reorder
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
id_1: int = Account.find_by_code("1111-001").id
|
id_1: int = Account.find_by_code("1111-001").id
|
||||||
id_2: int = Account.find_by_code("1111-002").id
|
id_2: int = Account.find_by_code("1111-002").id
|
||||||
id_3: int = Account.find_by_code("1111-003").id
|
id_3: int = Account.find_by_code("1111-003").id
|
||||||
id_4: int = Account.find_by_code("1111-004").id
|
id_4: int = Account.find_by_code("1111-004").id
|
||||||
id_5: int = Account.find_by_code("1111-005").id
|
id_5: int = Account.find_by_code("1111-005").id
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/bases/1111",
|
response = self.__client.post(f"{PREFIX}/bases/1111",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
f"{id_1}-no": "4",
|
f"{id_1}-no": "4",
|
||||||
f"{id_2}-no": "1",
|
f"{id_2}-no": "1",
|
||||||
f"{id_3}-no": "5",
|
f"{id_3}-no": "5",
|
||||||
f"{id_4}-no": "2",
|
f"{id_4}-no": "2",
|
||||||
f"{id_5}-no": "3"})
|
f"{id_5}-no": "3"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], NEXT_URI)
|
self.assertEqual(response.headers["Location"], NEXT_URI)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual(db.session.get(Account, id_1).code, "1111-004")
|
self.assertEqual(db.session.get(Account, id_1).code, "1111-004")
|
||||||
self.assertEqual(db.session.get(Account, id_2).code, "1111-001")
|
self.assertEqual(db.session.get(Account, id_2).code, "1111-001")
|
||||||
self.assertEqual(db.session.get(Account, id_3).code, "1111-005")
|
self.assertEqual(db.session.get(Account, id_3).code, "1111-005")
|
||||||
@ -726,7 +737,7 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(db.session.get(Account, id_5).code, "1111-003")
|
self.assertEqual(db.session.get(Account, id_5).code, "1111-003")
|
||||||
|
|
||||||
# Malformed orders
|
# Malformed orders
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
db.session.get(Account, id_1).no = 3
|
db.session.get(Account, id_1).no = 3
|
||||||
db.session.get(Account, id_2).no = 4
|
db.session.get(Account, id_2).no = 4
|
||||||
db.session.get(Account, id_3).no = 6
|
db.session.get(Account, id_3).no = 6
|
||||||
@ -734,16 +745,16 @@ class AccountTestCase(unittest.TestCase):
|
|||||||
db.session.get(Account, id_5).no = 9
|
db.session.get(Account, id_5).no = 9
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/bases/1111",
|
response = self.__client.post(f"{PREFIX}/bases/1111",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
f"{id_2}-no": "3a",
|
f"{id_2}-no": "3a",
|
||||||
f"{id_3}-no": "5",
|
f"{id_3}-no": "5",
|
||||||
f"{id_4}-no": "2"})
|
f"{id_4}-no": "2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], NEXT_URI)
|
self.assertEqual(response.headers["Location"], NEXT_URI)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual(db.session.get(Account, id_1).code, "1111-003")
|
self.assertEqual(db.session.get(Account, id_1).code, "1111-003")
|
||||||
self.assertEqual(db.session.get(Account, id_2).code, "1111-004")
|
self.assertEqual(db.session.get(Account, id_2).code, "1111-004")
|
||||||
self.assertEqual(db.session.get(Account, id_3).code, "1111-002")
|
self.assertEqual(db.session.get(Account, id_3).code, "1111-002")
|
||||||
|
@ -39,14 +39,15 @@ class BaseAccountTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
def test_nobody(self) -> None:
|
def test_nobody(self) -> None:
|
||||||
"""Test the permission as nobody.
|
"""Test the permission as nobody.
|
||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client: httpx.Client = get_client(self.__app, "nobody")
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(LIST_URI)
|
response = client.get(LIST_URI)
|
||||||
@ -60,7 +61,7 @@ class BaseAccountTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client: httpx.Client = get_client(self.__app, "viewer")
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(LIST_URI)
|
response = client.get(LIST_URI)
|
||||||
@ -74,7 +75,7 @@ class BaseAccountTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "editor")
|
client: httpx.Client = get_client(self.__app, "editor")
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(LIST_URI)
|
response = client.get(LIST_URI)
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
import typing as t
|
import datetime as dt
|
||||||
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from click.testing import Result
|
from click.testing import Result
|
||||||
@ -40,11 +42,17 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
with self.app.app_context():
|
def test_init_db(self) -> None:
|
||||||
# Drop every accounting table, to see if accounting-init recreates
|
"""Tests the "accounting-init-db" console command.
|
||||||
# them correctly.
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
with self.__app.app_context():
|
||||||
|
# Drop every accounting table, to see if accounting-init-db
|
||||||
|
# recreates them correctly.
|
||||||
tables: list[sa.Table] \
|
tables: list[sa.Table] \
|
||||||
= [db.metadata.tables[x] for x in db.metadata.tables
|
= [db.metadata.tables[x] for x in db.metadata.tables
|
||||||
if x.startswith("accounting_")]
|
if x.startswith("accounting_")]
|
||||||
@ -56,13 +64,8 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
if x.startswith("accounting_")}),
|
if x.startswith("accounting_")}),
|
||||||
0)
|
0)
|
||||||
|
|
||||||
def test_init(self) -> None:
|
runner: FlaskCliRunner = self.__app.test_cli_runner()
|
||||||
"""Tests the "accounting-init" console command.
|
with self.__app.app_context():
|
||||||
|
|
||||||
:return: None.
|
|
||||||
"""
|
|
||||||
runner: FlaskCliRunner = self.app.test_cli_runner()
|
|
||||||
with self.app.app_context():
|
|
||||||
result: Result = runner.invoke(
|
result: Result = runner.invoke(
|
||||||
args=["accounting-init-db", "-u", "editor"])
|
args=["accounting-init-db", "-u", "editor"])
|
||||||
self.assertEqual(result.exit_code, 0,
|
self.assertEqual(result.exit_code, 0,
|
||||||
@ -80,20 +83,23 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
from accounting.models import BaseAccount
|
from accounting.models import BaseAccount
|
||||||
|
|
||||||
with open(data_dir / "base_accounts.csv") as fp:
|
with open(data_dir / "base_accounts.csv") as fp:
|
||||||
data: dict[dict[str, t.Any]] \
|
rows: list[dict[str, str]] = list(csv.DictReader(fp))
|
||||||
= {x["code"]: {"code": x["code"],
|
data: dict[dict[str, Any]] \
|
||||||
"title": x["title"],
|
= {x["code"]: {"code": x["code"],
|
||||||
"l10n": {y[5:]: x[y]
|
"title": x["title"],
|
||||||
for y in x if y.startswith("l10n-")}}
|
"l10n": {y[5:]: x[y]
|
||||||
for x in csv.DictReader(fp)}
|
for y in x if y.startswith("l10n-")}}
|
||||||
|
for x in rows}
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
accounts: list[BaseAccount] = BaseAccount.query.all()
|
accounts: list[BaseAccount] = BaseAccount.query.all()
|
||||||
|
|
||||||
self.assertEqual(len(accounts), len(data))
|
self.assertEqual(len(accounts), len(data))
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
self.assertIn(account.code, data)
|
self.assertIn(account.code, data)
|
||||||
self.assertEqual(account.title_l10n, data[account.code]["title"])
|
self.assertEqual(account.title_l10n.lower(),
|
||||||
|
data[account.code]["title"].lower())
|
||||||
|
self.__test_title_case(account.title_l10n)
|
||||||
l10n: dict[str, str] = {x.locale: x.title for x in account.l10n}
|
l10n: dict[str, str] = {x.locale: x.title for x in account.l10n}
|
||||||
self.assertEqual(len(l10n), len(data[account.code]["l10n"]))
|
self.assertEqual(len(l10n), len(data[account.code]["l10n"]))
|
||||||
for locale in l10n:
|
for locale in l10n:
|
||||||
@ -101,6 +107,23 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(l10n[locale],
|
self.assertEqual(l10n[locale],
|
||||||
data[account.code]["l10n"][locale])
|
data[account.code]["l10n"][locale])
|
||||||
|
|
||||||
|
def __test_title_case(self, s: str) -> None:
|
||||||
|
"""Tests the case of a base account title.
|
||||||
|
|
||||||
|
:param s: The base account title.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from accounting.utils.title_case import MINOR_WORDS
|
||||||
|
|
||||||
|
self.assertTrue(s[0].isupper(), s)
|
||||||
|
for word in re.findall(r"\w+", s):
|
||||||
|
if len(word) >= 4:
|
||||||
|
self.assertTrue(word.istitle(), s)
|
||||||
|
elif word in MINOR_WORDS:
|
||||||
|
self.assertTrue(word.islower(), s)
|
||||||
|
else:
|
||||||
|
self.assertTrue(word.istitle(), s)
|
||||||
|
|
||||||
def __test_account_data(self) -> None:
|
def __test_account_data(self) -> None:
|
||||||
"""Tests the account data.
|
"""Tests the account data.
|
||||||
|
|
||||||
@ -108,7 +131,7 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
from accounting.models import BaseAccount, Account, AccountL10n
|
from accounting.models import BaseAccount, Account, AccountL10n
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
bases: list[BaseAccount] = BaseAccount.query\
|
bases: list[BaseAccount] = BaseAccount.query\
|
||||||
.filter(sa.func.char_length(BaseAccount.code) == 4).all()
|
.filter(sa.func.char_length(BaseAccount.code) == 4).all()
|
||||||
accounts: list[Account] = Account.query.all()
|
accounts: list[Account] = Account.query.all()
|
||||||
@ -135,14 +158,14 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
from accounting.models import Currency
|
from accounting.models import Currency
|
||||||
|
|
||||||
with open(data_dir / "currencies.csv") as fp:
|
with open(data_dir / "currencies.csv") as fp:
|
||||||
data: dict[dict[str, t.Any]] \
|
data: dict[dict[str, Any]] \
|
||||||
= {x["code"]: {"code": x["code"],
|
= {x["code"]: {"code": x["code"],
|
||||||
"name": x["name"],
|
"name": x["name"],
|
||||||
"l10n": {y[5:]: x[y]
|
"l10n": {y[5:]: x[y]
|
||||||
for y in x if y.startswith("l10n-")}}
|
for y in x if y.startswith("l10n-")}}
|
||||||
for x in csv.DictReader(fp)}
|
for x in csv.DictReader(fp)}
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currencies: list[Currency] = Currency.query.all()
|
currencies: list[Currency] = Currency.query.all()
|
||||||
|
|
||||||
self.assertEqual(len(currencies), len(data))
|
self.assertEqual(len(currencies), len(data))
|
||||||
@ -155,3 +178,69 @@ class ConsoleCommandTestCase(unittest.TestCase):
|
|||||||
self.assertIn(locale, data[currency.code]["l10n"])
|
self.assertIn(locale, data[currency.code]["l10n"])
|
||||||
self.assertEqual(l10n[locale],
|
self.assertEqual(l10n[locale],
|
||||||
data[currency.code]["l10n"][locale])
|
data[currency.code]["l10n"][locale])
|
||||||
|
|
||||||
|
def test_titleize(self) -> None:
|
||||||
|
"""Tests the "accounting-titleize" console command.
|
||||||
|
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
from accounting.models import BaseAccount, Account
|
||||||
|
from accounting.utils.random_id import new_id
|
||||||
|
from accounting.utils.user import get_user_pk
|
||||||
|
runner: FlaskCliRunner = self.__app.test_cli_runner()
|
||||||
|
|
||||||
|
with self.__app.app_context():
|
||||||
|
# Resets the accounts.
|
||||||
|
tables: list[sa.Table] \
|
||||||
|
= [db.metadata.tables[x] for x in db.metadata.tables
|
||||||
|
if x.startswith("accounting_")]
|
||||||
|
for table in tables:
|
||||||
|
db.session.execute(DropTable(table))
|
||||||
|
db.session.commit()
|
||||||
|
inspector: sa.Inspector = sa.inspect(db.session.connection())
|
||||||
|
self.assertEqual(len({x for x in inspector.get_table_names()
|
||||||
|
if x.startswith("accounting_")}),
|
||||||
|
0)
|
||||||
|
result: Result = runner.invoke(
|
||||||
|
args=["accounting-init-db", "-u", "editor"])
|
||||||
|
self.assertEqual(result.exit_code, 0,
|
||||||
|
result.output + str(result.exception))
|
||||||
|
|
||||||
|
# Turns the titles into lowercase.
|
||||||
|
for base in BaseAccount.query:
|
||||||
|
base.title_l10n = base.title_l10n.lower()
|
||||||
|
for account in Account.query:
|
||||||
|
account.title_l10n = account.title_l10n.lower()
|
||||||
|
account.created_at \
|
||||||
|
= account.created_at - dt.timedelta(seconds=5)
|
||||||
|
account.updated_at = account.created_at
|
||||||
|
|
||||||
|
# Adds a custom account.
|
||||||
|
custom_title = "MBK Bank"
|
||||||
|
creator_pk: int = get_user_pk("editor")
|
||||||
|
new_account: Account = Account(
|
||||||
|
id=new_id(Account),
|
||||||
|
base_code="1112",
|
||||||
|
no="2",
|
||||||
|
title_l10n=custom_title,
|
||||||
|
is_need_offset=False,
|
||||||
|
created_by_id=creator_pk,
|
||||||
|
updated_by_id=creator_pk)
|
||||||
|
db.session.add(new_account)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
result: Result = runner.invoke(
|
||||||
|
args=["accounting-titleize", "-u", "editor"])
|
||||||
|
self.assertEqual(result.exit_code, 0,
|
||||||
|
result.output + str(result.exception))
|
||||||
|
for base in BaseAccount.query:
|
||||||
|
self.__test_title_case(base.title_l10n)
|
||||||
|
for account in Account.query:
|
||||||
|
if account.id != new_account.id:
|
||||||
|
self.__test_title_case(account.title_l10n)
|
||||||
|
self.assertNotEqual(account.created_at, account.updated_at)
|
||||||
|
else:
|
||||||
|
self.assertEqual(account.title_l10n, custom_title)
|
||||||
|
|
||||||
|
db.session.delete(new_account)
|
||||||
|
db.session.commit()
|
||||||
|
@ -17,15 +17,16 @@
|
|||||||
"""The test for the currency management.
|
"""The test for the currency management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime as dt
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import timedelta, date
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from accounting.utils.next_uri import encode_next
|
||||||
from test_site import db
|
from test_site import db
|
||||||
from testlib import NEXT_URI, create_test_app, get_client, set_locale, \
|
from testlib import NEXT_URI, create_test_app, get_client, get_csrf_token, \
|
||||||
add_journal_entry
|
set_locale, add_journal_entry
|
||||||
|
|
||||||
|
|
||||||
class CurrencyData:
|
class CurrencyData:
|
||||||
@ -64,28 +65,32 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
from accounting.models import Currency, CurrencyL10n
|
from accounting.models import Currency, CurrencyL10n
|
||||||
CurrencyL10n.query.delete()
|
CurrencyL10n.query.delete()
|
||||||
Currency.query.delete()
|
Currency.query.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
self.__client: httpx.Client = get_client(self.__app, "editor")
|
||||||
|
"""The user client."""
|
||||||
|
self.__csrf_token: str = get_csrf_token(self.__client)
|
||||||
|
"""The CSRF token."""
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": USD.code,
|
"code": USD.code,
|
||||||
"name": USD.name})
|
"name": USD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": EUR.code,
|
"code": EUR.code,
|
||||||
"name": EUR.name})
|
"name": EUR.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
|
||||||
|
|
||||||
@ -94,7 +99,8 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client: httpx.Client = get_client(self.__app, "nobody")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(PREFIX)
|
response = client.get(PREFIX)
|
||||||
@ -130,7 +136,8 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client: httpx.Client = get_client(self.__app, "viewer")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(PREFIX)
|
response = client.get(PREFIX)
|
||||||
@ -168,34 +175,34 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.get(PREFIX)
|
response = self.__client.get(PREFIX)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{USD.code}")
|
response = self.__client.get(f"{PREFIX}/{USD.code}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/create")
|
response = self.__client.get(f"{PREFIX}/create")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": TWD.code,
|
"code": TWD.code,
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}")
|
||||||
|
|
||||||
response = self.client.get(f"{PREFIX}/{USD.code}/edit")
|
response = self.__client.get(f"{PREFIX}/{USD.code}/edit")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/{USD.code}/update",
|
response = self.__client.post(f"{PREFIX}/{USD.code}/update",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": JPY.code,
|
"code": JPY.code,
|
||||||
"name": JPY.name})
|
"name": JPY.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}")
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/{EUR.code}/delete",
|
response = self.__client.post(f"{PREFIX}/{EUR.code}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], PREFIX)
|
self.assertEqual(response.headers["Location"], PREFIX)
|
||||||
|
|
||||||
@ -210,72 +217,73 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
detail_uri: str = f"{PREFIX}/{TWD.code}"
|
detail_uri: str = f"{PREFIX}/{TWD.code}"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Currency.query.all()},
|
self.assertEqual({x.code for x in Currency.query.all()},
|
||||||
{USD.code, EUR.code})
|
{USD.code, EUR.code})
|
||||||
|
|
||||||
# Missing CSRF token
|
# Missing CSRF token
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"code": TWD.code,
|
data={"code": TWD.code,
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
# CSRF token mismatch
|
# CSRF token mismatch
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": f"{self.csrf_token}-2",
|
data={"csrf_token":
|
||||||
"code": TWD.code,
|
f"{self.__csrf_token}-2",
|
||||||
"name": TWD.name})
|
"code": TWD.code,
|
||||||
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
# Empty code
|
# Empty code
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": " ",
|
"code": " ",
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Blocked code, with spaces to be stripped
|
# Blocked code, with spaces to be stripped
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": " create ",
|
"code": " create ",
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Bad code
|
# Bad code
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": " zzc ",
|
"code": " zzc ",
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Empty name
|
# Empty name
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": TWD.code,
|
"code": TWD.code,
|
||||||
"name": " "})
|
"name": " "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Success, with spaces to be stripped
|
# Success, with spaces to be stripped
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": f" {TWD.code} ",
|
"code": f" {TWD.code} ",
|
||||||
"name": f" {TWD.name} "})
|
"name": f" {TWD.name} "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
# Duplicated code
|
# Duplicated code
|
||||||
response = self.client.post(store_uri,
|
response = self.__client.post(store_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": TWD.code,
|
"code": TWD.code,
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Currency.query.all()},
|
self.assertEqual({x.code for x in Currency.query.all()},
|
||||||
{USD.code, EUR.code, TWD.code})
|
{USD.code, EUR.code, TWD.code})
|
||||||
|
|
||||||
@ -296,70 +304,70 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
# Success, with spaces to be stripped
|
# Success, with spaces to be stripped
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": f" {USD.code} ",
|
"code": f" {USD.code} ",
|
||||||
"name": f" {USD.name}-1 "})
|
"name": f" {USD.name}-1 "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency: Currency = db.session.get(Currency, USD.code)
|
currency: Currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.code, USD.code)
|
self.assertEqual(currency.code, USD.code)
|
||||||
self.assertEqual(currency.name_l10n, f"{USD.name}-1")
|
self.assertEqual(currency.name_l10n, f"{USD.name}-1")
|
||||||
|
|
||||||
# Empty code
|
# Empty code
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": " ",
|
"code": " ",
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Blocked code, with spaces to be stripped
|
# Blocked code, with spaces to be stripped
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": " create ",
|
"code": " create ",
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Bad code
|
# Bad code
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": "abc/def",
|
"code": "abc/def",
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Empty name
|
# Empty name
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": TWD.code,
|
"code": TWD.code,
|
||||||
"name": " "})
|
"name": " "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Duplicated code
|
# Duplicated code
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": EUR.code,
|
"code": EUR.code,
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Change code
|
# Change code
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": TWD.code,
|
"code": TWD.code,
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_c_uri)
|
self.assertEqual(response.headers["Location"], detail_c_uri)
|
||||||
|
|
||||||
response = self.client.get(detail_uri)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
response = self.client.get(detail_c_uri)
|
response = self.__client.get(detail_c_uri)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_update_not_modified(self) -> None:
|
def test_update_not_modified(self) -> None:
|
||||||
@ -373,29 +381,29 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
currency: Currency | None
|
currency: Currency | None
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": f" {USD.code} ",
|
"code": f" {USD.code} ",
|
||||||
"name": f" {USD.name} "})
|
"name": f" {USD.name} "})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertIsNotNone(currency)
|
self.assertIsNotNone(currency)
|
||||||
currency.created_at \
|
currency.created_at \
|
||||||
= currency.created_at - timedelta(seconds=5)
|
= currency.created_at - dt.timedelta(seconds=5)
|
||||||
currency.updated_at = currency.created_at
|
currency.updated_at = currency.created_at
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": USD.code,
|
"code": USD.code,
|
||||||
"name": TWD.name})
|
"name": TWD.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertIsNotNone(currency)
|
self.assertIsNotNone(currency)
|
||||||
self.assertLess(currency.created_at,
|
self.assertLess(currency.created_at,
|
||||||
@ -408,13 +416,14 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
from accounting.models import Currency
|
from accounting.models import Currency
|
||||||
editor_username, admin_username = "editor", "admin"
|
editor_username, admin_username = "editor", "admin"
|
||||||
client, csrf_token = get_client(self.app, admin_username)
|
client: httpx.Client = get_client(self.__app, admin_username)
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
detail_uri: str = f"{PREFIX}/{USD.code}"
|
detail_uri: str = f"{PREFIX}/{USD.code}"
|
||||||
update_uri: str = f"{PREFIX}/{USD.code}/update"
|
update_uri: str = f"{PREFIX}/{USD.code}/update"
|
||||||
currency: Currency
|
currency: Currency
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.created_by.username, editor_username)
|
self.assertEqual(currency.created_by.username, editor_username)
|
||||||
self.assertEqual(currency.updated_by.username, editor_username)
|
self.assertEqual(currency.updated_by.username, editor_username)
|
||||||
@ -426,7 +435,7 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.created_by.username, editor_username)
|
self.assertEqual(currency.created_by.username, editor_username)
|
||||||
self.assertEqual(currency.updated_by.username, admin_username)
|
self.assertEqual(currency.updated_by.username, admin_username)
|
||||||
@ -438,14 +447,14 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.__client.get(
|
||||||
f"/accounting/api/currencies/exists-code?q={USD.code}")
|
f"/accounting/api/currencies/exists-code?q={USD.code}")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertEqual(set(data.keys()), {"exists"})
|
self.assertEqual(set(data.keys()), {"exists"})
|
||||||
self.assertTrue(data["exists"])
|
self.assertTrue(data["exists"])
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.__client.get(
|
||||||
f"/accounting/api/currencies/exists-code?q={USD.code}-1")
|
f"/accounting/api/currencies/exists-code?q={USD.code}-1")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@ -463,51 +472,51 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
currency: Currency
|
currency: Currency
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.name_l10n, USD.name)
|
self.assertEqual(currency.name_l10n, USD.name)
|
||||||
self.assertEqual(currency.l10n, [])
|
self.assertEqual(currency.l10n, [])
|
||||||
|
|
||||||
set_locale(self.client, self.csrf_token, "zh_Hant")
|
set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": USD.code,
|
"code": USD.code,
|
||||||
"name": f"{USD.name}-zh_Hant"})
|
"name": f"{USD.name}-zh_Hant"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.name_l10n, USD.name)
|
self.assertEqual(currency.name_l10n, USD.name)
|
||||||
self.assertEqual({(x.locale, x.name) for x in currency.l10n},
|
self.assertEqual({(x.locale, x.name) for x in currency.l10n},
|
||||||
{("zh_Hant", f"{USD.name}-zh_Hant")})
|
{("zh_Hant", f"{USD.name}-zh_Hant")})
|
||||||
|
|
||||||
set_locale(self.client, self.csrf_token, "en")
|
set_locale(self.__app, self.__client, self.__csrf_token, "en")
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": USD.code,
|
"code": USD.code,
|
||||||
"name": f"{USD.name}-2"})
|
"name": f"{USD.name}-2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.name_l10n, f"{USD.name}-2")
|
self.assertEqual(currency.name_l10n, f"{USD.name}-2")
|
||||||
self.assertEqual({(x.locale, x.name) for x in currency.l10n},
|
self.assertEqual({(x.locale, x.name) for x in currency.l10n},
|
||||||
{("zh_Hant", f"{USD.name}-zh_Hant")})
|
{("zh_Hant", f"{USD.name}-zh_Hant")})
|
||||||
|
|
||||||
set_locale(self.client, self.csrf_token, "zh_Hant")
|
set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
|
||||||
|
|
||||||
response = self.client.post(update_uri,
|
response = self.__client.post(update_uri,
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": USD.code,
|
"code": USD.code,
|
||||||
"name": f"{USD.name}-zh_Hant-2"})
|
"name": f"{USD.name}-zh_Hant-2"})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
currency = db.session.get(Currency, USD.code)
|
currency = db.session.get(Currency, USD.code)
|
||||||
self.assertEqual(currency.name_l10n, f"{USD.name}-2")
|
self.assertEqual(currency.name_l10n, f"{USD.name}-2")
|
||||||
self.assertEqual({(x.locale, x.name) for x in currency.l10n},
|
self.assertEqual({(x.locale, x.name) for x in currency.l10n},
|
||||||
@ -521,54 +530,56 @@ class CurrencyTestCase(unittest.TestCase):
|
|||||||
from accounting.models import Currency
|
from accounting.models import Currency
|
||||||
detail_uri: str = f"{PREFIX}/{JPY.code}"
|
detail_uri: str = f"{PREFIX}/{JPY.code}"
|
||||||
delete_uri: str = f"{PREFIX}/{JPY.code}/delete"
|
delete_uri: str = f"{PREFIX}/{JPY.code}/delete"
|
||||||
|
with self.__app.app_context():
|
||||||
|
encoded_next_uri: str = encode_next(NEXT_URI)
|
||||||
list_uri: str = PREFIX
|
list_uri: str = PREFIX
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(f"{PREFIX}/store",
|
response = self.__client.post(f"{PREFIX}/store",
|
||||||
data={"csrf_token": self.csrf_token,
|
data={"csrf_token": self.__csrf_token,
|
||||||
"code": JPY.code,
|
"code": JPY.code,
|
||||||
"name": JPY.name})
|
"name": JPY.name})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], detail_uri)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
add_journal_entry(self.client,
|
add_journal_entry(self.__client,
|
||||||
form={"csrf_token": self.csrf_token,
|
form={"csrf_token": self.__csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": date.today().isoformat(),
|
"date": dt.date.today().isoformat(),
|
||||||
"currency-1-code": EUR.code,
|
"currency-1-code": EUR.code,
|
||||||
"currency-1-credit-1-account_code": "1111-001",
|
"currency-1-credit-1-account_code": "1111-001",
|
||||||
"currency-1-credit-1-amount": "20"})
|
"currency-1-credit-1-amount": "20"})
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Currency.query.all()},
|
self.assertEqual({x.code for x in Currency.query.all()},
|
||||||
{USD.code, EUR.code, JPY.code})
|
{USD.code, EUR.code, JPY.code})
|
||||||
|
|
||||||
# Cannot delete the default currency
|
# Cannot delete the default currency
|
||||||
response = self.client.post(f"{PREFIX}/{USD.code}/delete",
|
response = self.__client.post(f"{PREFIX}/{USD.code}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
|
||||||
|
|
||||||
# Cannot delete the account that is in use
|
# Cannot delete the account that is in use
|
||||||
response = self.client.post(f"{PREFIX}/{EUR.code}/delete",
|
response = self.__client.post(f"{PREFIX}/{EUR.code}/delete",
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
|
self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
response = self.client.get(detail_uri)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
response = self.client.post(delete_uri,
|
response = self.__client.post(delete_uri,
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], list_uri)
|
self.assertEqual(response.headers["Location"], list_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual({x.code for x in Currency.query.all()},
|
self.assertEqual({x.code for x in Currency.query.all()},
|
||||||
{USD.code, EUR.code})
|
{USD.code, EUR.code})
|
||||||
|
|
||||||
response = self.client.get(detail_uri)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
response = self.client.post(delete_uri,
|
response = self.__client.post(delete_uri,
|
||||||
data={"csrf_token": self.csrf_token})
|
data={"csrf_token": self.__csrf_token})
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
@ -17,13 +17,15 @@
|
|||||||
"""The test for the description editor.
|
"""The test for the description editor.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime as dt
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
|
import httpx
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from accounting.utils.next_uri import encode_next
|
||||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
|
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
|
||||||
add_journal_entry
|
get_csrf_token, add_journal_entry
|
||||||
|
|
||||||
|
|
||||||
class DescriptionEditorTestCase(unittest.TestCase):
|
class DescriptionEditorTestCase(unittest.TestCase):
|
||||||
@ -35,14 +37,20 @@ class DescriptionEditorTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
from accounting.models import JournalEntry, JournalEntryLineItem
|
from accounting.models import JournalEntry, JournalEntryLineItem
|
||||||
JournalEntry.query.delete()
|
JournalEntry.query.delete()
|
||||||
JournalEntryLineItem.query.delete()
|
JournalEntryLineItem.query.delete()
|
||||||
|
self.__encoded_next_uri: str = encode_next(NEXT_URI)
|
||||||
|
"""The encoded next URI."""
|
||||||
|
|
||||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
self.__client: httpx.Client = get_client(self.__app, "editor")
|
||||||
|
"""The user client."""
|
||||||
|
self.__csrf_token: str = get_csrf_token(self.__client)
|
||||||
|
"""The CSRF token."""
|
||||||
|
|
||||||
def test_description_editor(self) -> None:
|
def test_description_editor(self) -> None:
|
||||||
"""Test the description editor.
|
"""Test the description editor.
|
||||||
@ -51,9 +59,9 @@ class DescriptionEditorTestCase(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
from accounting.journal_entry.utils.description_editor import \
|
from accounting.journal_entry.utils.description_editor import \
|
||||||
DescriptionEditor
|
DescriptionEditor
|
||||||
for form in get_form_data(self.csrf_token):
|
for form in get_form_data(self.__csrf_token, self.__encoded_next_uri):
|
||||||
add_journal_entry(self.client, form)
|
add_journal_entry(self.__client, form)
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
editor: DescriptionEditor = DescriptionEditor()
|
editor: DescriptionEditor = DescriptionEditor()
|
||||||
|
|
||||||
# Debit-General
|
# Debit-General
|
||||||
@ -143,22 +151,24 @@ class DescriptionEditorTestCase(unittest.TestCase):
|
|||||||
Accounts.PREPAID)
|
Accounts.PREPAID)
|
||||||
|
|
||||||
|
|
||||||
def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
def get_form_data(csrf_token: str, encoded_next_uri: str) \
|
||||||
|
-> list[dict[str, str]]:
|
||||||
"""Returns the form data for multiple journal entry forms.
|
"""Returns the form data for multiple journal entry forms.
|
||||||
|
|
||||||
:param csrf_token: The CSRF token.
|
:param csrf_token: The CSRF token.
|
||||||
|
:param encoded_next_uri: The encoded next URI.
|
||||||
:return: A list of the form data.
|
:return: A list of the form data.
|
||||||
"""
|
"""
|
||||||
journal_entry_date: str = date.today().isoformat()
|
journal_entry_date: str = dt.date.today().isoformat()
|
||||||
return [{"csrf_token": csrf_token,
|
return [{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-credit-0-account_code": Accounts.SERVICE,
|
"currency-0-credit-0-account_code": Accounts.SERVICE,
|
||||||
"currency-0-credit-0-description": " Salary ",
|
"currency-0-credit-0-description": " Salary ",
|
||||||
"currency-0-credit-0-amount": "2500"},
|
"currency-0-credit-0-amount": "2500"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
@ -180,7 +190,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-credit-2-description": " Dinner—Hamburger ",
|
"currency-0-credit-2-description": " Dinner—Hamburger ",
|
||||||
"currency-0-credit-2-amount": "4.25"},
|
"currency-0-credit-2-amount": "4.25"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
@ -196,7 +206,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-credit-1-description": " Dinner—Steak ",
|
"currency-0-credit-1-description": " Dinner—Steak ",
|
||||||
"currency-0-credit-1-amount": "8.28"},
|
"currency-0-credit-1-amount": "8.28"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.MEAL,
|
"currency-0-debit-0-account_code": Accounts.MEAL,
|
||||||
@ -212,14 +222,14 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-credit-1-description": " Lunch—Noodles ",
|
"currency-0-credit-1-description": " Lunch—Noodles ",
|
||||||
"currency-0-credit-1-amount": "7.47"},
|
"currency-0-credit-1-amount": "7.47"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||||
"currency-0-debit-0-description": " Airplane—Lake City↔Hill Town",
|
"currency-0-debit-0-description": " Airplane—Lake City↔Hill Town",
|
||||||
"currency-0-debit-0-amount": "800"},
|
"currency-0-debit-0-amount": "800"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||||
@ -247,7 +257,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-credit-3-description": " Train—Red—Mall→Museum ",
|
"currency-0-credit-3-description": " Train—Red—Mall→Museum ",
|
||||||
"currency-0-credit-3-amount": "4.4"},
|
"currency-0-credit-3-amount": "4.4"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
"currency-0-debit-0-account_code": Accounts.TRAVEL,
|
||||||
@ -293,7 +303,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
|
|||||||
"currency-0-credit-6-description": " Bike—Theatre→Home ",
|
"currency-0-credit-6-description": " Bike—Theatre→Home ",
|
||||||
"currency-0-credit-6-amount": "5.5"},
|
"currency-0-credit-6-amount": "5.5"},
|
||||||
{"csrf_token": csrf_token,
|
{"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": encoded_next_uri,
|
||||||
"date": journal_entry_date,
|
"date": journal_entry_date,
|
||||||
"currency-0-code": "USD",
|
"currency-0-code": "USD",
|
||||||
"currency-0-debit-0-account_code": Accounts.PETTY_CASH,
|
"currency-0-debit-0-account_code": Accounts.PETTY_CASH,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -25,11 +25,12 @@ from decimal import Decimal
|
|||||||
import httpx
|
import httpx
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from accounting.utils.next_uri import encode_next
|
||||||
from test_site import db
|
from test_site import db
|
||||||
from test_site.lib import JournalEntryLineItemData, JournalEntryCurrencyData, \
|
from test_site.lib import JournalEntryLineItemData, JournalEntryCurrencyData, \
|
||||||
JournalEntryData, BaseTestData
|
JournalEntryData, BaseTestData
|
||||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
|
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
|
||||||
match_journal_entry_detail
|
get_csrf_token, match_journal_entry_detail
|
||||||
|
|
||||||
PREFIX: str = "/accounting/journal-entries"
|
PREFIX: str = "/accounting/journal-entries"
|
||||||
"""The URL prefix for the journal entry management."""
|
"""The URL prefix for the journal entry management."""
|
||||||
@ -44,16 +45,23 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
from accounting.models import JournalEntry, JournalEntryLineItem
|
from accounting.models import JournalEntry, JournalEntryLineItem
|
||||||
JournalEntry.query.delete()
|
JournalEntry.query.delete()
|
||||||
JournalEntryLineItem.query.delete()
|
JournalEntryLineItem.query.delete()
|
||||||
|
self.__encoded_next_uri: str = encode_next(NEXT_URI)
|
||||||
|
"""The encoded next URI."""
|
||||||
|
|
||||||
self.client, self.csrf_token = get_client(self.app, "editor")
|
self.__client: httpx.Client = get_client(self.__app, "editor")
|
||||||
self.data: OffsetTestData = OffsetTestData(self.app, "editor")
|
"""The user client."""
|
||||||
self.data.populate()
|
self.__csrf_token: str = get_csrf_token(self.__client)
|
||||||
|
"""The CSRF token."""
|
||||||
|
self.__data: OffsetTestData = OffsetTestData(self.__app, "editor")
|
||||||
|
"""The offset test data."""
|
||||||
|
self.__data.populate()
|
||||||
|
|
||||||
def test_add_receivable_offset(self) -> None:
|
def test_add_receivable_offset(self) -> None:
|
||||||
"""Tests to add the receivable offset.
|
"""Tests to add the receivable offset.
|
||||||
@ -61,118 +69,129 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account, JournalEntry
|
from accounting.models import Account, JournalEntry
|
||||||
create_uri: str = f"{PREFIX}/create/receipt?next=%2F_next"
|
create_uri: str = (f"{PREFIX}/create/receipt?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
store_uri: str = f"{PREFIX}/store/receipt"
|
store_uri: str = f"{PREFIX}/store/receipt"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
old_amount: Decimal
|
old_amount: Decimal
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
journal_entry_data: JournalEntryData = JournalEntryData(
|
journal_entry_data: JournalEntryData = JournalEntryData(
|
||||||
self.data.l_r_or3d.journal_entry.days, [JournalEntryCurrencyData(
|
self.__data.l_r_or3d.journal_entry.days, [JournalEntryCurrencyData(
|
||||||
"USD",
|
"USD",
|
||||||
[],
|
[],
|
||||||
[JournalEntryLineItemData(
|
[JournalEntryLineItemData(
|
||||||
Accounts.RECEIVABLE,
|
Accounts.RECEIVABLE,
|
||||||
self.data.l_r_or1d.description, "300",
|
self.__data.l_r_or1d.description, "300",
|
||||||
original_line_item=self.data.l_r_or1d),
|
original_line_item=self.__data.l_r_or1d),
|
||||||
JournalEntryLineItemData(
|
JournalEntryLineItemData(
|
||||||
Accounts.RECEIVABLE,
|
Accounts.RECEIVABLE,
|
||||||
self.data.l_r_or1d.description, "100",
|
self.__data.l_r_or1d.description, "100",
|
||||||
original_line_item=self.data.l_r_or1d),
|
original_line_item=self.__data.l_r_or1d),
|
||||||
JournalEntryLineItemData(
|
JournalEntryLineItemData(
|
||||||
Accounts.RECEIVABLE,
|
Accounts.RECEIVABLE,
|
||||||
self.data.l_r_or3d.description, "100",
|
self.__data.l_r_or3d.description, "100",
|
||||||
original_line_item=self.data.l_r_or3d)])])
|
original_line_item=self.__data.l_r_or3d)])])
|
||||||
|
|
||||||
# Non-existing original line item ID
|
# Non-existing original line item ID
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-original_line_item_id"] = "9999"
|
form["currency-1-credit-1-original_line_item_id"] = "9999"
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# The same debit or credit
|
# The same debit or credit
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-original_line_item_id"] \
|
form["currency-1-credit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_p_or1c.id)
|
= str(self.__data.l_p_or1c.id)
|
||||||
form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account
|
form["currency-1-credit-1-account_code"] = self.__data.l_p_or1c.account
|
||||||
form["currency-1-credit-1-amount"] = "100"
|
form["currency-1-credit-1-amount"] = "100"
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# The original line item does not need offset
|
# The original line item does not need offset
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||||
account.is_need_offset = False
|
account.is_need_offset = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
response = self.client.post(
|
response = self.__client.post(
|
||||||
store_uri,
|
store_uri,
|
||||||
data=journal_entry_data.new_form(self.csrf_token, NEXT_URI))
|
data=journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||||
account.is_need_offset = True
|
account.is_need_offset = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# The original line item is also an offset
|
# The original line item is also an offset
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-original_line_item_id"] \
|
form["currency-1-credit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_p_of1d.id)
|
= str(self.__data.l_p_of1d.id)
|
||||||
form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account
|
form["currency-1-credit-1-account_code"] = self.__data.l_p_of1d.account
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not the same currency
|
# Not the same currency
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-code"] = "EUR"
|
form["currency-1-code"] = "EUR"
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not the same account
|
# Not the same account
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - partially offset
|
# Not exceeding net balance - partially offset
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-amount"] \
|
form["currency-1-credit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[0].amount
|
= str(journal_entry_data.currencies[0].credit[0].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - unmatched
|
# Not exceeding net balance - unmatched
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-3-amount"] \
|
form["currency-1-credit-3-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[2].amount
|
= str(journal_entry_data.currencies[0].credit[2].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not before the original line items
|
# Not before the original line items
|
||||||
old_days = journal_entry_data.days
|
old_days = journal_entry_data.days
|
||||||
journal_entry_data.days = old_days + 1
|
journal_entry_data.days = old_days + 1
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
response = self.client.post(store_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
journal_entry_data.days = old_days
|
journal_entry_data.days = old_days
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
response = self.client.post(store_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
journal_entry_id: int \
|
journal_entry_id: int \
|
||||||
= match_journal_entry_detail(response.headers["Location"])
|
= match_journal_entry_detail(response.headers["Location"])
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
journal_entry = db.session.get(JournalEntry, journal_entry_id)
|
journal_entry = db.session.get(JournalEntry, journal_entry_id)
|
||||||
for offset in journal_entry.currencies[0].credit:
|
for offset in journal_entry.currencies[0].credit:
|
||||||
self.assertIsNotNone(offset.original_line_item_id)
|
self.assertIsNotNone(offset.original_line_item_id)
|
||||||
@ -183,113 +202,125 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account
|
from accounting.models import Account
|
||||||
journal_entry_data: JournalEntryData = self.data.j_r_of2
|
journal_entry_data: JournalEntryData = self.__data.j_r_of2
|
||||||
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
|
edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
journal_entry_data.days = self.data.j_r_or2.days
|
journal_entry_data.days = self.__data.j_r_or2.days
|
||||||
journal_entry_data.currencies[0].debit[0].amount = Decimal("600")
|
journal_entry_data.currencies[0].debit[0].amount = Decimal("600")
|
||||||
journal_entry_data.currencies[0].credit[0].amount = Decimal("600")
|
journal_entry_data.currencies[0].credit[0].amount = Decimal("600")
|
||||||
journal_entry_data.currencies[0].debit[2].amount = Decimal("600")
|
journal_entry_data.currencies[0].debit[2].amount = Decimal("600")
|
||||||
journal_entry_data.currencies[0].credit[2].amount = Decimal("600")
|
journal_entry_data.currencies[0].credit[2].amount = Decimal("600")
|
||||||
|
|
||||||
# Non-existing original line item ID
|
# Non-existing original line item ID
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-original_line_item_id"] = "9999"
|
form["currency-1-credit-1-original_line_item_id"] = "9999"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# The same debit or credit
|
# The same debit or credit
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-original_line_item_id"] \
|
form["currency-1-credit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_p_or1c.id)
|
= str(self.__data.l_p_or1c.id)
|
||||||
form["currency-1-credit-1-account_code"] = self.data.l_p_or1c.account
|
form["currency-1-credit-1-account_code"] = self.__data.l_p_or1c.account
|
||||||
form["currency-1-debit-1-amount"] = "100"
|
form["currency-1-debit-1-amount"] = "100"
|
||||||
form["currency-1-credit-1-amount"] = "100"
|
form["currency-1-credit-1-amount"] = "100"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# The original line item does not need offset
|
# The original line item does not need offset
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||||
account.is_need_offset = False
|
account.is_need_offset = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
response = self.client.post(
|
response = self.__client.post(
|
||||||
update_uri,
|
update_uri,
|
||||||
data=journal_entry_data.update_form(self.csrf_token, NEXT_URI))
|
data=journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.RECEIVABLE)
|
account = Account.find_by_code(Accounts.RECEIVABLE)
|
||||||
account.is_need_offset = True
|
account.is_need_offset = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# The original line item is also an offset
|
# The original line item is also an offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-original_line_item_id"] \
|
form["currency-1-credit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_p_of1d.id)
|
= str(self.__data.l_p_of1d.id)
|
||||||
form["currency-1-credit-1-account_code"] = self.data.l_p_of1d.account
|
form["currency-1-credit-1-account_code"] = self.__data.l_p_of1d.account
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not the same currency
|
# Not the same currency
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-code"] = "EUR"
|
form["currency-1-code"] = "EUR"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not the same account
|
# Not the same account
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
form["currency-1-credit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - partially offset
|
# Not exceeding net balance - partially offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-amount"] \
|
form["currency-1-debit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[0].amount
|
= str(journal_entry_data.currencies[0].debit[0].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
form["currency-1-credit-1-amount"] \
|
form["currency-1-credit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[0].amount
|
= str(journal_entry_data.currencies[0].credit[0].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - unmatched
|
# Not exceeding net balance - unmatched
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-3-amount"] \
|
form["currency-1-debit-3-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[2].amount
|
= str(journal_entry_data.currencies[0].debit[2].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
form["currency-1-credit-3-amount"] \
|
form["currency-1-credit-3-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[2].amount
|
= str(journal_entry_data.currencies[0].credit[2].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not before the original line items
|
# Not before the original line items
|
||||||
old_days: int = journal_entry_data.days
|
old_days: int = journal_entry_data.days
|
||||||
journal_entry_data.days = old_days + 1
|
journal_entry_data.days = old_days + 1
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
journal_entry_data.days = old_days
|
journal_entry_data.days = old_days
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{journal_entry_data.id}?next=%2F_next")
|
f"{PREFIX}/{journal_entry_data.id}?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
|
|
||||||
def test_edit_receivable_original_line_item(self) -> None:
|
def test_edit_receivable_original_line_item(self) -> None:
|
||||||
"""Tests to edit the receivable original line item.
|
"""Tests to edit the receivable original line item.
|
||||||
@ -297,87 +328,96 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
journal_entry_data: JournalEntryData = self.data.j_r_or1
|
journal_entry_data: JournalEntryData = self.__data.j_r_or1
|
||||||
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
|
edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
journal_entry_data.days = self.data.j_r_of1.days
|
journal_entry_data.days = self.__data.j_r_of1.days
|
||||||
journal_entry_data.currencies[0].debit[0].amount = Decimal("800")
|
journal_entry_data.currencies[0].debit[0].amount = Decimal("800")
|
||||||
journal_entry_data.currencies[0].credit[0].amount = Decimal("800")
|
journal_entry_data.currencies[0].credit[0].amount = Decimal("800")
|
||||||
journal_entry_data.currencies[0].debit[1].amount = Decimal("3.4")
|
journal_entry_data.currencies[0].debit[1].amount = Decimal("3.4")
|
||||||
journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4")
|
journal_entry_data.currencies[0].credit[1].amount = Decimal("3.4")
|
||||||
|
|
||||||
# Not the same currency
|
# Not the same currency
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-code"] = "EUR"
|
form["currency-1-code"] = "EUR"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not the same account
|
# Not the same account
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
form["currency-1-debit-1-account_code"] = Accounts.NOTES_RECEIVABLE
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not less than offset total - partially offset
|
# Not less than offset total - partially offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-amount"] \
|
form["currency-1-debit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[0].amount
|
= str(journal_entry_data.currencies[0].debit[0].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
form["currency-1-credit-1-amount"] \
|
form["currency-1-credit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[0].amount
|
= str(journal_entry_data.currencies[0].credit[0].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not less than offset total - fully offset
|
# Not less than offset total - fully offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-2-amount"] \
|
form["currency-1-debit-2-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[1].amount
|
= str(journal_entry_data.currencies[0].debit[1].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
form["currency-1-credit-2-amount"] \
|
form["currency-1-credit-2-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[1].amount
|
= str(journal_entry_data.currencies[0].credit[1].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not after the offset items
|
# Not after the offset items
|
||||||
old_days: int = journal_entry_data.days
|
old_days: int = journal_entry_data.days
|
||||||
journal_entry_data.days = old_days - 1
|
journal_entry_data.days = old_days - 1
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
journal_entry_data.days = old_days
|
journal_entry_data.days = old_days
|
||||||
|
|
||||||
# Not deleting matched original line items
|
# Not deleting matched original line items
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
del form["currency-1-debit-1-id"]
|
del form["currency-1-debit-1-id"]
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{journal_entry_data.id}?next=%2F_next")
|
f"{PREFIX}/{journal_entry_data.id}?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
|
|
||||||
# The original line item is always before the offset item, even when
|
# The original line item is always before the offset item, even when
|
||||||
# they happen in the same day.
|
# they happen in the same day.
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
journal_entry_or: JournalEntry | None = db.session.get(
|
journal_entry_or: JournalEntry | None = db.session.get(
|
||||||
JournalEntry, journal_entry_data.id)
|
JournalEntry, journal_entry_data.id)
|
||||||
self.assertIsNotNone(journal_entry_or)
|
self.assertIsNotNone(journal_entry_or)
|
||||||
journal_entry_of: JournalEntry | None = db.session.get(
|
journal_entry_of: JournalEntry | None = db.session.get(
|
||||||
JournalEntry, self.data.j_r_of1.id)
|
JournalEntry, self.__data.j_r_of1.id)
|
||||||
self.assertIsNotNone(journal_entry_of)
|
self.assertIsNotNone(journal_entry_of)
|
||||||
self.assertEqual(journal_entry_or.date, journal_entry_of.date)
|
self.assertEqual(journal_entry_or.date, journal_entry_of.date)
|
||||||
self.assertLess(journal_entry_or.no, journal_entry_of.no)
|
self.assertLess(journal_entry_or.no, journal_entry_of.no)
|
||||||
@ -388,117 +428,128 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account, JournalEntry
|
from accounting.models import Account, JournalEntry
|
||||||
create_uri: str = f"{PREFIX}/create/disbursement?next=%2F_next"
|
create_uri: str = (f"{PREFIX}/create/disbursement?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
store_uri: str = f"{PREFIX}/store/disbursement"
|
store_uri: str = f"{PREFIX}/store/disbursement"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
journal_entry_data: JournalEntryData = JournalEntryData(
|
journal_entry_data: JournalEntryData = JournalEntryData(
|
||||||
self.data.l_p_or3c.journal_entry.days, [JournalEntryCurrencyData(
|
self.__data.l_p_or3c.journal_entry.days, [JournalEntryCurrencyData(
|
||||||
"USD",
|
"USD",
|
||||||
[JournalEntryLineItemData(
|
[JournalEntryLineItemData(
|
||||||
Accounts.PAYABLE,
|
Accounts.PAYABLE,
|
||||||
self.data.l_p_or1c.description, "500",
|
self.__data.l_p_or1c.description, "500",
|
||||||
original_line_item=self.data.l_p_or1c),
|
original_line_item=self.__data.l_p_or1c),
|
||||||
JournalEntryLineItemData(
|
JournalEntryLineItemData(
|
||||||
Accounts.PAYABLE,
|
Accounts.PAYABLE,
|
||||||
self.data.l_p_or1c.description, "300",
|
self.__data.l_p_or1c.description, "300",
|
||||||
original_line_item=self.data.l_p_or1c),
|
original_line_item=self.__data.l_p_or1c),
|
||||||
JournalEntryLineItemData(
|
JournalEntryLineItemData(
|
||||||
Accounts.PAYABLE,
|
Accounts.PAYABLE,
|
||||||
self.data.l_p_or3c.description, "120",
|
self.__data.l_p_or3c.description, "120",
|
||||||
original_line_item=self.data.l_p_or3c)],
|
original_line_item=self.__data.l_p_or3c)],
|
||||||
[])])
|
[])])
|
||||||
|
|
||||||
# Non-existing original line item ID
|
# Non-existing original line item ID
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-original_line_item_id"] = "9999"
|
form["currency-1-debit-1-original_line_item_id"] = "9999"
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# The same debit or credit
|
# The same debit or credit
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-original_line_item_id"] \
|
form["currency-1-debit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_r_or1d.id)
|
= str(self.__data.l_r_or1d.id)
|
||||||
form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account
|
form["currency-1-debit-1-account_code"] = self.__data.l_r_or1d.account
|
||||||
form["currency-1-debit-1-amount"] = "100"
|
form["currency-1-debit-1-amount"] = "100"
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# The original line item does not need offset
|
# The original line item does not need offset
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.PAYABLE)
|
account = Account.find_by_code(Accounts.PAYABLE)
|
||||||
account.is_need_offset = False
|
account.is_need_offset = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
response = self.client.post(
|
response = self.__client.post(
|
||||||
store_uri,
|
store_uri,
|
||||||
data=journal_entry_data.new_form(self.csrf_token, NEXT_URI))
|
data=journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.PAYABLE)
|
account = Account.find_by_code(Accounts.PAYABLE)
|
||||||
account.is_need_offset = True
|
account.is_need_offset = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# The original line item is also an offset
|
# The original line item is also an offset
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-original_line_item_id"] \
|
form["currency-1-debit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_r_of1c.id)
|
= str(self.__data.l_r_of1c.id)
|
||||||
form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account
|
form["currency-1-debit-1-account_code"] = self.__data.l_r_of1c.account
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not the same currency
|
# Not the same currency
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-code"] = "EUR"
|
form["currency-1-code"] = "EUR"
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not the same account
|
# Not the same account
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
|
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - partially offset
|
# Not exceeding net balance - partially offset
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-amount"] \
|
form["currency-1-debit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[0].amount
|
= str(journal_entry_data.currencies[0].debit[0].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - unmatched
|
# Not exceeding net balance - unmatched
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-3-amount"] \
|
form["currency-1-debit-3-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[2].amount
|
= str(journal_entry_data.currencies[0].debit[2].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(store_uri, data=form)
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
|
|
||||||
# Not before the original line items
|
# Not before the original line items
|
||||||
old_days: int = journal_entry_data.days
|
old_days: int = journal_entry_data.days
|
||||||
journal_entry_data.days = old_days + 1
|
journal_entry_data.days = old_days + 1
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
response = self.client.post(store_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], create_uri)
|
self.assertEqual(response.headers["Location"], create_uri)
|
||||||
journal_entry_data.days = old_days
|
journal_entry_data.days = old_days
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
form = journal_entry_data.new_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.new_form(self.__csrf_token,
|
||||||
response = self.client.post(store_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(store_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
journal_entry_id: int \
|
journal_entry_id: int \
|
||||||
= match_journal_entry_detail(response.headers["Location"])
|
= match_journal_entry_detail(response.headers["Location"])
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
journal_entry = db.session.get(JournalEntry, journal_entry_id)
|
journal_entry = db.session.get(JournalEntry, journal_entry_id)
|
||||||
for offset in journal_entry.currencies[0].debit:
|
for offset in journal_entry.currencies[0].debit:
|
||||||
self.assertIsNotNone(offset.original_line_item_id)
|
self.assertIsNotNone(offset.original_line_item_id)
|
||||||
@ -509,114 +560,125 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account, JournalEntry
|
from accounting.models import Account, JournalEntry
|
||||||
journal_entry_data: JournalEntryData = self.data.j_p_of2
|
journal_entry_data: JournalEntryData = self.__data.j_p_of2
|
||||||
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
|
edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
journal_entry_data.days = self.data.j_p_or2.days
|
journal_entry_data.days = self.__data.j_p_or2.days
|
||||||
journal_entry_data.currencies[0].debit[0].amount = Decimal("1100")
|
journal_entry_data.currencies[0].debit[0].amount = Decimal("1100")
|
||||||
journal_entry_data.currencies[0].credit[0].amount = Decimal("1100")
|
journal_entry_data.currencies[0].credit[0].amount = Decimal("1100")
|
||||||
journal_entry_data.currencies[0].debit[2].amount = Decimal("900")
|
journal_entry_data.currencies[0].debit[2].amount = Decimal("900")
|
||||||
journal_entry_data.currencies[0].credit[2].amount = Decimal("900")
|
journal_entry_data.currencies[0].credit[2].amount = Decimal("900")
|
||||||
|
|
||||||
# Non-existing original line item ID
|
# Non-existing original line item ID
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-original_line_item_id"] = "9999"
|
form["currency-1-debit-1-original_line_item_id"] = "9999"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# The same debit or credit
|
# The same debit or credit
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-original_line_item_id"] \
|
form["currency-1-debit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_r_or1d.id)
|
= str(self.__data.l_r_or1d.id)
|
||||||
form["currency-1-debit-1-account_code"] = self.data.l_r_or1d.account
|
form["currency-1-debit-1-account_code"] = self.__data.l_r_or1d.account
|
||||||
form["currency-1-debit-1-amount"] = "100"
|
form["currency-1-debit-1-amount"] = "100"
|
||||||
form["currency-1-credit-1-amount"] = "100"
|
form["currency-1-credit-1-amount"] = "100"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# The original line item does not need offset
|
# The original line item does not need offset
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.PAYABLE)
|
account = Account.find_by_code(Accounts.PAYABLE)
|
||||||
account.is_need_offset = False
|
account.is_need_offset = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
response = self.client.post(
|
response = self.__client.post(
|
||||||
update_uri,
|
update_uri,
|
||||||
data=journal_entry_data.update_form(self.csrf_token, NEXT_URI))
|
data=journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
account = Account.find_by_code(Accounts.PAYABLE)
|
account = Account.find_by_code(Accounts.PAYABLE)
|
||||||
account.is_need_offset = True
|
account.is_need_offset = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# The original line item is also an offset
|
# The original line item is also an offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-original_line_item_id"] \
|
form["currency-1-debit-1-original_line_item_id"] \
|
||||||
= str(self.data.l_r_of1c.id)
|
= str(self.__data.l_r_of1c.id)
|
||||||
form["currency-1-debit-1-account_code"] = self.data.l_r_of1c.account
|
form["currency-1-debit-1-account_code"] = self.__data.l_r_of1c.account
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not the same currency
|
# Not the same currency
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-code"] = "EUR"
|
form["currency-1-code"] = "EUR"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not the same account
|
# Not the same account
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
|
form["currency-1-debit-1-account_code"] = Accounts.NOTES_PAYABLE
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - partially offset
|
# Not exceeding net balance - partially offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-amount"] \
|
form["currency-1-debit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[0].amount
|
= str(journal_entry_data.currencies[0].debit[0].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
form["currency-1-credit-1-amount"] \
|
form["currency-1-credit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[0].amount
|
= str(journal_entry_data.currencies[0].credit[0].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not exceeding net balance - unmatched
|
# Not exceeding net balance - unmatched
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-3-amount"] \
|
form["currency-1-debit-3-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[2].amount
|
= str(journal_entry_data.currencies[0].debit[2].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
form["currency-1-credit-3-amount"] \
|
form["currency-1-credit-3-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[2].amount
|
= str(journal_entry_data.currencies[0].credit[2].amount
|
||||||
+ Decimal("0.01"))
|
+ Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not before the original line items
|
# Not before the original line items
|
||||||
old_days: int = journal_entry_data.days
|
old_days: int = journal_entry_data.days
|
||||||
journal_entry_data.days = old_days + 1
|
journal_entry_data.days = old_days + 1
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
journal_entry_data.days = old_days
|
journal_entry_data.days = old_days
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
journal_entry_id: int \
|
journal_entry_id: int \
|
||||||
= match_journal_entry_detail(response.headers["Location"])
|
= match_journal_entry_detail(response.headers["Location"])
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
journal_entry = db.session.get(JournalEntry, journal_entry_id)
|
journal_entry = db.session.get(JournalEntry, journal_entry_id)
|
||||||
for offset in journal_entry.currencies[0].debit:
|
for offset in journal_entry.currencies[0].debit:
|
||||||
self.assertIsNotNone(offset.original_line_item_id)
|
self.assertIsNotNone(offset.original_line_item_id)
|
||||||
@ -627,87 +689,96 @@ class OffsetTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import JournalEntry
|
from accounting.models import JournalEntry
|
||||||
journal_entry_data: JournalEntryData = self.data.j_p_or1
|
journal_entry_data: JournalEntryData = self.__data.j_p_or1
|
||||||
edit_uri: str = f"{PREFIX}/{journal_entry_data.id}/edit?next=%2F_next"
|
edit_uri: str = (f"{PREFIX}/{journal_entry_data.id}/edit?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
update_uri: str = f"{PREFIX}/{journal_entry_data.id}/update"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
journal_entry_data.days = self.data.j_p_of1.days
|
journal_entry_data.days = self.__data.j_p_of1.days
|
||||||
journal_entry_data.currencies[0].debit[0].amount = Decimal("1200")
|
journal_entry_data.currencies[0].debit[0].amount = Decimal("1200")
|
||||||
journal_entry_data.currencies[0].credit[0].amount = Decimal("1200")
|
journal_entry_data.currencies[0].credit[0].amount = Decimal("1200")
|
||||||
journal_entry_data.currencies[0].debit[1].amount = Decimal("0.9")
|
journal_entry_data.currencies[0].debit[1].amount = Decimal("0.9")
|
||||||
journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9")
|
journal_entry_data.currencies[0].credit[1].amount = Decimal("0.9")
|
||||||
|
|
||||||
# Not the same currency
|
# Not the same currency
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-code"] = "EUR"
|
form["currency-1-code"] = "EUR"
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not the same account
|
# Not the same account
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
|
form["currency-1-credit-1-account_code"] = Accounts.NOTES_PAYABLE
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not less than offset total - partially offset
|
# Not less than offset total - partially offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-1-amount"] \
|
form["currency-1-debit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[0].amount
|
= str(journal_entry_data.currencies[0].debit[0].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
form["currency-1-credit-1-amount"] \
|
form["currency-1-credit-1-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[0].amount
|
= str(journal_entry_data.currencies[0].credit[0].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not less than offset total - fully offset
|
# Not less than offset total - fully offset
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
form["currency-1-debit-2-amount"] \
|
form["currency-1-debit-2-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].debit[1].amount
|
= str(journal_entry_data.currencies[0].debit[1].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
form["currency-1-credit-2-amount"] \
|
form["currency-1-credit-2-amount"] \
|
||||||
= str(journal_entry_data.currencies[0].credit[1].amount
|
= str(journal_entry_data.currencies[0].credit[1].amount
|
||||||
- Decimal("0.01"))
|
- Decimal("0.01"))
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not after the offset items
|
# Not after the offset items
|
||||||
old_days: int = journal_entry_data.days
|
old_days: int = journal_entry_data.days
|
||||||
journal_entry_data.days = old_days - 1
|
journal_entry_data.days = old_days - 1
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
journal_entry_data.days = old_days
|
journal_entry_data.days = old_days
|
||||||
|
|
||||||
# Not deleting matched original line items
|
# Not deleting matched original line items
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
|
self.__encoded_next_uri)
|
||||||
del form["currency-1-credit-1-id"]
|
del form["currency-1-credit-1-id"]
|
||||||
response = self.client.post(update_uri, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], edit_uri)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Success
|
# Success
|
||||||
form = journal_entry_data.update_form(self.csrf_token, NEXT_URI)
|
form = journal_entry_data.update_form(self.__csrf_token,
|
||||||
response = self.client.post(update_uri, data=form)
|
self.__encoded_next_uri)
|
||||||
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"],
|
self.assertEqual(response.headers["Location"],
|
||||||
f"{PREFIX}/{journal_entry_data.id}?next=%2F_next")
|
f"{PREFIX}/{journal_entry_data.id}?"
|
||||||
|
f"next={self.__encoded_next_uri}")
|
||||||
|
|
||||||
# The original line item is always before the offset item, even when
|
# The original line item is always before the offset item, even when
|
||||||
# they happen in the same day
|
# they happen in the same day
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
journal_entry_or: JournalEntry | None = db.session.get(
|
journal_entry_or: JournalEntry | None = db.session.get(
|
||||||
JournalEntry, journal_entry_data.id)
|
JournalEntry, journal_entry_data.id)
|
||||||
self.assertIsNotNone(journal_entry_or)
|
self.assertIsNotNone(journal_entry_or)
|
||||||
journal_entry_of: JournalEntry | None = db.session.get(
|
journal_entry_of: JournalEntry | None = db.session.get(
|
||||||
JournalEntry, self.data.j_p_of1.id)
|
JournalEntry, self.__data.j_p_of1.id)
|
||||||
self.assertIsNotNone(journal_entry_of)
|
self.assertIsNotNone(journal_entry_of)
|
||||||
self.assertEqual(journal_entry_or.date, journal_entry_of.date)
|
self.assertEqual(journal_entry_or.date, journal_entry_of.date)
|
||||||
self.assertLess(journal_entry_or.no, journal_entry_of.no)
|
self.assertLess(journal_entry_or.no, journal_entry_of.no)
|
||||||
@ -742,18 +813,22 @@ class OffsetTestData(BaseTestData):
|
|||||||
50, [JournalEntryCurrencyData(
|
50, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_r_or1d, self.l_r_or4d],
|
"USD", [self.l_r_or1d, self.l_r_or4d],
|
||||||
[self.l_r_or1c, self.l_r_or4c])])
|
[self.l_r_or1c, self.l_r_or4c])])
|
||||||
|
"""The receivable original journal entry #1."""
|
||||||
self.j_r_or2: JournalEntryData = JournalEntryData(
|
self.j_r_or2: JournalEntryData = JournalEntryData(
|
||||||
30, [JournalEntryCurrencyData(
|
30, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_r_or2d, self.l_r_or3d],
|
"USD", [self.l_r_or2d, self.l_r_or3d],
|
||||||
[self.l_r_or2c, self.l_r_or3c])])
|
[self.l_r_or2c, self.l_r_or3c])])
|
||||||
|
"""The receivable original journal entry #2."""
|
||||||
self.j_p_or1: JournalEntryData = JournalEntryData(
|
self.j_p_or1: JournalEntryData = JournalEntryData(
|
||||||
40, [JournalEntryCurrencyData(
|
40, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_p_or1d, self.l_p_or4d],
|
"USD", [self.l_p_or1d, self.l_p_or4d],
|
||||||
[self.l_p_or1c, self.l_p_or4c])])
|
[self.l_p_or1c, self.l_p_or4c])])
|
||||||
|
"""The payable original journal entry #1."""
|
||||||
self.j_p_or2: JournalEntryData = JournalEntryData(
|
self.j_p_or2: JournalEntryData = JournalEntryData(
|
||||||
20, [JournalEntryCurrencyData(
|
20, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_p_or2d, self.l_p_or3d],
|
"USD", [self.l_p_or2d, self.l_p_or3d],
|
||||||
[self.l_p_or2c, self.l_p_or3c])])
|
[self.l_p_or2c, self.l_p_or3c])])
|
||||||
|
"""The payable original journal entry #2."""
|
||||||
|
|
||||||
self._add_journal_entry(self.j_r_or1)
|
self._add_journal_entry(self.j_r_or1)
|
||||||
self._add_journal_entry(self.j_r_or2)
|
self._add_journal_entry(self.j_r_or2)
|
||||||
@ -798,23 +873,29 @@ class OffsetTestData(BaseTestData):
|
|||||||
self.j_r_of1: JournalEntryData = JournalEntryData(
|
self.j_r_of1: JournalEntryData = JournalEntryData(
|
||||||
25, [JournalEntryCurrencyData(
|
25, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_r_of1d], [self.l_r_of1c])])
|
"USD", [self.l_r_of1d], [self.l_r_of1c])])
|
||||||
|
"""The offset journal entry to the receivable #1."""
|
||||||
self.j_r_of2: JournalEntryData = JournalEntryData(
|
self.j_r_of2: JournalEntryData = JournalEntryData(
|
||||||
20, [JournalEntryCurrencyData(
|
20, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d],
|
"USD", [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d],
|
||||||
[self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])])
|
[self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])])
|
||||||
|
"""The offset journal entry to the receivable #2."""
|
||||||
self.j_r_of3: JournalEntryData = JournalEntryData(
|
self.j_r_of3: JournalEntryData = JournalEntryData(
|
||||||
15, [JournalEntryCurrencyData(
|
15, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_r_of5d], [self.l_r_of5c])])
|
"USD", [self.l_r_of5d], [self.l_r_of5c])])
|
||||||
|
"""The offset journal entry to the receivable #3."""
|
||||||
self.j_p_of1: JournalEntryData = JournalEntryData(
|
self.j_p_of1: JournalEntryData = JournalEntryData(
|
||||||
15, [JournalEntryCurrencyData(
|
15, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_p_of1d], [self.l_p_of1c])])
|
"USD", [self.l_p_of1d], [self.l_p_of1c])])
|
||||||
|
"""The offset journal entry to the payable #1."""
|
||||||
self.j_p_of2: JournalEntryData = JournalEntryData(
|
self.j_p_of2: JournalEntryData = JournalEntryData(
|
||||||
10, [JournalEntryCurrencyData(
|
10, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d],
|
"USD", [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d],
|
||||||
[self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])])
|
[self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])])
|
||||||
|
"""The offset journal entry to the payable #2."""
|
||||||
self.j_p_of3: JournalEntryData = JournalEntryData(
|
self.j_p_of3: JournalEntryData = JournalEntryData(
|
||||||
5, [JournalEntryCurrencyData(
|
5, [JournalEntryCurrencyData(
|
||||||
"USD", [self.l_p_of5d], [self.l_p_of5c])])
|
"USD", [self.l_p_of5d], [self.l_p_of5c])])
|
||||||
|
"""The offset journal entry to the payable #3."""
|
||||||
|
|
||||||
self._add_journal_entry(self.j_r_of1)
|
self._add_journal_entry(self.j_r_of1)
|
||||||
self._add_journal_entry(self.j_r_of2)
|
self._add_journal_entry(self.j_r_of2)
|
||||||
|
@ -17,23 +17,19 @@
|
|||||||
"""The test for the options.
|
"""The test for the options.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime as dt
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from accounting.utils.next_uri import encode_next
|
||||||
from test_site import db
|
from test_site import db
|
||||||
from testlib import NEXT_URI, Accounts, create_test_app, get_client
|
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
|
||||||
|
get_csrf_token
|
||||||
|
|
||||||
PREFIX: str = "/accounting/options"
|
PREFIX: str = "/accounting/options"
|
||||||
"""The URL prefix for the option management."""
|
"""The URL prefix for the option management."""
|
||||||
DETAIL_URI: str = f"{PREFIX}?next=%2F_next"
|
|
||||||
"""THE URI for the option detail."""
|
|
||||||
EDIT_URI: str = f"{PREFIX}/edit?next=%2F_next"
|
|
||||||
"""THE URI for the form to edit the options."""
|
|
||||||
UPDATE_URI: str = f"{PREFIX}/update"
|
|
||||||
"""THE URI to update the options."""
|
|
||||||
|
|
||||||
|
|
||||||
class OptionTestCase(unittest.TestCase):
|
class OptionTestCase(unittest.TestCase):
|
||||||
@ -45,29 +41,39 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
self.app: Flask = create_test_app()
|
self.__app: Flask = create_test_app()
|
||||||
|
"""The Flask application."""
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
from accounting.models import Option
|
from accounting.models import Option
|
||||||
Option.query.delete()
|
Option.query.delete()
|
||||||
|
self.__encoded_next_uri: str = encode_next(NEXT_URI)
|
||||||
|
"""The encoded next URI."""
|
||||||
|
|
||||||
self.client, self.csrf_token = get_client(self.app, "admin")
|
self.__client: httpx.Client = get_client(self.__app, "admin")
|
||||||
|
"""The user client."""
|
||||||
|
self.__csrf_token: str = get_csrf_token(self.__client)
|
||||||
|
"""The CSRF token."""
|
||||||
|
|
||||||
def test_nobody(self) -> None:
|
def test_nobody(self) -> None:
|
||||||
"""Test the permission as nobody.
|
"""Test the permission as nobody.
|
||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "nobody")
|
client: httpx.Client = get_client(self.__app, "nobody")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(DETAIL_URI)
|
response = client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.get(EDIT_URI)
|
response = client.get(edit_uri)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
|
response = client.post(update_uri, data=self.__get_form(csrf_token))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_viewer(self) -> None:
|
def test_viewer(self) -> None:
|
||||||
@ -75,16 +81,20 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "viewer")
|
client: httpx.Client = get_client(self.__app, "viewer")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(DETAIL_URI)
|
response = client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.get(EDIT_URI)
|
response = client.get(edit_uri)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
|
response = client.post(update_uri, data=self.__get_form(csrf_token))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_editor(self) -> None:
|
def test_editor(self) -> None:
|
||||||
@ -92,16 +102,20 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
client, csrf_token = get_client(self.app, "editor")
|
client: httpx.Client = get_client(self.__app, "editor")
|
||||||
|
csrf_token: str = get_csrf_token(client)
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = client.get(DETAIL_URI)
|
response = client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.get(EDIT_URI)
|
response = client.get(edit_uri)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
|
response = client.post(update_uri, data=self.__get_form(csrf_token))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_admin(self) -> None:
|
def test_admin(self) -> None:
|
||||||
@ -109,17 +123,20 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.get(DETAIL_URI)
|
response = self.__client.get(detail_uri)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(EDIT_URI)
|
response = self.__client.get(edit_uri)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
response = self.__client.post(update_uri, data=self.__get_form())
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
def test_set(self) -> None:
|
def test_set(self) -> None:
|
||||||
"""Test to set the options.
|
"""Test to set the options.
|
||||||
@ -127,59 +144,62 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.utils.options import options
|
from accounting.utils.options import options
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
# Empty currency code
|
# Empty currency code
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form["default_currency_code"] = " "
|
form["default_currency_code"] = " "
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Non-existing currency code
|
# Non-existing currency code
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form["default_currency_code"] = "ZZZ"
|
form["default_currency_code"] = "ZZZ"
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Empty current account
|
# Empty current account
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form["default_ie_account_code"] = " "
|
form["default_ie_account_code"] = " "
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Non-existing current account
|
# Non-existing current account
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form["default_ie_account_code"] = "9999-999"
|
form["default_ie_account_code"] = "9999-999"
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Not a current account
|
# Not a current account
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form["default_ie_account_code"] = Accounts.MEAL
|
form["default_ie_account_code"] = Accounts.MEAL
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item name empty
|
# Recurring item name empty
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
key = [x for x in form if x.endswith("-name")][0]
|
key = [x for x in form if x.endswith("-name")][0]
|
||||||
form[key] = " "
|
form[key] = " "
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item account empty
|
# Recurring item account empty
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
key = [x for x in form if x.endswith("-account_code")][0]
|
key = [x for x in form if x.endswith("-account_code")][0]
|
||||||
form[key] = " "
|
form[key] = " "
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item non-expense account
|
# Recurring item non-expense account
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
@ -187,9 +207,9 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
if x.startswith("recurring-expense-")
|
if x.startswith("recurring-expense-")
|
||||||
and x.endswith("-account_code")][0]
|
and x.endswith("-account_code")][0]
|
||||||
form[key] = Accounts.SERVICE
|
form[key] = Accounts.SERVICE
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item non-income account
|
# Recurring item non-income account
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
@ -197,9 +217,9 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
if x.startswith("recurring-income-")
|
if x.startswith("recurring-income-")
|
||||||
and x.endswith("-account_code")][0]
|
and x.endswith("-account_code")][0]
|
||||||
form[key] = Accounts.UTILITIES
|
form[key] = Accounts.UTILITIES
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item payable expense
|
# Recurring item payable expense
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
@ -207,9 +227,9 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
if x.startswith("recurring-expense-")
|
if x.startswith("recurring-expense-")
|
||||||
and x.endswith("-account_code")][0]
|
and x.endswith("-account_code")][0]
|
||||||
form[key] = Accounts.PAYABLE
|
form[key] = Accounts.PAYABLE
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item receivable income
|
# Recurring item receivable income
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
@ -217,30 +237,30 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
if x.startswith("recurring-income-")
|
if x.startswith("recurring-income-")
|
||||||
and x.endswith("-account_code")][0]
|
and x.endswith("-account_code")][0]
|
||||||
form[key] = Accounts.RECEIVABLE
|
form[key] = Accounts.RECEIVABLE
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Recurring item description template empty
|
# Recurring item description template empty
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
key = [x for x in form if x.endswith("-description_template")][0]
|
key = [x for x in form if x.endswith("-description_template")][0]
|
||||||
form[key] = " "
|
form[key] = " "
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], EDIT_URI)
|
self.assertEqual(response.headers["Location"], edit_uri)
|
||||||
|
|
||||||
# Success, with malformed order
|
# Success, with malformed order
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual(options.default_currency_code, "USD")
|
self.assertEqual(options.default_currency_code, "USD")
|
||||||
self.assertEqual(options.default_ie_account_code, "1111-001")
|
self.assertEqual(options.default_ie_account_code, "1111-001")
|
||||||
self.assertEqual(len(options.recurring.expenses), 0)
|
self.assertEqual(len(options.recurring.expenses), 0)
|
||||||
self.assertEqual(len(options.recurring.incomes), 0)
|
self.assertEqual(len(options.recurring.incomes), 0)
|
||||||
|
|
||||||
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
response = self.__client.post(update_uri, data=self.__get_form())
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual(options.default_currency_code, "EUR")
|
self.assertEqual(options.default_currency_code, "EUR")
|
||||||
self.assertEqual(options.default_ie_account_code, "0000-000")
|
self.assertEqual(options.default_ie_account_code, "0000-000")
|
||||||
self.assertEqual(len(options.recurring.expenses), 4)
|
self.assertEqual(len(options.recurring.expenses), 4)
|
||||||
@ -261,11 +281,11 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
# Success, with no recurring data
|
# Success, with no recurring data
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form = {x: form[x] for x in form if not x.startswith("recurring-")}
|
form = {x: form[x] for x in form if not x.startswith("recurring-")}
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
self.assertEqual(len(options.recurring.expenses), 0)
|
self.assertEqual(len(options.recurring.expenses), 0)
|
||||||
self.assertEqual(len(options.recurring.incomes), 0)
|
self.assertEqual(len(options.recurring.incomes), 0)
|
||||||
|
|
||||||
@ -275,18 +295,21 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
from accounting.models import Option
|
from accounting.models import Option
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
form: dict[str, str]
|
form: dict[str, str]
|
||||||
option: Option | None
|
option: Option | None
|
||||||
resource: httpx.Response
|
resource: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
response = self.__client.post(update_uri, data=self.__get_form())
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
option = db.session.get(Option, "recurring")
|
option = db.session.get(Option, "recurring")
|
||||||
self.assertIsNotNone(option)
|
self.assertIsNotNone(option)
|
||||||
timestamp: datetime = option.created_at - timedelta(seconds=5)
|
timestamp: dt.datetime \
|
||||||
|
= option.created_at - dt.timedelta(seconds=5)
|
||||||
option.created_at = timestamp
|
option.created_at = timestamp
|
||||||
option.updated_at = timestamp
|
option.updated_at = timestamp
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -294,11 +317,11 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
# The recurring setting was not modified
|
# The recurring setting was not modified
|
||||||
form = self.__get_form()
|
form = self.__get_form()
|
||||||
form["default_currency_code"] = "JPY"
|
form["default_currency_code"] = "JPY"
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
option = db.session.get(Option, "recurring")
|
option = db.session.get(Option, "recurring")
|
||||||
self.assertIsNotNone(option)
|
self.assertIsNotNone(option)
|
||||||
self.assertEqual(option.created_at, timestamp)
|
self.assertEqual(option.created_at, timestamp)
|
||||||
@ -310,11 +333,11 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
if x.startswith("recurring-expense-")
|
if x.startswith("recurring-expense-")
|
||||||
and x.endswith("-account_code")][0]
|
and x.endswith("-account_code")][0]
|
||||||
form[key] = Accounts.MEAL
|
form[key] = Accounts.MEAL
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
option = db.session.get(Option, "recurring")
|
option = db.session.get(Option, "recurring")
|
||||||
self.assertIsNotNone(option)
|
self.assertIsNotNone(option)
|
||||||
self.assertLess(option.created_at, option.updated_at)
|
self.assertLess(option.created_at, option.updated_at)
|
||||||
@ -327,14 +350,16 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
from accounting.models import Option
|
from accounting.models import Option
|
||||||
from accounting.utils.user import get_user_pk
|
from accounting.utils.user import get_user_pk
|
||||||
admin_username, editor_username = "admin", "editor"
|
admin_username, editor_username = "admin", "editor"
|
||||||
|
detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
|
||||||
|
update_uri: str = f"{PREFIX}/update"
|
||||||
option: Option | None
|
option: Option | None
|
||||||
response: httpx.Response
|
response: httpx.Response
|
||||||
|
|
||||||
response = self.client.post(UPDATE_URI, data=self.__get_form())
|
response = self.__client.post(update_uri, data=self.__get_form())
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
editor_pk: int = get_user_pk(editor_username)
|
editor_pk: int = get_user_pk(editor_username)
|
||||||
option = db.session.get(Option, "recurring")
|
option = db.session.get(Option, "recurring")
|
||||||
self.assertIsNotNone(option)
|
self.assertIsNotNone(option)
|
||||||
@ -347,11 +372,11 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
if x.startswith("recurring-expense-")
|
if x.startswith("recurring-expense-")
|
||||||
and x.endswith("-account_code")][0]
|
and x.endswith("-account_code")][0]
|
||||||
form[key] = Accounts.MEAL
|
form[key] = Accounts.MEAL
|
||||||
response = self.client.post(UPDATE_URI, data=form)
|
response = self.__client.post(update_uri, data=form)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.headers["Location"], DETAIL_URI)
|
self.assertEqual(response.headers["Location"], detail_uri)
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.__app.app_context():
|
||||||
option = db.session.get(Option, "recurring")
|
option = db.session.get(Option, "recurring")
|
||||||
self.assertIsNotNone(option)
|
self.assertIsNotNone(option)
|
||||||
self.assertEqual(option.created_by.username, editor_username)
|
self.assertEqual(option.created_by.username, editor_username)
|
||||||
@ -364,9 +389,9 @@ class OptionTestCase(unittest.TestCase):
|
|||||||
:return: The option form.
|
:return: The option form.
|
||||||
"""
|
"""
|
||||||
if csrf_token is None:
|
if csrf_token is None:
|
||||||
csrf_token = self.csrf_token
|
csrf_token = self.__csrf_token
|
||||||
return {"csrf_token": csrf_token,
|
return {"csrf_token": csrf_token,
|
||||||
"next": NEXT_URI,
|
"next": self.__encoded_next_uri,
|
||||||
"default_currency_code": "EUR",
|
"default_currency_code": "EUR",
|
||||||
"default_ie_account_code": "0000-000",
|
"default_ie_account_code": "0000-000",
|
||||||
"recurring-expense-1-name": "Water bill",
|
"recurring-expense-1-name": "Water bill",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user