9 Commits

19 changed files with 481 additions and 323 deletions

View File

@ -44,126 +44,18 @@ You may also download from the `PyPI project page`_ or the
`release page`_ on the `Git repository`_.
Prerequisites
=============
You need a running Flask application with database user login.
The primary key of the user data model must be integer. You also
need at least one user.
The following front-end JavaScript libraries must be loaded. You may
download it locally or use CDN_.
* Bootstrap_ 5.2.3 or above
* FontAwesome_ 6.2.1 or above
* `Decimal.js`_ 6.4.3 or above
* `Tempus-Dominus`_ 6.4.3 or above
Configuration
=============
You need to pass the Flask *app* and an implementation of
`UserUtilityInterface`_ to the `init_app`_ function.
``UserUtilityInterface`` contains everything *Mia! Accounting* needs.
The following is an example configuration for *Mia! Accounting*.
::
from flask import Response, redirect
from .auth import current_user()
from .modules import User
def create_app(test_config=None) -> Flask:
app: Flask = Flask(__name__)
... (Configuration of SQLAlchemy, CSRF, Babel_JS, ... etc) ...
import accounting
class UserUtils(accounting.UserUtilityInterface[User]):
def can_view(self) -> bool:
return True
def can_edit(self) -> bool:
return "editor" in current_user().roles
def can_admin(self) -> bool:
return current_user().is_admin
def unauthorized(self) -> Response:
return redirect("/login")
@property
def cls(self) -> t.Type[User]:
return User
@property
def pk_column(self) -> Column:
return User.id
@property
def current_user(self) -> User | None:
return current_user()
def get_by_username(self, username: str) -> User | None:
return User.query.filter(User.username == username).first()
def get_pk(self, user: User) -> int:
return user.id
accounting.init_app(app, UserUtils())
... (Any other configuration) ...
return app
Database Initialization
=======================
After the configuration, run the ``accounting-init-db`` console
command to initialize the accounting database. You need to specify
the username of a user as the data creator.
::
% flask --app myapp accounting-init-db -u username
Navigation Menu
===============
Include the navigation menu in the `Bootstrap navigation bar`_ in your
base template:
::
<nav class="navbar navbar-expand-lg bg-body-tertiary bg-dark navbar-dark">
<div class="container-fluid">
...
<div id="collapsible-navbar" class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
...
{% include "accounting/include/nav.html" %}
...
</ul>
...
</div>
</div>
</nav>
Check your Flask application and see how it works.
Documentation
=============
Refer to the `documentation on Read the Docs`_.
Change Log
==========
Refer to the `change log`_.
Copyright
=========
@ -198,12 +90,5 @@ Authors
.. _PyPI project page: https://pypi.org/project/mia-accounting
.. _release page: https://github.com/imacat/mia-accounting/releases
.. _Git repository: https://github.com/imacat/mia-accounting
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
.. _Bootstrap: https://getbootstrap.com
.. _FontAwesome: https://fontawesome.com
.. _Decimal.js: https://mikemcl.github.io/decimal.js
.. _Tempus-Dominus: https://getdatepicker.com
.. _UserUtilityInterface: https://mia-accounting.readthedocs.io/en/latest/accounting.utils.html#accounting.utils.user.UserUtilityInterface
.. _init_app: https://mia-accounting.readthedocs.io/en/latest/accounting.html#accounting.init_app
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io
.. _change log: https://mia-accounting.readthedocs.io/en/latest/changelog.html

310
docs/source/changelog.rst Normal file
View File

