# The Mia! Accounting Flask Project. # Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/1 # Copyright (c) 2023 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """The forms for the account management. """ import sqlalchemy as sa from flask_wtf import FlaskForm from wtforms import StringField, BooleanField from wtforms.validators import DataRequired, ValidationError from accounting.database import db, user_utils from accounting.locale import lazy_gettext from accounting.models import BaseAccount, Account from accounting.utils.random_id import new_id from accounting.utils.strip_text import strip_text class BaseAccountExists: """The validator to check if the base account code exists.""" def __call__(self, form: FlaskForm, field: StringField) -> None: if field.data == "": return if db.session.get(BaseAccount, field.data) is None: raise ValidationError(lazy_gettext( "The base account does not exist.")) class AccountForm(FlaskForm): """The form to create or edit an account.""" base_code = StringField( filters=[strip_text], validators=[ DataRequired(lazy_gettext("Please select the base account.")), BaseAccountExists()]) """The code of the base account.""" title = StringField( filters=[strip_text], validators=[DataRequired(lazy_gettext("Please fill in the title"))]) """The title.""" is_offset_needed = BooleanField() """Whether the the entries of this account need offsets.""" def populate_obj(self, obj: Account) -> None: """Populates the form data into an account object. :param obj: The account object. :return: None. """ is_new: bool = obj.id is None prev_base_code: str | None = obj.base_code if is_new: obj.id = new_id(Account) obj.base_code = self.base_code.data if prev_base_code != self.base_code.data: last_same_base: Account = Account.query\ .filter(Account.base_code == self.base_code.data)\ .order_by(Account.base_code.desc()).first() obj.no = 1 if last_same_base is None else last_same_base.no + 1 obj.title = self.title.data obj.is_offset_needed = self.is_offset_needed.data if is_new: current_user_pk: int = user_utils.get_pk(user_utils.current_user) obj.created_by_id = current_user_pk obj.updated_by_id = current_user_pk if prev_base_code is not None \ and prev_base_code != self.base_code.data: setattr(self, "__post_update", lambda: sort_accounts_in(prev_base_code, obj.id)) def post_update(self, obj) -> None: """The post-processing after the update. :return: None """ current_user_pk: int = user_utils.get_pk(user_utils.current_user) obj.updated_by_id = current_user_pk obj.updated_at = sa.func.now() if hasattr(self, "__post_update"): getattr(self, "__post_update")() @property def selected_base(self) -> BaseAccount | None: """The selected base account in the form. :return: The selected base account in the form. """ return db.session.get(BaseAccount, self.base_code.data) @property def base_options(self) -> list[BaseAccount]: """The selectable base accounts. :return: The selectable base accounts. """ return BaseAccount.query\ .filter(sa.func.char_length(BaseAccount.code) == 4)\ .order_by(BaseAccount.code).all() def sort_accounts_in(base_code: str, exclude: int) -> None: """Sorts the accounts under a base account after changing the base account or deleting an account. :param base_code: The code of the base account. :param exclude: The account ID to exclude. :return: None. """ accounts: list[Account] = Account.query\ .filter(Account.base_code == base_code, Account.id != exclude)\ .order_by(Account.no).all() for i in range(len(accounts)): if accounts[i].no != i + 1: accounts[i].no = i + 1