2023-02-01 15:37:56 +08:00
|
|
|
# The Mia! Accounting Flask Project.
|
2023-02-01 15:44:58 +08:00
|
|
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
# 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.
|
2023-02-01 15:44:58 +08:00
|
|
|
"""The data models.
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
"""
|
2023-02-27 15:28:45 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2023-02-01 15:37:56 +08:00
|
|
|
import re
|
|
|
|
import typing as t
|
2023-02-27 15:28:45 +08:00
|
|
|
from decimal import Decimal
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
import sqlalchemy as sa
|
2023-03-21 22:34:44 +08:00
|
|
|
from babel import Locale
|
|
|
|
from flask_babel import get_locale, get_babel
|
2023-02-01 15:37:56 +08:00
|
|
|
from sqlalchemy import text
|
|
|
|
|
2023-02-08 11:13:09 +08:00
|
|
|
from accounting import db
|
2023-02-27 15:28:45 +08:00
|
|
|
from accounting.locale import gettext
|
2023-02-01 22:05:12 +08:00
|
|
|
from accounting.utils.user import user_cls, user_pk_column
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
|
2023-02-01 15:44:58 +08:00
|
|
|
class BaseAccount(db.Model):
|
|
|
|
"""A base account."""
|
|
|
|
__tablename__ = "accounting_base_accounts"
|
|
|
|
"""The table name."""
|
|
|
|
code = db.Column(db.String, nullable=False, primary_key=True)
|
|
|
|
"""The code."""
|
|
|
|
title_l10n = db.Column("title", db.String, nullable=False)
|
|
|
|
"""The title."""
|
|
|
|
l10n = db.relationship("BaseAccountL10n", back_populates="account",
|
|
|
|
lazy=False)
|
|
|
|
"""The localized titles."""
|
2023-02-01 16:54:45 +08:00
|
|
|
accounts = db.relationship("Account", back_populates="base")
|
|
|
|
"""The descendant accounts under the base account."""
|
2023-02-01 15:44:58 +08:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
"""Returns the string representation of the base account.
|
|
|
|
|
|
|
|
:return: The string representation of the base account.
|
|
|
|
"""
|
2023-03-20 23:45:17 +08:00
|
|
|
return f"{self.code} {self.title.title()}"
|
2023-02-01 15:44:58 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self) -> str:
|
|
|
|
"""Returns the title in the current locale.
|
|
|
|
|
|
|
|
:return: The title in the current locale.
|
|
|
|
"""
|
2023-03-21 22:34:44 +08:00
|
|
|
current_locale: Locale = get_locale()
|
|
|
|
if current_locale == get_babel().instance.default_locale:
|
2023-02-01 15:44:58 +08:00
|
|
|
return self.title_l10n
|
|
|
|
for l10n in self.l10n:
|
2023-03-21 22:34:44 +08:00
|
|
|
if l10n.locale == str(current_locale):
|
2023-02-01 15:44:58 +08:00
|
|
|
return l10n.title
|
|
|
|
return self.title_l10n
|
|
|
|
|
2023-02-07 20:18:57 +08:00
|
|
|
@property
|
|
|
|
def query_values(self) -> list[str]:
|
|
|
|
"""Returns the values to be queried.
|
|
|
|
|
|
|
|
:return: The values to be queried.
|
|
|
|
"""
|
|
|
|
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
|
|
|
|
|
2023-02-01 15:44:58 +08:00
|
|
|
|
|
|
|
class BaseAccountL10n(db.Model):
|
|
|
|
"""A localized base account title."""
|
|
|
|
__tablename__ = "accounting_base_accounts_l10n"
|
|
|
|
"""The table name."""
|
2023-02-03 17:14:32 +08:00
|
|
|
account_code = db.Column(db.String,
|
|
|
|
db.ForeignKey(BaseAccount.code,
|
|
|
|
onupdate="CASCADE",
|
|
|
|
ondelete="CASCADE"),
|
2023-02-01 15:44:58 +08:00
|
|
|
nullable=False, primary_key=True)
|
|
|
|
"""The code of the account."""
|
|
|
|
account = db.relationship(BaseAccount, back_populates="l10n")
|
|
|
|
"""The account."""
|
|
|
|
locale = db.Column(db.String, nullable=False, primary_key=True)
|
|
|
|
"""The locale."""
|
|
|
|
title = db.Column(db.String, nullable=False)
|
|
|
|
"""The localized title."""
|
|
|
|
|
|
|
|
|
2023-02-01 15:37:56 +08:00
|
|
|
class Account(db.Model):
|
|
|
|
"""An account."""
|
|
|
|
__tablename__ = "accounting_accounts"
|
|
|
|
"""The table name."""
|
2023-02-03 13:26:20 +08:00
|
|
|
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
|
|
|
autoincrement=False)
|
2023-02-01 15:37:56 +08:00
|
|
|
"""The account ID."""
|
2023-02-03 17:14:32 +08:00
|
|
|
base_code = db.Column(db.String,
|
|
|
|
db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
|
|
|
ondelete="CASCADE"),
|
2023-02-01 15:37:56 +08:00
|
|
|
nullable=False)
|
|
|
|
"""The code of the base account."""
|
2023-02-01 16:54:45 +08:00
|
|
|
base = db.relationship(BaseAccount, back_populates="accounts")
|
2023-02-01 15:37:56 +08:00
|
|
|
"""The base account."""
|
|
|
|
no = db.Column(db.Integer, nullable=False, default=text("1"))
|
|
|
|
"""The account number under the base account."""
|
|
|
|
title_l10n = db.Column("title", db.String, nullable=False)
|
|
|
|
"""The title."""
|
2023-03-18 22:52:29 +08:00
|
|
|
is_need_offset = db.Column(db.Boolean, nullable=False, default=False)
|
2023-03-20 20:52:35 +08:00
|
|
|
"""Whether the journal entry line items of this account need offset."""
|
2023-02-01 15:37:56 +08:00
|
|
|
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of creation."""
|
2023-02-03 17:14:32 +08:00
|
|
|
created_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
2023-02-01 15:37:56 +08:00
|
|
|
nullable=False)
|
|
|
|
"""The ID of the creator."""
|
|
|
|
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
|
|
|
"""The creator."""
|
|
|
|
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of last update."""
|
2023-02-03 17:14:32 +08:00
|
|
|
updated_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
2023-02-01 15:37:56 +08:00
|
|
|
nullable=False)
|
|
|
|
"""The ID of the updator."""
|
|
|
|
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
|
|
|
"""The updator."""
|
|
|
|
l10n = db.relationship("AccountL10n", back_populates="account",
|
|
|
|
lazy=False)
|
|
|
|
"""The localized titles."""
|
2023-03-20 20:52:35 +08:00
|
|
|
line_items = db.relationship("JournalEntryLineItem",
|
|
|
|
back_populates="account")
|
|
|
|
"""The journal entry line items."""
|
2023-02-01 15:37:56 +08:00
|
|
|
|
2023-03-08 18:32:27 +08:00
|
|
|
CASH_CODE: str = "1111-001"
|
2023-02-01 15:37:56 +08:00
|
|
|
"""The code of the cash account,"""
|
2023-03-08 18:32:27 +08:00
|
|
|
ACCUMULATED_CHANGE_CODE: str = "3351-001"
|
2023-02-01 15:37:56 +08:00
|
|
|
"""The code of the accumulated-change account,"""
|
2023-03-08 18:32:27 +08:00
|
|
|
NET_CHANGE_CODE: str = "3353-001"
|
2023-02-01 15:37:56 +08:00
|
|
|
"""The code of the net-change account,"""
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
"""Returns the string representation of this account.
|
|
|
|
|
|
|
|
:return: The string representation of this account.
|
|
|
|
"""
|
2023-03-20 23:45:17 +08:00
|
|
|
return f"{self.base_code}-{self.no:03d} {self.title.title()}"
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def code(self) -> str:
|
|
|
|
"""Returns the code.
|
|
|
|
|
|
|
|
:return: The code.
|
|
|
|
"""
|
2023-03-08 12:13:59 +08:00
|
|
|
return f"{self.base_code}-{self.no:03d}"
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self) -> str:
|
|
|
|
"""Returns the title in the current locale.
|
|
|
|
|
|
|
|
:return: The title in the current locale.
|
|
|
|
"""
|
2023-03-21 22:34:44 +08:00
|
|
|
current_locale: Locale = get_locale()
|
|
|
|
if current_locale == get_babel().instance.default_locale:
|
2023-02-01 15:37:56 +08:00
|
|
|
return self.title_l10n
|
|
|
|
for l10n in self.l10n:
|
2023-03-21 22:34:44 +08:00
|
|
|
if l10n.locale == str(current_locale):
|
2023-02-01 15:37:56 +08:00
|
|
|
return l10n.title
|
|
|
|
return self.title_l10n
|
|
|
|
|
|
|
|
@title.setter
|
|
|
|
def title(self, value: str) -> None:
|
|
|
|
"""Sets the title in the current locale.
|
|
|
|
|
|
|
|
:param value: The new title.
|
|
|
|
:return: None.
|
|
|
|
"""
|
|
|
|
if self.title_l10n is None:
|
|
|
|
self.title_l10n = value
|
|
|
|
return
|
2023-03-21 22:34:44 +08:00
|
|
|
current_locale: Locale = get_locale()
|
|
|
|
if current_locale == get_babel().instance.default_locale:
|
2023-02-01 15:37:56 +08:00
|
|
|
self.title_l10n = value
|
|
|
|
return
|
|
|
|
for l10n in self.l10n:
|
2023-03-21 22:34:44 +08:00
|
|
|
if l10n.locale == str(current_locale):
|
2023-02-01 15:37:56 +08:00
|
|
|
l10n.title = value
|
|
|
|
return
|
2023-03-21 22:34:44 +08:00
|
|
|
self.l10n.append(AccountL10n(locale=str(current_locale), title=value))
|
2023-02-01 15:37:56 +08:00
|
|
|
|
2023-03-18 19:33:41 +08:00
|
|
|
@property
|
|
|
|
def is_real(self) -> bool:
|
|
|
|
"""Returns whether the account is a real account.
|
|
|
|
|
|
|
|
:return: True if the account is a real account, or False otherwise.
|
|
|
|
"""
|
|
|
|
return self.base_code[0] in {"1", "2", "3"}
|
|
|
|
|
2023-03-18 19:20:50 +08:00
|
|
|
@property
|
|
|
|
def is_nominal(self) -> bool:
|
|
|
|
"""Returns whether the account is a nominal account.
|
|
|
|
|
|
|
|
:return: True if the account is a nominal account, or False otherwise.
|
|
|
|
"""
|
2023-03-18 19:33:41 +08:00
|
|
|
return not self.is_real
|
2023-03-18 19:20:50 +08:00
|
|
|
|
2023-03-18 19:06:12 +08:00
|
|
|
@property
|
|
|
|
def query_values(self) -> list[str]:
|
|
|
|
"""Returns the values to be queried.
|
|
|
|
|
|
|
|
:return: The values to be queried.
|
|
|
|
"""
|
|
|
|
return [self.code, self.title_l10n] + [x.title for x in self.l10n]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_modified(self) -> bool:
|
|
|
|
"""Returns whether a product account was modified.
|
|
|
|
|
|
|
|
:return: True if modified, or False otherwise.
|
|
|
|
"""
|
|
|
|
if db.session.is_modified(self):
|
|
|
|
return True
|
|
|
|
for l10n in self.l10n:
|
|
|
|
if db.session.is_modified(l10n):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2023-03-22 00:09:06 +08:00
|
|
|
@property
|
|
|
|
def can_delete(self) -> bool:
|
|
|
|
"""Returns whether the account can be deleted.
|
|
|
|
|
|
|
|
:return: True if the account can be deleted, or False otherwise.
|
|
|
|
"""
|
|
|
|
if self.code in {"1111-001", "3351-001", "3353-001"}:
|
|
|
|
return False
|
|
|
|
return len(self.line_items) == 0
|
|
|
|
|
2023-03-18 19:06:12 +08:00
|
|
|
def delete(self) -> None:
|
|
|
|
"""Deletes this account.
|
|
|
|
|
|
|
|
:return: None.
|
|
|
|
"""
|
|
|
|
AccountL10n.query.filter(AccountL10n.account == self).delete()
|
|
|
|
cls: t.Type[t.Self] = self.__class__
|
|
|
|
cls.query.filter(cls.id == self.id).delete()
|
|
|
|
|
2023-02-01 15:37:56 +08:00
|
|
|
@classmethod
|
|
|
|
def find_by_code(cls, code: str) -> t.Self | None:
|
2023-02-07 11:30:25 +08:00
|
|
|
"""Finds an account by its code.
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
:param code: The code.
|
2023-02-07 11:30:25 +08:00
|
|
|
:return: The account, or None if this account does not exist.
|
2023-02-01 15:37:56 +08:00
|
|
|
"""
|
2023-02-27 10:50:49 +08:00
|
|
|
m = re.match(r"^([1-9]{4})-(\d{3})$", code)
|
2023-02-01 15:37:56 +08:00
|
|
|
if m is None:
|
|
|
|
return None
|
|
|
|
return cls.query.filter(cls.base_code == m.group(1),
|
|
|
|
cls.no == int(m.group(2))).first()
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-22 20:21:52 +08:00
|
|
|
def selectable_debit(cls) -> list[t.Self]:
|
|
|
|
"""Returns the selectable debit accounts.
|
|
|
|
Payable line items can not start from debit.
|
2023-02-01 15:37:56 +08:00
|
|
|
|
2023-03-22 20:21:52 +08:00
|
|
|
:return: The selectable debit accounts.
|
2023-02-01 15:37:56 +08:00
|
|
|
"""
|
|
|
|
return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
|
2023-03-22 19:54:27 +08:00
|
|
|
sa.and_(cls.base_code.startswith("2"),
|
|
|
|
sa.not_(cls.is_need_offset)),
|
2023-02-01 15:37:56 +08:00
|
|
|
cls.base_code.startswith("3"),
|
|
|
|
cls.base_code.startswith("5"),
|
|
|
|
cls.base_code.startswith("6"),
|
|
|
|
cls.base_code.startswith("75"),
|
|
|
|
cls.base_code.startswith("76"),
|
|
|
|
cls.base_code.startswith("77"),
|
|
|
|
cls.base_code.startswith("78"),
|
|
|
|
cls.base_code.startswith("8"),
|
|
|
|
cls.base_code.startswith("9")),
|
|
|
|
cls.base_code != "3351",
|
|
|
|
cls.base_code != "3353")\
|
|
|
|
.order_by(cls.base_code, cls.no).all()
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-22 20:21:52 +08:00
|
|
|
def selectable_credit(cls) -> list[t.Self]:
|
|
|
|
"""Returns the selectable debit accounts.
|
|
|
|
Receivable line items can not start from credit.
|
2023-02-01 15:37:56 +08:00
|
|
|
|
2023-03-22 20:21:52 +08:00
|
|
|
:return: The selectable debit accounts.
|
2023-02-01 15:37:56 +08:00
|
|
|
"""
|
2023-03-22 19:54:27 +08:00
|
|
|
return cls.query.filter(sa.or_(sa.and_(cls.base_code.startswith("1"),
|
|
|
|
sa.not_(cls.is_need_offset)),
|
2023-02-01 15:37:56 +08:00
|
|
|
cls.base_code.startswith("2"),
|
|
|
|
cls.base_code.startswith("3"),
|
|
|
|
cls.base_code.startswith("4"),
|
|
|
|
cls.base_code.startswith("71"),
|
|
|
|
cls.base_code.startswith("72"),
|
|
|
|
cls.base_code.startswith("73"),
|
|
|
|
cls.base_code.startswith("74"),
|
|
|
|
cls.base_code.startswith("8"),
|
|
|
|
cls.base_code.startswith("9")),
|
|
|
|
cls.base_code != "3351",
|
|
|
|
cls.base_code != "3353")\
|
|
|
|
.order_by(cls.base_code, cls.no).all()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def cash(cls) -> t.Self:
|
|
|
|
"""Returns the cash account.
|
|
|
|
|
|
|
|
:return: The cash account
|
|
|
|
"""
|
2023-03-08 18:31:35 +08:00
|
|
|
return cls.find_by_code(cls.CASH_CODE)
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def accumulated_change(cls) -> t.Self:
|
|
|
|
"""Returns the accumulated-change account.
|
|
|
|
|
|
|
|
:return: The accumulated-change account
|
|
|
|
"""
|
2023-03-08 18:31:35 +08:00
|
|
|
return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
|
2023-02-01 15:37:56 +08:00
|
|
|
|
|
|
|
|
|
|
|
class AccountL10n(db.Model):
|
|
|
|
"""A localized account title."""
|
|
|
|
__tablename__ = "accounting_accounts_l10n"
|
2023-02-07 00:23:45 +08:00
|
|
|
"""The table name."""
|
2023-02-03 17:14:32 +08:00
|
|
|
account_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(Account.id, onupdate="CASCADE",
|
|
|
|
ondelete="CASCADE"),
|
2023-02-01 15:37:56 +08:00
|
|
|
nullable=False, primary_key=True)
|
2023-02-07 00:23:45 +08:00
|
|
|
"""The account ID."""
|
2023-02-01 15:37:56 +08:00
|
|
|
account = db.relationship(Account, back_populates="l10n")
|
2023-02-07 00:23:45 +08:00
|
|
|
"""The account."""
|
2023-02-01 15:37:56 +08:00
|
|
|
locale = db.Column(db.String, nullable=False, primary_key=True)
|
2023-02-07 00:23:45 +08:00
|
|
|
"""The locale."""
|
2023-02-01 15:37:56 +08:00
|
|
|
title = db.Column(db.String, nullable=False)
|
2023-02-07 00:23:45 +08:00
|
|
|
"""The localized title."""
|
2023-02-07 00:07:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
class Currency(db.Model):
|
|
|
|
"""A currency."""
|
|
|
|
__tablename__ = "accounting_currencies"
|
|
|
|
"""The table name."""
|
|
|
|
code = db.Column(db.String, nullable=False, primary_key=True)
|
|
|
|
"""The code."""
|
|
|
|
name_l10n = db.Column("name", db.String, nullable=False)
|
|
|
|
"""The name."""
|
|
|
|
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of creation."""
|
|
|
|
created_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The ID of the creator."""
|
|
|
|
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
|
|
|
"""The creator."""
|
|
|
|
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of last update."""
|
|
|
|
updated_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The ID of the updator."""
|
|
|
|
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
|
|
|
"""The updator."""
|
|
|
|
l10n = db.relationship("CurrencyL10n", back_populates="currency",
|
|
|
|
lazy=False)
|
|
|
|
"""The localized names."""
|
2023-03-20 20:52:35 +08:00
|
|
|
line_items = db.relationship("JournalEntryLineItem",
|
|
|
|
back_populates="currency")
|
|
|
|
"""The journal entry line items."""
|
2023-02-07 00:07:23 +08:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
"""Returns the string representation of the currency.
|
|
|
|
|
|
|
|
:return: The string representation of the currency.
|
|
|
|
"""
|
2023-03-20 23:45:17 +08:00
|
|
|
return f"{self.name.title()} ({self.code})"
|
2023-02-07 00:07:23 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Returns the name in the current locale.
|
|
|
|
|
|
|
|
:return: The name in the current locale.
|
|
|
|
"""
|
2023-03-21 22:34:44 +08:00
|
|
|
current_locale: Locale = get_locale()
|
|
|
|
if current_locale == get_babel().instance.default_locale:
|
2023-02-07 00:07:23 +08:00
|
|
|
return self.name_l10n
|
|
|
|
for l10n in self.l10n:
|
2023-03-21 22:34:44 +08:00
|
|
|
if l10n.locale == str(current_locale):
|
2023-02-07 00:07:23 +08:00
|
|
|
return l10n.name
|
|
|
|
return self.name_l10n
|
|
|
|
|
|
|
|
@name.setter
|
|
|
|
def name(self, value: str) -> None:
|
|
|
|
"""Sets the name in the current locale.
|
|
|
|
|
|
|
|
:param value: The new name.
|
|
|
|
:return: None.
|
|
|
|
"""
|
|
|
|
if self.name_l10n is None:
|
|
|
|
self.name_l10n = value
|
|
|
|
return
|
2023-03-21 22:34:44 +08:00
|
|
|
current_locale: Locale = get_locale()
|
|
|
|
if current_locale == get_babel().instance.default_locale:
|
2023-02-07 00:07:23 +08:00
|
|
|
self.name_l10n = value
|
|
|
|
return
|
|
|
|
for l10n in self.l10n:
|
2023-03-21 22:34:44 +08:00
|
|
|
if l10n.locale == str(current_locale):
|
2023-02-07 00:07:23 +08:00
|
|
|
l10n.name = value
|
|
|
|
return
|
2023-03-21 22:34:44 +08:00
|
|
|
self.l10n.append(CurrencyL10n(locale=str(current_locale), name=value))
|
2023-02-07 00:07:23 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_modified(self) -> bool:
|
|
|
|
"""Returns whether a product account was modified.
|
|
|
|
|
|
|
|
:return: True if modified, or False otherwise.
|
|
|
|
"""
|
|
|
|
if db.session.is_modified(self):
|
|
|
|
return True
|
|
|
|
for l10n in self.l10n:
|
|
|
|
if db.session.is_modified(l10n):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2023-03-22 00:37:39 +08:00
|
|
|
@property
|
|
|
|
def can_delete(self) -> bool:
|
|
|
|
"""Returns whether the currency can be deleted.
|
|
|
|
|
|
|
|
:return: True if the currency can be deleted, or False otherwise.
|
|
|
|
"""
|
|
|
|
from accounting.template_globals import default_currency_code
|
|
|
|
if self.code == default_currency_code():
|
|
|
|
return False
|
|
|
|
return len(self.line_items) == 0
|
|
|
|
|
2023-02-07 00:07:23 +08:00
|
|
|
def delete(self) -> None:
|
|
|
|
"""Deletes the currency.
|
|
|
|
|
|
|
|
:return: None.
|
|
|
|
"""
|
|
|
|
CurrencyL10n.query.filter(CurrencyL10n.currency == self).delete()
|
|
|
|
cls: t.Type[t.Self] = self.__class__
|
|
|
|
cls.query.filter(cls.code == self.code).delete()
|
|
|
|
|
|
|
|
|
|
|
|
class CurrencyL10n(db.Model):
|
|
|
|
"""A localized currency name."""
|
|
|
|
__tablename__ = "accounting_currencies_l10n"
|
|
|
|
"""The table name."""
|
|
|
|
currency_code = db.Column(db.String,
|
|
|
|
db.ForeignKey(Currency.code, onupdate="CASCADE",
|
|
|
|
ondelete="CASCADE"),
|
|
|
|
nullable=False, primary_key=True)
|
|
|
|
"""The currency code."""
|
|
|
|
currency = db.relationship(Currency, back_populates="l10n")
|
|
|
|
"""The currency."""
|
|
|
|
locale = db.Column(db.String, nullable=False, primary_key=True)
|
|
|
|
"""The locale."""
|
|
|
|
name = db.Column(db.String, nullable=False)
|
|
|
|
"""The localized name."""
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
|
2023-03-20 22:08:58 +08:00
|
|
|
class JournalEntryCurrency:
|
|
|
|
"""A currency in a journal entry."""
|
2023-02-27 15:28:45 +08:00
|
|
|
|
2023-03-20 20:52:35 +08:00
|
|
|
def __init__(self, code: str, debit: list[JournalEntryLineItem],
|
|
|
|
credit: list[JournalEntryLineItem]):
|
2023-03-20 22:08:58 +08:00
|
|
|
"""Constructs the currency in the journal entry.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
:param code: The currency code.
|
2023-03-19 21:00:11 +08:00
|
|
|
:param debit: The debit line items.
|
|
|
|
:param credit: The credit line items.
|
2023-02-27 15:28:45 +08:00
|
|
|
"""
|
|
|
|
self.code: str = code
|
|
|
|
"""The currency code."""
|
2023-03-20 20:52:35 +08:00
|
|
|
self.debit: list[JournalEntryLineItem] = debit
|
2023-03-19 21:00:11 +08:00
|
|
|
"""The debit line items."""
|
2023-03-20 20:52:35 +08:00
|
|
|
self.credit: list[JournalEntryLineItem] = credit
|
2023-03-19 21:00:11 +08:00
|
|
|
"""The credit line items."""
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Returns the currency name.
|
|
|
|
|
|
|
|
:return: The currency name.
|
|
|
|
"""
|
|
|
|
return db.session.get(Currency, self.code).name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def debit_total(self) -> Decimal:
|
2023-03-19 21:00:11 +08:00
|
|
|
"""Returns the total amount of the debit line items.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
2023-03-19 21:00:11 +08:00
|
|
|
:return: The total amount of the debit line items.
|
2023-02-27 15:28:45 +08:00
|
|
|
"""
|
|
|
|
return sum([x.amount for x in self.debit])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def credit_total(self) -> str:
|
2023-03-19 21:00:11 +08:00
|
|
|
"""Returns the total amount of the credit line items.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
2023-03-19 21:00:11 +08:00
|
|
|
:return: The total amount of the credit line items.
|
2023-02-27 15:28:45 +08:00
|
|
|
"""
|
|
|
|
return sum([x.amount for x in self.credit])
|
|
|
|
|
|
|
|
|
2023-03-20 22:08:58 +08:00
|
|
|
class JournalEntry(db.Model):
|
|
|
|
"""A journal entry."""
|
|
|
|
__tablename__ = "accounting_journal_entries"
|
2023-02-27 15:28:45 +08:00
|
|
|
"""The table name."""
|
|
|
|
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
|
|
|
autoincrement=False)
|
2023-03-20 22:08:58 +08:00
|
|
|
"""The journal entry ID."""
|
2023-02-27 15:28:45 +08:00
|
|
|
date = db.Column(db.Date, nullable=False)
|
|
|
|
"""The date."""
|
|
|
|
no = db.Column(db.Integer, nullable=False, default=text("1"))
|
|
|
|
"""The account number under the date."""
|
|
|
|
note = db.Column(db.String)
|
|
|
|
"""The note."""
|
|
|
|
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of creation."""
|
|
|
|
created_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The ID of the creator."""
|
|
|
|
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
|
|
|
"""The creator."""
|
|
|
|
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of last update."""
|
|
|
|
updated_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The ID of the updator."""
|
|
|
|
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
|
|
|
"""The updator."""
|
2023-03-20 20:52:35 +08:00
|
|
|
line_items = db.relationship("JournalEntryLineItem",
|
2023-03-20 22:08:58 +08:00
|
|
|
back_populates="journal_entry")
|
2023-03-19 21:00:11 +08:00
|
|
|
"""The line items."""
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
2023-03-20 22:08:58 +08:00
|
|
|
"""Returns the string representation of this journal entry.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
2023-03-20 22:08:58 +08:00
|
|
|
:return: The string representation of this journal entry.
|
2023-02-27 15:28:45 +08:00
|
|
|
"""
|
2023-03-19 13:44:51 +08:00
|
|
|
if self.is_cash_disbursement:
|
2023-03-20 22:08:58 +08:00
|
|
|
return gettext("Cash Disbursement Journal Entry#%(id)s",
|
|
|
|
id=self.id)
|
2023-03-19 13:44:51 +08:00
|
|
|
if self.is_cash_receipt:
|
2023-03-20 22:08:58 +08:00
|
|
|
return gettext("Cash Receipt Journal Entry#%(id)s", id=self.id)
|
|
|
|
return gettext("Transfer Journal Entry#%(id)s", id=self.id)
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
@property
|
2023-03-20 22:08:58 +08:00
|
|
|
def currencies(self) -> list[JournalEntryCurrency]:
|
2023-03-19 21:00:11 +08:00
|
|
|
"""Returns the line items categorized by their currencies.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
:return: The currency categories.
|
|
|
|
"""
|
2023-03-20 20:52:35 +08:00
|
|
|
line_items: list[JournalEntryLineItem] = sorted(self.line_items,
|
|
|
|
key=lambda x: x.no)
|
2023-02-27 15:28:45 +08:00
|
|
|
codes: list[str] = []
|
2023-03-20 20:52:35 +08:00
|
|
|
by_currency: dict[str, list[JournalEntryLineItem]] = {}
|
2023-03-19 21:00:11 +08:00
|
|
|
for line_item in line_items:
|
|
|
|
if line_item.currency_code not in by_currency:
|
|
|
|
codes.append(line_item.currency_code)
|
|
|
|
by_currency[line_item.currency_code] = []
|
|
|
|
by_currency[line_item.currency_code].append(line_item)
|
2023-03-20 22:08:58 +08:00
|
|
|
return [JournalEntryCurrency(code=x,
|
|
|
|
debit=[y for y in by_currency[x]
|
|
|
|
if y.is_debit],
|
|
|
|
credit=[y for y in by_currency[x]
|
|
|
|
if not y.is_debit])
|
2023-02-27 15:28:45 +08:00
|
|
|
for x in codes]
|
|
|
|
|
|
|
|
@property
|
2023-03-19 13:44:51 +08:00
|
|
|
def is_cash_receipt(self) -> bool:
|
2023-03-20 22:08:58 +08:00
|
|
|
"""Returns whether this is a cash receipt journal entry.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
2023-03-20 22:08:58 +08:00
|
|
|
:return: True if this is a cash receipt journal entry, or False
|
|
|
|
otherwise.
|
2023-02-27 15:28:45 +08:00
|
|
|
"""
|
|
|
|
for currency in self.currencies:
|
|
|
|
if len(currency.debit) > 1:
|
|
|
|
return False
|
2023-03-08 18:31:35 +08:00
|
|
|
if currency.debit[0].account.code != Account.CASH_CODE:
|
2023-02-27 15:28:45 +08:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
2023-03-19 13:44:51 +08:00
|
|
|
def is_cash_disbursement(self) -> bool:
|
2023-03-20 22:08:58 +08:00
|
|
|
"""Returns whether this is a cash disbursement journal entry.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
2023-03-20 22:08:58 +08:00
|
|
|
:return: True if this is a cash disbursement journal entry, or False
|
2023-02-27 15:28:45 +08:00
|
|
|
otherwise.
|
|
|
|
"""
|
|
|
|
for currency in self.currencies:
|
|
|
|
if len(currency.credit) > 1:
|
|
|
|
return False
|
2023-03-08 18:31:35 +08:00
|
|
|
if currency.credit[0].account.code != Account.CASH_CODE:
|
2023-02-27 15:28:45 +08:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2023-03-17 22:32:01 +08:00
|
|
|
@property
|
|
|
|
def can_delete(self) -> bool:
|
2023-03-20 22:08:58 +08:00
|
|
|
"""Returns whether the journal entry can be deleted.
|
2023-03-17 22:32:01 +08:00
|
|
|
|
2023-03-20 22:08:58 +08:00
|
|
|
:return: True if the journal entry can be deleted, or False otherwise.
|
2023-03-17 22:32:01 +08:00
|
|
|
"""
|
2023-03-22 01:02:09 +08:00
|
|
|
for line_item in self.line_items:
|
|
|
|
if len(line_item.offsets) > 0:
|
2023-03-17 22:32:01 +08:00
|
|
|
return False
|
2023-03-22 01:02:09 +08:00
|
|
|
return True
|
2023-03-17 22:32:01 +08:00
|
|
|
|
2023-02-27 15:28:45 +08:00
|
|
|
def delete(self) -> None:
|
2023-03-20 22:08:58 +08:00
|
|
|
"""Deletes the journal entry.
|
2023-02-27 15:28:45 +08:00
|
|
|
|
|
|
|
:return: None.
|
|
|
|
"""
|
2023-03-20 20:52:35 +08:00
|
|
|
JournalEntryLineItem.query\
|
2023-03-20 22:08:58 +08:00
|
|
|
.filter(JournalEntryLineItem.journal_entry_id == self.id).delete()
|
2023-02-27 15:28:45 +08:00
|
|
|
db.session.delete(self)
|
|
|
|
|
|
|
|
|
2023-03-20 20:52:35 +08:00
|
|
|
class JournalEntryLineItem(db.Model):
|
|
|
|
"""A line item in the journal entry."""
|
|
|
|
__tablename__ = "accounting_journal_entry_line_items"
|
2023-02-27 15:28:45 +08:00
|
|
|
"""The table name."""
|
|
|
|
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
|
|
|
autoincrement=False)
|
2023-03-19 21:00:11 +08:00
|
|
|
"""The line item ID."""
|
2023-03-20 22:08:58 +08:00
|
|
|
journal_entry_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(JournalEntry.id,
|
|
|
|
onupdate="CASCADE",
|
|
|
|
ondelete="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The journal entry ID."""
|
|
|
|
journal_entry = db.relationship(JournalEntry, back_populates="line_items")
|
|
|
|
"""The journal entry."""
|
2023-02-27 15:28:45 +08:00
|
|
|
is_debit = db.Column(db.Boolean, nullable=False)
|
2023-03-19 21:00:11 +08:00
|
|
|
"""True for a debit line item, or False for a credit line item."""
|
2023-02-27 15:28:45 +08:00
|
|
|
no = db.Column(db.Integer, nullable=False)
|
2023-03-20 22:08:58 +08:00
|
|
|
"""The line item number under the journal entry and debit or credit."""
|
2023-03-19 21:00:11 +08:00
|
|
|
original_line_item_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(id, onupdate="CASCADE"),
|
|
|
|
nullable=True)
|
|
|
|
"""The ID of the original line item."""
|
2023-03-20 20:52:35 +08:00
|
|
|
original_line_item = db.relationship("JournalEntryLineItem",
|
2023-03-19 21:00:11 +08:00
|
|
|
back_populates="offsets",
|
|
|
|
remote_side=id, passive_deletes=True)
|
|
|
|
"""The original line item."""
|
2023-03-20 20:52:35 +08:00
|
|
|
offsets = db.relationship("JournalEntryLineItem",
|
2023-03-19 21:00:11 +08:00
|
|
|
back_populates="original_line_item")
|
|
|
|
"""The offset items."""
|
2023-02-27 15:28:45 +08:00
|
|
|
currency_code = db.Column(db.String,
|
|
|
|
db.ForeignKey(Currency.code, onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The currency code."""
|
2023-03-19 21:00:11 +08:00
|
|
|
currency = db.relationship(Currency, back_populates="line_items")
|
2023-02-27 15:28:45 +08:00
|
|
|
"""The currency."""
|
|
|
|
account_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(Account.id,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The account ID."""
|
2023-03-19 21:00:11 +08:00
|
|
|
account = db.relationship(Account, back_populates="line_items", lazy=False)
|
2023-02-27 15:28:45 +08:00
|
|
|
"""The account."""
|
2023-03-20 16:01:25 +08:00
|
|
|
description = db.Column(db.String, nullable=True)
|
|
|
|
"""The description."""
|
2023-02-27 15:28:45 +08:00
|
|
|
amount = db.Column(db.Numeric(14, 2), nullable=False)
|
|
|
|
"""The amount."""
|
|
|
|
|
2023-03-17 22:32:01 +08:00
|
|
|
def __str__(self) -> str:
|
2023-03-19 21:00:11 +08:00
|
|
|
"""Returns the string representation of the line item.
|
2023-03-17 22:32:01 +08:00
|
|
|
|
2023-03-19 21:00:11 +08:00
|
|
|
:return: The string representation of the line item.
|
2023-03-17 22:32:01 +08:00
|
|
|
"""
|
|
|
|
if not hasattr(self, "__str"):
|
|
|
|
from accounting.template_filters import format_date, format_amount
|
|
|
|
setattr(self, "__str",
|
2023-03-20 16:01:25 +08:00
|
|
|
gettext("%(date)s %(description)s %(amount)s",
|
2023-03-20 22:08:58 +08:00
|
|
|
date=format_date(self.journal_entry.date),
|
2023-03-20 16:01:25 +08:00
|
|
|
description="" if self.description is None
|
|
|
|
else self.description,
|
2023-03-17 22:32:01 +08:00
|
|
|
amount=format_amount(self.amount)))
|
|
|
|
return getattr(self, "__str")
|
|
|
|
|
2023-02-27 15:28:45 +08:00
|
|
|
@property
|
|
|
|
def account_code(self) -> str:
|
|
|
|
"""Returns the account code.
|
|
|
|
|
|
|
|
:return: The account code.
|
|
|
|
"""
|
|
|
|
return self.account.code
|
2023-03-09 07:55:36 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def debit(self) -> Decimal | None:
|
|
|
|
"""Returns the debit amount.
|
|
|
|
|
2023-03-19 21:00:11 +08:00
|
|
|
:return: The debit amount, or None if this is not a debit line item.
|
2023-03-09 07:55:36 +08:00
|
|
|
"""
|
|
|
|
return self.amount if self.is_debit else None
|
|
|
|
|
2023-03-17 22:32:01 +08:00
|
|
|
@property
|
2023-03-18 18:55:31 +08:00
|
|
|
def is_need_offset(self) -> bool:
|
2023-03-19 21:00:11 +08:00
|
|
|
"""Returns whether the line item needs offset.
|
2023-03-17 22:32:01 +08:00
|
|
|
|
2023-03-19 21:00:11 +08:00
|
|
|
:return: True if the line item needs offset, or False otherwise.
|
2023-03-17 22:32:01 +08:00
|
|
|
"""
|
2023-03-18 22:52:29 +08:00
|
|
|
if not self.account.is_need_offset:
|
2023-03-17 22:32:01 +08:00
|
|
|
return False
|
|
|
|
if self.account.base_code[0] == "1" and not self.is_debit:
|
|
|
|
return False
|
|
|
|
if self.account.base_code[0] == "2" and self.is_debit:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2023-03-09 07:55:36 +08:00
|
|
|
@property
|
|
|
|
def credit(self) -> Decimal | None:
|
|
|
|
"""Returns the credit amount.
|
|
|
|
|
2023-03-19 21:00:11 +08:00
|
|
|
:return: The credit amount, or None if this is not a credit line item.
|
2023-03-09 07:55:36 +08:00
|
|
|
"""
|
|
|
|
return None if self.is_debit else self.amount
|
2023-03-17 22:32:01 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def net_balance(self) -> Decimal:
|
|
|
|
"""Returns the net balance.
|
|
|
|
|
|
|
|
:return: The net balance.
|
|
|
|
"""
|
|
|
|
if not hasattr(self, "__net_balance"):
|
|
|
|
setattr(self, "__net_balance", self.amount + sum(
|
|
|
|
[x.amount if x.is_debit == self.is_debit else -x.amount
|
|
|
|
for x in self.offsets]))
|
|
|
|
return getattr(self, "__net_balance")
|
|
|
|
|
|
|
|
@net_balance.setter
|
|
|
|
def net_balance(self, net_balance: Decimal) -> None:
|
|
|
|
"""Sets the net balance.
|
|
|
|
|
|
|
|
:param net_balance: The net balance.
|
|
|
|
:return: None.
|
|
|
|
"""
|
|
|
|
setattr(self, "__net_balance", net_balance)
|
|
|
|
|
|
|
|
@property
|
2023-03-24 07:38:17 +08:00
|
|
|
def query_values(self) -> list[str]:
|
2023-03-17 22:32:01 +08:00
|
|
|
"""Returns the values to be queried.
|
|
|
|
|
|
|
|
:return: The values to be queried.
|
|
|
|
"""
|
|
|
|
def format_amount(value: Decimal) -> str:
|
|
|
|
whole: int = int(value)
|
|
|
|
frac: Decimal = (value - whole).normalize()
|
|
|
|
return str(whole) + str(abs(frac))[1:]
|
|
|
|
|
2023-03-24 09:15:29 +08:00
|
|
|
return ["{}/{}/{}".format(self.journal_entry.date.year,
|
|
|
|
self.journal_entry.date.month,
|
|
|
|
self.journal_entry.date.day),
|
|
|
|
"" if self.description is None else self.description,
|
2023-03-24 07:47:41 +08:00
|
|
|
format_amount(self.amount)]
|
2023-03-22 15:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
class Option(db.Model):
|
|
|
|
"""An option."""
|
|
|
|
__tablename__ = "accounting_options"
|
|
|
|
"""The table name."""
|
|
|
|
name = db.Column(db.String, nullable=False, primary_key=True)
|
|
|
|
"""The name."""
|
|
|
|
value = db.Column(db.Text, nullable=False)
|
|
|
|
"""The option value."""
|
|
|
|
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of creation."""
|
|
|
|
created_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The ID of the creator."""
|
|
|
|
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
|
|
|
|
"""The creator."""
|
|
|
|
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
|
|
|
server_default=db.func.now())
|
|
|
|
"""The time of last update."""
|
|
|
|
updated_by_id = db.Column(db.Integer,
|
|
|
|
db.ForeignKey(user_pk_column,
|
|
|
|
onupdate="CASCADE"),
|
|
|
|
nullable=False)
|
|
|
|
"""The ID of the updator."""
|
|
|
|
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
|
|
|
|
"""The updator."""
|