@ -0,0 +1,310 @@
Changes
=======
Version 1.5.0
-------------
Released 2023/4/23
* Updated to require ``SQLAlchemy >= 2``.
* Added the change log.
* Added the ``VERSION`` constant to the ``accounting`` module for
the package version, and revised ``pyproject.toml`` and ``conf.py``
to read the version from it.
Version 1.4.1
-------------
Released 2023/4/22
* Updated to allow editing the description of the journal entry line
item with offsets or are offsetting to original line items.
* Updated not to override the existing description of a journal entry
line item after choosing the original line item to offset to.
Version 1.4.0
-------------
Released 2023/4/18
* Rewrote the unapplied original line items and unmatched offsets.
* The unapplied original line items and unmatched offsets are both
in the report submodule. They can be filtered with currency and
period now.
* Show the unapplied original line items and unmatched offsets
together, and added the accumulated balance in the unmatched
offset list, for ease of reference.
* Removed the account code from the journal entry detail and journal
entry form for mobile devices.
* Made the account options in the reports to be scrollable.
Version 1.3.3
-------------
Released 2023/4/13
Changed the sample data generation in the test site live demonstration
from pre-recorded data to real-time generation, to avoid the problem
with the start of months and weeks changed with the date of the
import.
Version 1.3.2
-------------
Released 2023/4/12
Added the sample data generation and database reset on the test site
for live demonstration.
Version 1.3.1
-------------
Released 2023/4/11
* Fixed the permission of the navigation menu of the unmatched offsets.
* Revised the test site to be more accessible as the live demonstration.
Version 1.3.0
-------------
Released 2023/4/11
Added the ``accounting-init-db`` console command to replace all the
other console commands to initialize the accounting database. The
test site does not work with previous versions (<1.3.0).
Version 1.2.1
-------------
Released 2023/4/9
Fixed the search result to allow full ``year/month/day``
specification.
Version 1.2.0
-------------
Released 2023/4/9
* Simplified the URL of the default reports.
* Fixed the crash with malformed Chinese translation.
* Fixed the crash when downloading CSV data with non-US-ASCII
filenames.
Version 1.1.0
-------------
Released 2023/4/9
* Added the unapplied original line item list, to track unpaid
payables, unreceived receivables, assets, prepaids, refundable
deposits, etc.
* Added the offset matcher to match unapplied original line items
with unmatched offsets.
Version 1.0.1
-------------
Released 2023/4/6
Documentation fixes.
Version 1.0.0
-------------
Released 2023/4/6
The first formal release in Flask.
Added the documentation.
Version 0.11.1 (Pre-release)
----------------------------
Released 2023/4/5
Removed the zero balances from the trial balance, the income
statement, and the balance sheet.
Version 0.11.0 (Pre-release)
----------------------------
Released 2023/4/5
* Renamed the project from ``mia-accounting-flask`` to
``mia-accounting``.
* Updated the URL of the reports, as the default views of the
accounting application.
* Updated ``README``.
* Various fixes.
Version 0.10.0 (Pre-release)
----------------------------
Released 2023/4/3
* Added the unauthorized method to the ``UserUtilityInterface``
interface to allow fine control to how to handle the case when the
user has not logged in.
* Revised the JavaScript description editor to respect the account
that the user has confirmed or specifically selected.
* Various fixes.
Version 0.9.1 (Pre-release)
---------------------------
Released 2023/3/24
* A distinguishable look in the option detail than the option form.
* A better look in the new journal entry forms when there is no line
item yet.
* Fixed the search in the original entry selector in the journal
entry form to always do a partial match, to fix the problem that
there is no match when typing is not finished yet.
* Fixed the search in the original entry selector to search the net
balance correctly.
* Replaced the ``editor`` and ``editor2`` accounts with the ``admin``
and ``editor`` accounts.
* Various fixes.
Version 0.9.0 (Pre-release)
---------------------------
Released 2023/3/23
Moved the settings from the ``.env`` file to the option table in the
database that can be set and updated on the web interface. Added the
settings page to show and update the settings.
Version 0.8.0 (Pre-release)
---------------------------
Released 2023/3/22
* Added the recurring transactions to the description editor.
* Added prevention to delete database objects that are essential or
referenced by others with foreign keys.
* Various fixes on the visual layout.
Version 0.7.0 (Pre-release)
---------------------------
Released 2023/3/21
* Renamed "transaction" to "journal entry", and "journal entry" to
"journal entry line item".
* Renamed ``summary`` to ``description``.
* Updated tempus-dominus from version 6.2.10 to 6.4.3.
* Fixed titles and capitalization.
* Fixed to search case-insensitively.
* Added favicon to the test site.
* Fixed the navigation menu when there is no matching endpoint.
* Various fixes.
Version 0.6.0 (Pre-release)
---------------------------
Released 2023/3/18
* Added offset tracking to the journal entries in the payable and
receivable accounts.
* Renamed the ``is_offset_needed`` column to ``is_need_offset`` in
the ``Account`` data model.
Version 0.5.0 (Pre-release)
---------------------------
Released 2023/3/10
Added the accounting reports.
Version 0.4.0 (Pre-release)
---------------------------
Released 2023/3/1
Added the transaction summary helper.
Version 0.3.1 (Pre-release)
---------------------------
Released 2023/2/28
* Fixed the error that cannot select any account when adding new
transactions.
* Fixed the database error when adding new transactions.
* Added the button to convert a cash income or cash expense
transaction to a transfer transaction.
Version 0.3.0 (Pre-release)
---------------------------
Released 2023/2/27
Added the transaction management.
Version 0.2.0 (Pre-release)
---------------------------
Released 2023/2/7
* Added the currency management.
* Changed the ``can_edit`` permission to at least require the user to
log in first.
* Changed the type hint of the ``current_user`` pseudo property of
the ``AbstractUserUtils`` class to return ``None`` when the user
has not logged in.
Version 0.1.1 (Pre-release)
---------------------------
Released 2023/2/3
Finalized the account management, with tests and reordering.
Version 0.1.0 (Pre-release)
---------------------------
Released 2023/2/3
Added the account management, and updated the API to initialize the
accounting application.
Version 0.0.0 (Pre-release)
---------------------------
Released 2023/2/3
Initial release with main account list, localization, pagination,
query, permission, Sphinx documentation, and a test case based on a
test demonstration site.

