Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
86637267d3 | |||
71e97721aa | |||
5815608288 | |||
5f75d93c6a | |||
118c4b458e | |||
3f7e4c0dda | |||
eed4c923f6 | |||
09dd5ae541 | |||
172a12b134 |
129
README.rst
129
README.rst
@ -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
310
docs/source/changelog.rst
Normal 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.
|
@ -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
|
||||
|
@ -14,6 +14,7 @@ Welcome to Mia! Accounting's documentation!
|
||||
accounting
|
||||
examples
|
||||
history
|
||||
changelog
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)\
|
||||
|
@ -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."""
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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"),
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
Reference in New Issue
Block a user