Compare commits
44 Commits
v1.5.10
...
84bd01087c
| Author | SHA1 | Date | |
|---|---|---|---|
| 84bd01087c | |||
| c10bf81d24 | |||
| acf2a0fa87 | |||
| 090acbd66b | |||
| 220dbaa683 | |||
| 800832d15e | |||
| 454ff8bb5f | |||
| 9b002cd9a9 | |||
| c1d5b46145 | |||
| 46c6767a90 | |||
| 381298adb1 | |||
| bfa42000d8 | |||
| 0d02f41417 | |||
| a26f1942f8 | |||
| 0a7bcdd9ec | |||
| b6ea944eb5 | |||
| be9f4f3d83 | |||
| dc42a05959 | |||
| 9c6cc1f3eb | |||
| e6d25882fc | |||
| 970c2e9946 | |||
| 356950e2c7 | |||
| cca3f89bf1 | |||
| 674b0de3b2 | |||
| 29dfc6c5a4 | |||
| aa3bc1ed69 | |||
| 1289d7cba6 | |||
| d62e295dc6 | |||
| 693c5890ca | |||
| 3adcaa61d3 | |||
| aea9dcae79 | |||
| 40278eaf06 | |||
| e00c14f277 | |||
| f20c462685 | |||
| 80ae4bd91c | |||
| 6ee3ee76ea | |||
| 2bfcc8b889 | |||
| 99564c02d0 | |||
| 25d9904180 | |||
| 1cf83adf87 | |||
| 8e3d1f11b5 | |||
| 0ab14aa34d | |||
| e0ed81ad1f | |||
| ece7481e9e |
@@ -25,6 +25,8 @@ venv
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
.claude
|
||||||
|
.codex
|
||||||
|
|
||||||
instance
|
instance
|
||||||
flask_session
|
flask_session
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ Refer to the `change log`_.
|
|||||||
Copyright
|
Copyright
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023-2024 imacat.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -2,6 +2,37 @@ Change Log
|
|||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.1
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2024/12/3
|
||||||
|
|
||||||
|
Fix test cases for compatibility with httpx 0.28.0.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.6.0
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2024/6/4
|
||||||
|
|
||||||
|
* Updated Python version to 3.12.
|
||||||
|
* Revised the calculation of "today" to use the client's timezone instead of
|
||||||
|
the server's timezone.
|
||||||
|
* Updated the Bootstrap, FontAwesome, and Tempus-Dominus versions in the test
|
||||||
|
site.
|
||||||
|
|
||||||
|
|
||||||
|
Version 1.5.11
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2023/12/26
|
||||||
|
|
||||||
|
Bug fix.
|
||||||
|
|
||||||
|
* Refined to enable the selection of the 3351-001 Accumulated Profit or Loss
|
||||||
|
account.
|
||||||
|
|
||||||
|
|
||||||
Version 1.5.10
|
Version 1.5.10
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ The following is an example configuration for *Mia! Accounting*.
|
|||||||
|
|
||||||
from flask import Response, redirect
|
from flask import Response, redirect
|
||||||
from .auth import current_user()
|
from .auth import current_user()
|
||||||
from .modules import User
|
from .modules import Base, User
|
||||||
|
|
||||||
def create_app(test_config=None) -> Flask:
|
def create_app(test_config=None) -> Flask:
|
||||||
app: Flask = Flask(__name__)
|
app: Flask = Flask(__name__)
|
||||||
@@ -37,7 +37,11 @@ The following is an example configuration for *Mia! Accounting*.
|
|||||||
return redirect("/login")
|
return redirect("/login")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cls(self) -> t.Type[User]:
|
def base(self) -> type[DeclarativeBase]:
|
||||||
|
return Base
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cls(self) -> type[User]:
|
||||||
return User
|
return User
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -49,7 +53,8 @@ The following is an example configuration for *Mia! Accounting*.
|
|||||||
return current_user()
|
return current_user()
|
||||||
|
|
||||||
def get_by_username(self, username: str) -> User | None:
|
def get_by_username(self, username: str) -> User | None:
|
||||||
return User.query.filter(User.username == username).first()
|
return db.session.scalar(
|
||||||
|
sa.select(User).where(User.username == username))
|
||||||
|
|
||||||
def get_pk(self, user: User) -> int:
|
def get_pk(self, user: User) -> int:
|
||||||
return user.id
|
return user.id
|
||||||
|
|||||||
+6
-7
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/8/21
|
||||||
|
|
||||||
# Copyright (c) 2022-2023 imacat.
|
# Copyright (c) 2022-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,7 +20,7 @@ name = "mia-accounting"
|
|||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "A Flask accounting module."
|
description = "A Flask accounting module."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.12"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "imacat", email = "imacat@mail.imacat.idv.tw"},
|
{name = "imacat", email = "imacat@mail.imacat.idv.tw"},
|
||||||
]
|
]
|
||||||
@@ -33,18 +33,17 @@ classifiers = [
|
|||||||
"Topic :: Office/Business :: Financial :: Accounting",
|
"Topic :: Office/Business :: Financial :: Accounting",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flask",
|
"Flask",
|
||||||
"SQLAlchemy >= 2",
|
"SQLAlchemy >= 2",
|
||||||
"Flask-SQLAlchemy",
|
"Flask-SQLAlchemy-Lite",
|
||||||
"Flask-WTF",
|
"Flask-WTF",
|
||||||
"Flask-Babel >= 3",
|
"Flask-Babel >= 3",
|
||||||
"Flask-Babel-JS",
|
"Flask-Babel-JS",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
devel = [
|
||||||
"unittest",
|
"httpx >= 0.28.0",
|
||||||
"httpx",
|
|
||||||
"OpenCC",
|
"OpenCC",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import Flask, Blueprint
|
from flask import Flask, Blueprint
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy_lite import SQLAlchemy
|
||||||
|
|
||||||
from accounting.utils.user import UserUtilityInterface
|
from .utils.user import UserUtilityInterface
|
||||||
|
|
||||||
VERSION: str = "1.5.10"
|
VERSION: str = "1.6.1"
|
||||||
"""The package version."""
|
"""The package version."""
|
||||||
db: SQLAlchemy = SQLAlchemy()
|
db: SQLAlchemy = SQLAlchemy()
|
||||||
"""The database instance."""
|
"""The database instance."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,11 +23,11 @@ from typing import Any
|
|||||||
import click
|
import click
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from .. import db
|
||||||
from accounting.models import BaseAccount, Account, AccountL10n
|
from ..models import BaseAccount, Account, AccountL10n
|
||||||
from accounting.utils.user import get_user_pk
|
from ..utils.user import get_user_pk
|
||||||
|
|
||||||
AccountData = tuple[int, str, int, str, str, str, bool]
|
type 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,
|
||||||
English, Traditional Chinese, Simplified Chinese, is-need-offset) tuples."""
|
English, Traditional Chinese, Simplified Chinese, is-need-offset) tuples."""
|
||||||
|
|
||||||
@@ -36,13 +36,14 @@ def init_accounts_command(username: str) -> None:
|
|||||||
"""Initializes the accounts."""
|
"""Initializes the accounts."""
|
||||||
creator_pk: int = get_user_pk(username)
|
creator_pk: int = get_user_pk(username)
|
||||||
|
|
||||||
bases: list[BaseAccount] = BaseAccount.query\
|
bases: list[BaseAccount] = db.session.scalars(
|
||||||
.filter(db.func.length(BaseAccount.code) == 4)\
|
sa.select(BaseAccount).where(sa.func.length(BaseAccount.code) == 4)
|
||||||
.order_by(BaseAccount.code).all()
|
.order_by(BaseAccount.code)).unique().all()
|
||||||
if len(bases) == 0:
|
if len(bases) == 0:
|
||||||
raise click.Abort
|
raise click.Abort
|
||||||
|
|
||||||
existing: list[Account] = Account.query.all()
|
existing: list[Account] = \
|
||||||
|
db.session.scalars(sa.select(Account)).unique().all()
|
||||||
|
|
||||||
existing_base_code: set[str] = {x.base_code for x in existing}
|
existing_base_code: set[str] = {x.base_code for x in existing}
|
||||||
bases_to_add: list[BaseAccount] = [x for x in bases
|
bases_to_add: list[BaseAccount] = [x for x in bases
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/31
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
from flask import abort
|
from flask import abort
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting.models import Account
|
from ..models import Account
|
||||||
|
|
||||||
|
|
||||||
class AccountConverter(BaseConverter):
|
class AccountConverter(BaseConverter):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,12 +23,12 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import StringField, BooleanField
|
from wtforms import StringField, BooleanField
|
||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
from accounting import db
|
from .. import db
|
||||||
from accounting.locale import lazy_gettext
|
from ..locale import lazy_gettext
|
||||||
from accounting.models import BaseAccount, Account
|
from ..models import BaseAccount, Account
|
||||||
from accounting.utils.random_id import new_id
|
from ..utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_text
|
from ..utils.strip_text import strip_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from ..utils.user import get_current_user_pk
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountExists:
|
class BaseAccountExists:
|
||||||
@@ -97,8 +97,9 @@ class AccountForm(FlaskForm):
|
|||||||
if obj.base_code is not None:
|
if obj.base_code is not None:
|
||||||
sort_accounts_in(obj.base_code, obj.id)
|
sort_accounts_in(obj.base_code, obj.id)
|
||||||
sort_accounts_in(self.base_code.data, obj.id)
|
sort_accounts_in(self.base_code.data, obj.id)
|
||||||
count: int = Account.query\
|
count: int = db.session.scalar(
|
||||||
.filter(Account.base_code == self.base_code.data).count()
|
sa.select(sa.func.count(Account.id))
|
||||||
|
.where(Account.base_code == self.base_code.data))
|
||||||
obj.base_code = self.base_code.data
|
obj.base_code = self.base_code.data
|
||||||
obj.no = count + 1
|
obj.no = count + 1
|
||||||
obj.title = self.title.data
|
obj.title = self.title.data
|
||||||
@@ -137,9 +138,10 @@ class AccountForm(FlaskForm):
|
|||||||
|
|
||||||
:return: The selectable base accounts.
|
:return: The selectable base accounts.
|
||||||
"""
|
"""
|
||||||
return BaseAccount.query\
|
return db.session.scalars(
|
||||||
.filter(sa.func.char_length(BaseAccount.code) == 4)\
|
sa.select(BaseAccount)
|
||||||
.order_by(BaseAccount.code).all()
|
.where(sa.func.char_length(BaseAccount.code) == 4)
|
||||||
|
.order_by(BaseAccount.code)).unique()
|
||||||
|
|
||||||
|
|
||||||
def sort_accounts_in(base_code: str, exclude: int) -> None:
|
def sort_accounts_in(base_code: str, exclude: int) -> None:
|
||||||
@@ -150,10 +152,10 @@ def sort_accounts_in(base_code: str, exclude: int) -> None:
|
|||||||
:param exclude: The account ID to exclude.
|
:param exclude: The account ID to exclude.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
accounts: list[Account] = Account.query\
|
accounts: list[Account] = db.session.scalars(
|
||||||
.filter(Account.base_code == base_code,
|
sa.select(Account)
|
||||||
Account.id != exclude)\
|
.where(Account.base_code == base_code, Account.id != exclude)
|
||||||
.order_by(Account.no).all()
|
.order_by(Account.no)).unique().all()
|
||||||
for i in range(len(accounts)):
|
for i in range(len(accounts)):
|
||||||
if accounts[i].no != i + 1:
|
if accounts[i].no != i + 1:
|
||||||
accounts[i].no = i + 1
|
accounts[i].no = i + 1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from .. import db
|
||||||
from accounting.models import Account, AccountL10n
|
from ..locale import gettext
|
||||||
from accounting.utils.query import parse_query_keywords
|
from ..models import Account, AccountL10n
|
||||||
|
from ..utils.query import parse_query_keywords
|
||||||
|
|
||||||
|
|
||||||
def get_account_query() -> list[Account]:
|
def get_account_query() -> list[Account]:
|
||||||
@@ -32,17 +33,20 @@ def get_account_query() -> list[Account]:
|
|||||||
"""
|
"""
|
||||||
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
||||||
if len(keywords) == 0:
|
if len(keywords) == 0:
|
||||||
return Account.query.order_by(Account.base_code, Account.no).all()
|
return db.session.scalars(
|
||||||
code: sa.BinaryExpression = Account.base_code + "-" \
|
sa.select(Account)
|
||||||
|
.order_by(Account.base_code, Account.no)).unique().all()
|
||||||
|
code: sa.ColumnElement[str] = Account.base_code + "-" \
|
||||||
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
|
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
|
||||||
sa.func.char_length(sa.cast(Account.no,
|
sa.func.char_length(sa.cast(Account.no,
|
||||||
sa.String)) + 1)
|
sa.String)) + 1)
|
||||||
conditions: list[sa.BinaryExpression] = []
|
conditions: list[sa.ColumnElement[bool]] = []
|
||||||
for k in keywords:
|
for k in keywords:
|
||||||
l10n: list[AccountL10n] = AccountL10n.query\
|
l10n: list[AccountL10n] = db.session.scalars(
|
||||||
.filter(AccountL10n.title.icontains(k)).all()
|
sa.select(AccountL10n)
|
||||||
l10n_matches: set[str] = {x.account_id for x in l10n}
|
.where(AccountL10n.title.icontains(k))).all()
|
||||||
sub_conditions: list[sa.BinaryExpression] \
|
l10n_matches: set[int] = {x.account_id for x in l10n}
|
||||||
|
sub_conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [Account.base_code.contains(k),
|
= [Account.base_code.contains(k),
|
||||||
Account.title_l10n.icontains(k),
|
Account.title_l10n.icontains(k),
|
||||||
code.contains(k),
|
code.contains(k),
|
||||||
@@ -51,5 +55,6 @@ def get_account_query() -> list[Account]:
|
|||||||
sub_conditions.append(Account.is_need_offset)
|
sub_conditions.append(Account.is_need_offset)
|
||||||
conditions.append(sa.or_(*sub_conditions))
|
conditions.append(sa.or_(*sub_conditions))
|
||||||
|
|
||||||
return Account.query.filter(*conditions)\
|
return db.session.scalars(
|
||||||
.order_by(Account.base_code, Account.no).all()
|
sa.select(Account).where(*conditions)
|
||||||
|
.order_by(Account.base_code, Account.no)).unique().all()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/30
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,20 +21,20 @@ from urllib.parse import parse_qsl, urlencode
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Blueprint, render_template, session, redirect, flash, \
|
from flask import Blueprint, render_template, session, redirect, flash, \
|
||||||
url_for, request
|
url_for, request, Response
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import lazy_gettext
|
|
||||||
from accounting.models import Account, BaseAccount
|
|
||||||
from accounting.utils.cast import s
|
|
||||||
from accounting.utils.flash_errors import flash_form_errors
|
|
||||||
from accounting.utils.next_uri import inherit_next, or_next
|
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
from accounting.utils.permission import can_view, has_permission, can_edit
|
|
||||||
from accounting.utils.user import get_current_user_pk
|
|
||||||
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
|
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
|
||||||
from .queries import get_account_query
|
from .queries import get_account_query
|
||||||
|
from .. import db
|
||||||
|
from ..locale import lazy_gettext
|
||||||
|
from ..models import Account, BaseAccount
|
||||||
|
from ..utils.cast import s
|
||||||
|
from ..utils.flash_errors import flash_form_errors
|
||||||
|
from ..utils.next_uri import inherit_next, or_next
|
||||||
|
from ..utils.pagination import Pagination
|
||||||
|
from ..utils.permission import can_view, has_permission, can_edit
|
||||||
|
from ..utils.user import get_current_user_pk
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("account", __name__)
|
bp: Blueprint = Blueprint("account", __name__)
|
||||||
"""The view blueprint for the account management."""
|
"""The view blueprint for the account management."""
|
||||||
@@ -47,8 +47,8 @@ def list_accounts() -> str:
|
|||||||
|
|
||||||
:return: The account list.
|
:return: The account list.
|
||||||
"""
|
"""
|
||||||
accounts: list[BaseAccount] = get_account_query()
|
accounts: list[Account] = get_account_query()
|
||||||
pagination: Pagination = Pagination[BaseAccount](accounts)
|
pagination: Pagination = Pagination[Account](accounts)
|
||||||
return render_template("accounting/account/list.html",
|
return render_template("accounting/account/list.html",
|
||||||
list=pagination.list, pagination=pagination)
|
list=pagination.list, pagination=pagination)
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ def show_add_account_form() -> str:
|
|||||||
|
|
||||||
@bp.post("store", endpoint="store")
|
@bp.post("store", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_account() -> redirect:
|
def add_account() -> Response:
|
||||||
"""Adds an account.
|
"""Adds an account.
|
||||||
|
|
||||||
:return: The redirection to the account detail on success, or the account
|
:return: The redirection to the account detail on success, or the account
|
||||||
@@ -123,7 +123,7 @@ def show_account_edit_form(account: Account) -> str:
|
|||||||
|
|
||||||
@bp.post("<account:account>/update", endpoint="update")
|
@bp.post("<account:account>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_account(account: Account) -> redirect:
|
def update_account(account: Account) -> Response:
|
||||||
"""Updates an account.
|
"""Updates an account.
|
||||||
|
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
@@ -150,7 +150,7 @@ def update_account(account: Account) -> redirect:
|
|||||||
|
|
||||||
@bp.post("<account:account>/delete", endpoint="delete")
|
@bp.post("<account:account>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_account(account: Account) -> redirect:
|
def delete_account(account: Account) -> Response:
|
||||||
"""Deletes an account.
|
"""Deletes an account.
|
||||||
|
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
@@ -180,7 +180,7 @@ def show_account_order(base: BaseAccount) -> str:
|
|||||||
|
|
||||||
@bp.post("bases/<baseAccount:base>", endpoint="sort")
|
@bp.post("bases/<baseAccount:base>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_accounts(base: BaseAccount) -> redirect:
|
def sort_accounts(base: BaseAccount) -> Response:
|
||||||
"""Reorders the accounts under a base account.
|
"""Reorders the accounts under a base account.
|
||||||
|
|
||||||
:param base: The base account.
|
:param base: The base account.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,22 +21,21 @@ import csv
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import data_dir
|
from .. import db, data_dir
|
||||||
from accounting import db
|
from ..models import BaseAccount, BaseAccountL10n
|
||||||
from accounting.models import BaseAccount, BaseAccountL10n
|
from ..utils.title_case import title_case
|
||||||
from accounting.utils.title_case import title_case
|
|
||||||
|
|
||||||
|
|
||||||
def init_base_accounts_command() -> None:
|
def init_base_accounts_command() -> None:
|
||||||
"""Initializes the base accounts."""
|
"""Initializes the base accounts."""
|
||||||
if BaseAccount.query.first() is not None:
|
if db.session.scalar(sa.select(BaseAccount)) is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
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]] = \
|
||||||
"title_l10n": title_case(x["title"])}
|
[{"code": x["code"], "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"],
|
||||||
"locale": y,
|
"locale": y,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
from flask import abort
|
from flask import abort
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting import db
|
from .. import db
|
||||||
from accounting.models import BaseAccount
|
from ..models import BaseAccount
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountConverter(BaseConverter):
|
class BaseAccountConverter(BaseConverter):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,8 +20,9 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from accounting.models import BaseAccount, BaseAccountL10n
|
from .. import db
|
||||||
from accounting.utils.query import parse_query_keywords
|
from ..models import BaseAccount, BaseAccountL10n
|
||||||
|
from ..utils.query import parse_query_keywords
|
||||||
|
|
||||||
|
|
||||||
def get_base_account_query() -> list[BaseAccount]:
|
def get_base_account_query() -> list[BaseAccount]:
|
||||||
@@ -31,14 +32,17 @@ def get_base_account_query() -> list[BaseAccount]:
|
|||||||
"""
|
"""
|
||||||
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
||||||
if len(keywords) == 0:
|
if len(keywords) == 0:
|
||||||
return BaseAccount.query.order_by(BaseAccount.code).all()
|
return db.session.scalars(
|
||||||
conditions: list[sa.BinaryExpression] = []
|
sa.select(BaseAccount).order_by(BaseAccount.code)).unique().all()
|
||||||
|
conditions: list[sa.ColumnElement[bool]] = []
|
||||||
for k in keywords:
|
for k in keywords:
|
||||||
l10n: list[BaseAccountL10n] = BaseAccountL10n.query\
|
l10n: list[BaseAccountL10n] = db.session.scalars(
|
||||||
.filter(BaseAccountL10n.title.icontains(k)).all()
|
sa.select(BaseAccountL10n)
|
||||||
|
.where((BaseAccountL10n.title.icontains(k)))).all()
|
||||||
l10n_matches: set[str] = {x.account_code for x in l10n}
|
l10n_matches: set[str] = {x.account_code for x in l10n}
|
||||||
conditions.append(sa.or_(BaseAccount.code.contains(k),
|
conditions.append(sa.or_(BaseAccount.code.contains(k),
|
||||||
BaseAccount.title_l10n.icontains(k),
|
BaseAccount.title_l10n.icontains(k),
|
||||||
BaseAccount.code.in_(l10n_matches)))
|
BaseAccount.code.in_(l10n_matches)))
|
||||||
return BaseAccount.query.filter(*conditions)\
|
return db.session.scalars(
|
||||||
.order_by(BaseAccount.code).all()
|
sa.select(BaseAccount).where(*conditions)
|
||||||
|
.order_by(BaseAccount.code)).unique().all()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/26
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
from accounting.models import BaseAccount
|
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
from accounting.utils.permission import has_permission, can_view
|
|
||||||
from .queries import get_base_account_query
|
from .queries import get_base_account_query
|
||||||
|
from ..models import BaseAccount
|
||||||
|
from ..utils.pagination import Pagination
|
||||||
|
from ..utils.permission import has_permission, can_view
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("base-account", __name__)
|
bp: Blueprint = Blueprint("base-account", __name__)
|
||||||
"""The view blueprint for the base account management."""
|
"""The view blueprint for the base account management."""
|
||||||
@@ -50,4 +50,3 @@ def show_account_detail(account: BaseAccount) -> str:
|
|||||||
:return: The detail.
|
:return: The detail.
|
||||||
"""
|
"""
|
||||||
return render_template("accounting/base-account/detail.html", obj=account)
|
return render_template("accounting/base-account/detail.html", obj=account)
|
||||||
|
|
||||||
|
|||||||
+24
-15
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/10
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/10
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,16 +20,16 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import sqlalchemy as sa
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
|
|
||||||
from accounting import db
|
from . import db
|
||||||
from accounting.account import init_accounts_command
|
from .account import init_accounts_command
|
||||||
from accounting.base_account import init_base_accounts_command
|
from .base_account import init_base_accounts_command
|
||||||
from accounting.currency import init_currencies_command
|
from .currency import init_currencies_command
|
||||||
from accounting.models import BaseAccount, Account
|
from .models import BaseAccount, Account
|
||||||
from accounting.utils.title_case import title_case
|
from .utils.title_case import title_case
|
||||||
from accounting.utils.user import has_user, get_user_pk
|
from .utils.user import base_cls, 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,
|
||||||
@@ -54,13 +54,22 @@ def __validate_username(ctx: click.core.Context, param: click.core.Option,
|
|||||||
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
|
@click.option("-u", "--username", metavar="USERNAME", prompt=True,
|
||||||
help="The username.", callback=__validate_username,
|
help="The username.", callback=__validate_username,
|
||||||
default=lambda: os.getlogin())
|
default=lambda: os.getlogin())
|
||||||
|
@click.option("--skip-accounts", is_flag=True, default=False,
|
||||||
|
help="Skip initializing accounts.")
|
||||||
|
@click.option("--skip-currencies", is_flag=True, default=False,
|
||||||
|
help="Skip initializing currencies.")
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def init_db_command(username: str) -> None:
|
def init_db_command(username: str, skip_accounts: bool,
|
||||||
|
skip_currencies: bool) -> None:
|
||||||
"""Initializes the accounting database."""
|
"""Initializes the accounting database."""
|
||||||
db.create_all()
|
base_cls.metadata.create_all(db.engine)
|
||||||
init_base_accounts_command()
|
init_base_accounts_command()
|
||||||
init_accounts_command(username)
|
if not skip_accounts:
|
||||||
init_currencies_command(username)
|
init_accounts_command(username)
|
||||||
|
print("OK 1")
|
||||||
|
if not skip_currencies:
|
||||||
|
init_currencies_command(username)
|
||||||
|
print("OK 2")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
click.echo("Accounting database initialized.")
|
click.echo("Accounting database initialized.")
|
||||||
|
|
||||||
@@ -74,12 +83,12 @@ def titleize_command(username: str) -> None:
|
|||||||
"""Capitalize the account titles."""
|
"""Capitalize the account titles."""
|
||||||
updater_pk: int = get_user_pk(username)
|
updater_pk: int = get_user_pk(username)
|
||||||
updated: int = 0
|
updated: int = 0
|
||||||
for base in BaseAccount.query:
|
for base in db.session.scalars(sa.select(BaseAccount)).unique():
|
||||||
new_title: str = title_case(base.title_l10n)
|
new_title: str = title_case(base.title_l10n)
|
||||||
if base.title_l10n != new_title:
|
if base.title_l10n != new_title:
|
||||||
base.title_l10n = new_title
|
base.title_l10n = new_title
|
||||||
updated = updated + 1
|
updated = updated + 1
|
||||||
for account in Account.query:
|
for account in db.session.scalars(sa.select(Account)).unique():
|
||||||
if account.title_l10n.lower() == account.base.title_l10n.lower():
|
if account.title_l10n.lower() == account.base.title_l10n.lower():
|
||||||
new_title: str = title_case(account.title_l10n)
|
new_title: str = title_case(account.title_l10n)
|
||||||
if account.title_l10n != new_title:
|
if account.title_l10n != new_title:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,14 +22,15 @@ from typing import Any
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db, data_dir
|
from .. import db, data_dir
|
||||||
from accounting.models import Currency, CurrencyL10n
|
from ..models import Currency, CurrencyL10n
|
||||||
from accounting.utils.user import get_user_pk
|
from ..utils.user import get_user_pk
|
||||||
|
|
||||||
|
|
||||||
def init_currencies_command(username: str) -> None:
|
def init_currencies_command(username: str) -> None:
|
||||||
"""Initializes the currencies."""
|
"""Initializes the currencies."""
|
||||||
existing_codes: set[str] = {x.code for x in Currency.query.all()}
|
existing_codes: set[str] = \
|
||||||
|
{x.code for x in db.session.scalars(sa.select(Currency)).unique()}
|
||||||
|
|
||||||
with open(data_dir / "currencies.csv") as fp:
|
with open(data_dir / "currencies.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)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
from flask import abort
|
from flask import abort
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting import db
|
from .. import db
|
||||||
from accounting.models import Currency
|
from ..models import Currency
|
||||||
|
|
||||||
|
|
||||||
class CurrencyConverter(BaseConverter):
|
class CurrencyConverter(BaseConverter):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,11 +21,11 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import StringField, ValidationError
|
from wtforms import StringField, ValidationError
|
||||||
from wtforms.validators import DataRequired, Regexp, NoneOf
|
from wtforms.validators import DataRequired, Regexp, NoneOf
|
||||||
|
|
||||||
from accounting import db
|
from .. import db
|
||||||
from accounting.locale import lazy_gettext
|
from ..locale import lazy_gettext
|
||||||
from accounting.models import Currency
|
from ..models import Currency
|
||||||
from accounting.utils.strip_text import strip_text
|
from ..utils.strip_text import strip_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from ..utils.user import get_current_user_pk
|
||||||
|
|
||||||
|
|
||||||
class CodeUnique:
|
class CodeUnique:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,8 +20,9 @@
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from accounting.models import Currency, CurrencyL10n
|
from .. import db
|
||||||
from accounting.utils.query import parse_query_keywords
|
from ..models import Currency, CurrencyL10n
|
||||||
|
from ..utils.query import parse_query_keywords
|
||||||
|
|
||||||
|
|
||||||
def get_currency_query() -> list[Currency]:
|
def get_currency_query() -> list[Currency]:
|
||||||
@@ -31,14 +32,17 @@ def get_currency_query() -> list[Currency]:
|
|||||||
"""
|
"""
|
||||||
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
||||||
if len(keywords) == 0:
|
if len(keywords) == 0:
|
||||||
return Currency.query.order_by(Currency.code).all()
|
return db.session.scalars(
|
||||||
conditions: list[sa.BinaryExpression] = []
|
sa.select(Currency).order_by(Currency.code)).unique().all()
|
||||||
|
conditions: list[sa.ColumnElement[bool]] = []
|
||||||
for k in keywords:
|
for k in keywords:
|
||||||
l10n: list[CurrencyL10n] = CurrencyL10n.query\
|
l10n: list[CurrencyL10n] = db.session.scalars(
|
||||||
.filter(CurrencyL10n.name.icontains(k)).all()
|
sa.select(CurrencyL10n)
|
||||||
|
.where(CurrencyL10n.name.icontains(k))).all()
|
||||||
l10n_matches: set[str] = {x.account_code for x in l10n}
|
l10n_matches: set[str] = {x.account_code for x in l10n}
|
||||||
conditions.append(sa.or_(Currency.code.icontains(k),
|
conditions.append(sa.or_(Currency.code.icontains(k),
|
||||||
Currency.name_l10n.icontains(k),
|
Currency.name_l10n.icontains(k),
|
||||||
Currency.code.in_(l10n_matches)))
|
Currency.code.in_(l10n_matches)))
|
||||||
return Currency.query.filter(*conditions)\
|
return db.session.scalars(
|
||||||
.order_by(Currency.code).all()
|
sa.select(Currency).where(*conditions)
|
||||||
|
.order_by(Currency.code)).unique().all()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/6
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,20 +21,20 @@ from urllib.parse import urlencode, parse_qsl
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Blueprint, render_template, redirect, session, request, \
|
from flask import Blueprint, render_template, redirect, session, request, \
|
||||||
flash, url_for
|
flash, url_for, Response
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import lazy_gettext
|
|
||||||
from accounting.models import Currency
|
|
||||||
from accounting.utils.cast import s
|
|
||||||
from accounting.utils.flash_errors import flash_form_errors
|
|
||||||
from accounting.utils.next_uri import inherit_next, or_next
|
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
|
||||||
from accounting.utils.user import get_current_user_pk
|
|
||||||
from .forms import CurrencyForm
|
from .forms import CurrencyForm
|
||||||
from .queries import get_currency_query
|
from .queries import get_currency_query
|
||||||
|
from .. import db
|
||||||
|
from ..locale import lazy_gettext
|
||||||
|
from ..models import Currency
|
||||||
|
from ..utils.cast import s
|
||||||
|
from ..utils.flash_errors import flash_form_errors
|
||||||
|
from ..utils.next_uri import inherit_next, or_next
|
||||||
|
from ..utils.pagination import Pagination
|
||||||
|
from ..utils.permission import has_permission, can_view, can_edit
|
||||||
|
from ..utils.user import get_current_user_pk
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("currency", __name__)
|
bp: Blueprint = Blueprint("currency", __name__)
|
||||||
"""The view blueprint for the currency management."""
|
"""The view blueprint for the currency management."""
|
||||||
@@ -74,7 +74,7 @@ def show_add_currency_form() -> str:
|
|||||||
|
|
||||||
@bp.post("store", endpoint="store")
|
@bp.post("store", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_currency() -> redirect:
|
def add_currency() -> Response:
|
||||||
"""Adds a currency.
|
"""Adds a currency.
|
||||||
|
|
||||||
:return: The redirection to the currency detail on success, or the currency
|
:return: The redirection to the currency detail on success, or the currency
|
||||||
@@ -125,7 +125,7 @@ def show_currency_edit_form(currency: Currency) -> str:
|
|||||||
|
|
||||||
@bp.post("<currency:currency>/update", endpoint="update")
|
@bp.post("<currency:currency>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_currency(currency: Currency) -> redirect:
|
def update_currency(currency: Currency) -> Response:
|
||||||
"""Updates a currency.
|
"""Updates a currency.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
@@ -153,7 +153,7 @@ def update_currency(currency: Currency) -> redirect:
|
|||||||
|
|
||||||
@bp.post("<currency:currency>/delete", endpoint="delete")
|
@bp.post("<currency:currency>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_currency(currency: Currency) -> redirect:
|
def delete_currency(currency: Currency) -> Response:
|
||||||
"""Deletes a currency.
|
"""Deletes a currency.
|
||||||
|
|
||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,10 +24,9 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import StringField, ValidationError
|
from wtforms import StringField, ValidationError
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
from accounting import db
|
from . import db
|
||||||
from accounting.locale import lazy_gettext
|
from .locale import lazy_gettext
|
||||||
from accounting.models import Currency, Account
|
from .models import Currency, Account
|
||||||
|
|
||||||
|
|
||||||
ACCOUNT_REQUIRED: DataRequired = DataRequired(
|
ACCOUNT_REQUIRED: DataRequired = DataRequired(
|
||||||
lazy_gettext("Please select the account."))
|
lazy_gettext("Please select the account."))
|
||||||
@@ -71,7 +70,6 @@ class IsDebitAccount:
|
|||||||
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 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)
|
||||||
@@ -92,7 +90,6 @@ class IsCreditAccount:
|
|||||||
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 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)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,9 +22,9 @@ import datetime as dt
|
|||||||
from flask import abort
|
from flask import abort
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting import db
|
from .. import db
|
||||||
from accounting.models import JournalEntry
|
from ..models import JournalEntry
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
from ..utils.journal_entry_types import JournalEntryType
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryConverter(BaseConverter):
|
class JournalEntryConverter(BaseConverter):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -26,13 +26,13 @@ from wtforms import StringField, ValidationError, FieldList, IntegerField, \
|
|||||||
BooleanField, FormField
|
BooleanField, FormField
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.forms import CurrencyExists
|
|
||||||
from accounting.locale import lazy_gettext
|
|
||||||
from accounting.models import JournalEntryLineItem
|
|
||||||
from accounting.utils.offset_alias import offset_alias
|
|
||||||
from accounting.utils.strip_text import strip_text
|
|
||||||
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
||||||
|
from ... import db
|
||||||
|
from ...forms import CurrencyExists
|
||||||
|
from ...locale import lazy_gettext
|
||||||
|
from ...models import JournalEntryLineItem
|
||||||
|
from ...utils.offset_alias import offset_alias
|
||||||
|
from ...utils.strip_text import strip_text
|
||||||
|
|
||||||
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
CURRENCY_REQUIRED: DataRequired = DataRequired(
|
||||||
lazy_gettext("Please select the currency."))
|
lazy_gettext("Please select the currency."))
|
||||||
@@ -55,7 +55,7 @@ class SameCurrencyAsOriginalLineItems:
|
|||||||
return
|
return
|
||||||
original_line_item_currency_codes: set[str] = set(db.session.scalars(
|
original_line_item_currency_codes: set[str] = set(db.session.scalars(
|
||||||
sa.select(JournalEntryLineItem.currency_code)
|
sa.select(JournalEntryLineItem.currency_code)
|
||||||
.filter(JournalEntryLineItem.id.in_(original_line_item_id))).all())
|
.where(JournalEntryLineItem.id.in_(original_line_item_id))).all())
|
||||||
for currency_code in original_line_item_currency_codes:
|
for currency_code in original_line_item_currency_codes:
|
||||||
if field.data != currency_code:
|
if field.data != currency_code:
|
||||||
raise ValidationError(lazy_gettext(
|
raise ValidationError(lazy_gettext(
|
||||||
@@ -72,17 +72,17 @@ class KeepCurrencyWhenHavingOffset:
|
|||||||
if field.data is None:
|
if field.data is None:
|
||||||
return
|
return
|
||||||
offset: sa.Alias = offset_alias()
|
offset: sa.Alias = offset_alias()
|
||||||
original_line_items: list[JournalEntryLineItem]\
|
original_line_items: list[JournalEntryLineItem] = db.session.scalars(
|
||||||
= JournalEntryLineItem.query\
|
sa.select(JournalEntryLineItem)
|
||||||
.join(offset,
|
.join(offset,
|
||||||
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)
|
||||||
.filter(JournalEntryLineItem.id
|
.where(JournalEntryLineItem.id
|
||||||
.in_({x.id.data for x in form.line_items
|
.in_({x.id.data for x in form.line_items
|
||||||
if x.id.data is not None}))\
|
if x.id.data is not None}))
|
||||||
.group_by(JournalEntryLineItem.id,
|
.group_by(JournalEntryLineItem.id,
|
||||||
JournalEntryLineItem.currency_code)\
|
JournalEntryLineItem.currency_code)
|
||||||
.having(sa.func.count(offset.c.id) > 0).all()
|
.having(sa.func.count(offset.c.id) > 0)).unique().all()
|
||||||
for original_line_item in original_line_items:
|
for original_line_item in original_line_items:
|
||||||
if original_line_item.currency_code != field.data:
|
if original_line_item.currency_code != field.data:
|
||||||
raise ValidationError(lazy_gettext(
|
raise ValidationError(lazy_gettext(
|
||||||
@@ -152,8 +152,8 @@ class CurrencyForm(FlaskForm):
|
|||||||
line_item_id: set[int] = {x.id.data for x in line_item_forms
|
line_item_id: set[int] = {x.id.data for x in line_item_forms
|
||||||
if x.id.data is not None}
|
if x.id.data is not None}
|
||||||
select: sa.Select = sa.select(sa.func.count(JournalEntryLineItem.id))\
|
select: sa.Select = sa.select(sa.func.count(JournalEntryLineItem.id))\
|
||||||
.filter(JournalEntryLineItem.original_line_item_id
|
.where(JournalEntryLineItem.original_line_item_id
|
||||||
.in_(line_item_id))
|
.in_(line_item_id))
|
||||||
return db.session.scalar(select) > 0
|
return db.session.scalar(select) > 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
"""
|
"""
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
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
|
||||||
@@ -28,21 +27,20 @@ from wtforms import DateField, FieldList, FormField, TextAreaField, \
|
|||||||
BooleanField
|
BooleanField
|
||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
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.models import JournalEntry, Account, JournalEntryLineItem, \
|
|
||||||
JournalEntryCurrency
|
|
||||||
from accounting.utils.random_id import new_id
|
|
||||||
from accounting.utils.strip_text import strip_multiline_text
|
|
||||||
from accounting.utils.user import get_current_user_pk
|
|
||||||
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
|
from .currency import CurrencyForm, CashReceiptCurrencyForm, \
|
||||||
CashDisbursementCurrencyForm, TransferCurrencyForm
|
CashDisbursementCurrencyForm, TransferCurrencyForm
|
||||||
from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
|
from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
|
||||||
from .reorder import sort_journal_entries_in
|
from .reorder import sort_journal_entries_in
|
||||||
|
from ..utils.account_option import AccountOption
|
||||||
|
from ..utils.description_editor import DescriptionEditor
|
||||||
|
from ..utils.original_line_items import get_selectable_original_line_items
|
||||||
|
from ... import db
|
||||||
|
from ...locale import lazy_gettext
|
||||||
|
from ...models import JournalEntry, Account, JournalEntryLineItem, \
|
||||||
|
JournalEntryCurrency
|
||||||
|
from ...utils.random_id import new_id
|
||||||
|
from ...utils.strip_text import strip_multiline_text
|
||||||
|
from ...utils.user import get_current_user_pk
|
||||||
|
|
||||||
DATE_REQUIRED: DataRequired = DataRequired(
|
DATE_REQUIRED: DataRequired = DataRequired(
|
||||||
lazy_gettext("Please fill in the date."))
|
lazy_gettext("Please fill in the date."))
|
||||||
@@ -123,7 +121,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: 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."""
|
||||||
@@ -154,15 +152,16 @@ class JournalEntryForm(FlaskForm):
|
|||||||
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: 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()
|
||||||
|
|
||||||
to_delete: set[int] = {x.id for x in obj.line_items
|
to_delete: set[int] = {x.id for x in obj.line_items
|
||||||
if x.id not in collector.to_keep}
|
if x.id not in collector.to_keep}
|
||||||
if len(to_delete) > 0:
|
if len(to_delete) > 0:
|
||||||
JournalEntryLineItem.query\
|
db.session.execute(
|
||||||
.filter(JournalEntryLineItem.id.in_(to_delete)).delete()
|
sa.delete(JournalEntryLineItem)
|
||||||
|
.where(JournalEntryLineItem.id.in_(to_delete)))
|
||||||
self.is_modified = True
|
self.is_modified = True
|
||||||
|
|
||||||
if is_new or db.session.is_modified(obj):
|
if is_new or db.session.is_modified(obj):
|
||||||
@@ -197,7 +196,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
if self.max_date is not None and new_date == self.max_date:
|
if self.max_date is not None and new_date == self.max_date:
|
||||||
db_min_no: int | None = db.session.scalar(
|
db_min_no: int | None = db.session.scalar(
|
||||||
sa.select(sa.func.min(JournalEntry.no))
|
sa.select(sa.func.min(JournalEntry.no))
|
||||||
.filter(JournalEntry.date == new_date))
|
.where(JournalEntry.date == new_date))
|
||||||
if db_min_no is None:
|
if db_min_no is None:
|
||||||
obj.date = new_date
|
obj.date = new_date
|
||||||
obj.no = 1
|
obj.no = 1
|
||||||
@@ -207,8 +206,9 @@ class JournalEntryForm(FlaskForm):
|
|||||||
sort_journal_entries_in(new_date)
|
sort_journal_entries_in(new_date)
|
||||||
else:
|
else:
|
||||||
sort_journal_entries_in(new_date, obj.id)
|
sort_journal_entries_in(new_date, obj.id)
|
||||||
count: int = JournalEntry.query\
|
count: int = db.session.scalar(
|
||||||
.filter(JournalEntry.date == new_date).count()
|
sa.select(sa.func.count(JournalEntry.id))
|
||||||
|
.where(JournalEntry.date == new_date))
|
||||||
obj.date = new_date
|
obj.date = new_date
|
||||||
obj.no = count + 1
|
obj.no = count + 1
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
if not (x.code[0] == "2" and x.is_need_offset)]
|
if not (x.code[0] == "2" and x.is_need_offset)]
|
||||||
in_use: set[int] = set(db.session.scalars(
|
in_use: set[int] = set(db.session.scalars(
|
||||||
sa.select(JournalEntryLineItem.account_id)
|
sa.select(JournalEntryLineItem.account_id)
|
||||||
.filter(JournalEntryLineItem.is_debit)
|
.where(JournalEntryLineItem.is_debit)
|
||||||
.group_by(JournalEntryLineItem.account_id)).all())
|
.group_by(JournalEntryLineItem.account_id)).all())
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
account.is_in_use = account.id in in_use
|
account.is_in_use = account.id in in_use
|
||||||
@@ -240,7 +240,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
if not (x.code[0] == "1" and x.is_need_offset)]
|
if not (x.code[0] == "1" and x.is_need_offset)]
|
||||||
in_use: set[int] = set(db.session.scalars(
|
in_use: set[int] = set(db.session.scalars(
|
||||||
sa.select(JournalEntryLineItem.account_id)
|
sa.select(JournalEntryLineItem.account_id)
|
||||||
.filter(sa.not_(JournalEntryLineItem.is_debit))
|
.where(sa.not_(JournalEntryLineItem.is_debit))
|
||||||
.group_by(JournalEntryLineItem.account_id)).all())
|
.group_by(JournalEntryLineItem.account_id)).all())
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
account.is_in_use = account.id in in_use
|
account.is_in_use = account.id in in_use
|
||||||
@@ -290,7 +290,7 @@ class JournalEntryForm(FlaskForm):
|
|||||||
return None
|
return None
|
||||||
select: sa.Select = sa.select(sa.func.max(JournalEntry.date))\
|
select: sa.Select = sa.select(sa.func.max(JournalEntry.date))\
|
||||||
.join(JournalEntryLineItem)\
|
.join(JournalEntryLineItem)\
|
||||||
.filter(JournalEntryLineItem.id.in_(original_line_item_id))
|
.where(JournalEntryLineItem.id.in_(original_line_item_id))
|
||||||
return db.session.scalar(select)
|
return db.session.scalar(select)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -303,16 +303,12 @@ class JournalEntryForm(FlaskForm):
|
|||||||
if x.id.data is not None}
|
if x.id.data is not None}
|
||||||
select: sa.Select = sa.select(sa.func.min(JournalEntry.date))\
|
select: sa.Select = sa.select(sa.func.min(JournalEntry.date))\
|
||||||
.join(JournalEntryLineItem)\
|
.join(JournalEntryLineItem)\
|
||||||
.filter(JournalEntryLineItem.original_line_item_id
|
.where(JournalEntryLineItem.original_line_item_id
|
||||||
.in_(line_item_id))
|
.in_(line_item_id))
|
||||||
return db.session.scalar(select)
|
return db.session.scalar(select)
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=JournalEntryForm)
|
class LineItemCollector[T: JournalEntryForm](ABC):
|
||||||
"""A journal entry form variant."""
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -27,15 +27,15 @@ from sqlalchemy.orm import selectinload
|
|||||||
from wtforms import StringField, ValidationError, DecimalField, IntegerField
|
from wtforms import StringField, ValidationError, DecimalField, IntegerField
|
||||||
from wtforms.validators import Optional
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
from accounting import db
|
from ... import db
|
||||||
from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
|
from ...forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
|
||||||
IsCreditAccount
|
IsCreditAccount
|
||||||
from accounting.locale import lazy_gettext
|
from ...locale import lazy_gettext
|
||||||
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
from ...models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.template_filters import format_amount
|
from ...template_filters import format_amount
|
||||||
from accounting.utils.random_id import new_id
|
from ...utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_text
|
from ...utils.strip_text import strip_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from ...utils.user import get_current_user_pk
|
||||||
|
|
||||||
|
|
||||||
class OriginalLineItemExists:
|
class OriginalLineItemExists:
|
||||||
@@ -202,9 +202,9 @@ class NotExceedingOriginalLineItemNetBalance:
|
|||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
offset_total_but_form: Decimal | None = db.session.scalar(
|
offset_total_but_form: Decimal | None = db.session.scalar(
|
||||||
sa.select(offset_total_func)
|
sa.select(offset_total_func)
|
||||||
.filter(JournalEntryLineItem.original_line_item_id
|
.where(JournalEntryLineItem.original_line_item_id
|
||||||
== original_line_item.id,
|
== original_line_item.id,
|
||||||
JournalEntryLineItem.id.not_in(existing_line_item_id)))
|
JournalEntryLineItem.id.not_in(existing_line_item_id)))
|
||||||
if offset_total_but_form is None:
|
if offset_total_but_form is None:
|
||||||
offset_total_but_form = Decimal("0")
|
offset_total_but_form = Decimal("0")
|
||||||
offset_total_on_form: Decimal = sum(
|
offset_total_on_form: Decimal = sum(
|
||||||
@@ -231,7 +231,7 @@ class NotLessThanOffsetTotal:
|
|||||||
(JournalEntryLineItem.is_debit != is_debit,
|
(JournalEntryLineItem.is_debit != is_debit,
|
||||||
JournalEntryLineItem.amount),
|
JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount)))\
|
else_=-JournalEntryLineItem.amount)))\
|
||||||
.filter(JournalEntryLineItem.original_line_item_id == form.id.data)
|
.where(JournalEntryLineItem.original_line_item_id == form.id.data)
|
||||||
offset_total: Decimal | None = db.session.scalar(select_offset_total)
|
offset_total: Decimal | None = db.session.scalar(select_offset_total)
|
||||||
if offset_total is not None and field.data < offset_total:
|
if offset_total is not None and field.data < offset_total:
|
||||||
raise ValidationError(lazy_gettext(
|
raise ValidationError(lazy_gettext(
|
||||||
@@ -353,13 +353,14 @@ class LineItemForm(FlaskForm):
|
|||||||
def get_offsets() -> list[JournalEntryLineItem]:
|
def get_offsets() -> list[JournalEntryLineItem]:
|
||||||
if not self.is_need_offset or self.id.data is None:
|
if not self.is_need_offset or self.id.data is None:
|
||||||
return []
|
return []
|
||||||
return JournalEntryLineItem.query.join(JournalEntry)\
|
return db.session.scalars(
|
||||||
.filter(JournalEntryLineItem.original_line_item_id
|
sa.select(JournalEntryLineItem).join(JournalEntry)
|
||||||
== self.id.data)\
|
.where(JournalEntryLineItem.original_line_item_id
|
||||||
|
== self.id.data)
|
||||||
.order_by(JournalEntry.date, JournalEntry.no,
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
JournalEntryLineItem.no)\
|
JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.journal_entry),
|
.options(selectinload(JournalEntryLineItem.journal_entry),
|
||||||
selectinload(JournalEntryLineItem.account)).all()
|
selectinload(JournalEntryLineItem.account))).all()
|
||||||
setattr(self, "__offsets", get_offsets())
|
setattr(self, "__offsets", get_offsets())
|
||||||
return getattr(self, "__offsets")
|
return getattr(self, "__offsets")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,8 +22,8 @@ import datetime as dt
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from accounting import db
|
from ... import db
|
||||||
from accounting.models import JournalEntry
|
from ...models import JournalEntry
|
||||||
|
|
||||||
|
|
||||||
def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
|
def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
|
||||||
@@ -34,12 +34,12 @@ def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
|
|||||||
:param exclude: The journal entry ID to exclude.
|
:param exclude: The journal entry ID to exclude.
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] = [JournalEntry.date == date]
|
conditions: list[sa.ColumnElement[bool]] = [JournalEntry.date == 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] = db.session.scalars(
|
||||||
.filter(*conditions)\
|
sa.select(JournalEntry).where(*conditions)
|
||||||
.order_by(JournalEntry.no).all()
|
.order_by(JournalEntry.no)).all()
|
||||||
for i in range(len(journal_entries)):
|
for i in range(len(journal_entries)):
|
||||||
if journal_entries[i].no != i + 1:
|
if journal_entries[i].no != i + 1:
|
||||||
journal_entries[i].no = i + 1
|
journal_entries[i].no = i + 1
|
||||||
@@ -63,8 +63,9 @@ class JournalEntryReorderForm:
|
|||||||
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
journal_entries: list[JournalEntry] = JournalEntry.query\
|
journal_entries: list[JournalEntry] = db.session.scalars(
|
||||||
.filter(JournalEntry.date == self.date).all()
|
sa.select(JournalEntry)
|
||||||
|
.where(JournalEntry.date == self.date)).all()
|
||||||
|
|
||||||
# Collects the specified order.
|
# Collects the specified order.
|
||||||
orders: dict[JournalEntry, int] = {}
|
orders: dict[JournalEntry, int] = {}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"""The account option for the journal entry management.
|
"""The account option for the journal entry management.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from accounting.models import Account
|
from ...models import Account
|
||||||
|
|
||||||
|
|
||||||
class AccountOption:
|
class AccountOption:
|
||||||
@@ -28,7 +28,7 @@ class AccountOption:
|
|||||||
|
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
"""
|
"""
|
||||||
self.id: str = account.id
|
self.id: int = account.id
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
self.code: str = account.code
|
self.code: str = account.code
|
||||||
"""The account code."""
|
"""The account code."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,9 +22,9 @@ from typing import Literal
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from ... import db
|
||||||
from accounting.models import Account, JournalEntryLineItem
|
from ...models import Account, JournalEntryLineItem
|
||||||
from accounting.utils.options import options, Recurring
|
from ...utils.options import options, Recurring
|
||||||
|
|
||||||
|
|
||||||
class DescriptionAccount:
|
class DescriptionAccount:
|
||||||
@@ -272,15 +272,17 @@ class DescriptionEditor:
|
|||||||
select: sa.Select = sa.Select(debit_credit, tag_type, tag,
|
select: sa.Select = sa.Select(debit_credit, tag_type, tag,
|
||||||
JournalEntryLineItem.account_id,
|
JournalEntryLineItem.account_id,
|
||||||
sa.func.count().label("freq"))\
|
sa.func.count().label("freq"))\
|
||||||
.filter(JournalEntryLineItem.description.is_not(None),
|
.where(JournalEntryLineItem.description.is_not(None),
|
||||||
JournalEntryLineItem.description.like("_%—_%"),
|
JournalEntryLineItem.description.like("_%—_%"),
|
||||||
JournalEntryLineItem.original_line_item_id.is_(None))\
|
JournalEntryLineItem.original_line_item_id.is_(None))\
|
||||||
.group_by(debit_credit, tag_type, tag,
|
.group_by(debit_credit, tag_type, tag,
|
||||||
JournalEntryLineItem.account_id)
|
JournalEntryLineItem.account_id)
|
||||||
result: list[sa.Row] = db.session.execute(select).all()
|
result: list[sa.Row] = db.session.execute(select).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in db.session.scalars(
|
||||||
.filter(Account.id.in_({x.account_id for x in result})).all()}
|
sa.select(Account)
|
||||||
|
.where(Account.id.in_({x.account_id for x in result})))
|
||||||
|
.unique()}
|
||||||
debit_credit_dict: dict[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}}
|
||||||
@@ -315,20 +317,21 @@ class DescriptionEditor:
|
|||||||
if len(codes) == 0:
|
if len(codes) == 0:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_condition(code0: str) -> sa.BinaryExpression:
|
def get_condition(code0: str) -> sa.ColumnElement[bool]:
|
||||||
m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
|
m: re.Match[str] | None = re.match(r"^(\d{4})-(\d{3})$", code0)
|
||||||
assert m is not None,\
|
assert m is not None, \
|
||||||
f"Malformed account code \"{code0}\" for regular transactions."
|
f"Malformed account code \"{code0}\" for regular transactions."
|
||||||
return sa.and_(Account.base_code == m.group(1),
|
return sa.and_(Account.base_code == m.group(1),
|
||||||
Account.no == int(m.group(2)))
|
Account.no == int(m.group(2)))
|
||||||
|
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [get_condition(x) for x in codes]
|
= [get_condition(x) for x in codes]
|
||||||
accounts: dict[str, Account] \
|
accounts: dict[str, Account] \
|
||||||
= {x.code: x for x in
|
= {x.code: x for x in
|
||||||
Account.query.filter(sa.or_(*conditions)).all()}
|
db.session.scalars(
|
||||||
|
sa.select(Account).where(sa.or_(*conditions))).unique()}
|
||||||
for code in codes:
|
for code in codes:
|
||||||
assert code in accounts,\
|
assert code in accounts, \
|
||||||
f"Unknown account \"{code}\" for regular transactions."
|
f"Unknown account \"{code}\" for regular transactions."
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,18 +18,16 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
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.journal_entry.forms import JournalEntryForm, \
|
from ..forms import JournalEntryForm, CashReceiptJournalEntryForm, \
|
||||||
CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
|
CashDisbursementJournalEntryForm, TransferJournalEntryForm
|
||||||
TransferJournalEntryForm
|
from ..forms.line_item import LineItemForm
|
||||||
from accounting.journal_entry.forms.line_item import LineItemForm
|
from ...models import JournalEntry
|
||||||
from accounting.models import JournalEntry
|
from ...template_globals import default_currency_code
|
||||||
from accounting.template_globals import default_currency_code
|
from ...utils.journal_entry_types import JournalEntryType
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryOperator(ABC):
|
class JournalEntryOperator(ABC):
|
||||||
@@ -39,7 +37,7 @@ class JournalEntryOperator(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def form(self) -> 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 +98,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) -> 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 +168,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) -> 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 +241,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) -> Type[JournalEntryForm]:
|
def form(self) -> type[JournalEntryForm]:
|
||||||
"""Returns the form class.
|
"""Returns the form class.
|
||||||
|
|
||||||
:return: The form class.
|
:return: The form class.
|
||||||
@@ -334,3 +332,4 @@ def get_journal_entry_op(journal_entry: JournalEntry,
|
|||||||
key=lambda x: x.CHECK_ORDER):
|
key=lambda x: x.CHECK_ORDER):
|
||||||
if journal_entry_type.is_my_type(journal_entry):
|
if journal_entry_type.is_my_type(journal_entry):
|
||||||
return journal_entry_type
|
return journal_entry_type
|
||||||
|
assert False
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,9 +22,9 @@ from decimal import Decimal
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting import db
|
from ... import db
|
||||||
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
from ...models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.utils.offset_alias import offset_alias
|
from ...utils.offset_alias import offset_alias
|
||||||
|
|
||||||
|
|
||||||
def get_selectable_original_line_items(
|
def get_selectable_original_line_items(
|
||||||
@@ -46,8 +46,8 @@ def get_selectable_original_line_items(
|
|||||||
(offset.c.id.in_(line_item_id_on_form), 0),
|
(offset.c.id.in_(line_item_id_on_form), 0),
|
||||||
(offset.c.is_debit == JournalEntryLineItem.is_debit, offset.c.amount),
|
(offset.c.is_debit == JournalEntryLineItem.is_debit, offset.c.amount),
|
||||||
else_=-offset.c.amount))).label("net_balance")
|
else_=-offset.c.amount))).label("net_balance")
|
||||||
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
|
conditions: list[sa.ColumnElement[bool]] = [Account.is_need_offset]
|
||||||
sub_conditions: list[sa.BinaryExpression] = []
|
sub_conditions: list[sa.ColumnElement[bool]] = []
|
||||||
if is_payable:
|
if is_payable:
|
||||||
sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
|
sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit)))
|
sa.not_(JournalEntryLineItem.is_debit)))
|
||||||
@@ -61,20 +61,21 @@ def get_selectable_original_line_items(
|
|||||||
.join(offset,
|
.join(offset,
|
||||||
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(*conditions)\
|
.where(*conditions)\
|
||||||
.group_by(JournalEntryLineItem.id)\
|
.group_by(JournalEntryLineItem.id)\
|
||||||
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
||||||
net_balances: dict[int, Decimal] \
|
net_balances: dict[int, Decimal] \
|
||||||
= {x.id: x.net_balance
|
= {x.id: x.net_balance
|
||||||
for x in db.session.execute(select_net_balances).all()}
|
for x in db.session.execute(select_net_balances)}
|
||||||
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
|
line_items: list[JournalEntryLineItem] = db.session.scalars(
|
||||||
.filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
|
sa.select(JournalEntryLineItem)
|
||||||
.join(JournalEntry)\
|
.where(JournalEntryLineItem.id.in_({x for x in net_balances}))
|
||||||
|
.join(JournalEntry)
|
||||||
.order_by(JournalEntry.date, JournalEntry.no,
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)\
|
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.currency),
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.account),
|
selectinload(JournalEntryLineItem.account),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry))).all()
|
||||||
line_items.reverse()
|
line_items.reverse()
|
||||||
for line_item in line_items:
|
for line_item in line_items:
|
||||||
line_item.net_balance = line_item.amount \
|
line_item.net_balance = line_item.amount \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,23 +22,24 @@ from urllib.parse import parse_qsl, urlencode
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Blueprint, render_template, session, redirect, request, \
|
from flask import Blueprint, render_template, session, redirect, request, \
|
||||||
flash, url_for
|
flash, url_for, Response
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import lazy_gettext
|
|
||||||
from accounting.models import JournalEntry
|
|
||||||
from accounting.utils.cast import s
|
|
||||||
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.permission import has_permission, can_view, can_edit
|
|
||||||
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, \
|
||||||
text2html
|
text2html
|
||||||
from .utils.operators import JournalEntryOperator, JOURNAL_ENTRY_TYPE_TO_OP, \
|
from .utils.operators import JournalEntryOperator, JOURNAL_ENTRY_TYPE_TO_OP, \
|
||||||
get_journal_entry_op
|
get_journal_entry_op
|
||||||
|
from .. import db
|
||||||
|
from ..locale import lazy_gettext
|
||||||
|
from ..models import JournalEntry
|
||||||
|
from ..utils.cast import s
|
||||||
|
from ..utils.flash_errors import flash_form_errors
|
||||||
|
from ..utils.journal_entry_types import JournalEntryType
|
||||||
|
from ..utils.next_uri import inherit_next, or_next
|
||||||
|
from ..utils.permission import has_permission, can_view, can_edit
|
||||||
|
from ..utils.timezone import get_tz_today
|
||||||
|
from ..utils.user import get_current_user_pk
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("journal-entry", __name__)
|
bp: Blueprint = Blueprint("journal-entry", __name__)
|
||||||
"""The view blueprint for the journal entry management."""
|
"""The view blueprint for the journal entry management."""
|
||||||
@@ -67,13 +68,13 @@ 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 = dt.date.today()
|
form.date.data = get_tz_today()
|
||||||
return journal_entry_op.render_create_template(form)
|
return journal_entry_op.render_create_template(form)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store")
|
@bp.post("store/<journalEntryType:journal_entry_type>", endpoint="store")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
|
def add_journal_entry(journal_entry_type: JournalEntryType) -> Response:
|
||||||
"""Adds a journal entry.
|
"""Adds a journal entry.
|
||||||
|
|
||||||
:param journal_entry_type: The journal entry type.
|
:param journal_entry_type: The journal entry type.
|
||||||
@@ -135,7 +136,7 @@ def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
|
|||||||
|
|
||||||
@bp.post("<journalEntry:journal_entry>/update", endpoint="update")
|
@bp.post("<journalEntry:journal_entry>/update", endpoint="update")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
def update_journal_entry(journal_entry: JournalEntry) -> Response:
|
||||||
"""Updates a journal entry.
|
"""Updates a journal entry.
|
||||||
|
|
||||||
:param journal_entry: The journal entry.
|
:param journal_entry: The journal entry.
|
||||||
@@ -168,7 +169,7 @@ def update_journal_entry(journal_entry: JournalEntry) -> redirect:
|
|||||||
|
|
||||||
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete")
|
@bp.post("<journalEntry:journal_entry>/delete", endpoint="delete")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
|
def delete_journal_entry(journal_entry: JournalEntry) -> Response:
|
||||||
"""Deletes a journal entry.
|
"""Deletes a journal entry.
|
||||||
|
|
||||||
:param journal_entry: The journal entry.
|
:param journal_entry: The journal entry.
|
||||||
@@ -194,16 +195,16 @@ def show_journal_entry_order(date: dt.date) -> str:
|
|||||||
:param 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] = db.session.scalars(
|
||||||
.filter(JournalEntry.date == date) \
|
sa.select(JournalEntry).where(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=date, list=journal_entries)
|
date=date, list=journal_entries)
|
||||||
|
|
||||||
|
|
||||||
@bp.post("dates/<date:date>", endpoint="sort")
|
@bp.post("dates/<date:date>", endpoint="sort")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def sort_journal_entries(date: dt.date) -> redirect:
|
def sort_journal_entries(date: dt.date) -> Response:
|
||||||
"""Reorders the journal entries in a date.
|
"""Reorders the journal entries in a date.
|
||||||
|
|
||||||
:param date: The date.
|
:param date: The date.
|
||||||
|
|||||||
+133
-125
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,20 +22,19 @@ from __future__ import annotations
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Type, Self
|
from typing import Self
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
from flask_babel import get_locale, get_babel
|
from flask_babel import get_locale, get_babel
|
||||||
from sqlalchemy import text
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
from accounting import db
|
from . import db
|
||||||
from accounting.locale import gettext
|
from .locale import gettext
|
||||||
from accounting.utils.user import user_cls, user_pk_column
|
from .utils.user import base_cls, user_cls, user_pk_column
|
||||||
|
|
||||||
|
|
||||||
class BaseAccount(db.Model):
|
class BaseAccount(base_cls):
|
||||||
"""A base account."""
|
"""A base account."""
|
||||||
__tablename__ = "accounting_base_accounts"
|
__tablename__ = "accounting_base_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
@@ -44,9 +43,9 @@ class BaseAccount(db.Model):
|
|||||||
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]] \
|
||||||
= db.relationship(back_populates="account", lazy=False)
|
= relationship(back_populates="account", lazy=False)
|
||||||
"""The localized titles."""
|
"""The localized titles."""
|
||||||
accounts: Mapped[list[Account]] = db.relationship(back_populates="base")
|
accounts: Mapped[list[Account]] = relationship(back_populates="base")
|
||||||
"""The descendant accounts under the base account."""
|
"""The descendant accounts under the base account."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -79,16 +78,16 @@ class BaseAccount(db.Model):
|
|||||||
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
|
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountL10n(db.Model):
|
class BaseAccountL10n(base_cls):
|
||||||
"""A localized base account title."""
|
"""A localized base account title."""
|
||||||
__tablename__ = "accounting_base_accounts_l10n"
|
__tablename__ = "accounting_base_accounts_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
account_code: Mapped[str] \
|
account_code: Mapped[str] \
|
||||||
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
= mapped_column(sa.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
"""The account code."""
|
"""The account code."""
|
||||||
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
|
account: Mapped[BaseAccount] = relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale: Mapped[str] = mapped_column(primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
@@ -96,47 +95,47 @@ class BaseAccountL10n(db.Model):
|
|||||||
"""The localized title."""
|
"""The localized title."""
|
||||||
|
|
||||||
|
|
||||||
class Account(db.Model):
|
class Account(base_cls):
|
||||||
"""An account."""
|
"""An account."""
|
||||||
__tablename__ = "accounting_accounts"
|
__tablename__ = "accounting_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
base_code: Mapped[str] \
|
base_code: Mapped[str] \
|
||||||
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
= mapped_column(sa.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"))
|
ondelete="CASCADE"))
|
||||||
"""The code of the base account."""
|
"""The code of the base account."""
|
||||||
base: Mapped[BaseAccount] = db.relationship(back_populates="accounts")
|
base: Mapped[BaseAccount] = relationship(back_populates="accounts")
|
||||||
"""The base account."""
|
"""The base account."""
|
||||||
no: Mapped[int] = mapped_column(default=text("1"))
|
no: Mapped[int] = mapped_column(default=sa.text("1"))
|
||||||
"""The account number under the base account."""
|
"""The account number under the base account."""
|
||||||
title_l10n: Mapped[str] = mapped_column("title")
|
title_l10n: Mapped[str] = mapped_column("title")
|
||||||
"""The title."""
|
"""The title."""
|
||||||
is_need_offset: Mapped[bool] = mapped_column(default=False)
|
is_need_offset: Mapped[bool] = mapped_column(default=False)
|
||||||
"""Whether the journal entry line items of this account need offset."""
|
"""Whether the journal entry line items of this account need offset."""
|
||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was created."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the user who created the record."""
|
"""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] = relationship(foreign_keys=created_by_id)
|
||||||
"""The user who created the record."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was last updated."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the last user who updated the record."""
|
"""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] = relationship(foreign_keys=updated_by_id)
|
||||||
"""The last user who updated the record."""
|
"""The last user who updated the record."""
|
||||||
l10n: Mapped[list[AccountL10n]] \
|
l10n: Mapped[list[AccountL10n]] \
|
||||||
= db.relationship(back_populates="account", lazy=False)
|
= relationship(back_populates="account", lazy=False)
|
||||||
"""The localized titles."""
|
"""The localized titles."""
|
||||||
line_items: Mapped[list[JournalEntryLineItem]] \
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
= db.relationship(back_populates="account")
|
= relationship(back_populates="account")
|
||||||
"""The journal entry line items."""
|
"""The journal entry line items."""
|
||||||
|
|
||||||
CASH_CODE: str = "1111-001"
|
CASH_CODE: str = "1111-001"
|
||||||
@@ -268,9 +267,10 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
AccountL10n.query.filter(AccountL10n.account == self).delete()
|
db.session.execute(sa.delete(AccountL10n)
|
||||||
cls: Type[Self] = self.__class__
|
.where(AccountL10n.account == self))
|
||||||
cls.query.filter(cls.id == self.id).delete()
|
cls: type[Self] = self.__class__
|
||||||
|
db.session.execute(sa.delete(cls).where(cls.id == self.id))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_by_code(cls, code: str) -> Self | None:
|
def find_by_code(cls, code: str) -> Self | None:
|
||||||
@@ -279,11 +279,12 @@ class Account(db.Model):
|
|||||||
:param code: The code.
|
:param code: The code.
|
||||||
:return: The account, or None if this account does not exist.
|
:return: The account, or None if this account does not exist.
|
||||||
"""
|
"""
|
||||||
m = re.match(r"^([1-9]{4})-(\d{3})$", code)
|
m: re.Match[str] | None = re.match(r"^([1-9]{4})-(\d{3})$", code)
|
||||||
if m is None:
|
if m is None:
|
||||||
return None
|
return None
|
||||||
return cls.query.filter(cls.base_code == m.group(1),
|
return db.session.scalar(
|
||||||
cls.no == int(m.group(2))).first()
|
sa.select(cls).where(cls.base_code == m.group(1),
|
||||||
|
cls.no == int(m.group(2))))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectable_debit(cls) -> list[Self]:
|
def selectable_debit(cls) -> list[Self]:
|
||||||
@@ -292,22 +293,22 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The selectable debit accounts.
|
:return: The selectable debit accounts.
|
||||||
"""
|
"""
|
||||||
return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
|
return db.session.scalars(
|
||||||
sa.and_(cls.base_code.startswith("2"),
|
sa.select(cls)
|
||||||
sa.not_(cls.is_need_offset)),
|
.where(sa.or_(cls.base_code.startswith("1"),
|
||||||
cls.base_code.startswith("3"),
|
sa.and_(cls.base_code.startswith("2"),
|
||||||
cls.base_code.startswith("5"),
|
sa.not_(cls.is_need_offset)),
|
||||||
cls.base_code.startswith("6"),
|
cls.base_code.startswith("3"),
|
||||||
cls.base_code.startswith("75"),
|
cls.base_code.startswith("5"),
|
||||||
cls.base_code.startswith("76"),
|
cls.base_code.startswith("6"),
|
||||||
cls.base_code.startswith("77"),
|
cls.base_code.startswith("75"),
|
||||||
cls.base_code.startswith("78"),
|
cls.base_code.startswith("76"),
|
||||||
cls.base_code.startswith("8"),
|
cls.base_code.startswith("77"),
|
||||||
cls.base_code.startswith("9")),
|
cls.base_code.startswith("78"),
|
||||||
sa.not_(sa.and_(cls.base_code == "3351",
|
cls.base_code.startswith("8"),
|
||||||
cls.no == 1)),
|
cls.base_code.startswith("9")),
|
||||||
cls.base_code != "3353")\
|
cls.base_code != "3353")
|
||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no)).unique().all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def selectable_credit(cls) -> list[Self]:
|
def selectable_credit(cls) -> list[Self]:
|
||||||
@@ -316,21 +317,21 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The selectable debit accounts.
|
:return: The selectable debit accounts.
|
||||||
"""
|
"""
|
||||||
return cls.query.filter(sa.or_(sa.and_(cls.base_code.startswith("1"),
|
return db.session.scalars(
|
||||||
sa.not_(cls.is_need_offset)),
|
sa.select(cls)
|
||||||
cls.base_code.startswith("2"),
|
.where(sa.or_(sa.and_(cls.base_code.startswith("1"),
|
||||||
cls.base_code.startswith("3"),
|
sa.not_(cls.is_need_offset)),
|
||||||
cls.base_code.startswith("4"),
|
cls.base_code.startswith("2"),
|
||||||
cls.base_code.startswith("71"),
|
cls.base_code.startswith("3"),
|
||||||
cls.base_code.startswith("72"),
|
cls.base_code.startswith("4"),
|
||||||
cls.base_code.startswith("73"),
|
cls.base_code.startswith("71"),
|
||||||
cls.base_code.startswith("74"),
|
cls.base_code.startswith("72"),
|
||||||
cls.base_code.startswith("8"),
|
cls.base_code.startswith("73"),
|
||||||
cls.base_code.startswith("9")),
|
cls.base_code.startswith("74"),
|
||||||
sa.not_(sa.and_(cls.base_code == "3351",
|
cls.base_code.startswith("8"),
|
||||||
cls.no == 1)),
|
cls.base_code.startswith("9")),
|
||||||
cls.base_code != "3353")\
|
cls.base_code != "3353")
|
||||||
.order_by(cls.base_code, cls.no).all()
|
.order_by(cls.base_code, cls.no)).unique().all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cash(cls) -> Self:
|
def cash(cls) -> Self:
|
||||||
@@ -338,7 +339,9 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The cash account
|
:return: The cash account
|
||||||
"""
|
"""
|
||||||
return cls.find_by_code(cls.CASH_CODE)
|
account: Self | None = cls.find_by_code(cls.CASH_CODE)
|
||||||
|
assert account is not None
|
||||||
|
return account
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def accumulated_change(cls) -> Self:
|
def accumulated_change(cls) -> Self:
|
||||||
@@ -346,19 +349,21 @@ class Account(db.Model):
|
|||||||
|
|
||||||
:return: The accumulated-change account
|
:return: The accumulated-change account
|
||||||
"""
|
"""
|
||||||
return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
|
account: Self | None = cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
|
||||||
|
assert account is not None
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
class AccountL10n(db.Model):
|
class AccountL10n(base_cls):
|
||||||
"""A localized account title."""
|
"""A localized account title."""
|
||||||
__tablename__ = "accounting_accounts_l10n"
|
__tablename__ = "accounting_accounts_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
account_id: Mapped[int] \
|
account_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE",
|
= mapped_column(sa.ForeignKey(Account.id, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
account: Mapped[Account] = db.relationship(back_populates="l10n")
|
account: Mapped[Account] = relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale: Mapped[str] = mapped_column(primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
@@ -366,7 +371,7 @@ class AccountL10n(db.Model):
|
|||||||
"""The localized title."""
|
"""The localized title."""
|
||||||
|
|
||||||
|
|
||||||
class Currency(db.Model):
|
class Currency(base_cls):
|
||||||
"""A currency."""
|
"""A currency."""
|
||||||
__tablename__ = "accounting_currencies"
|
__tablename__ = "accounting_currencies"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
@@ -375,29 +380,29 @@ class Currency(db.Model):
|
|||||||
name_l10n: Mapped[str] = mapped_column("name")
|
name_l10n: Mapped[str] = mapped_column("name")
|
||||||
"""The currency name."""
|
"""The currency name."""
|
||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was created."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the user who created the record."""
|
"""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] = relationship(foreign_keys=created_by_id)
|
||||||
"""The user who created the record."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was last updated."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the last user who updated the record."""
|
"""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)
|
= relationship(foreign_keys=updated_by_id)
|
||||||
"""The last user who updated the record."""
|
"""The last user who updated the record."""
|
||||||
l10n: Mapped[list[CurrencyL10n]] \
|
l10n: Mapped[list[CurrencyL10n]] \
|
||||||
= db.relationship(back_populates="currency", lazy=False)
|
= relationship(back_populates="currency", lazy=False)
|
||||||
"""The localized names."""
|
"""The localized names."""
|
||||||
line_items: Mapped[list[JournalEntryLineItem]] \
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
= db.relationship(back_populates="currency")
|
= relationship(back_populates="currency")
|
||||||
"""The journal entry line items."""
|
"""The journal entry line items."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -462,7 +467,7 @@ class Currency(db.Model):
|
|||||||
|
|
||||||
:return: True if the currency can be deleted, or False otherwise.
|
:return: True if the currency can be deleted, or False otherwise.
|
||||||
"""
|
"""
|
||||||
from accounting.template_globals import default_currency_code
|
from .template_globals import default_currency_code
|
||||||
if self.code == default_currency_code():
|
if self.code == default_currency_code():
|
||||||
return False
|
return False
|
||||||
return len(self.line_items) == 0
|
return len(self.line_items) == 0
|
||||||
@@ -472,21 +477,22 @@ class Currency(db.Model):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
|
db.session.execute(
|
||||||
cls: Type[Self] = self.__class__
|
sa.delete(CurrencyL10n)
|
||||||
cls.query.filter(cls.code == self.code).delete()
|
.where(CurrencyL10n.currency_code == self.code))
|
||||||
|
db.session.delete(self)
|
||||||
|
|
||||||
|
|
||||||
class CurrencyL10n(db.Model):
|
class CurrencyL10n(base_cls):
|
||||||
"""A localized currency name."""
|
"""A localized currency name."""
|
||||||
__tablename__ = "accounting_currencies_l10n"
|
__tablename__ = "accounting_currencies_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
currency_code: Mapped[str] \
|
currency_code: Mapped[str] \
|
||||||
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE",
|
= mapped_column(sa.ForeignKey(Currency.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
"""The currency code."""
|
"""The currency code."""
|
||||||
currency: Mapped[Currency] = db.relationship(back_populates="l10n")
|
currency: Mapped[Currency] = relationship(back_populates="l10n")
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
locale: Mapped[str] = mapped_column(primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
@@ -537,7 +543,7 @@ class JournalEntryCurrency:
|
|||||||
return sum([x.amount for x in self.credit])
|
return sum([x.amount for x in self.credit])
|
||||||
|
|
||||||
|
|
||||||
class JournalEntry(db.Model):
|
class JournalEntry(base_cls):
|
||||||
"""A journal entry."""
|
"""A journal entry."""
|
||||||
__tablename__ = "accounting_journal_entries"
|
__tablename__ = "accounting_journal_entries"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
@@ -545,30 +551,30 @@ class JournalEntry(db.Model):
|
|||||||
"""The journal entry ID."""
|
"""The journal entry ID."""
|
||||||
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=sa.text("1"))
|
||||||
"""The journal entry 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(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was created."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the user who created the record."""
|
"""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] = relationship(foreign_keys=created_by_id)
|
||||||
"""The user who created the record."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was last updated."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the last user who updated the record."""
|
"""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] = relationship(foreign_keys=updated_by_id)
|
||||||
"""The last user who updated the record."""
|
"""The last user who updated the record."""
|
||||||
line_items: Mapped[list[JournalEntryLineItem]] \
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
= db.relationship(back_populates="journal_entry")
|
= relationship(back_populates="journal_entry")
|
||||||
"""The line items."""
|
"""The line items."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -649,48 +655,49 @@ class JournalEntry(db.Model):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
JournalEntryLineItem.query\
|
db.session.execute(
|
||||||
.filter(JournalEntryLineItem.journal_entry_id == self.id).delete()
|
sa.delete(JournalEntryLineItem)
|
||||||
|
.where(JournalEntryLineItem.journal_entry_id == self.id))
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryLineItem(db.Model):
|
class JournalEntryLineItem(base_cls):
|
||||||
"""A line item in the journal entry."""
|
"""A line item in the journal entry."""
|
||||||
__tablename__ = "accounting_journal_entry_line_items"
|
__tablename__ = "accounting_journal_entry_line_items"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
"""The line item ID."""
|
"""The line item ID."""
|
||||||
journal_entry_id: Mapped[int] \
|
journal_entry_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(JournalEntry.id, onupdate="CASCADE",
|
= mapped_column(sa.ForeignKey(JournalEntry.id, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"))
|
ondelete="CASCADE"))
|
||||||
"""The journal entry ID."""
|
"""The journal entry ID."""
|
||||||
journal_entry: Mapped[JournalEntry] \
|
journal_entry: Mapped[JournalEntry] \
|
||||||
= db.relationship(back_populates="line_items")
|
= relationship(back_populates="line_items")
|
||||||
"""The journal entry."""
|
"""The journal entry."""
|
||||||
is_debit: Mapped[bool]
|
is_debit: Mapped[bool]
|
||||||
"""True for a debit line item, or False for a credit line item."""
|
"""True for a debit line item, or False for a credit line item."""
|
||||||
no: Mapped[int]
|
no: Mapped[int]
|
||||||
"""The line item number under the journal entry and debit or credit."""
|
"""The line item number under the journal entry and debit or credit."""
|
||||||
original_line_item_id: Mapped[int | None] \
|
original_line_item_id: Mapped[int | None] \
|
||||||
= mapped_column(db.ForeignKey(id, onupdate="CASCADE"))
|
= mapped_column(sa.ForeignKey(id, onupdate="CASCADE"))
|
||||||
"""The ID of the original line item."""
|
"""The ID of the original line item."""
|
||||||
original_line_item: Mapped[JournalEntryLineItem | None] \
|
original_line_item: Mapped[JournalEntryLineItem | None] \
|
||||||
= db.relationship(remote_side=id, passive_deletes=True)
|
= relationship(remote_side=id, passive_deletes=True)
|
||||||
"""The original line item."""
|
"""The original line item."""
|
||||||
currency_code: Mapped[str] \
|
currency_code: Mapped[str] \
|
||||||
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE"))
|
= mapped_column(sa.ForeignKey(Currency.code, onupdate="CASCADE"))
|
||||||
"""The currency code."""
|
"""The currency code."""
|
||||||
currency: Mapped[Currency] = db.relationship(back_populates="line_items")
|
currency: Mapped[Currency] = relationship(back_populates="line_items")
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
account_id: Mapped[int] \
|
account_id: Mapped[int] \
|
||||||
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE"))
|
= mapped_column(sa.ForeignKey(Account.id, onupdate="CASCADE"))
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
account: Mapped[Account] \
|
account: Mapped[Account] \
|
||||||
= db.relationship(back_populates="line_items", lazy=False)
|
= relationship(back_populates="line_items", lazy=False)
|
||||||
"""The account."""
|
"""The account."""
|
||||||
description: Mapped[str | None]
|
description: Mapped[str | None]
|
||||||
"""The description."""
|
"""The description."""
|
||||||
amount: Mapped[Decimal] = mapped_column(db.Numeric(14, 2))
|
amount: Mapped[Decimal] = mapped_column(sa.Numeric(14, 2))
|
||||||
"""The amount."""
|
"""The amount."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -699,7 +706,7 @@ class JournalEntryLineItem(db.Model):
|
|||||||
:return: The string representation of the line item.
|
:return: The string representation of the line item.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "__str"):
|
if not hasattr(self, "__str"):
|
||||||
from accounting.template_filters import format_date, format_amount
|
from .template_filters import format_date, format_amount
|
||||||
setattr(self, "__str",
|
setattr(self, "__str",
|
||||||
gettext("%(date)s %(description)s %(amount)s",
|
gettext("%(date)s %(description)s %(amount)s",
|
||||||
date=format_date(self.journal_entry.date),
|
date=format_date(self.journal_entry.date),
|
||||||
@@ -815,11 +822,12 @@ class JournalEntryLineItem(db.Model):
|
|||||||
:return: The offset items.
|
:return: The offset items.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "__offsets"):
|
if not hasattr(self, "__offsets"):
|
||||||
cls: Type[Self] = self.__class__
|
cls: type[Self] = self.__class__
|
||||||
offsets: list[Self] = cls.query.join(JournalEntry)\
|
offsets: list[Self] = db.session.scalars(
|
||||||
.filter(JournalEntryLineItem.original_line_item_id == self.id)\
|
sa.select(cls).join(JournalEntry)
|
||||||
|
.where(cls.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)).unique().all()
|
||||||
setattr(self, "__offsets", offsets)
|
setattr(self, "__offsets", offsets)
|
||||||
return getattr(self, "__offsets")
|
return getattr(self, "__offsets")
|
||||||
|
|
||||||
@@ -880,29 +888,29 @@ class JournalEntryLineItem(db.Model):
|
|||||||
format_amount(self.amount)]
|
format_amount(self.amount)]
|
||||||
|
|
||||||
|
|
||||||
class Option(db.Model):
|
class Option(base_cls):
|
||||||
"""An option."""
|
"""An option."""
|
||||||
__tablename__ = "accounting_options"
|
__tablename__ = "accounting_options"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
name: Mapped[str] = mapped_column(primary_key=True)
|
name: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The name."""
|
"""The name."""
|
||||||
value: Mapped[str] = mapped_column(db.Text)
|
value: Mapped[str] = mapped_column(sa.Text)
|
||||||
"""The option value."""
|
"""The option value."""
|
||||||
created_at: Mapped[dt.datetime] \
|
created_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was created."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the user who created the record."""
|
"""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] = relationship(foreign_keys=created_by_id)
|
||||||
"""The user who created the record."""
|
"""The user who created the record."""
|
||||||
updated_at: Mapped[dt.datetime] \
|
updated_at: Mapped[dt.datetime] \
|
||||||
= mapped_column(db.DateTime(timezone=True),
|
= mapped_column(sa.DateTime(timezone=True),
|
||||||
server_default=db.func.now())
|
server_default=sa.func.now())
|
||||||
"""The date and time when this record was last updated."""
|
"""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(sa.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
"""The ID of the last user who updated the record."""
|
"""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] = relationship(foreign_keys=updated_by_id)
|
||||||
"""The last user who updated the record."""
|
"""The last user who updated the record."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,13 +23,13 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import StringField, FieldList, FormField, IntegerField
|
from wtforms import StringField, FieldList, FormField, IntegerField
|
||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, ValidationError
|
||||||
|
|
||||||
from accounting.forms import ACCOUNT_REQUIRED, CurrencyExists, AccountExists, \
|
from ..forms import ACCOUNT_REQUIRED, CurrencyExists, AccountExists, \
|
||||||
IsDebitAccount, IsCreditAccount
|
IsDebitAccount, IsCreditAccount
|
||||||
from accounting.locale import lazy_gettext
|
from ..locale import lazy_gettext
|
||||||
from accounting.models import Account
|
from ..models import Account
|
||||||
from accounting.utils.current_account import CurrentAccount
|
from ..utils.current_account import CurrentAccount
|
||||||
from accounting.utils.options import Options
|
from ..utils.options import Options
|
||||||
from accounting.utils.strip_text import strip_text
|
from ..utils.strip_text import strip_text
|
||||||
|
|
||||||
|
|
||||||
class CurrentAccountExists:
|
class CurrentAccountExists:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,16 +20,16 @@
|
|||||||
from urllib.parse import parse_qsl, urlencode
|
from urllib.parse import parse_qsl, urlencode
|
||||||
|
|
||||||
from flask import Blueprint, render_template, redirect, session, request, \
|
from flask import Blueprint, render_template, redirect, session, request, \
|
||||||
flash, url_for
|
flash, url_for, Response
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
from accounting.locale import lazy_gettext
|
|
||||||
from accounting.utils.cast import s
|
|
||||||
from accounting.utils.flash_errors import flash_form_errors
|
|
||||||
from accounting.utils.next_uri import inherit_next
|
|
||||||
from accounting.utils.options import options
|
|
||||||
from accounting.utils.permission import has_permission, can_admin
|
|
||||||
from .forms import OptionForm
|
from .forms import OptionForm
|
||||||
|
from ..locale import lazy_gettext
|
||||||
|
from ..utils.cast import s
|
||||||
|
from ..utils.flash_errors import flash_form_errors
|
||||||
|
from ..utils.next_uri import inherit_next
|
||||||
|
from ..utils.options import options
|
||||||
|
from ..utils.permission import has_permission, can_admin
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("option", __name__)
|
bp: Blueprint = Blueprint("option", __name__)
|
||||||
"""The view blueprint for the currency management."""
|
"""The view blueprint for the currency management."""
|
||||||
@@ -64,7 +64,7 @@ def show_option_form() -> str:
|
|||||||
|
|
||||||
@bp.post("update", endpoint="update")
|
@bp.post("update", endpoint="update")
|
||||||
@has_permission(can_admin)
|
@has_permission(can_admin)
|
||||||
def update_options() -> redirect:
|
def update_options() -> Response:
|
||||||
"""Updates the options.
|
"""Updates the options.
|
||||||
|
|
||||||
:return: The redirection to the option form.
|
:return: The redirection to the option form.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,9 +22,9 @@ import re
|
|||||||
from flask import abort
|
from flask import abort
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from accounting.models import Account
|
|
||||||
from accounting.utils.current_account import CurrentAccount
|
|
||||||
from .period import Period, get_period
|
from .period import Period, get_period
|
||||||
|
from ..models import Account
|
||||||
|
from ..utils.current_account import CurrentAccount
|
||||||
|
|
||||||
|
|
||||||
class PeriodConverter(BaseConverter):
|
class PeriodConverter(BaseConverter):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,10 +23,14 @@ This file is largely taken from the NanoParma ERP project, first written in
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
from accounting.models import JournalEntry
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from .period import Period
|
from .period import Period
|
||||||
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
|
||||||
LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
|
LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
|
||||||
|
from ... import db
|
||||||
|
from ...models import JournalEntry
|
||||||
|
from ...utils.timezone import get_tz_today
|
||||||
|
|
||||||
|
|
||||||
class PeriodChooser:
|
class PeriodChooser:
|
||||||
@@ -61,8 +65,8 @@ class PeriodChooser:
|
|||||||
self.url_template: str = get_url(TemplatePeriod())
|
self.url_template: str = get_url(TemplatePeriod())
|
||||||
"""The URL template."""
|
"""The URL template."""
|
||||||
|
|
||||||
first: JournalEntry | None \
|
first: JournalEntry | None = db.session.scalar(
|
||||||
= JournalEntry.query.order_by(JournalEntry.date).first()
|
sa.select(JournalEntry).order_by(JournalEntry.date))
|
||||||
start: dt.date | None = None if first is None else first.date
|
start: dt.date | None = None if first is None else first.date
|
||||||
|
|
||||||
# Attributes
|
# Attributes
|
||||||
@@ -80,7 +84,7 @@ class PeriodChooser:
|
|||||||
"""The available years."""
|
"""The available years."""
|
||||||
|
|
||||||
if self.has_data:
|
if self.has_data:
|
||||||
today: dt.date = dt.date.today()
|
today: dt.date = get_tz_today()
|
||||||
self.has_last_month = start < dt.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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"""
|
"""
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from ...locale import gettext
|
||||||
|
|
||||||
|
|
||||||
def get_desc(start: dt.date | None, end: dt.date | None) -> str:
|
def get_desc(start: dt.date | None, end: dt.date | None) -> str:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,7 +21,6 @@ import calendar
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
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, \
|
||||||
@@ -40,7 +39,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, Type[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(),
|
||||||
@@ -68,6 +67,7 @@ def __parse_spec(text: str) -> tuple[dt.date | None, dt.date | None]:
|
|||||||
"""
|
"""
|
||||||
if text == "-":
|
if text == "-":
|
||||||
return None, None
|
return None, None
|
||||||
|
m: re.Match[str] | None
|
||||||
m = re.match(f"^{DATE_SPEC_RE}$", text)
|
m = re.match(f"^{DATE_SPEC_RE}$", text)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return __get_start(m[1], m[2], m[3]), \
|
return __get_start(m[1], m[2], m[3]), \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,15 +19,16 @@
|
|||||||
"""
|
"""
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
from accounting.locale import gettext
|
|
||||||
from .month_end import month_end
|
from .month_end import month_end
|
||||||
from .period import Period
|
from .period import Period
|
||||||
|
from ...locale import gettext
|
||||||
|
from ...utils.timezone import get_tz_today
|
||||||
|
|
||||||
|
|
||||||
class ThisMonth(Period):
|
class ThisMonth(Period):
|
||||||
"""The period of this month."""
|
"""The period of this month."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
today: dt.date = dt.date.today()
|
today: dt.date = get_tz_today()
|
||||||
this_month_start: dt.date = dt.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
|
||||||
@@ -43,7 +44,7 @@ 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: dt.date = dt.date.today()
|
today: dt.date = get_tz_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:
|
||||||
@@ -63,7 +64,7 @@ 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: dt.date = dt.date.today()
|
today: dt.date = get_tz_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:
|
||||||
@@ -82,7 +83,7 @@ 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 = dt.date.today().year
|
year: int = get_tz_today().year
|
||||||
start: dt.date = dt.date(year, 1, 1)
|
start: dt.date = dt.date(year, 1, 1)
|
||||||
end: dt.date = dt.date(year, 12, 31)
|
end: dt.date = dt.date(year, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
@@ -97,7 +98,7 @@ 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 = dt.date.today().year
|
year: int = get_tz_today().year
|
||||||
start: dt.date = dt.date(year - 1, 1, 1)
|
start: dt.date = dt.date(year - 1, 1, 1)
|
||||||
end: dt.date = dt.date(year - 1, 12, 31)
|
end: dt.date = dt.date(year - 1, 12, 31)
|
||||||
super().__init__(start, end)
|
super().__init__(start, end)
|
||||||
@@ -112,7 +113,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: dt.date = dt.date.today()
|
today: dt.date = get_tz_today()
|
||||||
super().__init__(today, today)
|
super().__init__(today, today)
|
||||||
self.is_today = True
|
self.is_today = True
|
||||||
|
|
||||||
@@ -125,7 +126,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: dt.date = dt.date.today() - dt.timedelta(days=1)
|
yesterday: dt.date = get_tz_today() - dt.timedelta(days=1)
|
||||||
super().__init__(yesterday, yesterday)
|
super().__init__(yesterday, yesterday)
|
||||||
self.is_yesterday = True
|
self.is_yesterday = True
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ def get_spec(start: dt.date | None, end: dt.date | None) -> str:
|
|||||||
:param end: The end of the period.
|
:param end: The end of the period.
|
||||||
:return: The period specification.
|
:return: The period specification.
|
||||||
"""
|
"""
|
||||||
if start is None and end is None:
|
|
||||||
return "-"
|
|
||||||
if end is None:
|
|
||||||
return __get_since_spec(start)
|
|
||||||
if start is None:
|
if start is None:
|
||||||
return __get_until_spec(end)
|
return "-" if end is None else __get_until_spec(end)
|
||||||
|
return __get_since_spec(start) if end is None else __get_spec(start, end)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_spec(start: dt.date, end: dt.date) -> str:
|
||||||
|
"""Returns the period specification with both start and end.
|
||||||
|
|
||||||
|
:param start: The start of the period.
|
||||||
|
:param end: The end of the period.
|
||||||
|
:return: The period specification.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return __get_year_spec(start, end)
|
return __get_year_spec(start, end)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,20 +22,18 @@ from decimal import Decimal
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
|
|
||||||
from accounting import db
|
from ..period import Period, PeriodChooser
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
|
from ..utils.base_report import BaseReport
|
||||||
|
from ..utils.csv_export import BaseCSVRow, csv_download, period_spec
|
||||||
|
from ..utils.option_link import OptionLink
|
||||||
|
from ..utils.report_chooser import ReportChooser
|
||||||
|
from ..utils.report_type import ReportType
|
||||||
|
from ..utils.urls import ledger_url, balance_sheet_url, income_statement_url
|
||||||
|
from ... import db
|
||||||
|
from ...locale import gettext
|
||||||
|
from ...models import Currency, BaseAccount, Account, JournalEntry, \
|
||||||
JournalEntryLineItem
|
JournalEntryLineItem
|
||||||
from accounting.report.period import Period, PeriodChooser
|
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
|
||||||
from accounting.report.utils.base_report import BaseReport
|
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
|
||||||
period_spec
|
|
||||||
from accounting.report.utils.option_link import OptionLink
|
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
|
||||||
from accounting.report.utils.report_type import ReportType
|
|
||||||
from accounting.report.utils.urls import ledger_url, balance_sheet_url, \
|
|
||||||
income_statement_url
|
|
||||||
|
|
||||||
|
|
||||||
class ReportAccount:
|
class ReportAccount:
|
||||||
@@ -121,9 +119,9 @@ class AccountCollector:
|
|||||||
|
|
||||||
:return: The balances.
|
:return: The balances.
|
||||||
"""
|
"""
|
||||||
sub_conditions: list[sa.BinaryExpression] \
|
sub_conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
|
= [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
sa.or_(*sub_conditions)]
|
sa.or_(*sub_conditions)]
|
||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
@@ -135,16 +133,17 @@ class AccountCollector:
|
|||||||
= sa.select(Account.id, Account.base_code, Account.no,
|
= sa.select(Account.id, Account.base_code, Account.no,
|
||||||
balance_func)\
|
balance_func)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.where(*conditions)\
|
||||||
.group_by(Account.id, Account.base_code, Account.no)\
|
.group_by(Account.id, Account.base_code, Account.no)\
|
||||||
.having(balance_func != 0)\
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
account_balances: list[sa.Row] \
|
account_balances: list[sa.Row] \
|
||||||
= db.session.execute(select_balance).all()
|
= db.session.execute(select_balance).all()
|
||||||
self.__all_accounts: list[Account] = Account.query\
|
self.__all_accounts: list[Account] = db.session.scalars(
|
||||||
.filter(sa.or_(Account.id.in_({x.id for x in account_balances}),
|
sa.select(Account)
|
||||||
Account.base_code == "3351",
|
.where(sa.or_(Account.id.in_({x.id for x in account_balances}),
|
||||||
Account.base_code == "3353")).all()
|
Account.base_code == "3351",
|
||||||
|
Account.base_code == "3353"))).unique().all()
|
||||||
"""The accounts."""
|
"""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}
|
||||||
@@ -180,7 +179,7 @@ class AccountCollector:
|
|||||||
"""
|
"""
|
||||||
if self.__period.start is None:
|
if self.__period.start is None:
|
||||||
return None
|
return None
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
JournalEntry.date < self.__period.start]
|
JournalEntry.date < self.__period.start]
|
||||||
return self.__query_balance(conditions)
|
return self.__query_balance(conditions)
|
||||||
@@ -199,7 +198,7 @@ class AccountCollector:
|
|||||||
|
|
||||||
:return: The net income or loss for current period.
|
:return: The net income or loss for current period.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code]
|
= [JournalEntryLineItem.currency_code == self.__currency.code]
|
||||||
if self.__period.start is not None:
|
if self.__period.start is not None:
|
||||||
conditions.append(JournalEntry.date >= self.__period.start)
|
conditions.append(JournalEntry.date >= self.__period.start)
|
||||||
@@ -208,7 +207,7 @@ class AccountCollector:
|
|||||||
return self.__query_balance(conditions)
|
return self.__query_balance(conditions)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __query_balance(conditions: list[sa.BinaryExpression])\
|
def __query_balance(conditions: list[sa.ColumnElement[bool]])\
|
||||||
-> Decimal:
|
-> Decimal:
|
||||||
"""Queries the balance.
|
"""Queries the balance.
|
||||||
|
|
||||||
@@ -221,7 +220,7 @@ class AccountCollector:
|
|||||||
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select_balance: sa.Select = sa.select(balance_func)\
|
select_balance: sa.Select = sa.select(balance_func)\
|
||||||
.join(JournalEntry).join(Account).filter(*conditions)
|
.join(JournalEntry).join(Account).where(*conditions)
|
||||||
return db.session.scalar(select_balance)
|
return db.session.scalar(select_balance)
|
||||||
|
|
||||||
def __add_owner_s_equity(self, code: str, amount: Decimal | None,
|
def __add_owner_s_equity(self, code: str, amount: Decimal | None,
|
||||||
@@ -385,11 +384,13 @@ class BalanceSheet(BaseReport):
|
|||||||
balances: list[ReportAccount] = AccountCollector(
|
balances: list[ReportAccount] = AccountCollector(
|
||||||
self.__currency, self.__period).accounts
|
self.__currency, self.__period).accounts
|
||||||
|
|
||||||
titles: list[BaseAccount] = BaseAccount.query\
|
titles: list[BaseAccount] = db.session.scalars(
|
||||||
.filter(BaseAccount.code.in_({"1", "2", "3"})).all()
|
sa.select(BaseAccount)
|
||||||
subtitles: list[BaseAccount] = BaseAccount.query\
|
.where(BaseAccount.code.in_({"1", "2", "3"}))).unique().all()
|
||||||
.filter(BaseAccount.code.in_({x.account.base_code[:2]
|
subtitle_codes: set[str] = {x.account.base_code[:2] for x in balances}
|
||||||
for x in balances})).all()
|
subtitles: list[BaseAccount] = db.session.scalars(
|
||||||
|
sa.select(BaseAccount)
|
||||||
|
.where(BaseAccount.code.in_(subtitle_codes))).unique().all()
|
||||||
|
|
||||||
sections: dict[str, Section] = {x.code: Section(x) for x in titles}
|
sections: dict[str, Section] = {x.code: Section(x) for x in titles}
|
||||||
subsections: dict[str, Subsection] = {x.code: Subsection(x)
|
subsections: dict[str, Subsection] = {x.code: Subsection(x)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,21 +24,19 @@ import sqlalchemy as sa
|
|||||||
from flask import url_for, render_template, Response
|
from flask import url_for, render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting import db
|
from ..period import Period, PeriodChooser
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ..utils.base_report import BaseReport
|
||||||
JournalEntryLineItem
|
from ..utils.csv_export import BaseCSVRow, csv_download, period_spec
|
||||||
from accounting.report.period import Period, PeriodChooser
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
from ..utils.urls import income_expenses_url
|
||||||
period_spec
|
from ... import db
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ...locale import gettext
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ...utils.current_account import CurrentAccount
|
||||||
from accounting.report.utils.urls import income_expenses_url
|
from ...utils.pagination import Pagination
|
||||||
from accounting.utils.current_account import CurrentAccount
|
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
|
|
||||||
|
|
||||||
class ReportLineItem:
|
class ReportLineItem:
|
||||||
@@ -119,12 +117,12 @@ class LineItemCollector:
|
|||||||
balance_func: sa.Function = sa.func.sum(sa.case(
|
balance_func: sa.Function = sa.func.sum(sa.case(
|
||||||
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select: sa.Select = sa.Select(balance_func)\
|
select: sa.Select[tuple[Decimal]] = sa.Select(balance_func)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
|
.where(JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
self.__account_condition,
|
self.__account_condition,
|
||||||
JournalEntry.date < self.__period.start)
|
JournalEntry.date < self.__period.start)
|
||||||
balance: int | None = db.session.scalar(select)
|
balance: Decimal | None = db.session.scalar(select)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
return None
|
return None
|
||||||
line_item: ReportLineItem = ReportLineItem()
|
line_item: ReportLineItem = ReportLineItem()
|
||||||
@@ -144,7 +142,7 @@ class LineItemCollector:
|
|||||||
|
|
||||||
:return: The line items.
|
:return: The line items.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
self.__account_condition]
|
self.__account_condition]
|
||||||
if self.__period.start is not None:
|
if self.__period.start is not None:
|
||||||
@@ -152,25 +150,25 @@ class LineItemCollector:
|
|||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
conditions.append(JournalEntry.date <= self.__period.end)
|
conditions.append(JournalEntry.date <= self.__period.end)
|
||||||
journal_entry_with_account: sa.Select = sa.Select(JournalEntry.id).\
|
journal_entry_with_account: sa.Select = sa.Select(JournalEntry.id).\
|
||||||
join(JournalEntryLineItem).join(Account).filter(*conditions)
|
join(JournalEntryLineItem).join(Account).where(*conditions)
|
||||||
|
|
||||||
return [ReportLineItem(x)
|
return [ReportLineItem(x) for x in db.session.scalars(
|
||||||
for x in JournalEntryLineItem.query
|
sa.select(JournalEntryLineItem)
|
||||||
.join(JournalEntry).join(Account)
|
.join(JournalEntry).join(Account)
|
||||||
.filter(JournalEntryLineItem.journal_entry_id
|
.where(JournalEntryLineItem.journal_entry_id
|
||||||
.in_(journal_entry_with_account),
|
.in_(journal_entry_with_account),
|
||||||
JournalEntryLineItem.currency_code
|
JournalEntryLineItem.currency_code
|
||||||
== self.__currency.code,
|
== self.__currency.code,
|
||||||
sa.not_(self.__account_condition))
|
sa.not_(self.__account_condition))
|
||||||
.order_by(JournalEntry.date,
|
.order_by(JournalEntry.date,
|
||||||
JournalEntry.no,
|
JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit,
|
JournalEntryLineItem.is_debit,
|
||||||
JournalEntryLineItem.no)
|
JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.account),
|
.options(selectinload(JournalEntryLineItem.account),
|
||||||
selectinload(JournalEntryLineItem.journal_entry))]
|
selectinload(JournalEntryLineItem.journal_entry)))]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __account_condition(self) -> sa.BinaryExpression:
|
def __account_condition(self) -> sa.ColumnElement[bool]:
|
||||||
if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
|
if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
|
||||||
return CurrentAccount.sql_condition()
|
return CurrentAccount.sql_condition()
|
||||||
return Account.id == self.__account.id
|
return Account.id == self.__account.id
|
||||||
@@ -345,8 +343,8 @@ class PageParams(BasePageParams):
|
|||||||
self.account.id == 0)]
|
self.account.id == 0)]
|
||||||
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
||||||
.join(Account)\
|
.join(Account)\
|
||||||
.filter(JournalEntryLineItem.currency_code == self.currency.code,
|
.where(JournalEntryLineItem.currency_code == self.currency.code,
|
||||||
CurrentAccount.sql_condition())\
|
CurrentAccount.sql_condition())\
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
options.extend([OptionLink(str(x),
|
options.extend([OptionLink(str(x),
|
||||||
income_expenses_url(
|
income_expenses_url(
|
||||||
@@ -354,8 +352,10 @@ class PageParams(BasePageParams):
|
|||||||
CurrentAccount(x),
|
CurrentAccount(x),
|
||||||
self.period),
|
self.period),
|
||||||
x.id == self.account.id)
|
x.id == self.account.id)
|
||||||
for x in Account.query.filter(Account.id.in_(in_use))
|
for x in db.session.scalars(
|
||||||
.order_by(Account.base_code, Account.no).all()])
|
sa.select(Account).where(Account.id.in_(in_use))
|
||||||
|
.order_by(Account.base_code, Account.no))
|
||||||
|
.unique()])
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,19 +22,18 @@ from decimal import Decimal
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
|
|
||||||
from accounting import db
|
from ..period import Period, PeriodChooser
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
|
from ..utils.base_report import BaseReport
|
||||||
|
from ..utils.csv_export import BaseCSVRow, csv_download, period_spec
|
||||||
|
from ..utils.option_link import OptionLink
|
||||||
|
from ..utils.report_chooser import ReportChooser
|
||||||
|
from ..utils.report_type import ReportType
|
||||||
|
from ..utils.urls import ledger_url, income_statement_url
|
||||||
|
from ... import db
|
||||||
|
from ...locale import gettext
|
||||||
|
from ...models import Currency, BaseAccount, Account, JournalEntry, \
|
||||||
JournalEntryLineItem
|
JournalEntryLineItem
|
||||||
from accounting.report.period import Period, PeriodChooser
|
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
|
||||||
from accounting.report.utils.base_report import BaseReport
|
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
|
||||||
period_spec
|
|
||||||
from accounting.report.utils.option_link import OptionLink
|
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
|
||||||
from accounting.report.utils.report_type import ReportType
|
|
||||||
from accounting.report.utils.urls import ledger_url, income_statement_url
|
|
||||||
|
|
||||||
|
|
||||||
class ReportAccount:
|
class ReportAccount:
|
||||||
@@ -219,11 +218,14 @@ class IncomeStatement(BaseReport):
|
|||||||
"""
|
"""
|
||||||
balances: list[ReportAccount] = self.__query_balances()
|
balances: list[ReportAccount] = self.__query_balances()
|
||||||
|
|
||||||
titles: list[BaseAccount] = BaseAccount.query\
|
title_codes: set[str] = {"4", "5", "6", "7", "8", "9"}
|
||||||
.filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all()
|
titles: list[BaseAccount] = db.session.scalars(
|
||||||
subtitles: list[BaseAccount] = BaseAccount.query\
|
sa.select(BaseAccount)
|
||||||
.filter(BaseAccount.code.in_({x.account.base_code[:2]
|
.where(BaseAccount.code.in_(title_codes))).unique().all()
|
||||||
for x in balances})).all()
|
subtitle_codes: set[str] = {x.account.base_code[:2] for x in balances}
|
||||||
|
subtitles: list[BaseAccount] = db.session.scalars(
|
||||||
|
sa.select(BaseAccount)
|
||||||
|
.where(BaseAccount.code.in_(subtitle_codes))).unique().all()
|
||||||
|
|
||||||
total_titles: dict[str, str] \
|
total_titles: dict[str, str] \
|
||||||
= {"4": gettext("Total Operating Revenue"),
|
= {"4": gettext("Total Operating Revenue"),
|
||||||
@@ -254,9 +256,9 @@ class IncomeStatement(BaseReport):
|
|||||||
|
|
||||||
:return: The balances.
|
:return: The balances.
|
||||||
"""
|
"""
|
||||||
sub_conditions: list[sa.BinaryExpression] \
|
sub_conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [Account.base_code.startswith(str(x)) for x in range(4, 10)]
|
= [Account.base_code.startswith(str(x)) for x in range(4, 10)]
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
sa.or_(*sub_conditions)]
|
sa.or_(*sub_conditions)]
|
||||||
if self.__period.start is not None:
|
if self.__period.start is not None:
|
||||||
@@ -268,14 +270,15 @@ class IncomeStatement(BaseReport):
|
|||||||
else_=JournalEntryLineItem.amount)).label("balance")
|
else_=JournalEntryLineItem.amount)).label("balance")
|
||||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.where(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
.having(balance_func != 0)\
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in db.session.scalars(
|
||||||
.filter(Account.id.in_([x.id for x in balances])).all()}
|
sa.select(Account)
|
||||||
|
.where(Account.id.in_([x.id for x in balances]))).unique()}
|
||||||
return [ReportAccount(account=accounts[x.id],
|
return [ReportAccount(account=accounts[x.id],
|
||||||
amount=x.balance,
|
amount=x.balance,
|
||||||
url=ledger_url(self.__currency,
|
url=ledger_url(self.__currency,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,18 +24,17 @@ import sqlalchemy as sa
|
|||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from ..period import Period, PeriodChooser
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ..utils.base_page_params import BasePageParams
|
||||||
JournalEntryLineItem
|
from ..utils.base_report import BaseReport
|
||||||
from accounting.report.period import Period, PeriodChooser
|
from ..utils.csv_export import BaseCSVRow, csv_download, period_spec
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
from ..utils.urls import journal_url
|
||||||
period_spec
|
from ... import db
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ...locale import gettext
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.report.utils.urls import journal_url
|
from ...utils.pagination import Pagination
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
|
|
||||||
|
|
||||||
class ReportLineItem:
|
class ReportLineItem:
|
||||||
@@ -185,20 +184,21 @@ class Journal(BaseReport):
|
|||||||
|
|
||||||
:return: The line items.
|
:return: The line items.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] = []
|
conditions: list[sa.ColumnElement[bool]] = []
|
||||||
if self.__period.start is not None:
|
if self.__period.start is not None:
|
||||||
conditions.append(JournalEntry.date >= self.__period.start)
|
conditions.append(JournalEntry.date >= self.__period.start)
|
||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
conditions.append(JournalEntry.date <= self.__period.end)
|
conditions.append(JournalEntry.date <= self.__period.end)
|
||||||
return JournalEntryLineItem.query.join(JournalEntry)\
|
return db.session.scalars(
|
||||||
.filter(*conditions)\
|
sa.select(JournalEntryLineItem).join(JournalEntry)
|
||||||
|
.where(*conditions)
|
||||||
.order_by(JournalEntry.date,
|
.order_by(JournalEntry.date,
|
||||||
JournalEntry.no,
|
JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit.desc(),
|
JournalEntryLineItem.is_debit.desc(),
|
||||||
JournalEntryLineItem.no)\
|
JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.account),
|
.options(selectinload(JournalEntryLineItem.account),
|
||||||
selectinload(JournalEntryLineItem.currency),
|
selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry))).all()
|
||||||
|
|
||||||
def csv(self) -> Response:
|
def csv(self) -> Response:
|
||||||
"""Returns the report as CSV for download.
|
"""Returns the report as CSV for download.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,20 +24,18 @@ import sqlalchemy as sa
|
|||||||
from flask import url_for, render_template, Response
|
from flask import url_for, render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting import db
|
from ..period import Period, PeriodChooser
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ..utils.base_report import BaseReport
|
||||||
JournalEntryLineItem
|
from ..utils.csv_export import BaseCSVRow, csv_download, period_spec
|
||||||
from accounting.report.period import Period, PeriodChooser
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
from ..utils.urls import ledger_url
|
||||||
period_spec
|
from ... import db
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ...locale import gettext
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ...utils.pagination import Pagination
|
||||||
from accounting.report.utils.urls import ledger_url
|
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
|
|
||||||
|
|
||||||
class ReportLineItem:
|
class ReportLineItem:
|
||||||
@@ -117,9 +115,9 @@ class LineItemCollector:
|
|||||||
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
|
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
|
||||||
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
|
.where(JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
JournalEntryLineItem.account_id == self.__account.id,
|
JournalEntryLineItem.account_id == self.__account.id,
|
||||||
JournalEntry.date < self.__period.start)
|
JournalEntry.date < self.__period.start)
|
||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
return None
|
return None
|
||||||
@@ -139,22 +137,22 @@ class LineItemCollector:
|
|||||||
|
|
||||||
:return: The line items.
|
:return: The line items.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
= [JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
JournalEntryLineItem.account_id == self.__account.id]
|
JournalEntryLineItem.account_id == self.__account.id]
|
||||||
if self.__period.start is not None:
|
if self.__period.start is not None:
|
||||||
conditions.append(JournalEntry.date >= self.__period.start)
|
conditions.append(JournalEntry.date >= self.__period.start)
|
||||||
if self.__period.end is not None:
|
if self.__period.end is not None:
|
||||||
conditions.append(JournalEntry.date <= self.__period.end)
|
conditions.append(JournalEntry.date <= self.__period.end)
|
||||||
return [ReportLineItem(x) for x in JournalEntryLineItem.query
|
return [ReportLineItem(x) for x in db.session.scalars(
|
||||||
.join(JournalEntry)
|
sa.select(JournalEntryLineItem).join(JournalEntry)
|
||||||
.filter(*conditions)
|
.where(*conditions)
|
||||||
.order_by(JournalEntry.date,
|
.order_by(JournalEntry.date,
|
||||||
JournalEntry.no,
|
JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit.desc(),
|
JournalEntryLineItem.is_debit.desc(),
|
||||||
JournalEntryLineItem.no)
|
JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.journal_entry))
|
.options(selectinload(JournalEntryLineItem.journal_entry)))
|
||||||
.all()]
|
.unique()]
|
||||||
|
|
||||||
def __get_total(self) -> ReportLineItem | None:
|
def __get_total(self) -> ReportLineItem | None:
|
||||||
"""Composes the total line item.
|
"""Composes the total line item.
|
||||||
@@ -310,12 +308,13 @@ class PageParams(BasePageParams):
|
|||||||
:return: The account options.
|
:return: The account options.
|
||||||
"""
|
"""
|
||||||
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
||||||
.filter(JournalEntryLineItem.currency_code == self.currency.code)\
|
.where(JournalEntryLineItem.currency_code == self.currency.code)\
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
||||||
x.id == self.account.id)
|
x.id == self.account.id)
|
||||||
for x in Account.query.filter(Account.id.in_(in_use))
|
for x in db.session.scalars(
|
||||||
.order_by(Account.base_code, Account.no).all()]
|
sa.select(Account).where(Account.id.in_(in_use))
|
||||||
|
.order_by(Account.base_code, Account.no)).unique()]
|
||||||
|
|
||||||
|
|
||||||
class Ledger(BaseReport):
|
class Ledger(BaseReport):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,17 +24,18 @@ import sqlalchemy as sa
|
|||||||
from flask import Response, render_template, request
|
from flask import Response, render_template, request
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting.locale import gettext
|
|
||||||
from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
|
|
||||||
JournalEntry, JournalEntryLineItem
|
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
|
||||||
from accounting.report.utils.base_report import BaseReport
|
|
||||||
from accounting.report.utils.csv_export import csv_download
|
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
|
||||||
from accounting.report.utils.report_type import ReportType
|
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
from accounting.utils.query import parse_query_keywords
|
|
||||||
from .journal import get_csv_rows
|
from .journal import get_csv_rows
|
||||||
|
from ..utils.base_page_params import BasePageParams
|
||||||
|
from ..utils.base_report import BaseReport
|
||||||
|
from ..utils.csv_export import csv_download
|
||||||
|
from ..utils.report_chooser import ReportChooser
|
||||||
|
from ..utils.report_type import ReportType
|
||||||
|
from ... import db
|
||||||
|
from ...locale import gettext
|
||||||
|
from ...models import Currency, CurrencyL10n, Account, AccountL10n, \
|
||||||
|
JournalEntry, JournalEntryLineItem
|
||||||
|
from ...utils.pagination import Pagination
|
||||||
|
from ...utils.query import parse_query_keywords
|
||||||
|
|
||||||
|
|
||||||
class LineItemCollector:
|
class LineItemCollector:
|
||||||
@@ -53,9 +54,9 @@ class LineItemCollector:
|
|||||||
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
keywords: list[str] = parse_query_keywords(request.args.get("q"))
|
||||||
if len(keywords) == 0:
|
if len(keywords) == 0:
|
||||||
return []
|
return []
|
||||||
conditions: list[sa.BinaryExpression] = []
|
conditions: list[sa.ColumnElement[bool]] = []
|
||||||
for k in keywords:
|
for k in keywords:
|
||||||
sub_conditions: list[sa.BinaryExpression] \
|
sub_conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.description.icontains(k),
|
= [JournalEntryLineItem.description.icontains(k),
|
||||||
JournalEntryLineItem.account_id.in_(
|
JournalEntryLineItem.account_id.in_(
|
||||||
self.__get_account_condition(k)),
|
self.__get_account_condition(k)),
|
||||||
@@ -69,15 +70,16 @@ class LineItemCollector:
|
|||||||
except ArithmeticError:
|
except ArithmeticError:
|
||||||
pass
|
pass
|
||||||
conditions.append(sa.or_(*sub_conditions))
|
conditions.append(sa.or_(*sub_conditions))
|
||||||
return JournalEntryLineItem.query.join(JournalEntry)\
|
return db.session.scalars(
|
||||||
.filter(*conditions)\
|
sa.select(JournalEntryLineItem).join(JournalEntry)
|
||||||
|
.where(*conditions)
|
||||||
.order_by(JournalEntry.date,
|
.order_by(JournalEntry.date,
|
||||||
JournalEntry.no,
|
JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit,
|
JournalEntryLineItem.is_debit,
|
||||||
JournalEntryLineItem.no)\
|
JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.account),
|
.options(selectinload(JournalEntryLineItem.account),
|
||||||
selectinload(JournalEntryLineItem.currency),
|
selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry))).all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_account_condition(k: str) -> sa.Select:
|
def __get_account_condition(k: str) -> sa.Select:
|
||||||
@@ -86,20 +88,20 @@ class LineItemCollector:
|
|||||||
:param k: The keyword.
|
:param k: The keyword.
|
||||||
:return: The condition to filter the account.
|
:return: The condition to filter the account.
|
||||||
"""
|
"""
|
||||||
code: sa.BinaryExpression = Account.base_code + "-" \
|
code: sa.ColumnElement[str] = Account.base_code + "-" \
|
||||||
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
|
+ sa.func.substr("000" + sa.cast(Account.no, sa.String),
|
||||||
sa.func.char_length(sa.cast(Account.no,
|
sa.func.char_length(sa.cast(Account.no,
|
||||||
sa.String)) + 1)
|
sa.String)) + 1)
|
||||||
select_l10n: sa.Select = sa.select(AccountL10n.account_id)\
|
select_l10n: sa.Select = sa.select(AccountL10n.account_id)\
|
||||||
.filter(AccountL10n.title.icontains(k))
|
.where(AccountL10n.title.icontains(k))
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [Account.base_code.contains(k),
|
= [Account.base_code.contains(k),
|
||||||
Account.title_l10n.icontains(k),
|
Account.title_l10n.icontains(k),
|
||||||
code.contains(k),
|
code.contains(k),
|
||||||
Account.id.in_(select_l10n)]
|
Account.id.in_(select_l10n)]
|
||||||
if k in gettext("Needs Offset"):
|
if k in gettext("Needs Offset"):
|
||||||
conditions.append(Account.is_need_offset)
|
conditions.append(Account.is_need_offset)
|
||||||
return sa.select(Account.id).filter(sa.or_(*conditions))
|
return sa.select(Account.id).where(sa.or_(*conditions))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_currency_condition(k: str) -> sa.Select:
|
def __get_currency_condition(k: str) -> sa.Select:
|
||||||
@@ -109,11 +111,11 @@ class LineItemCollector:
|
|||||||
:return: The condition to filter the currency.
|
:return: The condition to filter the currency.
|
||||||
"""
|
"""
|
||||||
select_l10n: sa.Select = sa.select(CurrencyL10n.currency_code)\
|
select_l10n: sa.Select = sa.select(CurrencyL10n.currency_code)\
|
||||||
.filter(CurrencyL10n.name.icontains(k))
|
.where(CurrencyL10n.name.icontains(k))
|
||||||
return sa.select(Currency.code).filter(
|
return sa.select(Currency.code)\
|
||||||
sa.or_(Currency.code.icontains(k),
|
.where(sa.or_(Currency.code.icontains(k),
|
||||||
Currency.name_l10n.icontains(k),
|
Currency.name_l10n.icontains(k),
|
||||||
Currency.code.in_(select_l10n)))
|
Currency.code.in_(select_l10n)))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_journal_entry_condition(k: str) -> sa.Select:
|
def __get_journal_entry_condition(k: str) -> sa.Select:
|
||||||
@@ -122,7 +124,7 @@ class LineItemCollector:
|
|||||||
:param k: The keyword.
|
:param k: The keyword.
|
||||||
:return: The condition to filter the journal entry.
|
:return: The condition to filter the journal entry.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntry.note.icontains(k)]
|
= [JournalEntry.note.icontains(k)]
|
||||||
date: dt.datetime
|
date: dt.datetime
|
||||||
try:
|
try:
|
||||||
@@ -153,7 +155,7 @@ class LineItemCollector:
|
|||||||
sa.extract("day", JournalEntry.date) == date.day))
|
sa.extract("day", JournalEntry.date) == date.day))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
|
return sa.select(JournalEntry.id).where(sa.or_(*conditions))
|
||||||
|
|
||||||
|
|
||||||
class PageParams(BasePageParams):
|
class PageParams(BasePageParams):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,19 +22,17 @@ from decimal import Decimal
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import Response, render_template
|
from flask import Response, render_template
|
||||||
|
|
||||||
from accounting import db
|
from ..period import Period, PeriodChooser
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ..utils.base_report import BaseReport
|
||||||
JournalEntryLineItem
|
from ..utils.csv_export import BaseCSVRow, csv_download, period_spec
|
||||||
from accounting.report.period import Period, PeriodChooser
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
|
from ..utils.urls import ledger_url, trial_balance_url
|
||||||
period_spec
|
from ... import db
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ...locale import gettext
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.report.utils.report_type import ReportType
|
|
||||||
from accounting.report.utils.urls import ledger_url, trial_balance_url
|
|
||||||
|
|
||||||
|
|
||||||
class ReportAccount:
|
class ReportAccount:
|
||||||
@@ -178,7 +176,7 @@ class TrialBalance(BaseReport):
|
|||||||
|
|
||||||
:return: None.
|
:return: None.
|
||||||
"""
|
"""
|
||||||
conditions: list[sa.BinaryExpression] \
|
conditions: list[sa.ColumnElement[bool]] \
|
||||||
= [JournalEntryLineItem.currency_code == self.__currency.code]
|
= [JournalEntryLineItem.currency_code == self.__currency.code]
|
||||||
if self.__period.start is not None:
|
if self.__period.start is not None:
|
||||||
conditions.append(JournalEntry.date >= self.__period.start)
|
conditions.append(JournalEntry.date >= self.__period.start)
|
||||||
@@ -189,14 +187,15 @@ class TrialBalance(BaseReport):
|
|||||||
else_=-JournalEntryLineItem.amount)).label("balance")
|
else_=-JournalEntryLineItem.amount)).label("balance")
|
||||||
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
select_balances: sa.Select = sa.select(Account.id, balance_func)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(*conditions)\
|
.where(*conditions)\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
.having(balance_func != 0)\
|
.having(balance_func != 0)\
|
||||||
.order_by(Account.base_code, Account.no)
|
.order_by(Account.base_code, Account.no)
|
||||||
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
balances: list[sa.Row] = db.session.execute(select_balances).all()
|
||||||
accounts: dict[int, Account] \
|
accounts: dict[int, Account] \
|
||||||
= {x.id: x for x in Account.query
|
= {x.id: x for x in db.session.scalars(
|
||||||
.filter(Account.id.in_([x.id for x in balances])).all()}
|
sa.select(Account)
|
||||||
|
.where(Account.id.in_([x.id for x in balances]))).unique()}
|
||||||
self.__accounts = [ReportAccount(account=accounts[x.id],
|
self.__accounts = [ReportAccount(account=accounts[x.id],
|
||||||
amount=x.balance,
|
amount=x.balance,
|
||||||
url=ledger_url(self.__currency,
|
url=ledger_url(self.__currency,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,22 +20,22 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ..utils.base_report import BaseReport
|
||||||
JournalEntryLineItem
|
from ..utils.csv_export import BaseCSVRow, csv_download
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ..utils.unapplied import get_accounts_with_unapplied, get_net_balances
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ..utils.urls import unapplied_url
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ... import db
|
||||||
from accounting.report.utils.unapplied import get_accounts_with_unapplied, \
|
from ...locale import gettext
|
||||||
get_net_balances
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.report.utils.urls import unapplied_url
|
from ...utils.pagination import Pagination
|
||||||
from accounting.utils.pagination import Pagination
|
|
||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
@@ -178,13 +178,14 @@ class UnappliedOriginalLineItems(BaseReport):
|
|||||||
"""
|
"""
|
||||||
net_balances: dict[int, Decimal | None] \
|
net_balances: dict[int, Decimal | None] \
|
||||||
= get_net_balances(self.__currency, self.__account)
|
= get_net_balances(self.__currency, self.__account)
|
||||||
line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query \
|
line_items: list[JournalEntryLineItem] = db.session.scalars(
|
||||||
.join(Account).join(JournalEntry) \
|
sa.select(JournalEntryLineItem).join(Account).join(JournalEntry)
|
||||||
.filter(JournalEntryLineItem.id.in_(net_balances)) \
|
.where(JournalEntryLineItem.id.in_(net_balances))
|
||||||
.order_by(JournalEntry.date, JournalEntry.no,
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
|
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.currency),
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry)))\
|
||||||
|
.unique().all()
|
||||||
for line_item in line_items:
|
for line_item in line_items:
|
||||||
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 \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,16 +22,16 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account
|
from ..utils.base_report import BaseReport
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.csv_export import BaseCSVRow, csv_download
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ..utils.unapplied import get_accounts_with_unapplied
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ..utils.urls import unapplied_url
|
||||||
from accounting.report.utils.unapplied import get_accounts_with_unapplied
|
from ...locale import gettext
|
||||||
from accounting.report.utils.urls import unapplied_url
|
from ...models import Currency, Account
|
||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,18 +23,18 @@ from decimal import Decimal
|
|||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account, JournalEntryLineItem
|
from ..utils.base_report import BaseReport
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.csv_export import BaseCSVRow, csv_download
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.offset_matcher import OffsetMatcher, OffsetPair
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.offset_matcher import OffsetMatcher, OffsetPair
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ..utils.unmatched import get_accounts_with_unmatched
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ..utils.urls import unmatched_url
|
||||||
from accounting.report.utils.unmatched import get_accounts_with_unmatched
|
from ...locale import gettext
|
||||||
from accounting.report.utils.urls import unmatched_url
|
from ...models import Currency, Account, JournalEntryLineItem
|
||||||
from accounting.utils.pagination import Pagination
|
from ...utils.pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/17
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,16 +22,16 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from flask import render_template, Response
|
from flask import render_template, Response
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from ..utils.base_page_params import BasePageParams
|
||||||
from accounting.models import Currency, Account
|
from ..utils.base_report import BaseReport
|
||||||
from accounting.report.utils.base_page_params import BasePageParams
|
from ..utils.csv_export import BaseCSVRow, csv_download
|
||||||
from accounting.report.utils.base_report import BaseReport
|
from ..utils.option_link import OptionLink
|
||||||
from accounting.report.utils.csv_export import BaseCSVRow, csv_download
|
from ..utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.option_link import OptionLink
|
from ..utils.report_type import ReportType
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from ..utils.unmatched import get_accounts_with_unmatched
|
||||||
from accounting.report.utils.report_type import ReportType
|
from ..utils.urls import unmatched_url
|
||||||
from accounting.report.utils.unmatched import get_accounts_with_unmatched
|
from ...locale import gettext
|
||||||
from accounting.report.utils.urls import unmatched_url
|
from ...models import Currency, Account
|
||||||
|
|
||||||
|
|
||||||
class CSVRow(BaseCSVRow):
|
class CSVRow(BaseCSVRow):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"""
|
"""
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from accounting.template_filters import format_amount as core_format_amount
|
from ..template_filters import format_amount as core_format_amount
|
||||||
|
|
||||||
|
|
||||||
def format_amount(value: Decimal | None) -> str | None:
|
def format_amount(value: Decimal | None) -> str | None:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,18 +19,17 @@
|
|||||||
"""
|
"""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Callable
|
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
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.models import Currency, JournalEntryLineItem
|
|
||||||
from accounting.utils.journal_entry_types import JournalEntryType
|
|
||||||
from .option_link import OptionLink
|
from .option_link import OptionLink
|
||||||
from .report_chooser import ReportChooser
|
from .report_chooser import ReportChooser
|
||||||
|
from ... import db
|
||||||
|
from ...models import Currency, JournalEntryLineItem
|
||||||
|
from ...utils.journal_entry_types import JournalEntryType
|
||||||
|
|
||||||
|
|
||||||
class BasePageParams(ABC):
|
class BasePageParams(ABC):
|
||||||
@@ -53,7 +52,7 @@ class BasePageParams(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def journal_entry_types(self) -> 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.
|
||||||
@@ -85,5 +84,6 @@ class BasePageParams(ABC):
|
|||||||
sa.select(JournalEntryLineItem.currency_code)
|
sa.select(JournalEntryLineItem.currency_code)
|
||||||
.group_by(JournalEntryLineItem.currency_code)).all())
|
.group_by(JournalEntryLineItem.currency_code)).all())
|
||||||
return [OptionLink(str(x), get_url(x), x.code == active_currency.code)
|
return [OptionLink(str(x), get_url(x), x.code == active_currency.code)
|
||||||
for x in Currency.query.filter(Currency.code.in_(in_use))
|
for x in db.session.scalars(
|
||||||
.order_by(Currency.code).all()]
|
sa.select(Currency).where(Currency.code.in_(in_use))
|
||||||
|
.order_by(Currency.code)).unique()]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -26,7 +26,7 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
from flask import Response
|
from flask import Response
|
||||||
|
|
||||||
from accounting.report.period import Period
|
from ..period import Period
|
||||||
|
|
||||||
|
|
||||||
class BaseCSVRow(ABC):
|
class BaseCSVRow(ABC):
|
||||||
@@ -66,15 +66,19 @@ def period_spec(period: Period) -> str:
|
|||||||
"""
|
"""
|
||||||
start: str | None = __get_start_str(period.start)
|
start: str | None = __get_start_str(period.start)
|
||||||
end: str | None = __get_end_str(period.end)
|
end: str | None = __get_end_str(period.end)
|
||||||
if period.start is None and period.end is None:
|
if start is None:
|
||||||
return "all-time"
|
return "all-time" if end is None else f"until-{end}"
|
||||||
if start == end:
|
return f"since-{start}" if end is None else __get_spec(start, end)
|
||||||
return start
|
|
||||||
if period.start is None:
|
|
||||||
return f"until-{end}"
|
def __get_spec(start: str, end: str) -> str:
|
||||||
if period.end is None:
|
"""Constructs the period specification with both start and end
|
||||||
return f"since-{start}"
|
|
||||||
return f"{start}-{end}"
|
:param start: The start date.
|
||||||
|
:param end: The end date.
|
||||||
|
:return: The period specification.
|
||||||
|
"""
|
||||||
|
return start if start == end else f"{start}-{end}"
|
||||||
|
|
||||||
|
|
||||||
def __get_start_str(start: dt.date | None) -> str | None:
|
def __get_start_str(start: dt.date | None) -> str | None:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,10 +23,10 @@ import sqlalchemy as sa
|
|||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from accounting.locale import lazy_gettext
|
from ..utils.unapplied import get_net_balances
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ... import db
|
||||||
JournalEntryLineItem
|
from ...locale import lazy_gettext
|
||||||
from accounting.report.utils.unapplied import get_net_balances
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
|
|
||||||
|
|
||||||
class OffsetPair:
|
class OffsetPair:
|
||||||
@@ -54,7 +54,7 @@ class OffsetMatcher:
|
|||||||
:param currency: The currency.
|
:param currency: The currency.
|
||||||
:param account: The account.
|
:param account: The account.
|
||||||
"""
|
"""
|
||||||
self.__currency: Account = currency
|
self.__currency: Currency = currency
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
self.__account: Account = account
|
self.__account: Account = account
|
||||||
"""The account."""
|
"""The account."""
|
||||||
@@ -105,7 +105,7 @@ class OffsetMatcher:
|
|||||||
"""
|
"""
|
||||||
net_balances: dict[int, Decimal | None] \
|
net_balances: dict[int, Decimal | None] \
|
||||||
= get_net_balances(self.__currency, self.__account)
|
= get_net_balances(self.__currency, self.__account)
|
||||||
unmatched_offset_condition: sa.BinaryExpression \
|
unmatched_offset_condition: sa.ColumnElement[bool] \
|
||||||
= sa.and_(Account.id == self.__account.id,
|
= sa.and_(Account.id == self.__account.id,
|
||||||
JournalEntryLineItem.currency_code
|
JournalEntryLineItem.currency_code
|
||||||
== self.__currency.code,
|
== self.__currency.code,
|
||||||
@@ -114,14 +114,15 @@ class OffsetMatcher:
|
|||||||
JournalEntryLineItem.is_debit),
|
JournalEntryLineItem.is_debit),
|
||||||
sa.and_(Account.base_code.startswith("1"),
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit))))
|
sa.not_(JournalEntryLineItem.is_debit))))
|
||||||
self.line_items = JournalEntryLineItem.query \
|
self.line_items = db.session.scalars(
|
||||||
.join(Account).join(JournalEntry) \
|
sa.select(JournalEntryLineItem).join(Account).join(JournalEntry)
|
||||||
.filter(sa.or_(JournalEntryLineItem.id.in_(net_balances),
|
.where(sa.or_(JournalEntryLineItem.id.in_(net_balances),
|
||||||
unmatched_offset_condition)) \
|
unmatched_offset_condition))
|
||||||
.order_by(JournalEntry.date, JournalEntry.no,
|
.order_by(JournalEntry.date, JournalEntry.no,
|
||||||
JournalEntryLineItem.is_debit, JournalEntryLineItem.no) \
|
JournalEntryLineItem.is_debit, JournalEntryLineItem.no)
|
||||||
.options(selectinload(JournalEntryLineItem.currency),
|
.options(selectinload(JournalEntryLineItem.currency),
|
||||||
selectinload(JournalEntryLineItem.journal_entry)).all()
|
selectinload(JournalEntryLineItem.journal_entry)))\
|
||||||
|
.unique().all()
|
||||||
for line_item in self.line_items:
|
for line_item in self.line_items:
|
||||||
line_item.is_offset = line_item.id not in net_balances
|
line_item.is_offset = line_item.id not in net_balances
|
||||||
self.unapplied = [x for x in self.line_items if not x.is_offset]
|
self.unapplied = [x for x in self.line_items if not x.is_offset]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -25,18 +25,18 @@ from collections.abc import Iterator
|
|||||||
|
|
||||||
from flask_babel import LazyString
|
from flask_babel import LazyString
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import gettext
|
|
||||||
from accounting.models import Currency, Account
|
|
||||||
from accounting.report.period import Period, get_period
|
|
||||||
from accounting.template_globals import default_currency_code
|
|
||||||
from accounting.utils.current_account import CurrentAccount
|
|
||||||
from accounting.utils.permission import can_edit
|
|
||||||
from .option_link import OptionLink
|
from .option_link import OptionLink
|
||||||
from .report_type import ReportType
|
from .report_type import ReportType
|
||||||
from .urls import journal_url, ledger_url, income_expenses_url, \
|
from .urls import journal_url, ledger_url, income_expenses_url, \
|
||||||
trial_balance_url, income_statement_url, balance_sheet_url, \
|
trial_balance_url, income_statement_url, balance_sheet_url, \
|
||||||
unapplied_url, unmatched_url
|
unapplied_url, unmatched_url
|
||||||
|
from ..period import Period, get_period
|
||||||
|
from ... import db
|
||||||
|
from ...locale import gettext
|
||||||
|
from ...models import Currency, Account
|
||||||
|
from ...template_globals import default_currency_code
|
||||||
|
from ...utils.current_account import CurrentAccount
|
||||||
|
from ...utils.permission import can_edit
|
||||||
|
|
||||||
|
|
||||||
class ReportChooser:
|
class ReportChooser:
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ from enum import Enum
|
|||||||
|
|
||||||
class ReportType(Enum):
|
class ReportType(Enum):
|
||||||
"""The report types."""
|
"""The report types."""
|
||||||
JOURNAL: str = "journal"
|
JOURNAL = "journal"
|
||||||
"""The journal."""
|
"""The journal."""
|
||||||
LEDGER: str = "ledger"
|
LEDGER = "ledger"
|
||||||
"""The ledger."""
|
"""The ledger."""
|
||||||
INCOME_EXPENSES: str = "income-expenses"
|
INCOME_EXPENSES = "income-expenses"
|
||||||
"""The income and expenses log."""
|
"""The income and expenses log."""
|
||||||
TRIAL_BALANCE: str = "trial-balance"
|
TRIAL_BALANCE = "trial-balance"
|
||||||
"""The trial balance."""
|
"""The trial balance."""
|
||||||
INCOME_STATEMENT: str = "income-statement"
|
INCOME_STATEMENT = "income-statement"
|
||||||
"""The income statement."""
|
"""The income statement."""
|
||||||
BALANCE_SHEET: str = "balance-sheet"
|
BALANCE_SHEET = "balance-sheet"
|
||||||
"""The balance sheet."""
|
"""The balance sheet."""
|
||||||
UNAPPLIED: str = "unapplied"
|
UNAPPLIED = "unapplied"
|
||||||
"""The unapplied original line items."""
|
"""The unapplied original line items."""
|
||||||
UNMATCHED: str = "unmatched"
|
UNMATCHED = "unmatched"
|
||||||
"""The unmatched offsets."""
|
"""The unmatched offsets."""
|
||||||
SEARCH: str = "search"
|
SEARCH = "search"
|
||||||
"""The search."""
|
"""The search."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/7
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,10 +21,9 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from ... import db
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
JournalEntryLineItem
|
from ...utils.offset_alias import offset_alias
|
||||||
from accounting.utils.offset_alias import offset_alias
|
|
||||||
|
|
||||||
|
|
||||||
def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
|
def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
|
||||||
@@ -46,12 +45,12 @@ def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
|
|||||||
.join(offset,
|
.join(offset,
|
||||||
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(Account.is_need_offset,
|
.where(Account.is_need_offset,
|
||||||
JournalEntryLineItem.currency_code == currency.code,
|
JournalEntryLineItem.currency_code == currency.code,
|
||||||
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit)),
|
sa.not_(JournalEntryLineItem.is_debit)),
|
||||||
sa.and_(Account.base_code.startswith("1"),
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
JournalEntryLineItem.is_debit)))\
|
JournalEntryLineItem.is_debit)))\
|
||||||
.group_by(JournalEntryLineItem.id)\
|
.group_by(JournalEntryLineItem.id)\
|
||||||
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
||||||
|
|
||||||
@@ -59,13 +58,14 @@ def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
|
|||||||
= sa.func.count(JournalEntryLineItem.id).label("count")
|
= sa.func.count(JournalEntryLineItem.id).label("count")
|
||||||
select: sa.Select = sa.select(Account.id, count_func)\
|
select: sa.Select = sa.select(Account.id, count_func)\
|
||||||
.join(JournalEntryLineItem, isouter=True)\
|
.join(JournalEntryLineItem, isouter=True)\
|
||||||
.filter(JournalEntryLineItem.id.in_(select_unapplied))\
|
.where(JournalEntryLineItem.id.in_(select_unapplied))\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
.having(count_func > 0)
|
.having(count_func > 0)
|
||||||
counts: dict[int, int] \
|
counts: dict[int, int] \
|
||||||
= {x.id: x.count for x in db.session.execute(select)}
|
= {x.id: x.count for x in db.session.execute(select)}
|
||||||
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
|
accounts: list[Account] = db.session.scalars(
|
||||||
.order_by(Account.base_code, Account.no).all()
|
sa.select(Account).where(Account.id.in_(counts))
|
||||||
|
.order_by(Account.base_code, Account.no)).unique().all()
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
account.count = counts[account.id]
|
account.count = counts[account.id]
|
||||||
return accounts
|
return accounts
|
||||||
@@ -92,13 +92,13 @@ def get_net_balances(currency: Currency, account: Account) \
|
|||||||
.join(offset,
|
.join(offset,
|
||||||
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True) \
|
isouter=True) \
|
||||||
.filter(Account.id == account.id,
|
.where(Account.id == account.id,
|
||||||
JournalEntryLineItem.currency_code == currency.code,
|
JournalEntryLineItem.currency_code == currency.code,
|
||||||
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit)),
|
sa.not_(JournalEntryLineItem.is_debit)),
|
||||||
sa.and_(Account.base_code.startswith("1"),
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
JournalEntryLineItem.is_debit))) \
|
JournalEntryLineItem.is_debit))) \
|
||||||
.group_by(JournalEntryLineItem.id) \
|
.group_by(JournalEntryLineItem.id) \
|
||||||
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
.having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
|
||||||
return {x.id: x.net_balance
|
return {x.id: x.net_balance
|
||||||
for x in db.session.execute(select_net_balances).all()}
|
for x in db.session.execute(select_net_balances)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/8
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,9 +19,8 @@
|
|||||||
"""
|
"""
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from accounting import db
|
from ... import db
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from ...models import Currency, Account, JournalEntry, JournalEntryLineItem
|
||||||
JournalEntryLineItem
|
|
||||||
|
|
||||||
|
|
||||||
def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
|
def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
|
||||||
@@ -36,19 +35,20 @@ def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
|
|||||||
select: sa.Select = sa.select(Account.id, count_func)\
|
select: sa.Select = sa.select(Account.id, count_func)\
|
||||||
.select_from(Account)\
|
.select_from(Account)\
|
||||||
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
|
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
|
||||||
.filter(Account.is_need_offset,
|
.where(Account.is_need_offset,
|
||||||
JournalEntryLineItem.currency_code == currency.code,
|
JournalEntryLineItem.currency_code == currency.code,
|
||||||
JournalEntryLineItem.original_line_item_id.is_(None),
|
JournalEntryLineItem.original_line_item_id.is_(None),
|
||||||
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
JournalEntryLineItem.is_debit),
|
JournalEntryLineItem.is_debit),
|
||||||
sa.and_(Account.base_code.startswith("1"),
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit))))\
|
sa.not_(JournalEntryLineItem.is_debit))))\
|
||||||
.group_by(Account.id)\
|
.group_by(Account.id)\
|
||||||
.having(count_func > 0)
|
.having(count_func > 0)
|
||||||
counts: dict[int, int] \
|
counts: dict[int, int] \
|
||||||
= {x.id: x.count for x in db.session.execute(select)}
|
= {x.id: x.count for x in db.session.execute(select)}
|
||||||
accounts: list[Account] = Account.query.filter(Account.id.in_(counts))\
|
accounts: list[Account] = db.session.scalars(
|
||||||
.order_by(Account.base_code, Account.no).all()
|
sa.select(Account).where(Account.id.in_(counts))
|
||||||
|
.order_by(Account.base_code, Account.no)).unique().all()
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
account.count = counts[account.id]
|
account.count = counts[account.id]
|
||||||
return accounts
|
return accounts
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,11 +19,11 @@
|
|||||||
"""
|
"""
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from accounting.models import Currency, Account
|
from ...models import Currency, Account
|
||||||
from accounting.report.period import Period
|
from ...report.period import Period
|
||||||
from accounting.template_globals import default_currency_code
|
from ...template_globals import default_currency_code
|
||||||
from accounting.utils.current_account import CurrentAccount
|
from ...utils.current_account import CurrentAccount
|
||||||
from accounting.utils.options import options
|
from ...utils.options import options
|
||||||
|
|
||||||
|
|
||||||
def journal_url(period: Period) \
|
def journal_url(period: Period) \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,15 +19,6 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint, request, Response, redirect, flash
|
from flask import Blueprint, request, Response, redirect, flash
|
||||||
|
|
||||||
from accounting import db
|
|
||||||
from accounting.locale import lazy_gettext
|
|
||||||
from accounting.models import Currency, Account
|
|
||||||
from accounting.template_globals import default_currency_code
|
|
||||||
from accounting.utils.cast import s
|
|
||||||
from accounting.utils.current_account import CurrentAccount
|
|
||||||
from accounting.utils.next_uri import or_next
|
|
||||||
from accounting.utils.options import options
|
|
||||||
from accounting.utils.permission import has_permission, can_view, can_edit
|
|
||||||
from .period import Period, get_period
|
from .period import Period, get_period
|
||||||
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
|
||||||
IncomeStatement, BalanceSheet, Search
|
IncomeStatement, BalanceSheet, Search
|
||||||
@@ -38,6 +29,15 @@ from .reports.unmatched_accounts import AccountsWithUnmatchedOffsets
|
|||||||
from .template_filters import format_amount
|
from .template_filters import format_amount
|
||||||
from .utils.offset_matcher import OffsetMatcher
|
from .utils.offset_matcher import OffsetMatcher
|
||||||
from .utils.urls import unmatched_url
|
from .utils.urls import unmatched_url
|
||||||
|
from .. import db
|
||||||
|
from ..locale import lazy_gettext
|
||||||
|
from ..models import Currency, Account
|
||||||
|
from ..template_globals import default_currency_code
|
||||||
|
from ..utils.cast import s
|
||||||
|
from ..utils.current_account import CurrentAccount
|
||||||
|
from ..utils.next_uri import or_next
|
||||||
|
from ..utils.options import options
|
||||||
|
from ..utils.permission import has_permission, can_view, can_edit
|
||||||
|
|
||||||
bp: Blueprint = Blueprint("accounting-report", __name__)
|
bp: Blueprint = Blueprint("accounting-report", __name__)
|
||||||
"""The view blueprint for the reports."""
|
"""The view blueprint for the reports."""
|
||||||
@@ -391,7 +391,7 @@ def get_unmatched(currency: Currency, account: Account) -> str | Response:
|
|||||||
@bp.post("match-offsets/<currency:currency>/<needOffsetAccount:account>",
|
@bp.post("match-offsets/<currency:currency>/<needOffsetAccount:account>",
|
||||||
endpoint="match-offsets")
|
endpoint="match-offsets")
|
||||||
@has_permission(can_edit)
|
@has_permission(can_edit)
|
||||||
def match_offsets(currency: Currency, account: Account) -> redirect:
|
def match_offsets(currency: Currency, account: Account) -> Response:
|
||||||
"""Matches the original line items with their offsets.
|
"""Matches the original line items with their offsets.
|
||||||
|
|
||||||
:return: Redirection to the view of the unmatched offsets.
|
:return: Redirection to the view of the unmatched offsets.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class AccountForm {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The control of the base account
|
* The control of the base account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#baseControl;
|
#baseControl;
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class AccountForm {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The base account
|
* The base account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#base;
|
#base;
|
||||||
|
|
||||||
@@ -225,15 +225,16 @@ class AccountForm {
|
|||||||
/**
|
/**
|
||||||
* The base account selector.
|
* The base account selector.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<BaseAccountOption>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class BaseAccountSelector {
|
class BaseAccountSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account form
|
* The account form
|
||||||
* @type {AccountForm}
|
* @type {AccountForm}
|
||||||
*/
|
*/
|
||||||
form;
|
#form;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selector modal
|
* The selector modal
|
||||||
@@ -241,12 +242,6 @@ class BaseAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#modal;
|
#modal;
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
* @type {HTMLParagraphElement}
|
* @type {HTMLParagraphElement}
|
||||||
@@ -259,12 +254,6 @@ class BaseAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The options
|
|
||||||
* @type {BaseAccountOption[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The button to clear the base account value
|
* The button to clear the base account value
|
||||||
* @type {HTMLButtonElement}
|
* @type {HTMLButtonElement}
|
||||||
@@ -277,29 +266,32 @@ class BaseAccountSelector {
|
|||||||
* @param form {AccountForm} the form
|
* @param form {AccountForm} the form
|
||||||
*/
|
*/
|
||||||
constructor(form) {
|
constructor(form) {
|
||||||
this.form = form;
|
|
||||||
const prefix = "accounting-base-selector";
|
const prefix = "accounting-base-selector";
|
||||||
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(element, form.saveBaseAccount.bind(form)));
|
||||||
|
super(query, options);
|
||||||
|
this.#form = form;
|
||||||
this.#modal = document.getElementById(`${prefix}-modal`);
|
this.#modal = document.getElementById(`${prefix}-modal`);
|
||||||
this.#query = document.getElementById(`${prefix}-query`);
|
this.#modal.addEventListener("hidden.bs.modal", () => this.#form.onBaseAccountSelectorClosed());
|
||||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element));
|
|
||||||
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
|
||||||
|
|
||||||
this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
|
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
this.#clearButton.onclick = () => this.#form.clearBaseAccount();
|
||||||
this.#clearButton.onclick = () => this.form.clearBaseAccount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#query.value)) {
|
if (option.isMatched(this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
@@ -319,12 +311,11 @@ class BaseAccountSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#query.value = "";
|
this.query.value = "";
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
for (const option of this.#options) {
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
option.setActive(option.code === this.form.baseCode);
|
this.selectOption(this.shownOptions.find((option) => option.code === this.#form.baseCode));
|
||||||
}
|
if (this.#form.baseCode === null) {
|
||||||
if (this.form.baseCode === null) {
|
|
||||||
this.#clearButton.classList.add("btn-secondary")
|
this.#clearButton.classList.add("btn-secondary")
|
||||||
this.#clearButton.classList.remove("btn-danger");
|
this.#clearButton.classList.remove("btn-danger");
|
||||||
this.#clearButton.disabled = true;
|
this.#clearButton.disabled = true;
|
||||||
@@ -339,14 +330,9 @@ class BaseAccountSelector {
|
|||||||
/**
|
/**
|
||||||
* A base account option.
|
* A base account option.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class BaseAccountOption {
|
class BaseAccountOption extends BaseOption {
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account code
|
* The account code
|
||||||
@@ -369,16 +355,16 @@ class BaseAccountOption {
|
|||||||
/**
|
/**
|
||||||
* Constructs the account in the base account selector.
|
* Constructs the account in the base account selector.
|
||||||
*
|
*
|
||||||
* @param selector {BaseAccountSelector} the base account selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(BaseAccountOption): void} the callback to save the option
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save) {
|
||||||
this.#element = element;
|
super(element);
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
|
|
||||||
this.#element.onclick = () => selector.form.saveBaseAccount(this);
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,30 +384,4 @@ class BaseAccountOption {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
|
||||||
*
|
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
|
||||||
*/
|
|
||||||
setShown(isShown) {
|
|
||||||
if (isShown) {
|
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/* The Mia! Accounting Project
|
||||||
|
* base-combobox.js: The JavaScript for the base abstract combobox
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Copyright (c) 2026 imacat.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
* First written: 2026/4/16
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract combobox.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @template {BaseOption} T
|
||||||
|
*/
|
||||||
|
class BaseCombobox {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query input
|
||||||
|
* @type {HTMLInputElement}
|
||||||
|
*/
|
||||||
|
query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options
|
||||||
|
* @type {T[]}
|
||||||
|
*/
|
||||||
|
options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options that are shown
|
||||||
|
* @type {T[]}
|
||||||
|
*/
|
||||||
|
shownOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a base abstract combobox.
|
||||||
|
*
|
||||||
|
* @param query {HTMLInputElement} the query input
|
||||||
|
* @param options {T[]} the options
|
||||||
|
*/
|
||||||
|
constructor(query, options) {
|
||||||
|
this.query = query;
|
||||||
|
this.query.oninput = () => this.filterOptions();
|
||||||
|
this.query.onkeydown = this.onQueryKeyDown.bind(this);
|
||||||
|
this.options = options;
|
||||||
|
this.shownOptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions when keys are pressed on the query input.
|
||||||
|
*
|
||||||
|
* @param event {KeyboardEvent} the key event
|
||||||
|
*/
|
||||||
|
onQueryKeyDown(event) {
|
||||||
|
if (this.shownOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentID = this.query.getAttribute("aria-activedescendant");
|
||||||
|
const currentIndex = this.shownOptions.findIndex((option) => option.elementID === currentID);
|
||||||
|
|
||||||
|
let newIndex;
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowUp":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = this.shownOptions.length - 1;
|
||||||
|
} else {
|
||||||
|
newIndex = (currentIndex - 1 + this.shownOptions.length) % this.shownOptions.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = 0;
|
||||||
|
} else {
|
||||||
|
newIndex = (currentIndex + 1) % this.shownOptions.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
if (this.query.value !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newIndex = 0;
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
if (this.query.value !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newIndex = this.shownOptions.length - 1;
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = this.shownOptions.length - 1;
|
||||||
|
} else {
|
||||||
|
newIndex = Math.max(currentIndex - 10, 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = 0;
|
||||||
|
} else {
|
||||||
|
newIndex = Math.min(currentIndex + 10, this.shownOptions.length - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
event.preventDefault();
|
||||||
|
if (currentIndex !== -1) {
|
||||||
|
this.shownOptions[currentIndex].click();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "Escape":
|
||||||
|
if (this.query.value !== "") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.query.value = "";
|
||||||
|
this.filterOptions();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
this.selectOption(this.shownOptions[newIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the options.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
filterOptions() {
|
||||||
|
throw new Error("Method not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects an option.
|
||||||
|
*
|
||||||
|
* @param option {T|undefined} the option.
|
||||||
|
*/
|
||||||
|
selectOption(option) {
|
||||||
|
this.options.forEach((opt) => opt.setActive(false));
|
||||||
|
if (option === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
option.setActive(true);
|
||||||
|
this.query.setAttribute("aria-activedescendant", option.elementID);
|
||||||
|
option.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract option
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
class BaseOption {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element
|
||||||
|
* @type {HTMLLIElement}
|
||||||
|
*/
|
||||||
|
#element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element ID
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
elementID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the base abstract option.
|
||||||
|
*
|
||||||
|
* @param element {HTMLLIElement} the element
|
||||||
|
*/
|
||||||
|
constructor(element) {
|
||||||
|
this.#element = element;
|
||||||
|
this.elementID = element.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the option is shown.
|
||||||
|
*
|
||||||
|
* @param isShown {boolean} true to show, or false otherwise
|
||||||
|
*/
|
||||||
|
setShown(isShown) {
|
||||||
|
if (isShown) {
|
||||||
|
this.#element.classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
this.#element.classList.add("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the option is active.
|
||||||
|
*
|
||||||
|
* @param isActive {boolean} true if active, or false otherwise
|
||||||
|
*/
|
||||||
|
setActive(isActive) {
|
||||||
|
if (isActive) {
|
||||||
|
this.#element.classList.add("active");
|
||||||
|
this.#element.ariaSelected = "true";
|
||||||
|
} else {
|
||||||
|
this.#element.classList.remove("active");
|
||||||
|
this.#element.ariaSelected = "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks the option.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
click() {
|
||||||
|
this.#element.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls the option into view.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
scrollIntoView() {
|
||||||
|
this.#element.scrollIntoView({block: "nearest"});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/* The Mia! Accounting Project
|
||||||
|
* base-tablist.js: The JavaScript for base abstract tablist
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Copyright (c) 2026 imacat.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
* First written: 2026/4/16
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract tablist.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @template {BaseTab} T
|
||||||
|
*/
|
||||||
|
class BaseTablist {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tabs.
|
||||||
|
* @type {T[]}
|
||||||
|
*/
|
||||||
|
tabs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current tab.
|
||||||
|
* @type {T}
|
||||||
|
*/
|
||||||
|
currentTab;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new base abstract tablist.
|
||||||
|
*
|
||||||
|
* @param tablist {HTMLUListElement} the tab list
|
||||||
|
*/
|
||||||
|
constructor(tablist) {
|
||||||
|
tablist.onkeydown = this.onTabKeyDown.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions when keys are pressed on the tabs.
|
||||||
|
*
|
||||||
|
* @param event {KeyboardEvent} the key event
|
||||||
|
*/
|
||||||
|
onTabKeyDown(event) {
|
||||||
|
const currentIndex = this.tabs.indexOf(this.currentTab);
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newIndex = currentIndex;
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowRight":
|
||||||
|
newIndex = (newIndex + 1) % this.tabs.length;
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
newIndex = (newIndex - 1 + this.tabs.length) % this.tabs.length;
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
newIndex = 0;
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
newIndex = this.tabs.length - 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
this.tabs[newIndex].focus();
|
||||||
|
this.onTabFocus(this.tabs[newIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions when a tab is focused.
|
||||||
|
*
|
||||||
|
* @param tab {T} the tab
|
||||||
|
*/
|
||||||
|
onTabFocus(tab) { /* Do nothing */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to a tab.
|
||||||
|
*
|
||||||
|
* @param tab {T} the tab
|
||||||
|
*/
|
||||||
|
switchTo(tab) {
|
||||||
|
this.tabs.forEach(t => t.setActive(t === tab));
|
||||||
|
this.currentTab = tab;
|
||||||
|
this.currentTab.onActivated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract tab.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
class BaseTab {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab element.
|
||||||
|
* @type {HTMLButtonElement}
|
||||||
|
*/
|
||||||
|
#tab;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The panel element.
|
||||||
|
* @type {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
#panel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new base abstract tab.
|
||||||
|
*
|
||||||
|
* @param tab {HTMLButtonElement} The tab element.
|
||||||
|
* @param panel {HTMLDivElement} The panel element.
|
||||||
|
* @param switchTo {function(BaseTab): void} The function to switch to the tab.
|
||||||
|
*/
|
||||||
|
constructor(tab, panel, switchTo) {
|
||||||
|
this.#tab = tab;
|
||||||
|
this.#panel = panel;
|
||||||
|
this.#tab.onclick = () => switchTo(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active state of the tab.
|
||||||
|
*
|
||||||
|
* @param isActive {boolean} true if the tab is active, false otherwise
|
||||||
|
*/
|
||||||
|
setActive(isActive) {
|
||||||
|
if (isActive) {
|
||||||
|
this.#tab.classList.add("active");
|
||||||
|
this.#tab.tabIndex = 0;
|
||||||
|
this.#tab.ariaSelected = "true";
|
||||||
|
this.#panel.classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
this.#tab.classList.remove("active");
|
||||||
|
this.#tab.tabIndex = -1;
|
||||||
|
this.#tab.ariaSelected = "false";
|
||||||
|
this.#panel.classList.add("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the tab is active.
|
||||||
|
*
|
||||||
|
* @returns {boolean} true if the tab is active, false otherwise
|
||||||
|
*/
|
||||||
|
isActive() {
|
||||||
|
return this.#tab.classList.contains("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions when the tab is activated.
|
||||||
|
*/
|
||||||
|
onActivated() { /* Do nothing */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the tab.
|
||||||
|
*/
|
||||||
|
focus() {
|
||||||
|
this.#tab.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,9 @@
|
|||||||
/**
|
/**
|
||||||
* A description editor.
|
* A description editor.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseTablist<BaseDescriptionEditorTab>}
|
||||||
*/
|
*/
|
||||||
class DescriptionEditor {
|
class DescriptionEditor extends BaseTablist {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line item editor
|
* The line item editor
|
||||||
@@ -58,12 +59,6 @@ class DescriptionEditor {
|
|||||||
*/
|
*/
|
||||||
debitCredit;
|
debitCredit;
|
||||||
|
|
||||||
/**
|
|
||||||
* The current tab
|
|
||||||
* @type {DescriptionEditorTabPlane}
|
|
||||||
*/
|
|
||||||
currentTab;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description input
|
* The description input
|
||||||
* @type {HTMLInputElement}
|
* @type {HTMLInputElement}
|
||||||
@@ -125,10 +120,10 @@ class DescriptionEditor {
|
|||||||
selectedAccount = null;
|
selectedAccount = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab planes
|
* The tabs by their ID.
|
||||||
* @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}}
|
* @type {DescriptionEditorTabFactory}
|
||||||
*/
|
*/
|
||||||
tabPlanes = {};
|
#tabsByID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a description editor.
|
* Constructs a description editor.
|
||||||
@@ -137,31 +132,38 @@ class DescriptionEditor {
|
|||||||
* @param debitCredit {string} either "debit" or "credit"
|
* @param debitCredit {string} either "debit" or "credit"
|
||||||
*/
|
*/
|
||||||
constructor(lineItemEditor, debitCredit) {
|
constructor(lineItemEditor, debitCredit) {
|
||||||
|
const prefix = `accounting-description-editor-${debitCredit}`;
|
||||||
|
super(document.getElementById(`${prefix}-tab-list`));
|
||||||
|
this.prefix = prefix;
|
||||||
this.lineItemEditor = lineItemEditor;
|
this.lineItemEditor = lineItemEditor;
|
||||||
this.debitCredit = debitCredit;
|
this.debitCredit = debitCredit;
|
||||||
this.prefix = `accounting-description-editor-${debitCredit}`;
|
this.#form = document.getElementById(prefix);
|
||||||
this.#form = document.getElementById(this.prefix);
|
this.#modal = document.getElementById(`${prefix}-modal`);
|
||||||
this.#modal = document.getElementById(`${this.prefix}-modal`);
|
this.#descriptionInput = document.getElementById(`${prefix}-description`);
|
||||||
this.#descriptionInput = document.getElementById(`${this.prefix}-description`);
|
this.#offsetButton = document.getElementById(`${prefix}-offset`);
|
||||||
this.#offsetButton = document.getElementById(`${this.prefix}-offset`);
|
this.number = document.getElementById(`${prefix}-annotation-number`);
|
||||||
this.number = document.getElementById(`${this.prefix}-annotation-number`);
|
this.note = document.getElementById(`${prefix}-annotation-note`);
|
||||||
this.note = document.getElementById(`${this.prefix}-annotation-note`);
|
this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${prefix}-account-confirmed`));
|
||||||
this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`));
|
this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
|
||||||
this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
|
|
||||||
|
|
||||||
for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) {
|
this.#tabsByID = new DescriptionEditorTabFactory(this);
|
||||||
const tab = new cls(this);
|
this.tabs = [this.#tabsByID.general, this.#tabsByID.travel, this.#tabsByID.bus, this.#tabsByID.recurring, this.#tabsByID.annotation];
|
||||||
this.tabPlanes[tab.tabId()] = tab;
|
this.currentTab = this.tabs[0];
|
||||||
}
|
|
||||||
this.currentTab = this.tabPlanes.general;
|
|
||||||
this.#descriptionInput.onchange = () => this.#onDescriptionChange();
|
this.#descriptionInput.onchange = () => this.#onDescriptionChange();
|
||||||
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
|
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen(this.#modal.id);
|
||||||
this.#form.onsubmit = () => {
|
this.#form.onsubmit = () => {
|
||||||
if (this.currentTab.validate()) {
|
if (this.currentTab.validate()) {
|
||||||
this.#submit();
|
this.#submit();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeButton = document.getElementById(`${prefix}-close`);
|
||||||
|
this.#modal.onkeydown = (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeButton.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,26 +201,26 @@ class DescriptionEditor {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
#onDescriptionChange() {
|
#onDescriptionChange() {
|
||||||
this.#resetTabPlanes();
|
this.#resetTabs();
|
||||||
this.selectedAccount = null;
|
this.selectedAccount = null;
|
||||||
this.description = this.description.trim();
|
this.description = this.description.trim();
|
||||||
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) {
|
for (const tab of [this.#tabsByID.recurring, this.#tabsByID.bus, this.#tabsByID.travel, this.#tabsByID.general]) {
|
||||||
if (tabPlane.populate()) {
|
if (tab.populate()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.tabPlanes.annotation.populate();
|
this.#tabsByID.annotation.populate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the tab planes.
|
* Resets the tabs.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
#resetTabPlanes() {
|
#resetTabs() {
|
||||||
for (const tabPlane of Object.values(this.tabPlanes)) {
|
for (const tab of this.tabs) {
|
||||||
tabPlane.reset();
|
tab.reset();
|
||||||
}
|
}
|
||||||
this.tabPlanes.general.switchToMe();
|
this.switchTo(this.tabs[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -344,6 +346,7 @@ class DescriptionEditor {
|
|||||||
/**
|
/**
|
||||||
* An account option in the description editor.
|
* An account option in the description editor.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorAccount extends JournalEntryAccount {
|
class DescriptionEditorAccount extends JournalEntryAccount {
|
||||||
|
|
||||||
@@ -415,6 +418,7 @@ class DescriptionEditorAccount extends JournalEntryAccount {
|
|||||||
/**
|
/**
|
||||||
* A suggested account.
|
* A suggested account.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
|
class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
|
||||||
|
|
||||||
@@ -432,6 +436,7 @@ class DescriptionEditorSuggestedAccount extends DescriptionEditorAccount {
|
|||||||
/**
|
/**
|
||||||
* The account option that is specified or confirmed by the user.
|
* The account option that is specified or confirmed by the user.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
|
class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
|
||||||
|
|
||||||
@@ -460,12 +465,63 @@ class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tab plane.
|
* The tab factory.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class DescriptionEditorTabFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The general tag tab
|
||||||
|
* @type {GeneralTagTab}
|
||||||
|
*/
|
||||||
|
general;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The general trip tab
|
||||||
|
* @type {GeneralTripTab}
|
||||||
|
*/
|
||||||
|
travel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bus trip tab
|
||||||
|
* @type {BusTripTab}
|
||||||
|
*/
|
||||||
|
bus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The recurring transactions tab
|
||||||
|
* @type {RecurringTab}
|
||||||
|
*/
|
||||||
|
recurring;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The annotation tab
|
||||||
|
* @type {AnnotationTab}
|
||||||
|
*/
|
||||||
|
annotation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the tab factory
|
||||||
|
*
|
||||||
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
|
*/
|
||||||
|
constructor(editor) {
|
||||||
|
this.general = new GeneralTagTab(editor);
|
||||||
|
this.travel = new GeneralTripTab(editor);
|
||||||
|
this.bus = new BusTripTab(editor);
|
||||||
|
this.recurring = new RecurringTab(editor);
|
||||||
|
this.annotation = new AnnotationTab(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract tab in the description editor.
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorTabPlane {
|
class BaseDescriptionEditorTab extends BaseTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parent description editor
|
* The parent description editor
|
||||||
@@ -480,47 +536,29 @@ class DescriptionEditorTabPlane {
|
|||||||
prefix;
|
prefix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab
|
* Constructs a base abstract tab in the description editor.
|
||||||
* @type {HTMLSpanElement}
|
|
||||||
*/
|
|
||||||
#tab;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The page
|
|
||||||
* @type {HTMLDivElement}
|
|
||||||
*/
|
|
||||||
#page;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a tab plane.
|
|
||||||
*
|
*
|
||||||
|
* @param tabID {string} the tab ID
|
||||||
* @param editor {DescriptionEditor} the parent description editor
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(tabID, editor) {
|
||||||
|
const prefix = `${editor.prefix}-${tabID}`;
|
||||||
|
const tab = document.getElementById(`${prefix}-tab`);
|
||||||
|
const panel = document.getElementById(`${prefix}-panel`);
|
||||||
|
super(tab, panel, editor.switchTo.bind(editor));
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.prefix = `${this.editor.prefix}-${this.tabId()}`;
|
this.prefix = prefix;
|
||||||
this.#tab = document.getElementById(`${this.prefix}-tab`);
|
|
||||||
this.#page = document.getElementById(`${this.prefix}-page`);
|
|
||||||
this.#tab.onclick = () => this.switchToMe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Resets the tab panel input.
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
tabId() { throw new Error("Method not implemented.") };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the tab plane input.
|
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
reset() { throw new Error("Method not implemented."); }
|
reset() { throw new Error("Method not implemented."); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tab plane with the description input.
|
* Populates the tab panel with the description input.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if the description input matches this tab, or false otherwise
|
* @return {boolean} true if the description input matches this tab, or false otherwise
|
||||||
* @abstract
|
* @abstract
|
||||||
@@ -528,39 +566,21 @@ class DescriptionEditorTabPlane {
|
|||||||
populate() { throw new Error("Method not implemented."); }
|
populate() { throw new Error("Method not implemented."); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
validate() { throw new Error("Method not implemented."); }
|
validate() { throw new Error("Method not implemented."); }
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches to the tab plane.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
switchToMe() {
|
|
||||||
for (const tabPlane of Object.values(this.editor.tabPlanes)) {
|
|
||||||
tabPlane.#tab.classList.remove("active")
|
|
||||||
tabPlane.#tab.ariaCurrent = "false";
|
|
||||||
tabPlane.#page.classList.add("d-none");
|
|
||||||
tabPlane.#page.ariaCurrent = "false";
|
|
||||||
}
|
|
||||||
this.#tab.classList.add("active");
|
|
||||||
this.#tab.ariaCurrent = "page";
|
|
||||||
this.#page.classList.remove("d-none");
|
|
||||||
this.#page.ariaCurrent = "page";
|
|
||||||
this.editor.currentTab = this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tag plane with selectable tags.
|
* The base abstract tab with selectable tags.
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
class BaseTagTab extends BaseDescriptionEditorTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tag input
|
* The tag input
|
||||||
@@ -578,20 +598,21 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
* The tag buttons
|
* The tag buttons
|
||||||
* @type {HTMLButtonElement[]}
|
* @type {HTMLButtonElement[]}
|
||||||
*/
|
*/
|
||||||
tagButtons;
|
#tagButtons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a base abstract tab with selectable tags.
|
||||||
*
|
*
|
||||||
|
* @param tabID {string} the tab ID
|
||||||
* @param editor {DescriptionEditor} the parent description editor
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(tabID, editor) {
|
||||||
super(editor);
|
super(tabID, editor);
|
||||||
this.tag = document.getElementById(`${this.prefix}-tag`);
|
this.tag = document.getElementById(`${this.prefix}-tag`);
|
||||||
this.tagError = document.getElementById(`${this.prefix}-tag-error`);
|
this.tagError = document.getElementById(`${this.prefix}-tag-error`);
|
||||||
// noinspection JSValidateTypes
|
// noinspection JSValidateTypes
|
||||||
this.tagButtons = Array.from(document.getElementsByClassName(`${this.prefix}-btn-tag`));
|
this.#tagButtons = Array.from(document.getElementsByClassName(`${this.prefix}-btn-tag`));
|
||||||
this.initializeTagButtons();
|
this.initializeTagButtons();
|
||||||
this.tag.onchange = () => {
|
this.tag.onchange = () => {
|
||||||
this.onTagChange();
|
this.onTagChange();
|
||||||
@@ -606,7 +627,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
onTagChange() {
|
onTagChange() {
|
||||||
this.tag.value = this.tag.value.trim();
|
this.tag.value = this.tag.value.trim();
|
||||||
let isMatched = false;
|
let isMatched = false;
|
||||||
for (const tagButton of this.tagButtons) {
|
for (const tagButton of this.#tagButtons) {
|
||||||
if (tagButton.dataset.value === this.tag.value) {
|
if (tagButton.dataset.value === this.tag.value) {
|
||||||
tagButton.classList.remove("btn-outline-primary");
|
tagButton.classList.remove("btn-outline-primary");
|
||||||
tagButton.classList.add("btn-primary");
|
tagButton.classList.add("btn-primary");
|
||||||
@@ -624,19 +645,18 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the description according to the input in the tab plane.
|
* Updates the description according to the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
*/
|
*/
|
||||||
updateDescription() { throw new Error("Method not implemented."); }
|
updateDescription() { throw new Error("Method not implemented."); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches to the tab plane.
|
* @inheritDoc
|
||||||
*
|
* @override
|
||||||
*/
|
*/
|
||||||
switchToMe() {
|
onActivated() {
|
||||||
super.switchToMe();
|
for (const tagButton of this.#tagButtons) {
|
||||||
for (const tagButton of this.tagButtons) {
|
|
||||||
if (tagButton.classList.contains("btn-primary")) {
|
if (tagButton.classList.contains("btn-primary")) {
|
||||||
this.editor.updateCurrentSuggestedAccounts(tagButton);
|
this.editor.updateCurrentSuggestedAccounts(tagButton);
|
||||||
return;
|
return;
|
||||||
@@ -650,9 +670,9 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
initializeTagButtons() {
|
initializeTagButtons() {
|
||||||
for (const tagButton of this.tagButtons) {
|
for (const tagButton of this.#tagButtons) {
|
||||||
tagButton.onclick = () => {
|
tagButton.onclick = () => {
|
||||||
for (const otherButton of this.tagButtons) {
|
for (const otherButton of this.#tagButtons) {
|
||||||
otherButton.classList.remove("btn-primary");
|
otherButton.classList.remove("btn-primary");
|
||||||
otherButton.classList.add("btn-outline-primary");
|
otherButton.classList.add("btn-outline-primary");
|
||||||
}
|
}
|
||||||
@@ -698,7 +718,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the tab plane input.
|
* Resets the tab panel input.
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -706,7 +726,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
this.tag.value = "";
|
this.tag.value = "";
|
||||||
this.tag.classList.remove("is-invalid");
|
this.tag.classList.remove("is-invalid");
|
||||||
this.tagError.innerText = "";
|
this.tagError.innerText = "";
|
||||||
for (const tagButton of this.tagButtons) {
|
for (const tagButton of this.#tagButtons) {
|
||||||
tagButton.classList.remove("btn-primary");
|
tagButton.classList.remove("btn-primary");
|
||||||
tagButton.classList.add("btn-outline-primary");
|
tagButton.classList.add("btn-outline-primary");
|
||||||
}
|
}
|
||||||
@@ -714,24 +734,24 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The general tag tab plane.
|
* The general tag tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
|
class GeneralTagTab extends BaseTagTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Constructs a general tag tab.
|
||||||
*
|
*
|
||||||
* @return {string}
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
* @abstract
|
* @override
|
||||||
*/
|
*/
|
||||||
tabId() {
|
constructor(editor) {
|
||||||
return "general";
|
super("general", editor);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the description according to the input in the tab plane.
|
* Updates the description according to the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -746,7 +766,7 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tab plane with the description input.
|
* Populates the tab panel with the description input.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if the description input matches this tab, or false otherwise
|
* @return {boolean} true if the description input matches this tab, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -760,12 +780,12 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
|
|||||||
this.tag.value = found[1];
|
this.tag.value = found[1];
|
||||||
this.onTagChange();
|
this.onTagChange();
|
||||||
}
|
}
|
||||||
this.switchToMe();
|
this.editor.switchTo(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
*/
|
*/
|
||||||
@@ -775,11 +795,11 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The general trip tab plane.
|
* The general trip tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
class GeneralTripTab extends BaseTagTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The origin
|
* The origin
|
||||||
@@ -812,13 +832,13 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
#directionButtons;
|
#directionButtons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a general trip tab.
|
||||||
*
|
*
|
||||||
* @param editor {DescriptionEditor} the parent description editor
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
super(editor);
|
super("travel", editor);
|
||||||
this.#from = document.getElementById(`${this.prefix}-from`);
|
this.#from = document.getElementById(`${this.prefix}-from`);
|
||||||
this.#fromError = document.getElementById(`${this.prefix}-from-error`);
|
this.#fromError = document.getElementById(`${this.prefix}-from-error`);
|
||||||
this.#to = document.getElementById(`${this.prefix}-to`);
|
this.#to = document.getElementById(`${this.prefix}-to`);
|
||||||
@@ -849,17 +869,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Updates the description according to the input in the tab panel.
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "travel";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the description according to the input in the tab plane.
|
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -875,7 +885,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the tab plane input.
|
* Resets the tab panel input.
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -899,7 +909,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tab plane with the description input.
|
* Populates the tab panel with the description input.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if the description input matches this tab, or false otherwise
|
* @return {boolean} true if the description input matches this tab, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -924,12 +934,12 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#to.value = found[4];
|
this.#to.value = found[4];
|
||||||
this.switchToMe();
|
this.editor.switchTo(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -974,11 +984,11 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bus trip tab plane.
|
* The bus trip tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
class BusTripTab extends BaseTagTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The route
|
* The route
|
||||||
@@ -1017,13 +1027,13 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
#toError;
|
#toError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a bus trip tab.
|
||||||
*
|
*
|
||||||
* @param editor {DescriptionEditor} the parent description editor
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
super(editor);
|
super("bus", editor);
|
||||||
this.#route = document.getElementById(`${this.prefix}-route`);
|
this.#route = document.getElementById(`${this.prefix}-route`);
|
||||||
this.#routeError = document.getElementById(`${this.prefix}-route-error`);
|
this.#routeError = document.getElementById(`${this.prefix}-route-error`);
|
||||||
this.#from = document.getElementById(`${this.prefix}-from`);
|
this.#from = document.getElementById(`${this.prefix}-from`);
|
||||||
@@ -1048,17 +1058,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Updates the description according to the input in the tab panel.
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "bus";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the description according to the input in the tab plane.
|
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -1067,7 +1067,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the tab plane input.
|
* Resets the tab panel input.
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -1085,7 +1085,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tab plane with the description input.
|
* Populates the tab panel with the description input.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if the description input matches this tab, or false otherwise
|
* @return {boolean} true if the description input matches this tab, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -1102,12 +1102,12 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
this.#route.value = found[2];
|
this.#route.value = found[2];
|
||||||
this.#from.value = found[3];
|
this.#from.value = found[3];
|
||||||
this.#to.value = found[4];
|
this.#to.value = found[4];
|
||||||
this.switchToMe();
|
this.editor.switchTo(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
*/
|
*/
|
||||||
@@ -1162,11 +1162,11 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The recurring transaction tab plane.
|
* The recurring transaction tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
class RecurringTab extends BaseDescriptionEditorTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The month names
|
* The month names
|
||||||
@@ -1181,13 +1181,13 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
#itemButtons;
|
#itemButtons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a recurring transaction tab.
|
||||||
*
|
*
|
||||||
* @param editor {DescriptionEditor} the parent description editor
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
super(editor);
|
super("recurring", editor);
|
||||||
this.#monthNames = [
|
this.#monthNames = [
|
||||||
"",
|
"",
|
||||||
A_("January"), A_("February"), A_("March"), A_("April"),
|
A_("January"), A_("February"), A_("March"), A_("April"),
|
||||||
@@ -1229,17 +1229,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Resets the tab panel input.
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "recurring";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the tab plane input.
|
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -1251,7 +1241,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tab plane with the description input.
|
* Populates the tab panel with the description input.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if the description input matches this tab, or false otherwise
|
* @return {boolean} true if the description input matches this tab, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -1261,7 +1251,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
if (this.#getDescription(itemButton) === this.editor.description) {
|
if (this.#getDescription(itemButton) === this.editor.description) {
|
||||||
itemButton.classList.add("btn-primary");
|
itemButton.classList.add("btn-primary");
|
||||||
itemButton.classList.remove("btn-outline-primary");
|
itemButton.classList.remove("btn-outline-primary");
|
||||||
this.switchToMe();
|
this.editor.switchTo(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1269,11 +1259,10 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches to the tab plane.
|
* @inheritDoc
|
||||||
*
|
* @override
|
||||||
*/
|
*/
|
||||||
switchToMe() {
|
onActivated() {
|
||||||
super.switchToMe();
|
|
||||||
for (const itemButton of this.#itemButtons) {
|
for (const itemButton of this.#itemButtons) {
|
||||||
if (itemButton.classList.contains("btn-primary")) {
|
if (itemButton.classList.contains("btn-primary")) {
|
||||||
this.editor.updateCurrentSuggestedAccounts(itemButton);
|
this.editor.updateCurrentSuggestedAccounts(itemButton);
|
||||||
@@ -1284,7 +1273,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -1295,20 +1284,20 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The annotation tab plane.
|
* The annotation tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
|
class AnnotationTab extends BaseDescriptionEditorTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs an annotation tab.
|
||||||
*
|
*
|
||||||
* @param editor {DescriptionEditor} the parent description editor
|
* @param editor {DescriptionEditor} the parent description editor
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
super(editor);
|
super("annotation", editor);
|
||||||
this.editor.number.onchange = () => this.updateDescription();
|
this.editor.number.onchange = () => this.updateDescription();
|
||||||
this.editor.note.onchange = () => {
|
this.editor.note.onchange = () => {
|
||||||
this.editor.note.value = this.editor.note.value.trim();
|
this.editor.note.value = this.editor.note.value.trim();
|
||||||
@@ -1317,17 +1306,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Updates the description according to the input in the tab panel.
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "annotation";
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the description according to the input in the tab plane.
|
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -1345,7 +1324,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the tab plane input.
|
* Resets the tab panel input.
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
@@ -1355,7 +1334,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populates the tab plane with the description input.
|
* Populates the tab panel with the description input.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if the description input matches this tab, or false otherwise
|
* @return {boolean} true if the description input matches this tab, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
@@ -1379,7 +1358,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the input in the tab plane.
|
* Validates the input in the tab panel.
|
||||||
*
|
*
|
||||||
* @return {boolean} true if valid, or false otherwise
|
* @return {boolean} true if valid, or false otherwise
|
||||||
* @override
|
* @override
|
||||||
|
|||||||
@@ -25,14 +25,16 @@
|
|||||||
/**
|
/**
|
||||||
* The account selector.
|
* The account selector.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<BaseJournalEntryAccountOption>}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class JournalEntryAccountSelector {
|
class JournalEntryAccountSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line item editor
|
* The line item editor
|
||||||
* @type {JournalEntryLineItemEditor}
|
* @type {JournalEntryLineItemEditor}
|
||||||
*/
|
*/
|
||||||
lineItemEditor;
|
#lineItemEditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Either "debit" or "credit"
|
* Either "debit" or "credit"
|
||||||
@@ -46,12 +48,6 @@ class JournalEntryAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#clearButton
|
#clearButton
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
* @type {HTMLParagraphElement}
|
* @type {HTMLParagraphElement}
|
||||||
@@ -64,15 +60,9 @@ class JournalEntryAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The options
|
|
||||||
* @type {JournalEntryAccountOption[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The more item to show all accounts
|
* The more item to show all accounts
|
||||||
* @type {HTMLLIElement}
|
* @type {MoreItems}
|
||||||
*/
|
*/
|
||||||
#more;
|
#more;
|
||||||
|
|
||||||
@@ -89,40 +79,55 @@ class JournalEntryAccountSelector {
|
|||||||
* @param debitCredit {string} either "debit" or "credit"
|
* @param debitCredit {string} either "debit" or "credit"
|
||||||
*/
|
*/
|
||||||
constructor(lineItemEditor, debitCredit) {
|
constructor(lineItemEditor, debitCredit) {
|
||||||
this.lineItemEditor = lineItemEditor
|
|
||||||
this.#debitCredit = debitCredit;
|
|
||||||
const prefix = `accounting-account-selector-${debitCredit}`;
|
const prefix = `accounting-account-selector-${debitCredit}`;
|
||||||
this.#query = document.getElementById(`${prefix}-query`);
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(element, lineItemEditor.saveAccount.bind(lineItemEditor)));
|
||||||
|
super(query, options);
|
||||||
|
this.#lineItemEditor = lineItemEditor;
|
||||||
|
this.#debitCredit = debitCredit;
|
||||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(this, element));
|
const moreElement = document.getElementById(`${prefix}-more`);
|
||||||
this.#more = document.getElementById(`${prefix}-more`);
|
this.#more = new MoreItems(moreElement);
|
||||||
this.#clearButton = document.getElementById(`${prefix}-btn-clear`);
|
moreElement.onclick = () => {
|
||||||
|
|
||||||
this.#more.onclick = () => {
|
|
||||||
this.#isShowMore = true;
|
this.#isShowMore = true;
|
||||||
this.#more.classList.add("d-none");
|
this.#more.setShown(false);
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#clearButton = document.getElementById(`${prefix}-btn-clear`);
|
||||||
|
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
|
||||||
|
|
||||||
|
const modal = document.getElementById(`${prefix}-modal`);
|
||||||
|
const closeButton = document.getElementById(`${prefix}-close`);
|
||||||
|
modal.onkeydown = (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeButton.click();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
|
||||||
this.#clearButton.onclick = () => this.lineItemEditor.clearAccount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
const codesInUse = this.#getCodesUsedInForm();
|
const codesInUse = this.#getCodesUsedInForm();
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) {
|
if (option.isMatched(this.#isShowMore, codesInUse, this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!this.#isShowMore) {
|
||||||
|
this.shownOptions.push(this.#more);
|
||||||
|
}
|
||||||
if (!isAnyMatched && this.#isShowMore) {
|
if (!isAnyMatched && this.#isShowMore) {
|
||||||
this.#optionList.classList.add("d-none");
|
this.#optionList.classList.add("d-none");
|
||||||
this.#queryNoResult.classList.remove("d-none");
|
this.#queryNoResult.classList.remove("d-none");
|
||||||
@@ -138,9 +143,9 @@ class JournalEntryAccountSelector {
|
|||||||
* @return {string[]} the account codes that are used in the form
|
* @return {string[]} the account codes that are used in the form
|
||||||
*/
|
*/
|
||||||
#getCodesUsedInForm() {
|
#getCodesUsedInForm() {
|
||||||
const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
const inUse = this.#lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
||||||
if (this.lineItemEditor.account !== null) {
|
if (this.#lineItemEditor.account !== null) {
|
||||||
inUse.push(this.lineItemEditor.account.code);
|
inUse.push(this.#lineItemEditor.account.code);
|
||||||
}
|
}
|
||||||
return inUse
|
return inUse
|
||||||
}
|
}
|
||||||
@@ -150,14 +155,13 @@ class JournalEntryAccountSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#query.value = "";
|
this.query.value = "";
|
||||||
this.#isShowMore = false;
|
this.#isShowMore = false;
|
||||||
this.#more.classList.remove("d-none");
|
this.#more.setShown(true);
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
for (const option of this.#options) {
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
option.setActive(this.lineItemEditor.account !== null && option.code === this.lineItemEditor.account.code);
|
this.selectOption(this.shownOptions.find((option) => this.#lineItemEditor.account !== null && option.code === this.#lineItemEditor.account.code));
|
||||||
}
|
if (this.#lineItemEditor.account === null) {
|
||||||
if (this.lineItemEditor.account === null) {
|
|
||||||
this.#clearButton.classList.add("btn-secondary");
|
this.#clearButton.classList.add("btn-secondary");
|
||||||
this.#clearButton.classList.remove("btn-danger");
|
this.#clearButton.classList.remove("btn-danger");
|
||||||
this.#clearButton.disabled = true;
|
this.#clearButton.disabled = true;
|
||||||
@@ -168,6 +172,17 @@ class JournalEntryAccountSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects an option.
|
||||||
|
*
|
||||||
|
* @param option {BaseJournalEntryAccountOption|undefined} the option.
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
selectOption(option) {
|
||||||
|
this.#more.setActive(false);
|
||||||
|
super.selectOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the account selector instances.
|
* Returns the account selector instances.
|
||||||
*
|
*
|
||||||
@@ -185,22 +200,37 @@ class JournalEntryAccountSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An account option
|
* The base abstract account option
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class JournalEntryAccountOption {
|
class BaseJournalEntryAccountOption extends BaseOption {
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account code
|
* The account code
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
code;
|
code = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the account matches the query.
|
||||||
|
*
|
||||||
|
* @param isShowMore {boolean} true to show all accounts, or false to show only those in use
|
||||||
|
* @param codesInUse {string[]} the account codes that are used in the form
|
||||||
|
* @param query {string} the query term
|
||||||
|
* @return {boolean} true if the option matches, or false otherwise
|
||||||
|
*/
|
||||||
|
isMatched(isShowMore, codesInUse, query) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An account option
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class JournalEntryAccountOption extends BaseJournalEntryAccountOption {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account title
|
* The account title
|
||||||
@@ -235,11 +265,11 @@ class JournalEntryAccountOption {
|
|||||||
/**
|
/**
|
||||||
* Constructs the account in the account selector.
|
* Constructs the account in the account selector.
|
||||||
*
|
*
|
||||||
* @param selector {JournalEntryAccountSelector} the account selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(JournalEntryAccountOption): void} the callback to save the option
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save) {
|
||||||
this.#element = element;
|
super(element);
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
this.title = element.dataset.title;
|
this.title = element.dataset.title;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
@@ -247,7 +277,7 @@ class JournalEntryAccountOption {
|
|||||||
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
|
|
||||||
this.#element.onclick = () => selector.lineItemEditor.saveAccount(this);
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,6 +287,7 @@ class JournalEntryAccountOption {
|
|||||||
* @param codesInUse {string[]} the account codes that are used in the form
|
* @param codesInUse {string[]} the account codes that are used in the form
|
||||||
* @param query {string} the query term
|
* @param query {string} the query term
|
||||||
* @return {boolean} true if the option matches, or false otherwise
|
* @return {boolean} true if the option matches, or false otherwise
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
isMatched(isShowMore, codesInUse, query) {
|
isMatched(isShowMore, codesInUse, query) {
|
||||||
return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
|
return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
|
||||||
@@ -290,30 +321,12 @@ class JournalEntryAccountOption {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
/**
|
||||||
*
|
* The more item to show all accounts.
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
*
|
||||||
*/
|
* @private
|
||||||
setShown(isShown) {
|
*/
|
||||||
if (isShown) {
|
class MoreItems extends BaseJournalEntryAccountOption {
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -701,12 +701,23 @@ class DebitCreditSubForm {
|
|||||||
this.#element.classList.add("accounting-not-empty");
|
this.#element.classList.add("accounting-not-empty");
|
||||||
this.currency.form.lineItemEditor.onAddNew(this);
|
this.currency.form.lineItemEditor.onAddNew(this);
|
||||||
};
|
};
|
||||||
|
this.#element.role = "button";
|
||||||
|
this.#element.tabIndex = 0;
|
||||||
|
this.#element.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#element.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
this.#element.classList.add("accounting-not-empty");
|
this.#element.classList.add("accounting-not-empty");
|
||||||
this.#element.classList.remove("accounting-clickable");
|
this.#element.classList.remove("accounting-clickable");
|
||||||
delete this.#element.dataset.bsToggle;
|
delete this.#element.dataset.bsToggle;
|
||||||
delete this.#element.dataset.bsTarget;
|
delete this.#element.dataset.bsTarget;
|
||||||
this.#element.onclick = null;
|
this.#element.onclick = null;
|
||||||
|
this.#element.removeAttribute("role");
|
||||||
|
this.#element.tabIndex = -1;
|
||||||
|
this.#element.onkeydown = null;
|
||||||
}
|
}
|
||||||
setElementShown(this.#content, this.lineItems.length !== 0);
|
setElementShown(this.#content, this.lineItems.length !== 0);
|
||||||
}
|
}
|
||||||
@@ -986,6 +997,12 @@ class LineItemSubForm {
|
|||||||
this.#element.parentElement.removeChild(this.#element);
|
this.#element.parentElement.removeChild(this.#element);
|
||||||
this.debitCreditSubForm.deleteLineItem(this);
|
this.debitCreditSubForm.deleteLineItem(this);
|
||||||
};
|
};
|
||||||
|
this.#control.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#control.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ class JournalEntryLineItemEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The control of the original line item
|
* The control of the original line item
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#originalLineItemControl;
|
#originalLineItemControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The original line item
|
* The original line item
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#originalLineItemText;
|
#originalLineItemText;
|
||||||
|
|
||||||
@@ -90,13 +90,13 @@ class JournalEntryLineItemEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The control of the description
|
* The control of the description
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#descriptionControl;
|
#descriptionControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description
|
* The description
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#descriptionText;
|
#descriptionText;
|
||||||
|
|
||||||
@@ -108,13 +108,13 @@ class JournalEntryLineItemEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The control of the account
|
* The control of the account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#accountControl;
|
#accountControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account
|
* The account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#accountText;
|
#accountText;
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class JournalEntryLineItemEditor {
|
|||||||
this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
|
this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
|
||||||
this.originalLineItemSelector = new OriginalLineItemSelector(this);
|
this.originalLineItemSelector = new OriginalLineItemSelector(this);
|
||||||
|
|
||||||
this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
|
this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen(this.modal.id)
|
||||||
this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
|
this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
|
||||||
this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
|
this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
|
||||||
this.#accountControl.onclick = () => this.#accountSelectors[this.debitCredit].onOpen();
|
this.#accountControl.onclick = () => this.#accountSelectors[this.debitCredit].onOpen();
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ class OptionForm {
|
|||||||
/**
|
/**
|
||||||
* The recurring expenses or incomes sub-form.
|
* The recurring expenses or incomes sub-form.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringExpenseIncomeSubForm {
|
class RecurringExpenseIncomeSubForm {
|
||||||
|
|
||||||
@@ -297,6 +298,14 @@ class RecurringExpenseIncomeSubForm {
|
|||||||
this.#element.dataset.bsTarget = `#${this.editor.modal.id}`;
|
this.#element.dataset.bsTarget = `#${this.editor.modal.id}`;
|
||||||
this.#element.onclick = () => this.editor.onAddNew();
|
this.#element.onclick = () => this.editor.onAddNew();
|
||||||
this.#content.classList.add("d-none");
|
this.#content.classList.add("d-none");
|
||||||
|
this.#element.role = "button";
|
||||||
|
this.#element.tabIndex = 0;
|
||||||
|
this.#element.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#element.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
this.#element.classList.add("accounting-not-empty");
|
this.#element.classList.add("accounting-not-empty");
|
||||||
this.#element.classList.remove("accounting-clickable");
|
this.#element.classList.remove("accounting-clickable");
|
||||||
@@ -304,6 +313,9 @@ class RecurringExpenseIncomeSubForm {
|
|||||||
delete this.#element.dataset.bsTarget;
|
delete this.#element.dataset.bsTarget;
|
||||||
this.#element.onclick = null;
|
this.#element.onclick = null;
|
||||||
this.#content.classList.remove("d-none");
|
this.#content.classList.remove("d-none");
|
||||||
|
this.#element.removeAttribute("role");
|
||||||
|
this.#element.tabIndex = -1;
|
||||||
|
this.#element.onkeydown = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,6 +362,7 @@ class RecurringExpenseIncomeSubForm {
|
|||||||
/**
|
/**
|
||||||
* A recurring item sub-form.
|
* A recurring item sub-form.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringItemSubForm {
|
class RecurringItemSubForm {
|
||||||
|
|
||||||
@@ -373,7 +386,7 @@ class RecurringItemSubForm {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The control
|
* The control
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#control;
|
#control;
|
||||||
|
|
||||||
@@ -397,7 +410,7 @@ class RecurringItemSubForm {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The text display of the name
|
* The text display of the name
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#nameText;
|
#nameText;
|
||||||
|
|
||||||
@@ -409,7 +422,7 @@ class RecurringItemSubForm {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The text display of the account
|
* The text display of the account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#accountText;
|
#accountText;
|
||||||
|
|
||||||
@@ -421,7 +434,7 @@ class RecurringItemSubForm {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The text display of the description template
|
* The text display of the description template
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#descriptionTemplateText;
|
#descriptionTemplateText;
|
||||||
|
|
||||||
@@ -551,6 +564,7 @@ class RecurringItemSubForm {
|
|||||||
/**
|
/**
|
||||||
* The recurring item editor.
|
* The recurring item editor.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringItemEditor {
|
class RecurringItemEditor {
|
||||||
|
|
||||||
@@ -592,13 +606,13 @@ class RecurringItemEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The control of the account
|
* The control of the account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#accountControl;
|
#accountControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text display of the account
|
* The text display of the account
|
||||||
* @type {HTMLDivElement}
|
* @type {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
#accountContainer;
|
#accountContainer;
|
||||||
|
|
||||||
@@ -829,14 +843,16 @@ class RecurringItemEditor {
|
|||||||
/**
|
/**
|
||||||
* The account selector for the recurring item editor.
|
* The account selector for the recurring item editor.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<RecurringAccount>}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringAccountSelector {
|
class RecurringAccountSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The recurring item editor
|
* The recurring item editor
|
||||||
* @type {RecurringItemEditor}
|
* @type {RecurringItemEditor}
|
||||||
*/
|
*/
|
||||||
editor;
|
#editor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Either "expense" or "income"
|
* Either "expense" or "income"
|
||||||
@@ -844,12 +860,6 @@ class RecurringAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#expenseIncome;
|
#expenseIncome;
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
* @type {HTMLParagraphElement}
|
* @type {HTMLParagraphElement}
|
||||||
@@ -862,12 +872,6 @@ class RecurringAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The account options
|
|
||||||
* @type {RecurringAccount[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The button to clear the account
|
* The button to clear the account
|
||||||
* @type {HTMLButtonElement}
|
* @type {HTMLButtonElement}
|
||||||
@@ -880,28 +884,39 @@ class RecurringAccountSelector {
|
|||||||
* @param editor {RecurringItemEditor} the recurring item editor
|
* @param editor {RecurringItemEditor} the recurring item editor
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
this.editor = editor;
|
|
||||||
this.#expenseIncome = editor.expenseIncome;
|
|
||||||
const prefix = `accounting-recurring-accounting-selector-${editor.expenseIncome}`;
|
const prefix = `accounting-recurring-accounting-selector-${editor.expenseIncome}`;
|
||||||
this.#query = document.getElementById(`${prefix}-query`);
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new RecurringAccount(element, editor.saveAccount.bind(editor)));
|
||||||
|
super(query, options);
|
||||||
|
this.#editor = editor;
|
||||||
|
this.#expenseIncome = editor.expenseIncome;
|
||||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new RecurringAccount(this, element));
|
|
||||||
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
|
||||||
|
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
||||||
this.#clearButton.onclick = () => this.editor.clearAccount();
|
this.#clearButton.onclick = () => this.#editor.clearAccount();
|
||||||
|
|
||||||
|
const modal = document.getElementById(`${prefix}-modal`);
|
||||||
|
const closeButton = document.getElementById(`${prefix}-close`);
|
||||||
|
modal.onkeydown = (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeButton.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#query.value)) {
|
if (option.isMatched(this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
@@ -921,12 +936,11 @@ class RecurringAccountSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#query.value = "";
|
this.query.value = "";
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
for (const option of this.#options) {
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
option.setActive(option.code === this.editor.accountCode);
|
this.selectOption(this.shownOptions.find((option) => option.code === this.#editor.accountCode));
|
||||||
}
|
if (this.#editor.accountCode === null) {
|
||||||
if (this.editor.accountCode === null) {
|
|
||||||
this.#clearButton.classList.add("btn-secondary");
|
this.#clearButton.classList.add("btn-secondary");
|
||||||
this.#clearButton.classList.remove("btn-danger");
|
this.#clearButton.classList.remove("btn-danger");
|
||||||
this.#clearButton.disabled = true;
|
this.#clearButton.disabled = true;
|
||||||
@@ -941,14 +955,9 @@ class RecurringAccountSelector {
|
|||||||
/**
|
/**
|
||||||
* An account in the account selector for the recurring item editor.
|
* An account in the account selector for the recurring item editor.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringAccount {
|
class RecurringAccount extends BaseOption {
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account code
|
* The account code
|
||||||
@@ -971,16 +980,16 @@ class RecurringAccount {
|
|||||||
/**
|
/**
|
||||||
* Constructs the account in the account selector for the recurring item editor.
|
* Constructs the account in the account selector for the recurring item editor.
|
||||||
*
|
*
|
||||||
* @param selector {RecurringAccountSelector} the account selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(RecurringAccount): void} the callback to save the option
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save) {
|
||||||
this.#element = element;
|
super(element);
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
|
|
||||||
this.#element.onclick = () => selector.editor.saveAccount(this);
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1000,30 +1009,4 @@ class RecurringAccount {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
|
||||||
*
|
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
|
||||||
*/
|
|
||||||
setShown(isShown) {
|
|
||||||
if (isShown) {
|
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,26 +25,16 @@
|
|||||||
/**
|
/**
|
||||||
* The original line item selector.
|
* The original line item selector.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<OriginalLineItem>}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class OriginalLineItemSelector {
|
class OriginalLineItemSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line item editor
|
* The line item editor
|
||||||
* @type {JournalEntryLineItemEditor}
|
* @type {JournalEntryLineItemEditor}
|
||||||
*/
|
*/
|
||||||
lineItemEditor;
|
#lineItemEditor;
|
||||||
|
|
||||||
/**
|
|
||||||
* The prefix of the HTML ID and class names
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
#prefix = "accounting-original-line-item-selector";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
@@ -58,12 +48,6 @@ class OriginalLineItemSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The options
|
|
||||||
* @type {OriginalLineItem[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options by their ID
|
* The options by their ID
|
||||||
* @type {Object.<string, OriginalLineItem>}
|
* @type {Object.<string, OriginalLineItem>}
|
||||||
@@ -81,22 +65,37 @@ class OriginalLineItemSelector {
|
|||||||
*/
|
*/
|
||||||
#debitCredit;
|
#debitCredit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The close button.
|
||||||
|
* @type {HTMLButtonElement}
|
||||||
|
*/
|
||||||
|
#closeButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an original line item selector.
|
* Constructs an original line item selector.
|
||||||
*
|
*
|
||||||
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
||||||
*/
|
*/
|
||||||
constructor(lineItemEditor) {
|
constructor(lineItemEditor) {
|
||||||
this.lineItemEditor = lineItemEditor;
|
const prefix = "accounting-original-line-item-selector";
|
||||||
this.#query = document.getElementById(`${this.#prefix}-query`);
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
this.#queryNoResult = document.getElementById(`${this.#prefix}-option-no-result`);
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new OriginalLineItem(element, lineItemEditor.saveOriginalLineItem.bind(lineItemEditor), lineItemEditor.form));
|
||||||
this.#optionList = document.getElementById(`${this.#prefix}-option-list`);
|
super(query, options);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${this.#prefix}-option`)).map((element) => new OriginalLineItem(this, element));
|
this.#lineItemEditor = lineItemEditor;
|
||||||
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#optionById = {};
|
this.#optionById = {};
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
this.#optionById[option.id] = option;
|
this.#optionById[option.id] = option;
|
||||||
}
|
}
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
this.#closeButton = document.getElementById(`${prefix}-close`);
|
||||||
|
|
||||||
|
const modal = document.getElementById(`${prefix}-modal`);
|
||||||
|
modal.onkeydown = (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.#closeButton.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,7 +125,7 @@ class OriginalLineItemSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
#updateNetBalances() {
|
#updateNetBalances() {
|
||||||
const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
|
const otherLineItems = this.#lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.#lineItemEditor.lineItem);
|
||||||
const otherOffsets = {}
|
const otherOffsets = {}
|
||||||
for (const otherLineItem of otherLineItems) {
|
for (const otherLineItem of otherLineItems) {
|
||||||
const otherOriginalLineItemId = otherLineItem.originalLineItemId;
|
const otherOriginalLineItemId = otherLineItem.originalLineItemId;
|
||||||
@@ -139,7 +138,7 @@ class OriginalLineItemSelector {
|
|||||||
}
|
}
|
||||||
otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].plus(amount);
|
otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].plus(amount);
|
||||||
}
|
}
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.id in otherOffsets) {
|
if (option.id in otherOffsets) {
|
||||||
option.updateNetBalance(otherOffsets[option.id]);
|
option.updateNetBalance(otherOffsets[option.id]);
|
||||||
} else {
|
} else {
|
||||||
@@ -151,12 +150,15 @@ class OriginalLineItemSelector {
|
|||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
|
if (option.isMatched(this.#debitCredit, this.#currencyCode, this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
@@ -174,24 +176,26 @@ class OriginalLineItemSelector {
|
|||||||
/**
|
/**
|
||||||
* The callback when the original line item selector is shown.
|
* The callback when the original line item selector is shown.
|
||||||
*
|
*
|
||||||
|
* @param parentID {string} the ID of the parent element
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen(parentID) {
|
||||||
this.#currencyCode = this.lineItemEditor.currencyCode;
|
this.#closeButton.dataset.bsTarget = `#${parentID}`;
|
||||||
this.#debitCredit = this.lineItemEditor.debitCredit;
|
this.#currencyCode = this.#lineItemEditor.currencyCode;
|
||||||
for (const option of this.#options) {
|
this.#debitCredit = this.#lineItemEditor.debitCredit;
|
||||||
option.setActive(option.id === this.lineItemEditor.originalLineItemId);
|
this.query.value = "";
|
||||||
}
|
|
||||||
this.#query.value = "";
|
|
||||||
this.#updateNetBalances();
|
this.#updateNetBalances();
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
|
this.selectOption(this.shownOptions.find((option) => option.id === this.#lineItemEditor.originalLineItemId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An original line item.
|
* An original line item.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class OriginalLineItem {
|
class OriginalLineItem extends BaseOption {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The journal entry form
|
* The journal entry form
|
||||||
@@ -199,12 +203,6 @@ class OriginalLineItem {
|
|||||||
*/
|
*/
|
||||||
#form;
|
#form;
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID
|
* The ID
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -274,12 +272,13 @@ class OriginalLineItem {
|
|||||||
/**
|
/**
|
||||||
* Constructs an original line item.
|
* Constructs an original line item.
|
||||||
*
|
*
|
||||||
* @param selector {OriginalLineItemSelector} the original line item selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(OriginalLineItem): void} the callback to save the option
|
||||||
|
* @param form {JournalEntryForm} the journal entry form
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save, form) {
|
||||||
this.#form = selector.lineItemEditor.form;
|
super(element);
|
||||||
this.#element = element;
|
this.#form = form;
|
||||||
this.id = element.dataset.id;
|
this.id = element.dataset.id;
|
||||||
this.date = element.dataset.date;
|
this.date = element.dataset.date;
|
||||||
this.#debitCredit = element.dataset.debitCredit;
|
this.#debitCredit = element.dataset.debitCredit;
|
||||||
@@ -291,7 +290,8 @@ class OriginalLineItem {
|
|||||||
this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
|
this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
this.#element.onclick = () => selector.lineItemEditor.saveOriginalLineItem(this);
|
|
||||||
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -380,30 +380,4 @@ class OriginalLineItem {
|
|||||||
const whole = Number(this.netBalance.minus(frac));
|
const whole = Number(this.netBalance.minus(frac));
|
||||||
return String(whole) + String(frac).substring(1);
|
return String(whole) + String(frac).substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
|
||||||
*
|
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
|
||||||
*/
|
|
||||||
setShown(isShown) {
|
|
||||||
if (isShown) {
|
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* period-chooser.js: The JavaScript for the period chooser
|
* period-chooser.js: The JavaScript for the period chooser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Copyright (c) 2023 imacat.
|
/* Copyright (c) 2023-2026 imacat.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -30,20 +30,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
/**
|
/**
|
||||||
* The period chooser.
|
* The period chooser.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseTablist<BasePeriodTab>}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
class PeriodChooser {
|
class PeriodChooser extends BaseTablist {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The modal of the period chooser
|
* The URL template for different periods.
|
||||||
* @type {HTMLDivElement}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
modal;
|
urlTemplate;
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab planes
|
|
||||||
* @type {{month: MonthTab, year: YearTab, day: DayTab, custom: CustomTab}}
|
|
||||||
*/
|
|
||||||
tabPlanes = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs the period chooser.
|
* Constructs the period chooser.
|
||||||
@@ -51,13 +47,24 @@ class PeriodChooser {
|
|||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
const prefix = "accounting-period-chooser";
|
const prefix = "accounting-period-chooser";
|
||||||
this.modal = document.getElementById(`${prefix}-modal`);
|
super(document.getElementById(`${prefix}-tab-list`));
|
||||||
for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
|
this.tabs = [new MonthTab(this), new YearTab(this), new DayTab(this), new CustomTab(this)];
|
||||||
const tab = new cls(this);
|
const modal = document.getElementById(`${prefix}-modal`);
|
||||||
this.tabPlanes[tab.tabId()] = tab;
|
this.urlTemplate = modal.dataset.urlTemplate;
|
||||||
|
for (const tab of this.tabs) {
|
||||||
|
if (tab.isActive()) {
|
||||||
|
this.currentTab = tab;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
onTabFocus(tab) { this.switchTo(tab); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The period chooser.
|
* The period chooser.
|
||||||
* @type {PeriodChooser}
|
* @type {PeriodChooser}
|
||||||
@@ -74,12 +81,12 @@ class PeriodChooser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tab plane.
|
* A base abstract period tab.
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class TabPlane {
|
class BasePeriodTab extends BaseTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The period chooser
|
* The period chooser
|
||||||
@@ -94,62 +101,27 @@ class TabPlane {
|
|||||||
prefix;
|
prefix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab
|
* Constructs a base abstract period tab.
|
||||||
* @type {HTMLSpanElement}
|
|
||||||
*/
|
|
||||||
#tab;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The page
|
|
||||||
* @type {HTMLDivElement}
|
|
||||||
*/
|
|
||||||
#page;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a tab plane.
|
|
||||||
*
|
*
|
||||||
|
* @param tabID {string} the tab ID
|
||||||
* @param chooser {PeriodChooser} the period chooser
|
* @param chooser {PeriodChooser} the period chooser
|
||||||
*/
|
*/
|
||||||
constructor(chooser) {
|
constructor(tabID, chooser) {
|
||||||
|
const prefix = `accounting-period-chooser-${tabID}`;
|
||||||
|
const tab = document.getElementById(`${prefix}-tab`);
|
||||||
|
const panel = document.getElementById(`${prefix}-panel`);
|
||||||
|
super(tab, panel, chooser.switchTo.bind(chooser));
|
||||||
this.chooser = chooser;
|
this.chooser = chooser;
|
||||||
this.prefix = `accounting-period-chooser-${this.tabId()}`;
|
this.prefix = prefix;
|
||||||
this.#tab = document.getElementById(`${this.prefix}-tab`);
|
|
||||||
this.#page = document.getElementById(`${this.prefix}-page`);
|
|
||||||
this.#tab.onclick = () => this.#switchToMe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab ID
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
tabId() { throw new Error("Method not implemented.") };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches to the tab plane.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
#switchToMe() {
|
|
||||||
for (const tabPlane of Object.values(this.chooser.tabPlanes)) {
|
|
||||||
tabPlane.#tab.classList.remove("active")
|
|
||||||
tabPlane.#tab.ariaCurrent = "false";
|
|
||||||
tabPlane.#page.classList.add("d-none");
|
|
||||||
tabPlane.#page.ariaCurrent = "false";
|
|
||||||
}
|
|
||||||
this.#tab.classList.add("active");
|
|
||||||
this.#tab.ariaCurrent = "page";
|
|
||||||
this.#page.classList.remove("d-none");
|
|
||||||
this.#page.ariaCurrent = "page";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The month tab plane.
|
* The month tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class MonthTab extends TabPlane {
|
class MonthTab extends BasePeriodTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The month chooser.
|
* The month chooser.
|
||||||
@@ -158,12 +130,12 @@ class MonthTab extends TabPlane {
|
|||||||
#monthChooser
|
#monthChooser
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a month tab.
|
||||||
*
|
*
|
||||||
* @param chooser {PeriodChooser} the period chooser
|
* @param chooser {PeriodChooser} the period chooser
|
||||||
*/
|
*/
|
||||||
constructor(chooser) {
|
constructor(chooser) {
|
||||||
super(chooser);
|
super("month", chooser);
|
||||||
const monthChooser = document.getElementById(`${this.prefix}-chooser`);
|
const monthChooser = document.getElementById(`${this.prefix}-chooser`);
|
||||||
if (monthChooser !== null) {
|
if (monthChooser !== null) {
|
||||||
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
|
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
|
||||||
@@ -183,45 +155,36 @@ class MonthTab extends TabPlane {
|
|||||||
const date = e.detail.date;
|
const date = e.detail.date;
|
||||||
const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
|
const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
|
||||||
const period = `${date.year}-${zeroPaddedMonth}`;
|
const period = `${date.year}-${zeroPaddedMonth}`;
|
||||||
window.location = chooser.modal.dataset.urlTemplate
|
window.location = chooser.urlTemplate
|
||||||
.replaceAll("PERIOD", period);
|
.replaceAll("PERIOD", period);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The year tab.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class YearTab extends BasePeriodTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tab ID
|
* Constructs a year tab.
|
||||||
*
|
*
|
||||||
* @return {string}
|
* @param chooser {PeriodChooser} the period chooser
|
||||||
*/
|
*/
|
||||||
tabId() {
|
constructor(chooser) {
|
||||||
return "month";
|
super("year", chooser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The year tab plane.
|
* The day tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class YearTab extends TabPlane {
|
class DayTab extends BasePeriodTab {
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab ID
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "year";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The day tab plane.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
class DayTab extends TabPlane {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The day input
|
* The day input
|
||||||
@@ -236,18 +199,18 @@ class DayTab extends TabPlane {
|
|||||||
#dateError;
|
#dateError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a day tab.
|
||||||
*
|
*
|
||||||
* @param chooser {PeriodChooser} the period chooser
|
* @param chooser {PeriodChooser} the period chooser
|
||||||
*/
|
*/
|
||||||
constructor(chooser) {
|
constructor(chooser) {
|
||||||
super(chooser);
|
super("day", chooser);
|
||||||
this.#date = document.getElementById(`${this.prefix}-date`);
|
this.#date = document.getElementById(`${this.prefix}-date`);
|
||||||
this.#dateError = document.getElementById(`${this.prefix}-date-error`);
|
this.#dateError = document.getElementById(`${this.prefix}-date-error`);
|
||||||
if (this.#date !== null) {
|
if (this.#date !== null) {
|
||||||
this.#date.onchange = () => {
|
this.#date.onchange = () => {
|
||||||
if (this.#validateDate()) {
|
if (this.#validateDate()) {
|
||||||
window.location = chooser.modal.dataset.urlTemplate
|
window.location = chooser.urlTemplate
|
||||||
.replaceAll("PERIOD", this.#date.value);
|
.replaceAll("PERIOD", this.#date.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -274,23 +237,14 @@ class DayTab extends TabPlane {
|
|||||||
this.#dateError.innerText = "";
|
this.#dateError.innerText = "";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab ID
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "day";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The custom tab plane.
|
* The custom tab.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class CustomTab extends TabPlane {
|
class CustomTab extends BasePeriodTab {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The start of the period
|
* The start of the period
|
||||||
@@ -320,20 +274,20 @@ class CustomTab extends TabPlane {
|
|||||||
* The confirm button
|
* The confirm button
|
||||||
* @type {HTMLButtonElement}
|
* @type {HTMLButtonElement}
|
||||||
*/
|
*/
|
||||||
#conform;
|
#confirm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a tab plane.
|
* Constructs a custom tab.
|
||||||
*
|
*
|
||||||
* @param chooser {PeriodChooser} the period chooser
|
* @param chooser {PeriodChooser} the period chooser
|
||||||
*/
|
*/
|
||||||
constructor(chooser) {
|
constructor(chooser) {
|
||||||
super(chooser);
|
super("custom", chooser);
|
||||||
this.#start = document.getElementById(`${this.prefix}-start`);
|
this.#start = document.getElementById(`${this.prefix}-start`);
|
||||||
this.#startError = document.getElementById(`${this.prefix}-start-error`);
|
this.#startError = document.getElementById(`${this.prefix}-start-error`);
|
||||||
this.#end = document.getElementById(`${this.prefix}-end`);
|
this.#end = document.getElementById(`${this.prefix}-end`);
|
||||||
this.#endError = document.getElementById(`${this.prefix}-end-error`);
|
this.#endError = document.getElementById(`${this.prefix}-end-error`);
|
||||||
this.#conform = document.getElementById(`${this.prefix}-confirm`);
|
this.#confirm = document.getElementById(`${this.prefix}-confirm`);
|
||||||
if (this.#start !== null) {
|
if (this.#start !== null) {
|
||||||
this.#start.onchange = () => {
|
this.#start.onchange = () => {
|
||||||
if (this.#validateStart()) {
|
if (this.#validateStart()) {
|
||||||
@@ -345,12 +299,12 @@ class CustomTab extends TabPlane {
|
|||||||
this.#start.max = this.#end.value;
|
this.#start.max = this.#end.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.#conform.onclick = () => {
|
this.#confirm.onclick = () => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
isValid = this.#validateStart() && isValid;
|
isValid = this.#validateStart() && isValid;
|
||||||
isValid = this.#validateEnd() && isValid;
|
isValid = this.#validateEnd() && isValid;
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
window.location = chooser.modal.dataset.urlTemplate
|
window.location = chooser.urlTemplate
|
||||||
.replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`);
|
.replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -406,13 +360,4 @@ class CustomTab extends TabPlane {
|
|||||||
this.#endError.innerText = "";
|
this.#endError.innerText = "";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab ID
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
tabId() {
|
|
||||||
return "custom";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* The Mia! Accounting Project
|
||||||
|
* timezone.js: The JavaScript for the timezone
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Copyright (c) 2024 imacat.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
* First written: 2024/6/4
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Initializes the page JavaScript.
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
setTimeZone();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the time zone.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function setTimeZone() {
|
||||||
|
document.cookie = `accounting-tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}; SameSite=Strict`;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,7 +23,8 @@ from typing import Any
|
|||||||
|
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
|
|
||||||
from accounting.locale import gettext
|
from .locale import gettext
|
||||||
|
from .utils.timezone import get_tz_today
|
||||||
|
|
||||||
|
|
||||||
def format_amount(value: Decimal | None) -> str | None:
|
def format_amount(value: Decimal | None) -> str | None:
|
||||||
@@ -47,7 +48,7 @@ def format_date(value: dt.date) -> str:
|
|||||||
:param value: The date.
|
:param value: The date.
|
||||||
:return: The human-friendly date text.
|
:return: The human-friendly date text.
|
||||||
"""
|
"""
|
||||||
today: dt.date = dt.date.today()
|
today: dt.date = get_tz_today()
|
||||||
if value == today:
|
if value == today:
|
||||||
return gettext("Today")
|
return gettext("Today")
|
||||||
if value == today - dt.timedelta(days=1):
|
if value == today - dt.timedelta(days=1):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# The Mia! Accounting Project.
|
# The Mia! Accounting Project.
|
||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
|
||||||
|
|
||||||
# Copyright (c) 2023 imacat.
|
# Copyright (c) 2023-2026 imacat.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,8 +17,11 @@
|
|||||||
"""The template globals.
|
"""The template globals.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from accounting.models import Currency
|
import sqlalchemy as sa
|
||||||
from accounting.utils.options import options
|
|
||||||
|
from . import db
|
||||||
|
from .models import Currency
|
||||||
|
from .utils.options import options
|
||||||
|
|
||||||
|
|
||||||
def currency_options() -> list[Currency]:
|
def currency_options() -> list[Currency]:
|
||||||
@@ -26,7 +29,8 @@ def currency_options() -> list[Currency]:
|
|||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
"""
|
"""
|
||||||
return Currency.query.order_by(Currency.code).all()
|
return db.session.scalars(
|
||||||
|
sa.select(Currency).order_by(Currency.code)).unique().all()
|
||||||
|
|
||||||
|
|
||||||
def default_currency_code() -> str:
|
def default_currency_code() -> str:
|
||||||
|
|||||||
@@ -27,28 +27,28 @@ First written: 2023/1/31
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i>
|
||||||
{{ A_("Edit") }}
|
{{ A_("Edit") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-bars-staggered"></i>
|
<i class="fa-solid fa-bars-staggered" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
{% if obj.can_delete %}
|
{% if obj.can_delete %}
|
||||||
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
|
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-secondary" type="button" disabled="disabled">
|
<button class="btn btn-secondary" type="button" disabled="disabled">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -57,8 +57,8 @@ First written: 2023/1/31
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}" aria-label="{{ A_("Edit") }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ First written: 2023/2/1
|
|||||||
{% extends "accounting/base.html" %}
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/base-combobox.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/account-form.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/account-form.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,9 +42,9 @@ First written: 2023/2/1
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}">
|
<input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}">
|
||||||
<div id="accounting-base-control" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal">
|
<button id="accounting-base-control" class="form-control text-start accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" type="button" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal">
|
||||||
<label class="form-label" for="accounting-base">{{ A_("Base account") }}</label>
|
<span class="form-label">{{ A_("Base account") }}</span>
|
||||||
<div id="accounting-base">
|
<span id="accounting-base">
|
||||||
{% if form.base_code.data %}
|
{% if form.base_code.data %}
|
||||||
{% if form.base_code.errors %}
|
{% if form.base_code.errors %}
|
||||||
{{ A_("(Unknown)") }}
|
{{ A_("(Unknown)") }}
|
||||||
@@ -51,15 +52,15 @@ First written: 2023/2/1
|
|||||||
{{ form.selected_base }}
|
{{ form.selected_base }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
<div id="accounting-base-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
|
<div id="accounting-base-error" class="invalid-feedback" role="alert">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ form.title.data|accounting_default }}" placeholder=" " required="required">
|
<input id="accounting-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ form.title.data|accounting_default }}" placeholder=" " required="required">
|
||||||
<label class="form-label" for="accounting-title">{{ A_("Title") }}</label>
|
<label class="form-label" for="accounting-title">{{ A_("Title") }}</label>
|
||||||
<div id="accounting-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
|
<div id="accounting-title-error" class="invalid-feedback" role="alert">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="accounting-is-need-offset-control" class="form-check form-switch mb-3 {% if form.base_code.data[0] not in ["1", "2", "3"] %} d-none {% endif %}">
|
<div id="accounting-is-need-offset-control" class="form-check form-switch mb-3 {% if form.base_code.data[0] not in ["1", "2", "3"] %} d-none {% endif %}">
|
||||||
@@ -71,14 +72,14 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{ A_("Save") }}
|
{{ A_("Save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit" aria-label="{{ A_("Save") }}">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -92,21 +93,21 @@ First written: 2023/2/1
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-base-selector-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-base-selector-query">
|
<label class="input-group-text" for="accounting-base-selector-query">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-base-selector-modal-label">
|
||||||
{% for base in form.base_options %}
|
{% for base in form.base_options %}
|
||||||
<li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
|
<li id="accounting-base-selector-option-{{ base.code }}" class="list-group-item accounting-clickable accounting-base-selector-option" role="option" aria-selected="false" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
|
||||||
{{ base }}
|
{{ base }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<p id="accounting-base-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
<p id="accounting-base-selector-option-no-result" class="d-none" role="status">{{ A_("There is no data.") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ First written: 2023/1/30
|
|||||||
<div class="mb-2 accounting-toolbar">
|
<div class="mb-2 accounting-toolbar">
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
<a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -36,7 +36,7 @@ First written: 2023/1/30
|
|||||||
<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">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
@@ -45,8 +45,8 @@ First written: 2023/1/30
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}" aria-label="{{ A_("New") }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ First written: 2023/2/2
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,21 +51,21 @@ First written: 2023/2/2
|
|||||||
<span id="accounting-order-{{ account.id }}-code">{{ account.code }}</span>
|
<span id="accounting-order-{{ account.id }}-code">{{ account.code }}</span>
|
||||||
{{ account.title }}
|
{{ account.title }}
|
||||||
</div>
|
</div>
|
||||||
<i class="fa-solid fa-bars"></i>
|
<i class="fa-solid fa-bars" aria-hidden="true"></i>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{ A_("Save") }}
|
{{ A_("Save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit" aria-label="{{ A_("Save") }}">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ First written: 2023/2/1
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ First written: 2023/1/26
|
|||||||
<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">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
The Mia! Accounting Project
|
The Mia! Accounting Project
|
||||||
base.html: The application-wide base template.
|
base.html: The application-wide base template.
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023-2024 imacat.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -27,5 +27,6 @@ First written: 2023/1/27
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ url_for("accounting.babel_catalog") }}"></script>
|
<script src="{{ url_for("accounting.babel_catalog") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/timezone.js") }}"></script>
|
||||||
{% block accounting_scripts %}{% endblock %}
|
{% block accounting_scripts %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,24 +27,24 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.list")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i>
|
||||||
{{ A_("Edit") }}
|
{{ A_("Edit") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
{% if obj.can_delete %}
|
{% if obj.can_delete %}
|
||||||
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
|
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-secondary" type="button" disabled="disabled">
|
<button class="btn btn-secondary" type="button" disabled="disabled">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -53,8 +53,8 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.edit", currency=obj)|accounting_inherit_next }}" aria-label="{{ A_("Edit") }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,25 +42,25 @@ First written: 2023/2/6
|
|||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-code" class="form-control {% if form.code.errors %} is-invalid {% endif %}" type="text" name="code" value="{{ form.code.data|accounting_default }}" placeholder=" " required="required" data-exists-url="{{ url_for("accounting.currency-api.exists") }}" data-original="{% block original_code %}{% endblock %}" data-blocklist="{{ form.CODE_BLOCKLIST|tojson|forceescape }}">
|
<input id="accounting-code" class="form-control {% if form.code.errors %} is-invalid {% endif %}" type="text" name="code" value="{{ form.code.data|accounting_default }}" placeholder=" " required="required" data-exists-url="{{ url_for("accounting.currency-api.exists") }}" data-original="{% block original_code %}{% endblock %}" data-blocklist="{{ form.CODE_BLOCKLIST|tojson|forceescape }}">
|
||||||
<label class="form-label" for="accounting-code">{{ A_("Code") }}</label>
|
<label class="form-label" for="accounting-code">{{ A_("Code") }}</label>
|
||||||
<div id="accounting-code-error" class="invalid-feedback">{% if form.code.errors %}{{ form.code.errors[0] }}{% endif %}</div>
|
<div id="accounting-code-error" class="invalid-feedback" role="alert">{% if form.code.errors %}{{ form.code.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-name" class="form-control {% if form.name.errors %} is-invalid {% endif %}" type="text" name="name" value="{{ form.name.data|accounting_default }}" placeholder=" " required="required">
|
<input id="accounting-name" class="form-control {% if form.name.errors %} is-invalid {% endif %}" type="text" name="name" value="{{ form.name.data|accounting_default }}" placeholder=" " required="required">
|
||||||
<label class="form-label" for="accounting-name">{{ A_("Name") }}</label>
|
<label class="form-label" for="accounting-name">{{ A_("Name") }}</label>
|
||||||
<div id="accounting-name-error" class="invalid-feedback">{% if form.name.errors %}{{ form.name.errors[0] }}{% endif %}</div>
|
<div id="accounting-name-error" class="invalid-feedback" role="alert">{% if form.name.errors %}{{ form.name.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{ A_("Save") }}
|
{{ A_("Save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit" aria-label="{{ A_("Save") }}">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ First written: 2023/2/6
|
|||||||
<div class="mb-2 accounting-toolbar">
|
<div class="mb-2 accounting-toolbar">
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
<a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -36,7 +36,7 @@ First written: 2023/2/6
|
|||||||
<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">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Search") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
@@ -45,8 +45,8 @@ First written: 2023/2/6
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.create")|accounting_append_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.currency.create")|accounting_append_next }}" aria-label="{{ A_("New") }}">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -22,39 +22,39 @@ First written: 2023/1/26
|
|||||||
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
{# <ul> For SonarQube not to complain about incorrect HTML #}
|
||||||
{% if accounting_can_view() %}
|
{% if accounting_can_view() %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<span class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
|
<button class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fa-solid fa-file-invoice-dollar"></i>
|
<i class="fa-solid fa-file-invoice-dollar" aria-hidden="true"></i>
|
||||||
{{ A_("Accounting") }}
|
{{ A_("Accounting") }}
|
||||||
</span>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting-report.") %} active {% endif %}" href="{{ url_for("accounting-report.default") }}">
|
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting-report.") %} active {% endif %}" href="{{ url_for("accounting-report.default") }}">
|
||||||
<i class="fa-solid fa-book"></i>
|
<i class="fa-solid fa-book" aria-hidden="true"></i>
|
||||||
{{ A_("Reports") }}
|
{{ A_("Reports") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.account.") %} active {% endif %}" href="{{ url_for("accounting.account.list") }}">
|
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.account.") %} active {% endif %}" href="{{ url_for("accounting.account.list") }}">
|
||||||
<i class="fa-solid fa-clipboard"></i>
|
<i class="fa-solid fa-clipboard" aria-hidden="true"></i>
|
||||||
{{ A_("Accounts") }}
|
{{ A_("Accounts") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.base-account.") %} active {% endif %}" href="{{ url_for("accounting.base-account.list") }}">
|
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.base-account.") %} active {% endif %}" href="{{ url_for("accounting.base-account.list") }}">
|
||||||
<i class="fa-solid fa-list"></i>
|
<i class="fa-solid fa-list" aria-hidden="true"></i>
|
||||||
{{ A_("Base Accounts") }}
|
{{ A_("Base Accounts") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.currency.") %} active {% endif %}" href="{{ url_for("accounting.currency.list") }}">
|
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.currency.") %} active {% endif %}" href="{{ url_for("accounting.currency.list") }}">
|
||||||
<i class="fa-solid fa-money-bill-wave"></i>
|
<i class="fa-solid fa-money-bill-wave" aria-hidden="true"></i>
|
||||||
{{ A_("Currencies") }}
|
{{ A_("Currencies") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if accounting_can_admin() %}
|
{% if accounting_can_admin() %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.option.") %} active {% endif %}" href="{{ url_for("accounting.option.detail") }}">
|
<a class="dropdown-item {% if request.endpoint and request.endpoint.startswith("accounting.option.") %} active {% endif %}" href="{{ url_for("accounting.option.detail") }}">
|
||||||
<i class="fa-solid fa-gear"></i>
|
<i class="fa-solid fa-gear" aria-hidden="true"></i>
|
||||||
{{ A_("Settings") }}
|
{{ A_("Settings") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ First written: 2023/1/26
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="page-item d-none d-md-inline active dropdown">
|
<li class="page-item d-none d-md-inline active dropdown">
|
||||||
<div class="page-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<div class="page-link dropdown-toggle" role="button" tabindex="0" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
{{ pagination.page_size }}
|
{{ pagination.page_size }}
|
||||||
</div>
|
</div>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
The Mia! Accounting Project
|
The Mia! Accounting Project
|
||||||
detail.html: The cash disbursement journal entry detail
|
detail.html: The cash disbursement journal entry detail
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023-2026 imacat.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -21,9 +21,9 @@ First written: 2023/2/26
|
|||||||
#}
|
#}
|
||||||
{% extends "accounting/journal-entry/include/detail.html" %}
|
{% extends "accounting/journal-entry/include/detail.html" %}
|
||||||
|
|
||||||
{% block as_trasfer %}
|
{% block as_transfer %}
|
||||||
<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-table-columns"></i>
|
<i class="fa-solid fa-table-columns" aria-hidden="true"></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 %}
|
||||||
|
|||||||
+8
-8
@@ -19,31 +19,31 @@ account-selector-modal.html: The modal for the account selector
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/2/25
|
First written: 2023/2/25
|
||||||
#}
|
#}
|
||||||
<div id="accounting-account-selector-{{ debit_credit }}-modal" class="modal fade accounting-account-selector" data-debit-credit="{{ debit_credit }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ debit_credit }}-modal-label" aria-hidden="true">
|
<div id="accounting-account-selector-{{ debit_credit }}-modal" class="modal fade accounting-account-selector" data-debit-credit="{{ debit_credit }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ debit_credit }}-modal-label" aria-hidden="true" data-bs-keyboard="false">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5" id="accounting-account-selector-{{ debit_credit }}-modal-label">{{ A_("Select Account") }}</h1>
|
<h1 class="modal-title fs-5" id="accounting-account-selector-{{ debit_credit }}-modal-label">{{ A_("Select Account") }}</h1>
|
||||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
<button id="accounting-account-selector-{{ debit_credit }}-close" type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-account-selector-{{ debit_credit }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-account-selector-{{ debit_credit }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-account-selector-{{ debit_credit }}-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-account-selector-{{ debit_credit }}-query">
|
<label class="input-group-text" for="accounting-account-selector-{{ debit_credit }}-query">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-account-selector-{{ debit_credit }}-modal-label">
|
||||||
{% for account in account_options %}
|
{% for account in account_options %}
|
||||||
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" role="option" aria-selected="false" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li id="accounting-account-selector-{{ debit_credit }}-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
<li id="accounting-account-selector-{{ debit_credit }}-more" class="list-group-item accounting-clickable" role="option" aria-selected="false">{{ A_("More…") }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p id="accounting-account-selector-{{ debit_credit }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
<p id="accounting-account-selector-{{ debit_credit }}-option-no-result" class="d-none" role="status">{{ A_("There is no data.") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">{{ A_("Cancel") }}</button>
|
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">{{ A_("Cancel") }}</button>
|
||||||
|
|||||||
+28
-28
@@ -21,14 +21,14 @@ 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() }}">
|
<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" data-bs-keyboard="false">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5" id="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label">
|
<h1 class="modal-title fs-5" id="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label">
|
||||||
<label for="accounting-description-editor-{{ description_editor.debit_credit }}-description">{{ A_("Description") }}</label>
|
<label for="accounting-description-editor-{{ description_editor.debit_credit }}-description">{{ A_("Description") }}</label>
|
||||||
</h1>
|
</h1>
|
||||||
<button class="btn-close" type="button" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-close" class="btn-close" type="button" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="d-flex justify-content-between mb-3">
|
||||||
@@ -39,40 +39,40 @@ First written: 2023/2/28
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Tab navigation #}
|
{# Tab navigation #}
|
||||||
<ul class="nav nav-tabs mb-2">
|
<ul id="accounting-description-editor-{{ description_editor.debit_credit }}-tab-list" class="nav nav-tabs mb-2" role="tablist" aria-label="{{ A_("Description Type") }}">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab" class="nav-link active accounting-clickable" aria-current="page">
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab" class="nav-link active accounting-clickable" type="button" tabindex="0" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-general-panel" aria-selected="true">
|
||||||
{{ A_("General") }}
|
{{ A_("General") }}
|
||||||
</span>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab" class="nav-link accounting-clickable" aria-current="false">
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-travel-panel" aria-selected="false">
|
||||||
{{ A_("Travel") }}
|
{{ A_("Travel") }}
|
||||||
</span>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab" class="nav-link accounting-clickable" aria-current="false">
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-bus-panel" aria-selected="false">
|
||||||
{{ A_("Bus") }}
|
{{ A_("Bus") }}
|
||||||
</span>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" aria-current="false">
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-panel" aria-selected="false">
|
||||||
{{ A_("Recurring") }}
|
{{ A_("Recurring") }}
|
||||||
</span>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<span id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab" class="nav-link accounting-clickable" aria-current="false">
|
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-panel" aria-selected="false">
|
||||||
{{ A_("Annotation") }}
|
{{ A_("Annotation") }}
|
||||||
</span>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{# A general description with a tag #}
|
{# A general description with a tag #}
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-page" aria-current="page" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-panel" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab">
|
||||||
<div class="form-floating mb-2">
|
<div class="form-floating mb-2">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag">{{ A_("Tag") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag">{{ A_("Tag") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-description-editor-buttons">
|
<div class="accounting-description-editor-buttons">
|
||||||
@@ -85,11 +85,11 @@ First written: 2023/2/28
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# A general trip with the origin and distination #}
|
{# A general trip with the origin and distination #}
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab">
|
||||||
<div class="form-floating mb-2">
|
<div class="form-floating mb-2">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag">{{ A_("Tag") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag">{{ A_("Tag") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accounting-description-editor-buttons">
|
<div class="accounting-description-editor-buttons">
|
||||||
@@ -104,7 +104,7 @@ First written: 2023/2/28
|
|||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from">{{ A_("From") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from">{{ A_("From") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-from-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-vertical ms-1 me-1">
|
<div class="btn-group-vertical ms-1 me-1">
|
||||||
<button class="btn btn-primary accounting-description-editor-{{ description_editor.debit_credit }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</button>
|
<button class="btn btn-primary accounting-description-editor-{{ description_editor.debit_credit }}-travel-direction accounting-default" type="button" tabindex="-1" data-arrow="→">→</button>
|
||||||
@@ -113,23 +113,23 @@ First written: 2023/2/28
|
|||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-to" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-to" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-to">{{ A_("To") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-to">{{ A_("To") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-to-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-to-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# A bus trip with the route name or route number, the origin and distination #}
|
{# A bus trip with the route name or route number, the origin and distination #}
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab">
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<div class="form-floating me-2">
|
<div class="form-floating me-2">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag">{{ A_("Tag") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag">{{ A_("Tag") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-route" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-route" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-route">{{ A_("Route") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-route">{{ A_("Route") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-route-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-route-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -145,18 +145,18 @@ First written: 2023/2/28
|
|||||||
<div class="form-floating me-2">
|
<div class="form-floating me-2">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from">{{ A_("From") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-from-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-to" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-to" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-to">{{ A_("To") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-bus-to">{{ A_("To") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-to-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-to-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# A recurring transaction #}
|
{# A recurring transaction #}
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
|
||||||
<div class="accounting-description-editor-buttons">
|
<div class="accounting-description-editor-buttons">
|
||||||
{% for recurring in description_editor.recurring %}
|
{% for recurring in description_editor.recurring %}
|
||||||
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.description_template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}">
|
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.description_template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}">
|
||||||
@@ -167,17 +167,17 @@ First written: 2023/2/28
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# The annotation #}
|
{# The annotation #}
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-page" class="d-none" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab">
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number">{{ A_("The Number of Items") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number">{{ A_("The Number of Items") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mt-2">
|
<div class="form-floating mt-2">
|
||||||
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-note" class="form-control" type="text" value="" placeholder=" ">
|
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-note" class="form-control" type="text" value="" placeholder=" ">
|
||||||
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-note">{{ A_("Note") }}</label>
|
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-note">{{ A_("Note") }}</label>
|
||||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-note-error" class="invalid-feedback"></div>
|
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-note-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
The Mia! Accounting Project
|
The Mia! Accounting Project
|
||||||
detail.html: The account detail
|
detail.html: The account detail
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023-2026 imacat.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -27,29 +27,29 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting-report.default")|accounting_or_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting-report.default")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i>
|
||||||
{{ A_("Edit") }}
|
{{ A_("Edit") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.order", 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" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
{% block as_trasfer %}{% endblock %}
|
{% block as_transfer %}{% endblock %}
|
||||||
{% if obj.can_delete %}
|
{% if obj.can_delete %}
|
||||||
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
|
<button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-secondary" type="button" disabled="disabled">
|
<button class="btn btn-secondary" type="button" disabled="disabled">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -58,8 +58,8 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
{% if accounting_can_edit() %}
|
{% if accounting_can_edit() %}
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.edit", journal_entry=obj)|accounting_inherit_next }}" aria-label="{{ A_("Edit") }}">
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -104,7 +104,7 @@ First written: 2023/2/26
|
|||||||
{% if obj.note %}
|
{% if obj.note %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<i class="far fa-comment-dots"></i>
|
<i class="far fa-comment-dots" aria-hidden="true"></i>
|
||||||
{{ obj.note|accounting_journal_entry_text2html|safe }}
|
{{ obj.note|accounting_journal_entry_text2html|safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ First written: 2023/3/21
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
|
<label class="form-label" for="accounting-currency-{{ currency_index }}-code-select">{{ A_("Currency") }}</label>
|
||||||
<div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
|
<div id="accounting-currency-{{ currency_index }}-code-error" class="invalid-feedback" role="alert">{% if currency_code_errors %}{{ currency_code_errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}">
|
<button id="accounting-currency-{{ currency_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_currency_form %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}" aria-label="{{ A_("Remove") }}">
|
||||||
<i class="fas fa-minus"></i>
|
<i class="fas fa-minus" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block line_items %}{% endblock %}
|
{% block line_items %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
|
<div id="accounting-currency-{{ currency_index }}-error" class="invalid-feedback" role="alert">{% if currency_errors %}{{ currency_errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ Author: imacat@mail.imacat.idv.tw (imacat)
|
|||||||
First written: 2023/3/21
|
First written: 2023/3/21
|
||||||
#}
|
#}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}" class="form-control accounting-material-text-field {% if line_item_forms %} accounting-not-empty {% else %} accounting-clickable {% endif %} {% if debit_errors %} is-invalid {% endif %}">
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}" class="form-control accounting-material-text-field {% if line_item_forms %} accounting-not-empty {% else %} accounting-clickable {% endif %} {% if debit_errors %} is-invalid {% endif %}" {% if line_item_forms %} tabindex="-1" {% else %} role="button" tabindex="0" {% endif %}>
|
||||||
<label class="form-label" for="accounting-currency-{{ currency_index }}-{{ debit_credit }}">{{ header }}</label>
|
<span class="form-label">{{ header }}</span>
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-content" class="mt-2 {% if not line_item_forms %} d-none {% endif %}">
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-content" class="mt-2 {% if not line_item_forms %} d-none {% endif %}">
|
||||||
<ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list">
|
<ul id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-list" class="list-group accounting-line-item-list">
|
||||||
{% for line_item_form in line_item_forms %}
|
{% for line_item_form in line_item_forms %}
|
||||||
@@ -36,16 +36,16 @@ First written: 2023/3/21
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between mt-2 mb-2">
|
<div class="d-flex justify-content-between mt-2 mb-2">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-total" class="badge rounded-pill bg-primary">{{ debit_credit_total }}</span></div>
|
<div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-total" class="badge rounded-pill bg-primary" aria-live="polite">{{ debit_credit_total }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-add-line-item" class="btn btn-primary" type="button" data-currency-index="{{ currency_index }}" data-debit-credit="{{ debit_credit }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-error" class="invalid-feedback">{% if debit_credit_errors %}{{ debit_credit_errors[0] }}{% endif %}</div>
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-error" class="invalid-feedback" role="alert">{% if debit_credit_errors %}{{ debit_credit_errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ First written: 2023/2/25
|
|||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-description" value="{{ form.description.data|accounting_default }}">
|
||||||
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
|
<input id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" type="hidden" name="currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount" value="{{ form.amount.data|accounting_journal_entry_format_amount_input }}" data-min="{{ form.offset_total|accounting_default("0") }}">
|
||||||
<div class="accounting-line-item-content">
|
<div class="accounting-line-item-content">
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-control" class="form-control clickable d-flex justify-content-between {% if form.all_errors %} is-invalid {% endif %}" role="button" tabindex="0" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
<div>
|
<div>
|
||||||
<div class="small">
|
<div class="small">
|
||||||
<span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text-code" class="d-none d-md-inline">{{ form.account_code.data|accounting_default }}</span>
|
<span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-account-text-code" class="d-none d-md-inline">{{ form.account_code.data|accounting_default }}</span>
|
||||||
@@ -65,12 +65,12 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
<div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount-text" class="badge rounded-pill bg-primary">{{ form.amount.data|accounting_format_amount }}</span></div>
|
<div><span id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-amount-text" class="badge rounded-pill bg-primary">{{ form.amount.data|accounting_format_amount }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-error" class="invalid-feedback">{% if form.all_errors %}{{ form.all_errors[0] }}{% endif %}</div>
|
<div id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-error" class="invalid-feedback" role="alert">{% if form.all_errors %}{{ form.all_errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_form or form.offsets %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}">
|
<button id="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}-delete" class="btn btn-danger rounded-circle {% if only_one_form or form.offsets %} d-none {% endif %}" type="button" data-target="accounting-currency-{{ currency_index }}-{{ debit_credit }}-{{ line_item_index }}" aria-label="{{ A_("Remove") }}">
|
||||||
<i class="fas fa-minus"></i>
|
<i class="fas fa-minus" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/base-combobox.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-account-selector.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-account-selector.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
<a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +50,7 @@ First written: 2023/2/26
|
|||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-date" class="form-control {% if form.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{{ form.date.data|accounting_default }}" max="{{ form.max_date|accounting_default }}" min="{{ form.min_date|accounting_default }}" placeholder=" " required="required">
|
<input id="accounting-date" class="form-control {% if form.date.errors %} is-invalid {% endif %}" type="date" name="date" value="{{ form.date.data|accounting_default }}" max="{{ form.max_date|accounting_default }}" min="{{ form.min_date|accounting_default }}" placeholder=" " required="required">
|
||||||
<label class="form-label" for="accounting-date">{{ A_("Date") }}</label>
|
<label class="form-label" for="accounting-date">{{ A_("Date") }}</label>
|
||||||
<div id="accounting-date-error" class="invalid-feedback">{% if form.date.errors %}{{ form.date.errors[0] }}{% endif %}</div>
|
<div id="accounting-date-error" class="invalid-feedback" role="alert">{% if form.date.errors %}{{ form.date.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -60,30 +62,30 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button id="accounting-add-currency" class="btn btn-primary" type="button">
|
<button id="accounting-add-currency" class="btn btn-primary" type="button">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||||
{{ A_("New") }}
|
{{ A_("New") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="accounting-currencies-error" class="invalid-feedback">{% if form.currencies_errors %}{{ form.currencies_errors[0] }}{% endif %}</div>
|
<div id="accounting-currencies-error" class="invalid-feedback" role="alert">{% if form.currencies_errors %}{{ form.currencies_errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<textarea id="accounting-note" class="form-control form-control-lg {% if form.note.errors %} is-invalid {% endif %}" name="note" rows="5" placeholder=" ">{{ form.note.data|accounting_default }}</textarea>
|
<textarea id="accounting-note" class="form-control form-control-lg {% if form.note.errors %} is-invalid {% endif %}" name="note" rows="5" placeholder=" ">{{ form.note.data|accounting_default }}</textarea>
|
||||||
<label class="form-label" for="accounting-note">{{ A_("Note") }}</label>
|
<label class="form-label" for="accounting-note">{{ A_("Note") }}</label>
|
||||||
<div id="accounting-note-error" class="invalid-feedback">{% if form.note.errors %}{{ form.note.errors[0] }}{% endif %}</div>
|
<div id="accounting-note-error" class="invalid-feedback" role="alert">{% if form.note.errors %}{{ form.note.errors[0] }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{ A_("Save") }}
|
{{ A_("Save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit" aria-label="{{ A_("Save") }}">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+18
-18
@@ -31,40 +31,40 @@ First written: 2023/2/25
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="accounting-line-item-editor-original-line-item-container" class="d-flex justify-content-between mb-3">
|
<div id="accounting-line-item-editor-original-line-item-container" class="d-flex justify-content-between mb-3">
|
||||||
<div class="accounting-line-item-editor-original-line-item-content">
|
<div class="accounting-line-item-editor-original-line-item-content">
|
||||||
<div id="accounting-line-item-editor-original-line-item-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="#accounting-original-line-item-selector-modal">
|
<button id="accounting-line-item-editor-original-line-item-control" class="form-control text-start accounting-material-text-field" type="button" data-bs-toggle="modal" data-bs-target="#accounting-original-line-item-selector-modal">
|
||||||
<label class="form-label" for="accounting-line-item-editor-original-line-item">{{ A_("Original Line Item") }}</label>
|
<span class="form-label">{{ A_("Original Line Item") }}</span>
|
||||||
<div id="accounting-line-item-editor-original-line-item"></div>
|
<span id="accounting-line-item-editor-original-line-item"></span>
|
||||||
</div>
|
</button>
|
||||||
<div id="accounting-line-item-editor-original-line-item-error" class="invalid-feedback"></div>
|
<div id="accounting-line-item-editor-original-line-item-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button id="accounting-line-item-editor-original-line-item-delete" class="btn btn-danger rounded-circle" type="button">
|
<button id="accounting-line-item-editor-original-line-item-delete" class="btn btn-danger rounded-circle" type="button" aria-label="{{ A_("Remove") }}">
|
||||||
<i class="fas fa-minus"></i>
|
<i class="fas fa-minus" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div id="accounting-line-item-editor-description-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
<button id="accounting-line-item-editor-description-control" class="form-control text-start accounting-material-text-field" type="button" data-bs-toggle="modal" data-bs-target="">
|
||||||
<label class="form-label" for="accounting-line-item-editor-description">{{ A_("Description") }}</label>
|
<span class="form-label">{{ A_("Description") }}</span>
|
||||||
<div id="accounting-line-item-editor-description"></div>
|
<span id="accounting-line-item-editor-description"></span>
|
||||||
</div>
|
</button>
|
||||||
<div id="accounting-line-item-editor-description-error" class="invalid-feedback"></div>
|
<div id="accounting-line-item-editor-description-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div id="accounting-line-item-editor-account-control" class="form-control accounting-clickable accounting-material-text-field" data-bs-toggle="modal" data-bs-target="">
|
<button id="accounting-line-item-editor-account-control" class="form-control text-start accounting-material-text-field" type="button" data-bs-toggle="modal" data-bs-target="">
|
||||||
<label class="form-label" for="accounting-line-item-editor-account">{{ A_("Account") }}</label>
|
<span class="form-label">{{ A_("Account") }}</span>
|
||||||
<div id="accounting-line-item-editor-account"></div>
|
<span id="accounting-line-item-editor-account"></span>
|
||||||
</div>
|
</button>
|
||||||
<div id="accounting-line-item-editor-account-error" class="invalid-feedback"></div>
|
<div id="accounting-line-item-editor-account-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input id="accounting-line-item-editor-amount" class="form-control" type="number" value="" min="0" max="" step="0.01" placeholder=" " required="required">
|
<input id="accounting-line-item-editor-amount" class="form-control" type="number" value="" min="0" max="" step="0.01" placeholder=" " required="required">
|
||||||
<label for="accounting-line-item-editor-amount">{{ A_("Amount") }}</label>
|
<label for="accounting-line-item-editor-amount">{{ A_("Amount") }}</label>
|
||||||
<div id="accounting-line-item-editor-amount-error" class="invalid-feedback"></div>
|
<div id="accounting-line-item-editor-amount-error" class="invalid-feedback" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
+7
-7
@@ -19,25 +19,25 @@ original-line-item-selector-modal.html: The modal of the original line item sele
|
|||||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
First written: 2023/2/25
|
First written: 2023/2/25
|
||||||
#}
|
#}
|
||||||
<div id="accounting-original-line-item-selector-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-original-line-item-selector-modal-label" aria-hidden="true">
|
<div id="accounting-original-line-item-selector-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-original-line-item-selector-modal-label" aria-hidden="true" data-bs-keyboard="false">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5" id="accounting-original-line-item-selector-modal-label">{{ A_("Select Original Line Item") }}</h1>
|
<h1 class="modal-title fs-5" id="accounting-original-line-item-selector-modal-label">{{ A_("Select Original Line Item") }}</h1>
|
||||||
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
<button id="accounting-original-line-item-selector-close" type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal" aria-label="{{ A_("Close") }}"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-original-line-item-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-original-line-item-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-original-line-item-selector-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-original-line-item-selector-query">
|
<label class="input-group-text" for="accounting-original-line-item-selector-query">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-original-line-item-selector-modal-label">
|
||||||
{% for line_item in form.original_line_item_options %}
|
{% for line_item in form.original_line_item_options %}
|
||||||
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-title="{{ line_item.account.title }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-title="{{ line_item.account.title }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" role="option" aria-selected="false" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
<div>
|
<div>
|
||||||
<div class="small">
|
<div class="small">
|
||||||
{{ line_item.journal_entry.date|accounting_format_date }}
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
@@ -55,7 +55,7 @@ First written: 2023/2/25
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<p id="accounting-original-line-item-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
<p id="accounting-original-line-item-selector-option-no-result" class="d-none" role="status">{{ A_("There is no data.") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
<div class="mb-3 accounting-toolbar">
|
<div class="mb-3 accounting-toolbar">
|
||||||
<a class="btn btn-primary" role="button" href="{{ url_for("accounting-report.default")|accounting_or_next }}">
|
<a class="btn btn-primary" role="button" href="{{ url_for("accounting-report.default")|accounting_or_next }}">
|
||||||
<i class="fa-solid fa-circle-chevron-left"></i>
|
<i class="fa-solid fa-circle-chevron-left" aria-hidden="true"></i>
|
||||||
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
<span class="d-none d-md-inline">{{ A_("Back") }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,21 +50,21 @@ First written: 2023/2/26
|
|||||||
{% with journal_entry = item %}
|
{% with journal_entry = item %}
|
||||||
{% include "accounting/journal-entry/include/order-journal-entry.html" %}
|
{% include "accounting/journal-entry/include/order-journal-entry.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<i class="fa-solid fa-bars"></i>
|
<i class="fa-solid fa-bars" aria-hidden="true"></i>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
{{ A_("Save") }}
|
{{ A_("Save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-md-none accounting-material-fab">
|
<div class="d-md-none accounting-material-fab">
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit" aria-label="{{ A_("Save") }}">
|
||||||
<i class="fa-solid fa-floppy-disk"></i>
|
<i class="fa-solid fa-floppy-disk" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
The Mia! Accounting Project
|
The Mia! Accounting Project
|
||||||
detail.html: The cash receipt journal entry detail
|
detail.html: The cash receipt journal entry detail
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023-2026 imacat.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -21,9 +21,9 @@ First written: 2023/2/26
|
|||||||
#}
|
#}
|
||||||
{% extends "accounting/journal-entry/include/detail.html" %}
|
{% extends "accounting/journal-entry/include/detail.html" %}
|
||||||
|
|
||||||
{% block as_trasfer %}
|
{% block as_transfer %}
|
||||||
<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-table-columns"></i>
|
<i class="fa-solid fa-table-columns" aria-hidden="true"></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 %}
|
||||||
@@ -41,7 +41,7 @@ First written: 2023/2/26
|
|||||||
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
|
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
<div>{{ currency.credit_total|accounting_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
The Mia! Accounting Project
|
The Mia! Accounting Project
|
||||||
detail.html: The transfer journal entry detail
|
detail.html: The transfer journal entry detail
|
||||||
|
|
||||||
Copyright (c) 2023 imacat.
|
Copyright (c) 2023-2026 imacat.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -53,7 +53,7 @@ First written: 2023/2/26
|
|||||||
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
|
<li class="list-group-item accounting-journal-entry-line-item accounting-journal-entry-line-item-total">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<div>{{ A_("Total") }}</div>
|
<div>{{ A_("Total") }}</div>
|
||||||
<div>{{ currency.debit_total|accounting_format_amount }}</div>
|
<div>{{ currency.credit_total|accounting_format_amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user