View File

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

View File

@ -14,6 +14,7 @@ Welcome to Mia! Accounting's documentation!
accounting
examples
history
changelog

View File

@ -103,12 +103,6 @@ base template:
Check your Flask application and see how it works.
Documentation
-------------
Refer to the `documentation on Read the Docs`_.
.. _Flask: https://flask.palletsprojects.com
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
.. _live demonstration: https://accounting.imacat.idv.tw
@ -123,4 +117,3 @@ Refer to the `documentation on Read the Docs`_.
.. _Decimal.js: https://mikemcl.github.io/decimal.js
.. _Tempus-Dominus: https://getdatepicker.com
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
.. _documentation on Read the Docs: https://mia-accounting.readthedocs.io

View File

@ -17,7 +17,7 @@
[project]
name = "mia-accounting"
version = "1.4.1"
dynamic = ["version"]
description = "A Flask accounting module."
readme = "README.rst"
requires-python = ">=3.11"
@ -34,6 +34,7 @@ classifiers = [
]
dependencies = [
"flask",
"SQLAlchemy >= 2",
"Flask-SQLAlchemy",
"Flask-WTF",
"Flask-Babel >= 3",
@ -49,6 +50,7 @@ test = [
[project.urls]
"Documentation" = "https://mia-accounting.readthedocs.io"
"Change Log" = "https://mia-accounting.readthedocs.io/en/latest/changelog.html"
"Repository" = "https://github.com/imacat/mia-accounting"
"Bug Tracker" = "https://github.com/imacat/mia-accounting/issues"
"Demonstration" = "https://accounting.imacat.idv.tw"
@ -57,6 +59,9 @@ test = [
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic]
version = {attr = "accounting.VERSION"}
[tool.setuptools.exclude-package-data]
"*" = [
"babel.cfg",

View File

@ -24,6 +24,8 @@ from flask_sqlalchemy import SQLAlchemy
from accounting.utils.user import UserUtilityInterface
VERSION: str = "1.5.0"
"""The package version."""
db: SQLAlchemy = SQLAlchemy()
"""The database instance."""
data_dir: Path = Path(__file__).parent / "data"

View File

@ -37,7 +37,8 @@ class JournalEntryConverter(BaseConverter):
:param value: The journal entry ID.
:return: The corresponding journal entry.
"""
journal_entry: JournalEntry | None = db.session.get(JournalEntry, value)
journal_entry: JournalEntry | None \
= db.session.get(JournalEntry, value)
if journal_entry is None:
abort(404)
return journal_entry

View File

@ -30,7 +30,6 @@ from accounting import db
from accounting.forms import CurrencyExists
from accounting.locale import lazy_gettext
from accounting.models import JournalEntryLineItem
from accounting.utils.cast import be
from accounting.utils.offset_alias import offset_alias
from accounting.utils.strip_text import strip_text
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
@ -75,8 +74,8 @@ class KeepCurrencyWhenHavingOffset:
offset: sa.Alias = offset_alias()
original_line_items: list[JournalEntryLineItem]\
= JournalEntryLineItem.query\
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True)\
.filter(JournalEntryLineItem.id
.in_({x.id.data for x in form.line_items

View File

@ -33,7 +33,6 @@ from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
from accounting.locale import lazy_gettext
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.template_filters import format_amount
from accounting.utils.cast import be
from accounting.utils.random_id import new_id
from accounting.utils.strip_text import strip_text
from accounting.utils.user import get_current_user_pk
@ -198,13 +197,13 @@ class NotExceedingOriginalLineItemNetBalance:
existing_line_item_id \
= {x.id for x in form.journal_entry_form.obj.line_items}
offset_total_func: sa.Function = sa.func.sum(sa.case(
(be(JournalEntryLineItem.is_debit == is_debit),
(JournalEntryLineItem.is_debit == is_debit,
JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
offset_total_but_form: Decimal | None = db.session.scalar(
sa.select(offset_total_func)
.filter(be(JournalEntryLineItem.original_line_item_id
== original_line_item.id),
.filter(JournalEntryLineItem.original_line_item_id
== original_line_item.id,
JournalEntryLineItem.id.not_in(existing_line_item_id)))
if offset_total_but_form is None:
offset_total_but_form = Decimal("0")
@ -232,8 +231,7 @@ class NotLessThanOffsetTotal:
(JournalEntryLineItem.is_debit != is_debit,
JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount)))\
.filter(be(JournalEntryLineItem.original_line_item_id
== form.id.data))
.filter(JournalEntryLineItem.original_line_item_id == form.id.data)
offset_total: Decimal | None = db.session.scalar(select_offset_total)
if offset_total is not None and field.data < offset_total:
raise ValidationError(lazy_gettext(

View File

@ -24,7 +24,6 @@ from sqlalchemy.orm import selectinload
from accounting import db
from accounting.models import Account, JournalEntry, JournalEntryLineItem
from accounting.utils.cast import be
from accounting.utils.offset_alias import offset_alias
@ -45,8 +44,7 @@ def get_selectable_original_line_items(
offset: sa.Alias = offset_alias()
net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
(offset.c.id.in_(line_item_id_on_form), 0),
(be(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")
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
sub_conditions: list[sa.BinaryExpression] = []
@ -60,8 +58,8 @@ def get_selectable_original_line_items(
select_net_balances: sa.Select \
= sa.select(JournalEntryLineItem.id, net_balance)\
.join(Account)\
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True)\
.filter(*conditions)\
.group_by(JournalEntryLineItem.id)\

View File

@ -19,6 +19,7 @@
"""
from __future__ import annotations
import datetime as dt
import re
import typing as t
from decimal import Decimal
@ -27,6 +28,7 @@ import sqlalchemy as sa
from babel import Locale
from flask_babel import get_locale, get_babel
from sqlalchemy import text
from sqlalchemy.orm import Mapped, mapped_column
from accounting import db
from accounting.locale import gettext
@ -37,14 +39,14 @@ class BaseAccount(db.Model):
"""A base account."""
__tablename__ = "accounting_base_accounts"
"""The table name."""
code = db.Column(db.String, nullable=False, primary_key=True)
code: Mapped[str] = mapped_column(primary_key=True)
"""The code."""
title_l10n = db.Column("title", db.String, nullable=False)
title_l10n: Mapped[str] = mapped_column("title")
"""The title."""
l10n = db.relationship("BaseAccountL10n", back_populates="account",
lazy=False)
l10n: Mapped[list[BaseAccountL10n]] \
= db.relationship(back_populates="account", lazy=False)
"""The localized titles."""
accounts = db.relationship("Account", back_populates="base")
accounts: Mapped[list[Account]] = db.relationship(back_populates="base")
"""The descendant accounts under the base account."""
def __str__(self) -> str:
@ -81,17 +83,16 @@ class BaseAccountL10n(db.Model):
"""A localized base account title."""
__tablename__ = "accounting_base_accounts_l10n"
"""The table name."""
account_code = db.Column(db.String,
db.ForeignKey(BaseAccount.code,
onupdate="CASCADE",
account_code: Mapped[str] \
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False, primary_key=True)
primary_key=True)
"""The code of the account."""
account = db.relationship(BaseAccount, back_populates="l10n")
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
"""The account."""
locale = db.Column(db.String, nullable=False, primary_key=True)
locale: Mapped[str] = mapped_column(primary_key=True)
"""The locale."""
title = db.Column(db.String, nullable=False)
title: Mapped[str]
"""The localized title."""
@ -99,47 +100,43 @@ class Account(db.Model):
"""An account."""
__tablename__ = "accounting_accounts"
"""The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=False)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
"""The account ID."""
base_code = db.Column(db.String,
db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False)
base_code: Mapped[str] \
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"))
"""The code of the base account."""
base = db.relationship(BaseAccount, back_populates="accounts")
base: Mapped[BaseAccount] = db.relationship(back_populates="accounts")
"""The base account."""
no = db.Column(db.Integer, nullable=False, default=text("1"))
no: Mapped[int] = mapped_column(default=text("1"))
"""The account number under the base account."""
title_l10n = db.Column("title", db.String, nullable=False)
title_l10n: Mapped[str] = mapped_column("title")
"""The title."""
is_need_offset = db.Column(db.Boolean, nullable=False, default=False)
is_need_offset: Mapped[bool] = mapped_column(default=False)
"""Whether the journal entry line items of this account need offset."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the creator."""
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The creator."""
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the updator."""
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The updator."""
l10n = db.relationship("AccountL10n", back_populates="account",
lazy=False)
l10n: Mapped[list[AccountL10n]] \
= db.relationship(back_populates="account", lazy=False)
"""The localized titles."""
line_items = db.relationship("JournalEntryLineItem",
back_populates="account")
line_items: Mapped[list[JournalEntryLineItem]] \
= db.relationship(back_populates="account")
"""The journal entry line items."""
CASH_CODE: str = "1111-001"
@ -352,16 +349,16 @@ class AccountL10n(db.Model):
"""A localized account title."""
__tablename__ = "accounting_accounts_l10n"
"""The table name."""
account_id = db.Column(db.Integer,
db.ForeignKey(Account.id, onupdate="CASCADE",
account_id: Mapped[int] \
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False, primary_key=True)
primary_key=True)
"""The account ID."""
account = db.relationship(Account, back_populates="l10n")
account: Mapped[Account] = db.relationship(back_populates="l10n")
"""The account."""
locale = db.Column(db.String, nullable=False, primary_key=True)
locale: Mapped[str] = mapped_column(primary_key=True)
"""The locale."""
title = db.Column(db.String, nullable=False)
title: Mapped[str]
"""The localized title."""
@ -369,35 +366,34 @@ class Currency(db.Model):
"""A currency."""
__tablename__ = "accounting_currencies"
"""The table name."""
code = db.Column(db.String, nullable=False, primary_key=True)
code: Mapped[str] = mapped_column(primary_key=True)
"""The code."""
name_l10n = db.Column("name", db.String, nullable=False)
name_l10n: Mapped[str] = mapped_column("name")
"""The name."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the creator."""
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The creator."""
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the updator."""
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
updated_by: Mapped[user_cls] \
= db.relationship(foreign_keys=updated_by_id)
"""The updator."""
l10n = db.relationship("CurrencyL10n", back_populates="currency",
lazy=False)
l10n: Mapped[list[CurrencyL10n]] \
= db.relationship(back_populates="currency", lazy=False)
"""The localized names."""
line_items = db.relationship("JournalEntryLineItem",
back_populates="currency")
line_items: Mapped[list[JournalEntryLineItem]] \
= db.relationship(back_populates="currency")
"""The journal entry line items."""
def __str__(self) -> str:
@ -479,16 +475,16 @@ 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",
currency_code: Mapped[str] \
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False, primary_key=True)
primary_key=True)
"""The currency code."""
currency = db.relationship(Currency, back_populates="l10n")
currency: Mapped[Currency] = db.relationship(back_populates="l10n")
"""The currency."""
locale = db.Column(db.String, nullable=False, primary_key=True)
locale: Mapped[str] = mapped_column(primary_key=True)
"""The locale."""
name = db.Column(db.String, nullable=False)
name: Mapped[str]
"""The localized name."""
@ -539,37 +535,34 @@ class JournalEntry(db.Model):
"""A journal entry."""
__tablename__ = "accounting_journal_entries"
"""The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=False)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
"""The journal entry ID."""
date = db.Column(db.Date, nullable=False)
date: Mapped[dt.date]
"""The date."""
no = db.Column(db.Integer, nullable=False, default=text("1"))
no: Mapped[int] = mapped_column(default=text("1"))
"""The account number under the date."""
note = db.Column(db.String)
note: Mapped[str | None]
"""The note."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the creator."""
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The creator."""
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the updator."""
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The updator."""
line_items = db.relationship("JournalEntryLineItem",
back_populates="journal_entry")
line_items: Mapped[list[JournalEntryLineItem]] \
= db.relationship(back_populates="journal_entry")
"""The line items."""
def __str__(self) -> str:
@ -659,44 +652,39 @@ class JournalEntryLineItem(db.Model):
"""A line item in the journal entry."""
__tablename__ = "accounting_journal_entry_line_items"
"""The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=False)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
"""The line item ID."""
journal_entry_id = db.Column(db.Integer,
db.ForeignKey(JournalEntry.id,
onupdate="CASCADE",
ondelete="CASCADE"),
nullable=False)
journal_entry_id: Mapped[int] \
= mapped_column(db.ForeignKey(JournalEntry.id, onupdate="CASCADE",
ondelete="CASCADE"))
"""The journal entry ID."""
journal_entry = db.relationship(JournalEntry, back_populates="line_items")
journal_entry: Mapped[JournalEntry] \
= db.relationship(back_populates="line_items")
"""The journal entry."""
is_debit = db.Column(db.Boolean, nullable=False)
is_debit: Mapped[bool]
"""True for a debit line item, or False for a credit line item."""
no = db.Column(db.Integer, nullable=False)
no: Mapped[int]
"""The line item number under the journal entry and debit or credit."""
original_line_item_id = db.Column(db.Integer,
db.ForeignKey(id, onupdate="CASCADE"),
nullable=True)
original_line_item_id: Mapped[int | None] \
= mapped_column(db.ForeignKey(id, onupdate="CASCADE"))
"""The ID of the original line item."""
original_line_item = db.relationship("JournalEntryLineItem",
remote_side=id, passive_deletes=True)
original_line_item: Mapped[JournalEntryLineItem | None] \
= db.relationship(remote_side=id, passive_deletes=True)
"""The original line item."""
currency_code = db.Column(db.String,
db.ForeignKey(Currency.code, onupdate="CASCADE"),
nullable=False)
currency_code: Mapped[str] \
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE"))
"""The currency code."""
currency = db.relationship(Currency, back_populates="line_items")
currency: Mapped[Currency] = db.relationship(back_populates="line_items")
"""The currency."""
account_id = db.Column(db.Integer,
db.ForeignKey(Account.id,
onupdate="CASCADE"),
nullable=False)
account_id: Mapped[int] \
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE"))
"""The account ID."""
account = db.relationship(Account, back_populates="line_items", lazy=False)
account: Mapped[Account] \
= db.relationship(back_populates="line_items", lazy=False)
"""The account."""
description = db.Column(db.String, nullable=True)
description: Mapped[str | None]
"""The description."""
amount = db.Column(db.Numeric(14, 2), nullable=False)
amount: Mapped[Decimal] = mapped_column(db.Numeric(14, 2))
"""The amount."""
def __str__(self) -> str:
@ -891,27 +879,25 @@ class Option(db.Model):
"""An option."""
__tablename__ = "accounting_options"
"""The table name."""
name = db.Column(db.String, nullable=False, primary_key=True)
name: Mapped[str] = mapped_column(primary_key=True)
"""The name."""
value = db.Column(db.Text, nullable=False)
value: Mapped[str] = mapped_column(db.Text)
"""The option value."""
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
created_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the creator."""
created_by = db.relationship(user_cls, foreign_keys=created_by_id)
created_by: Mapped[user_cls] = db.relationship(foreign_keys=created_by_id)
"""The creator."""
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
updated_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True),
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)
updated_by_id: Mapped[int] \
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
"""The ID of the updator."""
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The updator."""

