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`_.
|
`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
|
Documentation
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Refer to the `documentation on Read the Docs`_.
|
Refer to the `documentation on Read the Docs`_.
|
||||||
|
|
||||||
|
|
||||||
|
Change Log
|
||||||
|
==========
|
||||||
|
|
||||||
|
Refer to the `change log`_.
|
||||||
|
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
=========
|
=========
|
||||||
|
|
||||||
@ -198,12 +90,5 @@ Authors
|
|||||||
.. _PyPI project page: https://pypi.org/project/mia-accounting
|
.. _PyPI project page: https://pypi.org/project/mia-accounting
|
||||||
.. _release page: https://github.com/imacat/mia-accounting/releases
|
.. _release page: https://github.com/imacat/mia-accounting/releases
|
||||||
.. _Git repository: https://github.com/imacat/mia-accounting
|
.. _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
|
.. _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
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../../src/'))
|
sys.path.insert(0, os.path.abspath('../../src/'))
|
||||||
|
import accounting
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#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'
|
project = 'Mia! Accounting'
|
||||||
copyright = '2023, imacat'
|
copyright = '2023, imacat'
|
||||||
author = 'imacat'
|
author = 'imacat'
|
||||||
release = '1.4.1'
|
release = accounting.VERSION
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#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
|
accounting
|
||||||
examples
|
examples
|
||||||
history
|
history
|
||||||
|
changelog
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,12 +103,6 @@ base template:
|
|||||||
Check your Flask application and see how it works.
|
Check your Flask application and see how it works.
|
||||||
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Refer to the `documentation on Read the Docs`_.
|
|
||||||
|
|
||||||
|
|
||||||
.. _Flask: https://flask.palletsprojects.com
|
.. _Flask: https://flask.palletsprojects.com
|
||||||
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
.. _double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
|
||||||
.. _live demonstration: https://accounting.imacat.idv.tw
|
.. _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
|
.. _Decimal.js: https://mikemcl.github.io/decimal.js
|
||||||
.. _Tempus-Dominus: https://getdatepicker.com
|
.. _Tempus-Dominus: https://getdatepicker.com
|
||||||
.. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/
|
.. _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]
|
[project]
|
||||||
name = "mia-accounting"
|
name = "mia-accounting"
|
||||||
version = "1.4.1"
|
dynamic = ["version"]
|
||||||
description = "A Flask accounting module."
|
description = "A Flask accounting module."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
@ -34,6 +34,7 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flask",
|
"flask",
|
||||||
|
"SQLAlchemy >= 2",
|
||||||
"Flask-SQLAlchemy",
|
"Flask-SQLAlchemy",
|
||||||
"Flask-WTF",
|
"Flask-WTF",
|
||||||
"Flask-Babel >= 3",
|
"Flask-Babel >= 3",
|
||||||
@ -49,6 +50,7 @@ test = [
|
|||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Documentation" = "https://mia-accounting.readthedocs.io"
|
"Documentation" = "https://mia-accounting.readthedocs.io"
|
||||||
|
"Change Log" = "https://mia-accounting.readthedocs.io/en/latest/changelog.html"
|
||||||
"Repository" = "https://github.com/imacat/mia-accounting"
|
"Repository" = "https://github.com/imacat/mia-accounting"
|
||||||
"Bug Tracker" = "https://github.com/imacat/mia-accounting/issues"
|
"Bug Tracker" = "https://github.com/imacat/mia-accounting/issues"
|
||||||
"Demonstration" = "https://accounting.imacat.idv.tw"
|
"Demonstration" = "https://accounting.imacat.idv.tw"
|
||||||
@ -57,6 +59,9 @@ test = [
|
|||||||
requires = ["setuptools>=42"]
|
requires = ["setuptools>=42"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "accounting.VERSION"}
|
||||||
|
|
||||||
[tool.setuptools.exclude-package-data]
|
[tool.setuptools.exclude-package-data]
|
||||||
"*" = [
|
"*" = [
|
||||||
"babel.cfg",
|
"babel.cfg",
|
||||||
|
@ -24,6 +24,8 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
|
|
||||||
from accounting.utils.user import UserUtilityInterface
|
from accounting.utils.user import UserUtilityInterface
|
||||||
|
|
||||||
|
VERSION: str = "1.5.0"
|
||||||
|
"""The package version."""
|
||||||
db: SQLAlchemy = SQLAlchemy()
|
db: SQLAlchemy = SQLAlchemy()
|
||||||
"""The database instance."""
|
"""The database instance."""
|
||||||
data_dir: Path = Path(__file__).parent / "data"
|
data_dir: Path = Path(__file__).parent / "data"
|
||||||
|
@ -37,7 +37,8 @@ class JournalEntryConverter(BaseConverter):
|
|||||||
:param value: The journal entry ID.
|
:param value: The journal entry ID.
|
||||||
:return: The corresponding journal entry.
|
: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:
|
if journal_entry is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
return journal_entry
|
return journal_entry
|
||||||
|
@ -30,7 +30,6 @@ from accounting import db
|
|||||||
from accounting.forms import CurrencyExists
|
from accounting.forms import CurrencyExists
|
||||||
from accounting.locale import lazy_gettext
|
from accounting.locale import lazy_gettext
|
||||||
from accounting.models import JournalEntryLineItem
|
from accounting.models import JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.offset_alias import offset_alias
|
from accounting.utils.offset_alias import offset_alias
|
||||||
from accounting.utils.strip_text import strip_text
|
from accounting.utils.strip_text import strip_text
|
||||||
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
|
||||||
@ -75,8 +74,8 @@ class KeepCurrencyWhenHavingOffset:
|
|||||||
offset: sa.Alias = offset_alias()
|
offset: sa.Alias = offset_alias()
|
||||||
original_line_items: list[JournalEntryLineItem]\
|
original_line_items: list[JournalEntryLineItem]\
|
||||||
= JournalEntryLineItem.query\
|
= JournalEntryLineItem.query\
|
||||||
.join(offset, be(JournalEntryLineItem.id
|
.join(offset,
|
||||||
== offset.c.original_line_item_id),
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(JournalEntryLineItem.id
|
.filter(JournalEntryLineItem.id
|
||||||
.in_({x.id.data for x in form.line_items
|
.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.locale import lazy_gettext
|
||||||
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.template_filters import format_amount
|
from accounting.template_filters import format_amount
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.random_id import new_id
|
from accounting.utils.random_id import new_id
|
||||||
from accounting.utils.strip_text import strip_text
|
from accounting.utils.strip_text import strip_text
|
||||||
from accounting.utils.user import get_current_user_pk
|
from accounting.utils.user import get_current_user_pk
|
||||||
@ -198,13 +197,13 @@ class NotExceedingOriginalLineItemNetBalance:
|
|||||||
existing_line_item_id \
|
existing_line_item_id \
|
||||||
= {x.id for x in form.journal_entry_form.obj.line_items}
|
= {x.id for x in form.journal_entry_form.obj.line_items}
|
||||||
offset_total_func: sa.Function = sa.func.sum(sa.case(
|
offset_total_func: sa.Function = sa.func.sum(sa.case(
|
||||||
(be(JournalEntryLineItem.is_debit == is_debit),
|
(JournalEntryLineItem.is_debit == is_debit,
|
||||||
JournalEntryLineItem.amount),
|
JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
offset_total_but_form: Decimal | None = db.session.scalar(
|
offset_total_but_form: Decimal | None = db.session.scalar(
|
||||||
sa.select(offset_total_func)
|
sa.select(offset_total_func)
|
||||||
.filter(be(JournalEntryLineItem.original_line_item_id
|
.filter(JournalEntryLineItem.original_line_item_id
|
||||||
== original_line_item.id),
|
== original_line_item.id,
|
||||||
JournalEntryLineItem.id.not_in(existing_line_item_id)))
|
JournalEntryLineItem.id.not_in(existing_line_item_id)))
|
||||||
if offset_total_but_form is None:
|
if offset_total_but_form is None:
|
||||||
offset_total_but_form = Decimal("0")
|
offset_total_but_form = Decimal("0")
|
||||||
@ -232,8 +231,7 @@ class NotLessThanOffsetTotal:
|
|||||||
(JournalEntryLineItem.is_debit != is_debit,
|
(JournalEntryLineItem.is_debit != is_debit,
|
||||||
JournalEntryLineItem.amount),
|
JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount)))\
|
else_=-JournalEntryLineItem.amount)))\
|
||||||
.filter(be(JournalEntryLineItem.original_line_item_id
|
.filter(JournalEntryLineItem.original_line_item_id == form.id.data)
|
||||||
== form.id.data))
|
|
||||||
offset_total: Decimal | None = db.session.scalar(select_offset_total)
|
offset_total: Decimal | None = db.session.scalar(select_offset_total)
|
||||||
if offset_total is not None and field.data < offset_total:
|
if offset_total is not None and field.data < offset_total:
|
||||||
raise ValidationError(lazy_gettext(
|
raise ValidationError(lazy_gettext(
|
||||||
|
@ -24,7 +24,6 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
from accounting.models import Account, JournalEntry, JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.offset_alias import offset_alias
|
from accounting.utils.offset_alias import offset_alias
|
||||||
|
|
||||||
|
|
||||||
@ -45,8 +44,7 @@ def get_selectable_original_line_items(
|
|||||||
offset: sa.Alias = offset_alias()
|
offset: sa.Alias = offset_alias()
|
||||||
net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
|
net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
|
||||||
(offset.c.id.in_(line_item_id_on_form), 0),
|
(offset.c.id.in_(line_item_id_on_form), 0),
|
||||||
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
|
(offset.c.is_debit == JournalEntryLineItem.is_debit, offset.c.amount),
|
||||||
offset.c.amount),
|
|
||||||
else_=-offset.c.amount))).label("net_balance")
|
else_=-offset.c.amount))).label("net_balance")
|
||||||
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
|
conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
|
||||||
sub_conditions: list[sa.BinaryExpression] = []
|
sub_conditions: list[sa.BinaryExpression] = []
|
||||||
@ -60,8 +58,8 @@ def get_selectable_original_line_items(
|
|||||||
select_net_balances: sa.Select \
|
select_net_balances: sa.Select \
|
||||||
= sa.select(JournalEntryLineItem.id, net_balance)\
|
= sa.select(JournalEntryLineItem.id, net_balance)\
|
||||||
.join(Account)\
|
.join(Account)\
|
||||||
.join(offset, be(JournalEntryLineItem.id
|
.join(offset,
|
||||||
== offset.c.original_line_item_id),
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(*conditions)\
|
.filter(*conditions)\
|
||||||
.group_by(JournalEntryLineItem.id)\
|
.group_by(JournalEntryLineItem.id)\
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
import re
|
import re
|
||||||
import typing as t
|
import typing as t
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@ -27,6 +28,7 @@ import sqlalchemy as sa
|
|||||||
from babel import Locale
|
from babel import Locale
|
||||||
from flask_babel import get_locale, get_babel
|
from flask_babel import get_locale, get_babel
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.locale import gettext
|
from accounting.locale import gettext
|
||||||
@ -37,14 +39,14 @@ class BaseAccount(db.Model):
|
|||||||
"""A base account."""
|
"""A base account."""
|
||||||
__tablename__ = "accounting_base_accounts"
|
__tablename__ = "accounting_base_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
code = db.Column(db.String, nullable=False, primary_key=True)
|
code: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The code."""
|
"""The code."""
|
||||||
title_l10n = db.Column("title", db.String, nullable=False)
|
title_l10n: Mapped[str] = mapped_column("title")
|
||||||
"""The title."""
|
"""The title."""
|
||||||
l10n = db.relationship("BaseAccountL10n", back_populates="account",
|
l10n: Mapped[list[BaseAccountL10n]] \
|
||||||
lazy=False)
|
= db.relationship(back_populates="account", lazy=False)
|
||||||
"""The localized titles."""
|
"""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."""
|
"""The descendant accounts under the base account."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -81,17 +83,16 @@ class BaseAccountL10n(db.Model):
|
|||||||
"""A localized base account title."""
|
"""A localized base account title."""
|
||||||
__tablename__ = "accounting_base_accounts_l10n"
|
__tablename__ = "accounting_base_accounts_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
account_code = db.Column(db.String,
|
account_code: Mapped[str] \
|
||||||
db.ForeignKey(BaseAccount.code,
|
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
onupdate="CASCADE",
|
ondelete="CASCADE"),
|
||||||
ondelete="CASCADE"),
|
primary_key=True)
|
||||||
nullable=False, primary_key=True)
|
|
||||||
"""The code of the account."""
|
"""The code of the account."""
|
||||||
account = db.relationship(BaseAccount, back_populates="l10n")
|
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale = db.Column(db.String, nullable=False, primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
title = db.Column(db.String, nullable=False)
|
title: Mapped[str]
|
||||||
"""The localized title."""
|
"""The localized title."""
|
||||||
|
|
||||||
|
|
||||||
@ -99,47 +100,43 @@ class Account(db.Model):
|
|||||||
"""An account."""
|
"""An account."""
|
||||||
__tablename__ = "accounting_accounts"
|
__tablename__ = "accounting_accounts"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
autoincrement=False)
|
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
base_code = db.Column(db.String,
|
base_code: Mapped[str] \
|
||||||
db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"))
|
||||||
nullable=False)
|
|
||||||
"""The code of the base account."""
|
"""The code of the base account."""
|
||||||
base = db.relationship(BaseAccount, back_populates="accounts")
|
base: Mapped[BaseAccount] = db.relationship(back_populates="accounts")
|
||||||
"""The base account."""
|
"""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."""
|
"""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."""
|
"""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."""
|
"""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] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The time of creation."""
|
||||||
created_by_id = db.Column(db.Integer,
|
created_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the creator."""
|
"""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."""
|
"""The creator."""
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
updated_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The time of last update."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the updator."""
|
"""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."""
|
"""The updator."""
|
||||||
l10n = db.relationship("AccountL10n", back_populates="account",
|
l10n: Mapped[list[AccountL10n]] \
|
||||||
lazy=False)
|
= db.relationship(back_populates="account", lazy=False)
|
||||||
"""The localized titles."""
|
"""The localized titles."""
|
||||||
line_items = db.relationship("JournalEntryLineItem",
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
back_populates="account")
|
= db.relationship(back_populates="account")
|
||||||
"""The journal entry line items."""
|
"""The journal entry line items."""
|
||||||
|
|
||||||
CASH_CODE: str = "1111-001"
|
CASH_CODE: str = "1111-001"
|
||||||
@ -352,16 +349,16 @@ class AccountL10n(db.Model):
|
|||||||
"""A localized account title."""
|
"""A localized account title."""
|
||||||
__tablename__ = "accounting_accounts_l10n"
|
__tablename__ = "accounting_accounts_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
account_id = db.Column(db.Integer,
|
account_id: Mapped[int] \
|
||||||
db.ForeignKey(Account.id, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
nullable=False, primary_key=True)
|
primary_key=True)
|
||||||
"""The account ID."""
|
"""The account ID."""
|
||||||
account = db.relationship(Account, back_populates="l10n")
|
account: Mapped[Account] = db.relationship(back_populates="l10n")
|
||||||
"""The account."""
|
"""The account."""
|
||||||
locale = db.Column(db.String, nullable=False, primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
title = db.Column(db.String, nullable=False)
|
title: Mapped[str]
|
||||||
"""The localized title."""
|
"""The localized title."""
|
||||||
|
|
||||||
|
|
||||||
@ -369,35 +366,34 @@ class Currency(db.Model):
|
|||||||
"""A currency."""
|
"""A currency."""
|
||||||
__tablename__ = "accounting_currencies"
|
__tablename__ = "accounting_currencies"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
code = db.Column(db.String, nullable=False, primary_key=True)
|
code: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The code."""
|
"""The code."""
|
||||||
name_l10n = db.Column("name", db.String, nullable=False)
|
name_l10n: Mapped[str] = mapped_column("name")
|
||||||
"""The name."""
|
"""The name."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The time of creation."""
|
||||||
created_by_id = db.Column(db.Integer,
|
created_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the creator."""
|
"""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."""
|
"""The creator."""
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
updated_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The time of last update."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the updator."""
|
"""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."""
|
"""The updator."""
|
||||||
l10n = db.relationship("CurrencyL10n", back_populates="currency",
|
l10n: Mapped[list[CurrencyL10n]] \
|
||||||
lazy=False)
|
= db.relationship(back_populates="currency", lazy=False)
|
||||||
"""The localized names."""
|
"""The localized names."""
|
||||||
line_items = db.relationship("JournalEntryLineItem",
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
back_populates="currency")
|
= db.relationship(back_populates="currency")
|
||||||
"""The journal entry line items."""
|
"""The journal entry line items."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -479,16 +475,16 @@ class CurrencyL10n(db.Model):
|
|||||||
"""A localized currency name."""
|
"""A localized currency name."""
|
||||||
__tablename__ = "accounting_currencies_l10n"
|
__tablename__ = "accounting_currencies_l10n"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
currency_code = db.Column(db.String,
|
currency_code: Mapped[str] \
|
||||||
db.ForeignKey(Currency.code, onupdate="CASCADE",
|
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE",
|
||||||
ondelete="CASCADE"),
|
ondelete="CASCADE"),
|
||||||
nullable=False, primary_key=True)
|
primary_key=True)
|
||||||
"""The currency code."""
|
"""The currency code."""
|
||||||
currency = db.relationship(Currency, back_populates="l10n")
|
currency: Mapped[Currency] = db.relationship(back_populates="l10n")
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
locale = db.Column(db.String, nullable=False, primary_key=True)
|
locale: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The locale."""
|
"""The locale."""
|
||||||
name = db.Column(db.String, nullable=False)
|
name: Mapped[str]
|
||||||
"""The localized name."""
|
"""The localized name."""
|
||||||
|
|
||||||
|
|
||||||
@ -539,37 +535,34 @@ class JournalEntry(db.Model):
|
|||||||
"""A journal entry."""
|
"""A journal entry."""
|
||||||
__tablename__ = "accounting_journal_entries"
|
__tablename__ = "accounting_journal_entries"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
autoincrement=False)
|
|
||||||
"""The journal entry ID."""
|
"""The journal entry ID."""
|
||||||
date = db.Column(db.Date, nullable=False)
|
date: Mapped[dt.date]
|
||||||
"""The 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."""
|
"""The account number under the date."""
|
||||||
note = db.Column(db.String)
|
note: Mapped[str | None]
|
||||||
"""The note."""
|
"""The note."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The time of creation."""
|
||||||
created_by_id = db.Column(db.Integer,
|
created_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the creator."""
|
"""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."""
|
"""The creator."""
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
updated_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The time of last update."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the updator."""
|
"""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."""
|
"""The updator."""
|
||||||
line_items = db.relationship("JournalEntryLineItem",
|
line_items: Mapped[list[JournalEntryLineItem]] \
|
||||||
back_populates="journal_entry")
|
= db.relationship(back_populates="journal_entry")
|
||||||
"""The line items."""
|
"""The line items."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -659,44 +652,39 @@ class JournalEntryLineItem(db.Model):
|
|||||||
"""A line item in the journal entry."""
|
"""A line item in the journal entry."""
|
||||||
__tablename__ = "accounting_journal_entry_line_items"
|
__tablename__ = "accounting_journal_entry_line_items"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
id = db.Column(db.Integer, nullable=False, primary_key=True,
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
|
||||||
autoincrement=False)
|
|
||||||
"""The line item ID."""
|
"""The line item ID."""
|
||||||
journal_entry_id = db.Column(db.Integer,
|
journal_entry_id: Mapped[int] \
|
||||||
db.ForeignKey(JournalEntry.id,
|
= mapped_column(db.ForeignKey(JournalEntry.id, onupdate="CASCADE",
|
||||||
onupdate="CASCADE",
|
ondelete="CASCADE"))
|
||||||
ondelete="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The journal entry ID."""
|
"""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."""
|
"""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."""
|
"""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."""
|
"""The line item number under the journal entry and debit or credit."""
|
||||||
original_line_item_id = db.Column(db.Integer,
|
original_line_item_id: Mapped[int | None] \
|
||||||
db.ForeignKey(id, onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(id, onupdate="CASCADE"))
|
||||||
nullable=True)
|
|
||||||
"""The ID of the original line item."""
|
"""The ID of the original line item."""
|
||||||
original_line_item = db.relationship("JournalEntryLineItem",
|
original_line_item: Mapped[JournalEntryLineItem | None] \
|
||||||
remote_side=id, passive_deletes=True)
|
= db.relationship(remote_side=id, passive_deletes=True)
|
||||||
"""The original line item."""
|
"""The original line item."""
|
||||||
currency_code = db.Column(db.String,
|
currency_code: Mapped[str] \
|
||||||
db.ForeignKey(Currency.code, onupdate="CASCADE"),
|
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE"))
|
||||||
nullable=False)
|
|
||||||
"""The currency code."""
|
"""The currency code."""
|
||||||
currency = db.relationship(Currency, back_populates="line_items")
|
currency: Mapped[Currency] = db.relationship(back_populates="line_items")
|
||||||
"""The currency."""
|
"""The currency."""
|
||||||
account_id = db.Column(db.Integer,
|
account_id: Mapped[int] \
|
||||||
db.ForeignKey(Account.id,
|
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The account ID."""
|
"""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."""
|
"""The account."""
|
||||||
description = db.Column(db.String, nullable=True)
|
description: Mapped[str | None]
|
||||||
"""The description."""
|
"""The description."""
|
||||||
amount = db.Column(db.Numeric(14, 2), nullable=False)
|
amount: Mapped[Decimal] = mapped_column(db.Numeric(14, 2))
|
||||||
"""The amount."""
|
"""The amount."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -891,27 +879,25 @@ class Option(db.Model):
|
|||||||
"""An option."""
|
"""An option."""
|
||||||
__tablename__ = "accounting_options"
|
__tablename__ = "accounting_options"
|
||||||
"""The table name."""
|
"""The table name."""
|
||||||
name = db.Column(db.String, nullable=False, primary_key=True)
|
name: Mapped[str] = mapped_column(primary_key=True)
|
||||||
"""The name."""
|
"""The name."""
|
||||||
value = db.Column(db.Text, nullable=False)
|
value: Mapped[str] = mapped_column(db.Text)
|
||||||
"""The option value."""
|
"""The option value."""
|
||||||
created_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
created_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of creation."""
|
"""The time of creation."""
|
||||||
created_by_id = db.Column(db.Integer,
|
created_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the creator."""
|
"""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."""
|
"""The creator."""
|
||||||
updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
|
updated_at: Mapped[dt.datetime] \
|
||||||
server_default=db.func.now())
|
= mapped_column(db.DateTime(timezone=True),
|
||||||
|
server_default=db.func.now())
|
||||||
"""The time of last update."""
|
"""The time of last update."""
|
||||||
updated_by_id = db.Column(db.Integer,
|
updated_by_id: Mapped[int] \
|
||||||
db.ForeignKey(user_pk_column,
|
= mapped_column(db.ForeignKey(user_pk_column, onupdate="CASCADE"))
|
||||||
onupdate="CASCADE"),
|
|
||||||
nullable=False)
|
|
||||||
"""The ID of the updator."""
|
"""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."""
|
"""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_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.report.utils.urls import income_expenses_url
|
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.current_account import CurrentAccount
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
@ -122,8 +121,7 @@ class LineItemCollector:
|
|||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select: sa.Select = sa.Select(balance_func)\
|
select: sa.Select = sa.Select(balance_func)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
== self.__currency.code),
|
|
||||||
self.__account_condition,
|
self.__account_condition,
|
||||||
JournalEntry.date < self.__period.start)
|
JournalEntry.date < self.__period.start)
|
||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
@ -347,8 +345,7 @@ class PageParams(BasePageParams):
|
|||||||
self.account.id == 0)]
|
self.account.id == 0)]
|
||||||
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
||||||
.join(Account)\
|
.join(Account)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.currency.code,
|
||||||
== self.currency.code),
|
|
||||||
CurrentAccount.sql_condition())\
|
CurrentAccount.sql_condition())\
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
options.extend([OptionLink(str(x),
|
options.extend([OptionLink(str(x),
|
||||||
|
@ -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_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.report.utils.urls import ledger_url
|
from accounting.report.utils.urls import ledger_url
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
@ -118,10 +117,8 @@ class LineItemCollector:
|
|||||||
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
(JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
|
||||||
else_=-JournalEntryLineItem.amount))
|
else_=-JournalEntryLineItem.amount))
|
||||||
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
|
select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.__currency.code,
|
||||||
== self.__currency.code),
|
JournalEntryLineItem.account_id == self.__account.id,
|
||||||
be(JournalEntryLineItem.account_id
|
|
||||||
== self.__account.id),
|
|
||||||
JournalEntry.date < self.__period.start)
|
JournalEntry.date < self.__period.start)
|
||||||
balance: int | None = db.session.scalar(select)
|
balance: int | None = db.session.scalar(select)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
@ -313,8 +310,7 @@ class PageParams(BasePageParams):
|
|||||||
:return: The account options.
|
:return: The account options.
|
||||||
"""
|
"""
|
||||||
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
|
||||||
.filter(be(JournalEntryLineItem.currency_code
|
.filter(JournalEntryLineItem.currency_code == self.currency.code)\
|
||||||
== self.currency.code))\
|
|
||||||
.group_by(JournalEntryLineItem.account_id)
|
.group_by(JournalEntryLineItem.account_id)
|
||||||
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
|
||||||
x.id == self.account.id)
|
x.id == self.account.id)
|
||||||
|
@ -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.csv_export import csv_download
|
||||||
from accounting.report.utils.report_chooser import ReportChooser
|
from accounting.report.utils.report_chooser import ReportChooser
|
||||||
from accounting.report.utils.report_type import ReportType
|
from accounting.report.utils.report_type import ReportType
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.pagination import Pagination
|
from accounting.utils.pagination import Pagination
|
||||||
from accounting.utils.query import parse_query_keywords
|
from accounting.utils.query import parse_query_keywords
|
||||||
from .journal import get_csv_rows
|
from .journal import get_csv_rows
|
||||||
@ -128,9 +127,8 @@ class LineItemCollector:
|
|||||||
journal_entry_date: datetime
|
journal_entry_date: datetime
|
||||||
try:
|
try:
|
||||||
journal_entry_date = datetime.strptime(k, "%Y")
|
journal_entry_date = datetime.strptime(k, "%Y")
|
||||||
conditions.append(
|
conditions.append(sa.extract("year", JournalEntry.date)
|
||||||
be(sa.extract("year", JournalEntry.date)
|
== journal_entry_date.year)
|
||||||
== journal_entry_date.year))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
|
@ -24,7 +24,6 @@ import sqlalchemy as sa
|
|||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from accounting.models import Currency, Account, JournalEntry, \
|
||||||
JournalEntryLineItem
|
JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
|
||||||
from accounting.utils.offset_alias import offset_alias
|
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 \
|
net_balance: sa.Label \
|
||||||
= (JournalEntryLineItem.amount
|
= (JournalEntryLineItem.amount
|
||||||
+ sa.func.sum(sa.case(
|
+ sa.func.sum(sa.case(
|
||||||
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
|
(offset.c.is_debit == JournalEntryLineItem.is_debit,
|
||||||
offset.c.amount),
|
offset.c.amount),
|
||||||
else_=-offset.c.amount))).label("net_balance")
|
else_=-offset.c.amount))).label("net_balance")
|
||||||
select_unapplied: sa.Select \
|
select_unapplied: sa.Select \
|
||||||
= sa.select(JournalEntryLineItem.id)\
|
= sa.select(JournalEntryLineItem.id)\
|
||||||
.join(JournalEntry).join(Account)\
|
.join(JournalEntry).join(Account)\
|
||||||
.join(offset, be(JournalEntryLineItem.id
|
.join(offset,
|
||||||
== offset.c.original_line_item_id),
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True)\
|
isouter=True)\
|
||||||
.filter(Account.is_need_offset,
|
.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.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit)),
|
sa.not_(JournalEntryLineItem.is_debit)),
|
||||||
sa.and_(Account.base_code.startswith("1"),
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
@ -84,17 +83,17 @@ def get_net_balances(currency: Currency, account: Account) \
|
|||||||
net_balance: sa.Label \
|
net_balance: sa.Label \
|
||||||
= (JournalEntryLineItem.amount
|
= (JournalEntryLineItem.amount
|
||||||
+ sa.func.sum(sa.case(
|
+ sa.func.sum(sa.case(
|
||||||
(be(offset.c.is_debit == JournalEntryLineItem.is_debit),
|
(offset.c.is_debit == JournalEntryLineItem.is_debit,
|
||||||
offset.c.amount),
|
offset.c.amount),
|
||||||
else_=-offset.c.amount))).label("net_balance")
|
else_=-offset.c.amount))).label("net_balance")
|
||||||
select_net_balances: sa.Select \
|
select_net_balances: sa.Select \
|
||||||
= sa.select(JournalEntryLineItem.id, net_balance) \
|
= sa.select(JournalEntryLineItem.id, net_balance) \
|
||||||
.join(JournalEntry).join(Account) \
|
.join(JournalEntry).join(Account) \
|
||||||
.join(offset, be(JournalEntryLineItem.id
|
.join(offset,
|
||||||
== offset.c.original_line_item_id),
|
JournalEntryLineItem.id == offset.c.original_line_item_id,
|
||||||
isouter=True) \
|
isouter=True) \
|
||||||
.filter(be(Account.id == account.id),
|
.filter(Account.id == account.id,
|
||||||
be(JournalEntryLineItem.currency_code == currency.code),
|
JournalEntryLineItem.currency_code == currency.code,
|
||||||
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
sa.not_(JournalEntryLineItem.is_debit)),
|
sa.not_(JournalEntryLineItem.is_debit)),
|
||||||
sa.and_(Account.base_code.startswith("1"),
|
sa.and_(Account.base_code.startswith("1"),
|
||||||
|
@ -22,7 +22,6 @@ import sqlalchemy as sa
|
|||||||
from accounting import db
|
from accounting import db
|
||||||
from accounting.models import Currency, Account, JournalEntry, \
|
from accounting.models import Currency, Account, JournalEntry, \
|
||||||
JournalEntryLineItem
|
JournalEntryLineItem
|
||||||
from accounting.utils.cast import be
|
|
||||||
|
|
||||||
|
|
||||||
def get_accounts_with_unmatched(currency: Currency) -> list[Account]:
|
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)\
|
.select_from(Account)\
|
||||||
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
|
.join(JournalEntryLineItem, isouter=True).join(JournalEntry)\
|
||||||
.filter(Account.is_need_offset,
|
.filter(Account.is_need_offset,
|
||||||
be(JournalEntryLineItem.currency_code == currency.code),
|
JournalEntryLineItem.currency_code == currency.code,
|
||||||
JournalEntryLineItem.original_line_item_id.is_(None),
|
JournalEntryLineItem.original_line_item_id.is_(None),
|
||||||
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
sa.or_(sa.and_(Account.base_code.startswith("2"),
|
||||||
JournalEntryLineItem.is_debit),
|
JournalEntryLineItem.is_debit),
|
||||||
|
@ -21,7 +21,7 @@ from accounting.models import Currency
|
|||||||
from accounting.utils.options import options
|
from accounting.utils.options import options
|
||||||
|
|
||||||
|
|
||||||
def currency_options() -> str:
|
def currency_options() -> list[Currency]:
|
||||||
"""Returns the currency options.
|
"""Returns the currency options.
|
||||||
|
|
||||||
:return: The currency options.
|
:return: The currency options.
|
||||||
|
@ -25,16 +25,6 @@ import typing as t
|
|||||||
import sqlalchemy as sa
|
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:
|
def s(message: t.Any) -> str:
|
||||||
"""Casts the LazyString message to the string type.
|
"""Casts the LazyString message to the string type.
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user