View File

@ -37,7 +37,6 @@ 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 income_expenses_url
from accounting.utils.cast import be
from accounting.utils.current_account import CurrentAccount
from accounting.utils.pagination import Pagination
@ -122,8 +121,7 @@ class LineItemCollector:
else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func)\
.join(JournalEntry).join(Account)\
.filter(be(JournalEntryLineItem.currency_code
== self.__currency.code),
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
self.__account_condition,
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
@ -347,8 +345,7 @@ class PageParams(BasePageParams):
self.account.id == 0)]
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.join(Account)\
.filter(be(JournalEntryLineItem.currency_code
== self.currency.code),
.filter(JournalEntryLineItem.currency_code == self.currency.code,
CurrentAccount.sql_condition())\
.group_by(JournalEntryLineItem.account_id)
options.extend([OptionLink(str(x),

View File

@ -37,7 +37,6 @@ 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
from accounting.utils.cast import be
from accounting.utils.pagination import Pagination
@ -118,10 +117,8 @@ class LineItemCollector:
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
else_=-JournalEntryLineItem.amount))
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
.filter(be(JournalEntryLineItem.currency_code
== self.__currency.code),
be(JournalEntryLineItem.account_id
== self.__account.id),
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
JournalEntryLineItem.account_id == self.__account.id,
JournalEntry.date < self.__period.start)
balance: int | None = db.session.scalar(select)
if balance is None:
@ -313,8 +310,7 @@ class PageParams(BasePageParams):
:return: The account options.
"""
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.filter(be(JournalEntryLineItem.currency_code
== self.currency.code))\
.filter(JournalEntryLineItem.currency_code == self.currency.code)\
.group_by(JournalEntryLineItem.account_id)
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
x.id == self.account.id)

View File

@ -32,7 +32,6 @@ 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.cast import be
from accounting.utils.pagination import Pagination
from accounting.utils.query import parse_query_keywords
from .journal import get_csv_rows
@ -128,9 +127,8 @@ class LineItemCollector:
journal_entry_date: datetime
try:
journal_entry_date = datetime.strptime(k, "%Y")
conditions.append(
be(sa.extract("year", JournalEntry.date)
== journal_entry_date.year))
conditions.append(sa.extract("year", JournalEntry.date)
== journal_entry_date.year)
except ValueError:
pass
try:

View File

@ -24,7 +24,6 @@ import sqlalchemy as sa
from accounting import db
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.utils.cast import be
from accounting.utils.offset_alias import offset_alias
@ -38,17 +37,17 @@ def get_accounts_with_unapplied(currency: Currency) -> list[Account]:
net_balance: sa.Label \
= (JournalEntryLineItem.amount
+ sa.func.sum(sa.case(
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
(offset.c.is_debit == JournalEntryLineItem.is_debit,
offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
select_unapplied: sa.Select \
= sa.select(JournalEntryLineItem.id)\
.join(JournalEntry).join(Account)\
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True)\
.filter(Account.is_need_offset,
be(JournalEntryLineItem.currency_code == currency.code),
JournalEntryLineItem.currency_code == currency.code,
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),
@ -84,17 +83,17 @@ def get_net_balances(currency: Currency, account: Account) \
net_balance: sa.Label \
= (JournalEntryLineItem.amount
+ sa.func.sum(sa.case(
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
(offset.c.is_debit == JournalEntryLineItem.is_debit,
offset.c.amount),
else_=-offset.c.amount))).label("net_balance")
select_net_balances: sa.Select \
= sa.select(JournalEntryLineItem.id, net_balance) \
.join(JournalEntry).join(Account) \
.join(offset, be(JournalEntryLineItem.id
== offset.c.original_line_item_id),
.join(offset,
JournalEntryLineItem.id == offset.c.original_line_item_id,
isouter=True) \
.filter(be(Account.id == account.id),
be(JournalEntryLineItem.currency_code == currency.code),
.filter(Account.id == account.id,
JournalEntryLineItem.currency_code == currency.code,
sa.or_(sa.and_(Account.base_code.startswith("2"),
sa.not_(JournalEntryLineItem.is_debit)),
sa.and_(Account.base_code.startswith("1"),

View File

@ -22,7 +22,6 @@ import sqlalchemy as sa
from accounting import db
from accounting.models import Currency, Account, JournalEntry, \
JournalEntryLineItem
from accounting.utils.cast import be
def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
@ -38,7 +37,7 @@ def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
.select_from(Account)\
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
.filter(Account.is_need_offset,
be(JournalEntryLineItem.currency_code == currency.code),
JournalEntryLineItem.currency_code == currency.code,
JournalEntryLineItem.original_line_item_id.is_(None),
sa.or_(sa.and_(Account.base_code.startswith("2"),
JournalEntryLineItem.is_debit),

View File

@ -21,7 +21,7 @@ from accounting.models import Currency
from accounting.utils.options import options
def currency_options() -> str:
def currency_options() -> list[Currency]:
"""Returns the currency options.
:return: The currency options.

View File

@ -25,16 +25,6 @@ import typing as t
import sqlalchemy as sa
def be(expression: t.Any) -> sa.BinaryExpression:
"""Casts the SQLAlchemy binary expression to the binary expression type.
:param expression: The binary expression.
:return: The binary expression itself.
"""
assert isinstance(expression, sa.BinaryExpression)
return expression
def s(message: t.Any) -> str:
"""Casts the LazyString message to the string type.