41 Commits

Author SHA1 Message Date
ee5b447c23 Renamed the "journal_entry_date" variable to "date" in the "__form" method of the JournalEntryData class in the lib module of the test site. 2023-04-26 13:42:47 +08:00
25bfcf4aa4 Fixed the documentation of the balance pseudo property of the JournalEntryLineItem data model. 2023-04-26 13:40:48 +08:00
5956d2cd4c Renamed the "match" parameter to "value" in the setter of the "match" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:48 +08:00
833285d924 Renamed the "is_offset" parameter to "value" in the setter of the "is_offset" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:43 +08:00
dee4f5e83f Renamed the "balance" parameter to "value" in the setter of the "balance" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:37 +08:00
f0d1cae32d Renamed the "net_balance" parameter to "value" in the setter of the "net_balance" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:25 +08:00
5dc71697b3 Renamed the "credit" parameter to "value" in the setter of the "credit" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:20 +08:00
1bb1e03c08 Renamed the "debit" parameter to "value" in the setter of the "debit" pseudo property of the JournalEntryLineItem data model, for consistency. 2023-04-26 13:40:09 +08:00
914ff92e0f Renamed the "count" parameter to "value" in the setter of the "count" pseudo property of the Account data model, for consistency. 2023-04-26 13:39:56 +08:00
8a1cf463b1 Renamed the "journal_entry_date" parameter to "date" in the constructor of the CSVRow class in the "accounting.report.reports.ledger" module. 2023-04-26 13:34:14 +08:00
d4cf224d6b Renamed the "journal_entry_date" parameter to "date" in the constructor of the CSVRow class in the "accounting.report.reports.income_expenses" module. 2023-04-26 13:33:50 +08:00
8d412ec00a Renamed the "journal_entry_date" parameter to "date" in the show_journal_entry_order route. 2023-04-26 13:32:42 +08:00
2986c518ce Renamed the "journal_entry_date" parameter to "date" in the sort_journal_entries route. 2023-04-26 13:32:30 +08:00
f1351243a6 Renamed the "journal_entry_date" parameter to "date" in the constructor of the JournalEntryReorderForm form. 2023-04-26 13:29:55 +08:00
969e8c76a6 Renamed the "journal_entry_date" parameter to "date" in the "sort_journal_entries_in" function in the "accounting.journal_entry.forms.reorder" module. 2023-04-26 13:29:14 +08:00
10f5e75752 Renamed the "journal_entry_date" variable to "date" in the "test_reorder" test of the JournalEntryReorderTestCase test case. 2023-04-26 13:28:07 +08:00
169b3c292a Renamed the "journal_entry_date" variable to "date" in the "__get_journal_entry_condition" method of the LineItemCollector class in the "accounting.report.reports.search" module. 2023-04-26 13:26:38 +08:00
3eb3aef2f2 Renamed the "j_date" parameter to "date" in the "__next_j_no" method of the BaseTestData class in the lib module of the test site. 2023-04-26 13:24:19 +08:00
6c455a615c Renamed the "j_date" variable to "date" in the "_add_journal_entry" method of the BaseTestData class in the lib module of the test site. 2023-04-26 13:23:53 +08:00
4f3339bf68 Renamed the "j_date" variable to "date" in the "__add_usd_recurring" method of the SampleData class in the reset module of the test site. 2023-04-26 13:23:23 +08:00
b5aa7e923f Renamed the "j_date" variable to "date" in the "_init_data" method of the ReportTestData class in test_report.py. 2023-04-26 13:22:46 +08:00
359c335662 Revised the way to import from the datetime package, to avoid name conflict with the common "date" and "time" names. 2023-04-26 13:17:31 +08:00
c11ae23885 Removed an unused import from the "accounting.utils.cast" module. 2023-04-26 13:15:18 +08:00
e083b11394 Removed the random_pk type alias, because autoincrement=False does not seem to work with it. 2023-04-24 23:29:58 +08:00
167990fc4c Renamed the random_id type alias to random_pk. 2023-04-24 20:22:33 +08:00
d5c1be3d80 Rewrote the data model declaration of the test site with the mapped type hint and the mapped columns in SQLAlchemy 2.0. 2023-04-24 14:02:56 +08:00
f6567794e0 Added the documentation to the authentication blueprint of the test site. 2023-04-24 14:00:32 +08:00
ded85d88f7 Added the timestamp, user_pk, and random_id type aliases to simplify the column definition of the data models. 2023-04-24 03:37:33 +08:00
6d780e9296 Revised the title of the change log. 2023-04-23 22:19:01 +08:00
86637267d3 Advanced to version 1.5.0. 2023-04-23 20:13:34 +08:00
71e97721aa Removed the documentation link from the documentation in intro.rst. It does not make sense for a circular link to itself. 2023-04-23 20:13:33 +08:00
5815608288 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. 2023-04-23 20:13:10 +08:00
5f75d93c6a Simplified README.rst. 2023-04-23 18:42:54 +08:00
118c4b458e Added the change log. 2023-04-23 18:42:42 +08:00
3f7e4c0dda Rewrote the data model declaration with the mapped type hint and the mapped columns in SQLAlchemy 2.0. Added "SQLAlchemy >= 2" to the dependencies. 2023-04-23 13:21:54 +08:00
eed4c923f6 Removed the "be" cast function to cast data type for the binary expressions. It is to be replaced by the mapped type hints. 2023-04-23 13:21:48 +08:00
09dd5ae541 Revised the long line in the JournalEntryConverter converter. 2023-04-23 09:52:21 +08:00
172a12b134 Fixed the type hint of the "currency_options" function. 2023-04-23 09:44:25 +08:00
f3c558f48a Advanced to version 1.4.1. 2023-04-22 18:22:47 +08:00
988757d30e Revised the JavaScript journal entry line item editor to only override the description with the description of the original line item when there is no existing description. 2023-04-20 00:28:28 +08:00
50cea90d1b Revised the JavaScript journal entry line item editor to allow editing the description for offsets and partially-offset original items. 2023-04-20 00:26:58 +08:00
48 changed files with 698 additions and 564 deletions

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@
"""The path converters for the journal entry management.
"""
from datetime import date
import datetime as dt
from flask import abort
from werkzeug.routing import BaseConverter
@ -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
@ -81,18 +82,18 @@ class DateConverter(BaseConverter):
"""The date converter to convert the ISO date from and to the
corresponding date in the routes."""
def to_python(self, value: str) -> date:
def to_python(self, value: str) -> dt.date:
"""Converts an ISO date to a date.
:param value: The ISO date.
:return: The corresponding date.
"""
try:
return date.fromisoformat(value)
return dt.date.fromisoformat(value)
except ValueError:
abort(404)
def to_url(self, value: date) -> str:
def to_url(self, value: dt.date) -> str:
"""Converts a date to its ISO date.
:param value: The date.

View File

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

View File

@ -17,7 +17,7 @@
"""The line item sub-forms for the journal entry management.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
@ -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(
@ -309,7 +307,7 @@ class LineItemForm(FlaskForm):
return getattr(self, "____original_line_item")
@property
def original_line_item_date(self) -> date | None:
def original_line_item_date(self) -> dt.date | None:
"""Returns the text representation of the original line item.
:return: The text representation of the original line item.

View File

@ -17,7 +17,7 @@
"""The reorder forms for the journal entry management.
"""
from datetime import date
import datetime as dt
import sqlalchemy as sa
from flask import request
@ -26,17 +26,15 @@ from accounting import db
from accounting.models import JournalEntry
def sort_journal_entries_in(journal_entry_date: date,
exclude: int | None = None) -> None:
def sort_journal_entries_in(date: dt.date, exclude: int | None = None) -> None:
"""Sorts the journal entries under a date after changing the date or
deleting a journal entry.
:param journal_entry_date: The date of the journal entry.
:param date: The date of the journal entry.
:param exclude: The journal entry ID to exclude.
:return: None.
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.date == journal_entry_date]
conditions: list[sa.BinaryExpression] = [JournalEntry.date == date]
if exclude is not None:
conditions.append(JournalEntry.id != exclude)
journal_entries: list[JournalEntry] = JournalEntry.query\
@ -50,12 +48,12 @@ def sort_journal_entries_in(journal_entry_date: date,
class JournalEntryReorderForm:
"""The form to reorder the journal entries."""
def __init__(self, journal_entry_date: date):
def __init__(self, date: dt.date):
"""Constructs the form to reorder the journal entries in a day.
:param journal_entry_date: The date.
:param date: The date.
"""
self.date: date = journal_entry_date
self.date: dt.date = date
self.is_modified: bool = False
def save_order(self) -> None:

View File

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

View File

@ -17,7 +17,7 @@
"""The views for the journal entry management.
"""
from datetime import date
import datetime as dt
from urllib.parse import parse_qsl, urlencode
import sqlalchemy as sa
@ -30,9 +30,9 @@ from accounting.locale import lazy_gettext
from accounting.models import JournalEntry
from accounting.utils.cast import s
from accounting.utils.flash_errors import flash_form_errors
from accounting.utils.journal_entry_types import JournalEntryType
from accounting.utils.next_uri import inherit_next, or_next
from accounting.utils.permission import has_permission, can_view, can_edit
from accounting.utils.journal_entry_types import JournalEntryType
from accounting.utils.user import get_current_user_pk
from .forms import sort_journal_entries_in, JournalEntryReorderForm
from .template_filters import with_type, to_transfer, format_amount_input, \
@ -67,7 +67,7 @@ def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
form.validate()
else:
form = journal_entry_op.form()
form.date.data = date.today()
form.date.data = dt.date.today()
return journal_entry_op.render_create_template(form)
@ -186,31 +186,31 @@ def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
return redirect(or_next(__get_default_page_uri()))
@bp.get("dates/<date:journal_entry_date>", endpoint="order")
@bp.get("dates/<date:date>", endpoint="order")
@has_permission(can_view)
def show_journal_entry_order(journal_entry_date: date) -> str:
def show_journal_entry_order(date: dt.date) -> str:
"""Shows the order of the journal entries in a same date.
:param journal_entry_date: The date.
:param date: The date.
:return: The order of the journal entries in the date.
"""
journal_entries: list[JournalEntry] = JournalEntry.query \
.filter(JournalEntry.date == journal_entry_date) \
.filter(JournalEntry.date == date) \
.order_by(JournalEntry.no).all()
return render_template("accounting/journal-entry/order.html",
date=journal_entry_date, list=journal_entries)
date=date, list=journal_entries)
@bp.post("dates/<date:journal_entry_date>", endpoint="sort")
@bp.post("dates/<date:date>", endpoint="sort")
@has_permission(can_edit)
def sort_journal_entries(journal_entry_date: date) -> redirect:
def sort_journal_entries(date: dt.date) -> redirect:
"""Reorders the journal entries in a date.
:param journal_entry_date: The date.
:param date: The date.
:return: The redirection to the incoming account or the account list. The
reordering operation does not fail.
"""
form: JournalEntryReorderForm = JournalEntryReorderForm(journal_entry_date)
form: JournalEntryReorderForm = JournalEntryReorderForm(date)
form.save_order()
if not form.is_modified:
flash(s(lazy_gettext("The order was not modified.")), "success")

View File

@ -19,6 +19,7 @@
"""
from __future__ import annotations
import datetime as dt
import re
import typing as t
from decimal import Decimal
@ -27,24 +28,37 @@ 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
from accounting.utils.user import user_cls, user_pk_column
timestamp: t.Type[dt.datetime] \
= t.Annotated[dt.datetime, mapped_column(db.DateTime(timezone=True),
server_default=db.func.now())]
"""The timestamp."""
user_pk: t.Type[int] \
= t.Annotated[int, mapped_column(db.ForeignKey(user_pk_column,
onupdate="CASCADE"))]
"""The user primary key."""
random_pk: t.Type[int] \
= t.Annotated[int, mapped_column(primary_key=True, autoincrement=False)]
"""The random primary key."""
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 +95,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",
ondelete="CASCADE"),
nullable=False, primary_key=True)
account_code: Mapped[str] \
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"),
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 +112,37 @@ 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,
server_default=db.func.now())
created_at: Mapped[timestamp]
"""The time of creation."""
created_by_id = db.Column(db.Integer,
db.ForeignKey(user_pk_column,
onupdate="CASCADE"),
nullable=False)
created_by_id: Mapped[user_pk] = mapped_column()
"""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,
server_default=db.func.now())
updated_at: Mapped[timestamp]
"""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[user_pk] = mapped_column()
"""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"
@ -225,13 +228,13 @@ class Account(db.Model):
return getattr(self, "__count")
@count.setter
def count(self, count: int) -> None:
def count(self, value: int) -> None:
"""Sets the number of items in the account.
:param count: The number of items in the account.
:param value: The number of items in the account.
:return: None.
"""
setattr(self, "__count", count)
setattr(self, "__count", value)
@property
def query_values(self) -> list[str]:
@ -352,16 +355,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",
ondelete="CASCADE"),
nullable=False, primary_key=True)
account_id: Mapped[int] \
= mapped_column(db.ForeignKey(Account.id, onupdate="CASCADE",
ondelete="CASCADE"),
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 +372,28 @@ 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,
server_default=db.func.now())
created_at: Mapped[timestamp]
"""The time of creation."""
created_by_id = db.Column(db.Integer,
db.ForeignKey(user_pk_column,
onupdate="CASCADE"),
nullable=False)
created_by_id: Mapped[user_pk] = mapped_column()
"""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,
server_default=db.func.now())
updated_at: Mapped[timestamp]
"""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[user_pk] = mapped_column()
"""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",
ondelete="CASCADE"),
nullable=False, primary_key=True)
currency_code: Mapped[str] \
= mapped_column(db.ForeignKey(Currency.code, onupdate="CASCADE",
ondelete="CASCADE"),
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,28 @@ 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,
server_default=db.func.now())
created_at: Mapped[timestamp]
"""The time of creation."""
created_by_id = db.Column(db.Integer,
db.ForeignKey(user_pk_column,
onupdate="CASCADE"),
nullable=False)
created_by_id: Mapped[user_pk] = mapped_column()
"""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,
server_default=db.func.now())
updated_at: Mapped[timestamp]
"""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[user_pk] = mapped_column()
"""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 +646,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:
@ -747,13 +729,13 @@ class JournalEntryLineItem(db.Model):
return getattr(self, "__debit")
@debit.setter
def debit(self, debit: Decimal | None) -> None:
def debit(self, value: Decimal | None) -> None:
"""Sets the debit amount.
:param debit: The debit amount.
:param value: The debit amount.
:return: None.
"""
setattr(self, "__debit", debit)
setattr(self, "__debit", value)
@property
def credit(self) -> Decimal | None:
@ -766,13 +748,13 @@ class JournalEntryLineItem(db.Model):
return getattr(self, "__credit")
@credit.setter
def credit(self, credit: Decimal | None) -> None:
def credit(self, value: Decimal | None) -> None:
"""Sets the credit amount.
:param credit: The credit amount.
:param value: The credit amount.
:return: None.
"""
setattr(self, "__credit", credit)
setattr(self, "__credit", value)
@property
def net_balance(self) -> Decimal:
@ -787,32 +769,32 @@ class JournalEntryLineItem(db.Model):
return getattr(self, "__net_balance")
@net_balance.setter
def net_balance(self, net_balance: Decimal) -> None:
def net_balance(self, value: Decimal) -> None:
"""Sets the net balance.
:param net_balance: The net balance.
:param value: The net balance.
:return: None.
"""
setattr(self, "__net_balance", net_balance)
setattr(self, "__net_balance", value)
@property
def balance(self) -> Decimal:
"""Returns the net balance.
"""Returns the balance.
:return: The net balance.
:return: The balance.
"""
if not hasattr(self, "__balance"):
setattr(self, "__balance", Decimal("0"))
return getattr(self, "__balance")
@balance.setter
def balance(self, balance: Decimal) -> None:
"""Sets the net balance.
def balance(self, value: Decimal) -> None:
"""Sets the balance.
:param balance: The net balance.
:param value: The balance.
:return: None.
"""
setattr(self, "__balance", balance)
setattr(self, "__balance", value)
@property
def offsets(self) -> list[t.Self]:
@ -840,14 +822,13 @@ class JournalEntryLineItem(db.Model):
return getattr(self, "__is_offset")
@is_offset.setter
def is_offset(self, is_offset: bool) -> None:
def is_offset(self, value: bool) -> None:
"""Sets whether the line item is an offset.
:param is_offset: True if the line item is an offset, or False
otherwise.
:param value: True if the line item is an offset, or False otherwise.
:return: None.
"""
setattr(self, "__is_offset", is_offset)
setattr(self, "__is_offset", value)
@property
def match(self) -> t.Self | None:
@ -860,13 +841,13 @@ class JournalEntryLineItem(db.Model):
return getattr(self, "__match")
@match.setter
def match(self, match: t.Self) -> None:
def match(self, value: t.Self) -> None:
"""Sets the match of the line item.
:param match: The matcho of the line item.
:param value: The matcho of the line item.
:return: None.
"""
setattr(self, "__match", match)
setattr(self, "__match", value)
@property
def query_values(self) -> list[str]:
@ -891,27 +872,19 @@ 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,
server_default=db.func.now())
created_at: Mapped[timestamp]
"""The time of creation."""
created_by_id = db.Column(db.Integer,
db.ForeignKey(user_pk_column,
onupdate="CASCADE"),
nullable=False)
created_by_id: Mapped[user_pk] = mapped_column()
"""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,
server_default=db.func.now())
updated_at: Mapped[timestamp]
"""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[user_pk] = mapped_column()
"""The ID of the updator."""
updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
updated_by: Mapped[user_cls] = db.relationship(foreign_keys=updated_by_id)
"""The updator."""

View File

@ -20,8 +20,8 @@ This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import datetime as dt
import typing as t
from datetime import date
from accounting.models import JournalEntry
from .period import Period
@ -63,10 +63,10 @@ class PeriodChooser:
first: JournalEntry | None \
= JournalEntry.query.order_by(JournalEntry.date).first()
start: date | None = None if first is None else first.date
start: dt.date | None = None if first is None else first.date
# Attributes
self.data_start: date | None = start
self.data_start: dt.date | None = start
"""The start of the data."""
self.has_data: bool = start is not None
"""Whether there is any data."""
@ -80,8 +80,8 @@ class PeriodChooser:
"""The available years."""
if self.has_data:
today: date = date.today()
self.has_last_month = start < date(today.year, today.month, 1)
today: dt.date = dt.date.today()
self.has_last_month = start < dt.date(today.year, today.month, 1)
self.has_last_year = start.year < today.year
self.has_yesterday = start < today
if start.year < today.year - 1:

View File

@ -17,12 +17,12 @@
"""The period description composer.
"""
from datetime import date, timedelta
import datetime as dt
from accounting.locale import gettext
def get_desc(start: date | None, end: date | None) -> str:
def get_desc(start: dt.date | None, end: dt.date | None) -> str:
"""Returns the period description.
:param start: The start of the period.
@ -46,7 +46,7 @@ def get_desc(start: date | None, end: date | None) -> str:
return __get_day_desc(start, end)
def __get_since_desc(start: date) -> str:
def __get_since_desc(start: dt.date) -> str:
"""Returns the description without the end day.
:param start: The start of the period.
@ -67,7 +67,7 @@ def __get_since_desc(start: date) -> str:
return gettext("since %(start)s", start=get_start_desc())
def __get_until_desc(end: date) -> str:
def __get_until_desc(end: dt.date) -> str:
"""Returns the description without the start day.
:param end: The end of the period.
@ -81,14 +81,14 @@ def __get_until_desc(end: date) -> str:
"""
if end.month == 12 and end.day == 31:
return str(end.year)
if (end + timedelta(days=1)).day == 1:
if (end + dt.timedelta(days=1)).day == 1:
return __format_month(end)
return __format_day(end)
return gettext("until %(end)s", end=get_end_desc())
def __get_year_desc(start: date, end: date) -> str:
def __get_year_desc(start: dt.date, end: dt.date) -> str:
"""Returns the description as a year range.
:param start: The start of the period.
@ -105,7 +105,7 @@ def __get_year_desc(start: date, end: date) -> str:
return __get_from_to_desc(start_text, str(end.year))
def __get_month_desc(start: date, end: date) -> str:
def __get_month_desc(start: dt.date, end: dt.date) -> str:
"""Returns the description as a month range.
:param start: The start of the period.
@ -113,7 +113,7 @@ def __get_month_desc(start: date, end: date) -> str:
:return: The description as a month range.
:raise ValueError: The period is not a month range.
"""
if start.day != 1 or (end + timedelta(days=1)).day != 1:
if start.day != 1 or (end + dt.timedelta(days=1)).day != 1:
raise ValueError
start_text: str = __format_month(start)
if start.year == end.year and start.month == end.month:
@ -123,7 +123,7 @@ def __get_month_desc(start: date, end: date) -> str:
return __get_from_to_desc(start_text, __format_month(end))
def __get_day_desc(start: date, end: date) -> str:
def __get_day_desc(start: dt.date, end: dt.date) -> str:
"""Returns the description as a day range.
:param start: The start of the period.
@ -142,7 +142,7 @@ def __get_day_desc(start: date, end: date) -> str:
return __get_from_to_desc(start_text, __format_day(end))
def __format_month(month: date) -> str:
def __format_month(month: dt.date) -> str:
"""Formats a month.
:param month: The month.
@ -151,7 +151,7 @@ def __format_month(month: date) -> str:
return f"{month.year}/{month.month}"
def __format_day(day: date) -> str:
def __format_day(day: dt.date) -> str:
"""Formats a day.
:param day: The day.

View File

@ -18,14 +18,14 @@
"""
import calendar
from datetime import date
import datetime as dt
def month_end(day: date) -> date:
def month_end(day: dt.date) -> dt.date:
"""Returns the end day of month for a date.
:param day: The date.
:return: The end day of the month of that day.
"""
last_day: int = calendar.monthrange(day.year, day.month)[1]
return date(day.year, day.month, last_day)
return dt.date(day.year, day.month, last_day)

View File

@ -18,9 +18,9 @@
"""
import calendar
import datetime as dt
import re
import typing as t
from datetime import date
from .period import Period
from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
@ -57,7 +57,7 @@ def get_period(spec: str | None = None) -> Period:
return Period(start, end)
def __parse_spec(text: str) -> tuple[date | None, date | None]:
def __parse_spec(text: str) -> tuple[dt.date | None, dt.date | None]:
"""Parses the period specification.
:param text: The period specification.
@ -84,7 +84,7 @@ def __parse_spec(text: str) -> tuple[date | None, date | None]:
raise ValueError
def __get_start(year: str, month: str | None, day: str | None) -> date:
def __get_start(year: str, month: str | None, day: str | None) -> dt.date:
"""Returns the start of the period from the date representation.
:param year: The year.
@ -94,13 +94,13 @@ def __get_start(year: str, month: str | None, day: str | None) -> date:
:raise ValueError: When the date is invalid.
"""
if day is not None:
return date(int(year), int(month), int(day))
return dt.date(int(year), int(month), int(day))
if month is not None:
return date(int(year), int(month), 1)
return date(int(year), 1, 1)
return dt.date(int(year), int(month), 1)
return dt.date(int(year), 1, 1)
def __get_end(year: str, month: str | None, day: str | None) -> date:
def __get_end(year: str, month: str | None, day: str | None) -> dt.date:
"""Returns the end of the period from the date representation.
:param year: The year.
@ -110,10 +110,10 @@ def __get_end(year: str, month: str | None, day: str | None) -> date:
:raise ValueError: When the date is invalid.
"""
if day is not None:
return date(int(year), int(month), int(day))
return dt.date(int(year), int(month), int(day))
if month is not None:
year_n: int = int(year)
month_n: int = int(month)
day_n: int = calendar.monthrange(year_n, month_n)[1]
return date(year_n, month_n, day_n)
return date(int(year), 12, 31)
return dt.date(year_n, month_n, day_n)
return dt.date(int(year), 12, 31)

View File

@ -20,8 +20,8 @@ This file is largely taken from the NanoParma ERP project, first written in
2021/9/16 by imacat (imacat@nanoparma.com).
"""
import datetime as dt
import typing as t
from datetime import date, timedelta
from .description import get_desc
from .month_end import month_end
@ -31,15 +31,15 @@ from .specification import get_spec
class Period:
"""A date period."""
def __init__(self, start: date | None, end: date | None):
def __init__(self, start: dt.date | None, end: dt.date | None):
"""Constructs a new date period.
:param start: The start date, or None from the very beginning.
:param end: The end date, or None till no end.
"""
self.start: date | None = start
self.start: dt.date | None = start
"""The start of the period."""
self.end: date | None = end
self.end: dt.date | None = end
"""The end of the period."""
self.is_default: bool = False
"""Whether the is the default period."""
@ -95,8 +95,8 @@ class Period:
self.is_a_month = self.start.day == 1 \
and self.end == month_end(self.start)
self.is_type_month = self.is_a_month
self.is_a_year = self.start == date(self.start.year, 1, 1) \
and self.end == date(self.start.year, 12, 31)
self.is_a_year = self.start == dt.date(self.start.year, 1, 1) \
and self.end == dt.date(self.start.year, 12, 31)
self.is_a_day = self.start == self.end
def is_year(self, year: int) -> bool:
@ -126,4 +126,4 @@ class Period:
"""
if self.start is None:
return None
return Period(None, self.start - timedelta(days=1))
return Period(None, self.start - dt.timedelta(days=1))

View File

@ -17,7 +17,7 @@
"""The named shortcut periods.
"""
from datetime import date, timedelta
import datetime as dt
from accounting.locale import gettext
from .month_end import month_end
@ -27,8 +27,8 @@ from .period import Period
class ThisMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
this_month_start: date = date(today.year, today.month, 1)
today: dt.date = dt.date.today()
this_month_start: dt.date = dt.date(today.year, today.month, 1)
super().__init__(this_month_start, month_end(today))
self.is_default = True
self.is_this_month = True
@ -43,13 +43,13 @@ class ThisMonth(Period):
class LastMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
today: dt.date = dt.date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: date = date(year, month, 1)
start: dt.date = dt.date(year, month, 1)
super().__init__(start, month_end(start))
self.is_last_month = True
@ -63,13 +63,13 @@ class LastMonth(Period):
class SinceLastMonth(Period):
"""The period of this month."""
def __init__(self):
today: date = date.today()
today: dt.date = dt.date.today()
year: int = today.year
month: int = today.month - 1
if month < 1:
year = year - 1
month = 12
start: date = date(year, month, 1)
start: dt.date = dt.date(year, month, 1)
super().__init__(start, None)
self.is_since_last_month = True
@ -82,9 +82,9 @@ class SinceLastMonth(Period):
class ThisYear(Period):
"""The period of this year."""
def __init__(self):
year: int = date.today().year
start: date = date(year, 1, 1)
end: date = date(year, 12, 31)
year: int = dt.date.today().year
start: dt.date = dt.date(year, 1, 1)
end: dt.date = dt.date(year, 12, 31)
super().__init__(start, end)
self.is_this_year = True
@ -97,9 +97,9 @@ class ThisYear(Period):
class LastYear(Period):
"""The period of last year."""
def __init__(self):
year: int = date.today().year
start: date = date(year - 1, 1, 1)
end: date = date(year - 1, 12, 31)
year: int = dt.date.today().year
start: dt.date = dt.date(year - 1, 1, 1)
end: dt.date = dt.date(year - 1, 12, 31)
super().__init__(start, end)
self.is_last_year = True
@ -112,7 +112,7 @@ class LastYear(Period):
class Today(Period):
"""The period of today."""
def __init__(self):
today: date = date.today()
today: dt.date = dt.date.today()
super().__init__(today, today)
self.is_today = True
@ -125,7 +125,7 @@ class Today(Period):
class Yesterday(Period):
"""The period of yesterday."""
def __init__(self):
yesterday: date = date.today() - timedelta(days=1)
yesterday: dt.date = dt.date.today() - dt.timedelta(days=1)
super().__init__(yesterday, yesterday)
self.is_yesterday = True
@ -163,6 +163,6 @@ class YearPeriod(Period):
:param year: The year.
"""
start: date = date(year, 1, 1)
end: date = date(year, 12, 31)
start: dt.date = dt.date(year, 1, 1)
end: dt.date = dt.date(year, 12, 31)
super().__init__(start, end)

View File

@ -17,10 +17,10 @@
"""The period specification composer.
"""
from datetime import date, timedelta
import datetime as dt
def get_spec(start: date | None, end: date | None) -> str:
def get_spec(start: dt.date | None, end: dt.date | None) -> str:
"""Returns the period specification.
:param start: The start of the period.
@ -44,7 +44,7 @@ def get_spec(start: date | None, end: date | None) -> str:
return __get_day_spec(start, end)
def __get_since_spec(start: date) -> str:
def __get_since_spec(start: dt.date) -> str:
"""Returns the period specification without the end day.
:param start: The start of the period.
@ -57,7 +57,7 @@ def __get_since_spec(start: date) -> str:
return start.strftime("%Y-%m-%d-")
def __get_until_spec(end: date) -> str:
def __get_until_spec(end: dt.date) -> str:
"""Returns the period specification without the start day.
:param end: The end of the period.
@ -65,12 +65,12 @@ def __get_until_spec(end: date) -> str:
"""
if end.month == 12 and end.day == 31:
return end.strftime("-%Y")
if (end + timedelta(days=1)).day == 1:
if (end + dt.timedelta(days=1)).day == 1:
return end.strftime("-%Y-%m")
return end.strftime("-%Y-%m-%d")
def __get_year_spec(start: date, end: date) -> str:
def __get_year_spec(start: dt.date, end: dt.date) -> str:
"""Returns the period specification as a year range.
:param start: The start of the period.
@ -88,7 +88,7 @@ def __get_year_spec(start: date, end: date) -> str:
return f"{start_spec}-{end_spec}"
def __get_month_spec(start: date, end: date) -> str:
def __get_month_spec(start: dt.date, end: dt.date) -> str:
"""Returns the period specification as a month range.
:param start: The start of the period.
@ -96,7 +96,7 @@ def __get_month_spec(start: date, end: date) -> str:
:return: The period specification as a month range.
:raise ValueError: The period is not a month range.
"""
if start.day != 1 or (end + timedelta(days=1)).day != 1:
if start.day != 1 or (end + dt.timedelta(days=1)).day != 1:
raise ValueError
start_spec: str = start.strftime("%Y-%m")
if start.year == end.year and start.month == end.month:
@ -105,7 +105,7 @@ def __get_month_spec(start: date, end: date) -> str:
return f"{start_spec}-{end_spec}"
def __get_day_spec(start: date, end: date) -> str:
def __get_day_spec(start: dt.date, end: dt.date) -> str:
"""Returns the period specification as a day range.
:param start: The start of the period.

View File

@ -17,7 +17,7 @@
"""The income and expenses log.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
@ -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
@ -54,7 +53,7 @@ class ReportLineItem:
"""Whether this is the brought-forward line item."""
self.is_total: bool = False
"""Whether this is the total line item."""
self.date: date | None = None
self.date: dt.date | None = None
"""The date."""
self.account: Account | None = None
"""The account."""
@ -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)
@ -215,7 +213,7 @@ class LineItemCollector:
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: date | str | None,
def __init__(self, date: dt.date | str | None,
account: str | None,
description: str | None,
income: str | Decimal | None,
@ -224,7 +222,7 @@ class CSVRow(BaseCSVRow):
note: str | None):
"""Constructs a row in the CSV.
:param journal_entry_date: The journal entry date.
:param date: The journal entry date.
:param account: The account.
:param description: The description.
:param income: The income.
@ -232,7 +230,7 @@ class CSVRow(BaseCSVRow):
:param balance: The balance.
:param note: The note.
"""
self.date: date | str | None = journal_entry_date
self.date: dt.date | str | None = date
"""The date."""
self.account: str | None = account
"""The account."""
@ -347,8 +345,7 @@ class PageParams(BasePageParams):
self.account.id == 0)]
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.join(Account)\
.filter(be(JournalEntryLineItem.currency_code
== self.currency.code),
.filter(JournalEntryLineItem.currency_code == self.currency.code,
CurrentAccount.sql_condition())\
.group_by(JournalEntryLineItem.account_id)
options.extend([OptionLink(str(x),

View File

@ -17,7 +17,7 @@
"""The journal.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
@ -67,7 +67,7 @@ class ReportLineItem:
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | date,
def __init__(self, journal_entry_date: str | dt.date,
currency: str,
account: str,
description: str | None,
@ -84,7 +84,7 @@ class CSVRow(BaseCSVRow):
:param credit: The credit amount.
:param note: The note.
"""
self.date: str | date = journal_entry_date
self.date: str | dt.date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""

View File

@ -17,7 +17,7 @@
"""The ledger.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
@ -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
@ -53,7 +52,7 @@ class ReportLineItem:
"""Whether this is the brought-forward line item."""
self.is_total: bool = False
"""Whether this is the total line item."""
self.date: date | None = None
self.date: dt.date | None = None
"""The date."""
self.description: str | None = None
"""The description."""
@ -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:
@ -199,7 +196,7 @@ class LineItemCollector:
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: date | str | None,
def __init__(self, date: dt.date | str | None,
description: str | None,
debit: str | Decimal | None,
credit: str | Decimal | None,
@ -207,14 +204,14 @@ class CSVRow(BaseCSVRow):
note: str | None):
"""Constructs a row in the CSV.
:param journal_entry_date: The journal entry date.
:param date: The journal entry date.
:param description: The description.
:param debit: The debit amount.
:param credit: The credit amount.
:param balance: The balance.
:param note: The note.
"""
self.date: date | str | None = journal_entry_date
self.date: dt.date | str | None = date
"""The date."""
self.description: str | None = description
"""The description."""
@ -313,8 +310,7 @@ class PageParams(BasePageParams):
:return: The account options.
"""
in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
.filter(be(JournalEntryLineItem.currency_code
== self.currency.code))\
.filter(JournalEntryLineItem.currency_code == self.currency.code)\
.group_by(JournalEntryLineItem.account_id)
return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
x.id == self.account.id)

View File

@ -17,7 +17,7 @@
"""The search.
"""
from datetime import datetime
import datetime as dt
from decimal import Decimal
import sqlalchemy as sa
@ -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
@ -125,41 +124,33 @@ class LineItemCollector:
"""
conditions: list[sa.BinaryExpression] \
= [JournalEntry.note.icontains(k)]
journal_entry_date: datetime
date: dt.datetime
try:
journal_entry_date = datetime.strptime(k, "%Y")
date = dt.datetime.strptime(k, "%Y")
conditions.append(
be(sa.extract("year", JournalEntry.date)
== journal_entry_date.year))
sa.extract("year", JournalEntry.date) == date.year)
except ValueError:
pass
try:
journal_entry_date = datetime.strptime(k, "%Y/%m")
date = dt.datetime.strptime(k, "%Y/%m")
conditions.append(sa.and_(
sa.extract("year", JournalEntry.date)
== journal_entry_date.year,
sa.extract("month", JournalEntry.date)
== journal_entry_date.month))
sa.extract("year", JournalEntry.date) == date.year,
sa.extract("month", JournalEntry.date) == date.month))
except ValueError:
pass
try:
journal_entry_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
date = dt.datetime.strptime(f"2000/{k}", "%Y/%m/%d")
conditions.append(sa.and_(
sa.extract("month", JournalEntry.date)
== journal_entry_date.month,
sa.extract("day", JournalEntry.date)
== journal_entry_date.day))
sa.extract("month", JournalEntry.date) == date.month,
sa.extract("day", JournalEntry.date) == date.day))
except ValueError:
pass
try:
journal_entry_date = datetime.strptime(k, "%Y/%m/%d")
date = dt.datetime.strptime(k, "%Y/%m/%d")
conditions.append(sa.and_(
sa.extract("year", JournalEntry.date)
== journal_entry_date.year,
sa.extract("month", JournalEntry.date)
== journal_entry_date.month,
sa.extract("day", JournalEntry.date)
== journal_entry_date.day))
sa.extract("year", JournalEntry.date) == date.year,
sa.extract("month", JournalEntry.date) == date.month,
sa.extract("day", JournalEntry.date) == date.day))
except ValueError:
pass
return sa.select(JournalEntry.id).filter(sa.or_(*conditions))

View File

@ -17,7 +17,7 @@
"""The unapplied original line items.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
@ -41,7 +41,7 @@ from accounting.utils.pagination import Pagination
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | date, currency: str,
def __init__(self, journal_entry_date: str | dt.date, currency: str,
description: str | None, amount: str | Decimal,
net_balance: str | Decimal):
"""Constructs a row in the CSV.
@ -52,7 +52,7 @@ class CSVRow(BaseCSVRow):
:param amount: The amount.
:param net_balance: The net balance.
"""
self.date: str | date = journal_entry_date
self.date: str | dt.date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
@ -64,7 +64,7 @@ class CSVRow(BaseCSVRow):
"""The net balance."""
@property
def values(self) -> list[str | date | Decimal | None]:
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.

View File

@ -17,7 +17,7 @@
"""The accounts with unapplied original line items.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
@ -49,7 +49,7 @@ class CSVRow(BaseCSVRow):
"""The number of unapplied original line items."""
@property
def values(self) -> list[str | date | Decimal | None]:
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.

View File

@ -17,7 +17,7 @@
"""The unmatched offsets.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
@ -40,7 +40,7 @@ from accounting.utils.pagination import Pagination
class CSVRow(BaseCSVRow):
"""A row in the CSV."""
def __init__(self, journal_entry_date: str | date, currency: str,
def __init__(self, journal_entry_date: str | dt.date, currency: str,
description: str | None, debit: str | Decimal,
credit: str | Decimal, balance: str | Decimal):
"""Constructs a row in the CSV.
@ -52,7 +52,7 @@ class CSVRow(BaseCSVRow):
:param credit: The credit amount.
:param balance: The balance.
"""
self.date: str | date = journal_entry_date
self.date: str | dt.date = journal_entry_date
"""The date."""
self.currency: str = currency
"""The currency."""
@ -66,7 +66,7 @@ class CSVRow(BaseCSVRow):
"""The balance."""
@property
def values(self) -> list[str | date | Decimal | None]:
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.

View File

@ -17,7 +17,7 @@
"""The accounts with unmatched offsets.
"""
from datetime import date
import datetime as dt
from decimal import Decimal
from flask import render_template, Response
@ -49,7 +49,7 @@ class CSVRow(BaseCSVRow):
"""The number of unapplied original line items."""
@property
def values(self) -> list[str | date | Decimal | None]:
def values(self) -> list[str | dt.date | Decimal | None]:
"""Returns the values of the row.
:return: The values of the row.

View File

@ -18,8 +18,8 @@
"""
import csv
import datetime as dt
from abc import ABC, abstractmethod
from datetime import timedelta, date
from decimal import Decimal
from io import StringIO
from urllib.parse import quote
@ -77,7 +77,7 @@ def period_spec(period: Period) -> str:
return f"{start}-{end}"
def __get_start_str(start: date | None) -> str | None:
def __get_start_str(start: dt.date | None) -> str | None:
"""Returns the string representation of the start date.
:param start: The start date.
@ -93,7 +93,7 @@ def __get_start_str(start: date | None) -> str | None:
return start.strftime("%Y%m%d")
def __get_end_str(end: date | None) -> str | None:
def __get_end_str(end: dt.date | None) -> str | None:
"""Returns the string representation of the end date.
:param end: The end date.
@ -104,6 +104,6 @@ def __get_end_str(end: date | None) -> str | None:
return None
if end.month == 12 and end.day == 31:
return str(end.year)
if (end + timedelta(days=1)).day == 1:
if (end + dt.timedelta(days=1)).day == 1:
return end.strftime("%Y%m")
return end.strftime("%Y%m%d")

View File

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

View File

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

View File

@ -277,13 +277,16 @@ class JournalEntryLineItemEditor {
this.originalLineItemText = originalLineItem.text;
this.#originalLineItemText.innerText = originalLineItem.text;
this.#setEnableDescriptionAccount(false);
if (originalLineItem.description === "") {
this.#descriptionControl.classList.remove("accounting-not-empty");
} else {
this.#descriptionControl.classList.add("accounting-not-empty");
if (this.description === null) {
if (originalLineItem.description === "") {
this.#descriptionControl.classList.remove("accounting-not-empty");
} else {
this.#descriptionControl.classList.add("accounting-not-empty");
}
this.description = originalLineItem.description === ""? null: originalLineItem.description;
this.#descriptionText.innerText = originalLineItem.description;
}
this.description = originalLineItem.description === ""? null: originalLineItem.description;
this.#descriptionText.innerText = originalLineItem.description;
this.#setEnableAccount(false);
this.#accountControl.classList.add("accounting-not-empty");
this.account = originalLineItem.account.copy();
this.isAccountConfirmed = false;
@ -305,7 +308,7 @@ class JournalEntryLineItemEditor {
this.originalLineItemDate = null;
this.originalLineItemText = null;
this.#originalLineItemText.innerText = "";
this.#setEnableDescriptionAccount(true);
this.#setEnableAccount(true);
this.#accountControl.classList.remove("accounting-not-empty");
this.account = null;
this.isAccountConfirmed = false;
@ -472,12 +475,13 @@ class JournalEntryLineItemEditor {
this.originalLineItemDate = null;
this.originalLineItemText = null;
this.#originalLineItemText.innerText = "";
this.#setEnableDescriptionAccount(true);
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#descriptionControl.classList.remove("accounting-not-empty");
this.#descriptionControl.classList.remove("is-invalid");
this.description = null;
this.#descriptionText.innerText = ""
this.#descriptionError.innerText = ""
this.#setEnableAccount(true);
this.#accountControl.classList.remove("accounting-not-empty");
this.#accountControl.classList.remove("is-invalid");
this.account = null;
@ -511,7 +515,7 @@ class JournalEntryLineItemEditor {
this.#originalLineItemContainer.classList.remove("d-none");
this.#originalLineItemControl.classList.add("accounting-not-empty");
}
this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.description = lineItem.description;
if (this.description === null) {
this.#descriptionControl.classList.remove("accounting-not-empty");
@ -519,6 +523,7 @@ class JournalEntryLineItemEditor {
this.#descriptionControl.classList.add("accounting-not-empty");
}
this.#descriptionText.innerText = this.description === null? "": this.description;
this.#setEnableAccount(!lineItem.isMatched && this.originalLineItemId === null);
this.account = lineItem.account;
this.isAccountConfirmed = true;
if (this.account === null) {
@ -547,25 +552,17 @@ class JournalEntryLineItemEditor {
}
/**
* Sets the enable status of the description and account.
* Sets the enable status of the account.
*
* @param isEnabled {boolean} true to enable, or false otherwise
*/
#setEnableDescriptionAccount(isEnabled) {
#setEnableAccount(isEnabled) {
if (isEnabled) {
this.#descriptionControl.dataset.bsToggle = "modal";
this.#descriptionControl.dataset.bsTarget = `#accounting-description-editor-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#descriptionControl.classList.remove("accounting-disabled");
this.#descriptionControl.classList.add("accounting-clickable");
this.#accountControl.dataset.bsToggle = "modal";
this.#accountControl.dataset.bsTarget = `#accounting-account-selector-${this.#debitCreditSubForm.debitCredit}-modal`;
this.#accountControl.classList.remove("accounting-disabled");
this.#accountControl.classList.add("accounting-clickable");
} else {
this.#descriptionControl.dataset.bsToggle = "";
this.#descriptionControl.dataset.bsTarget = "";
this.#descriptionControl.classList.add("accounting-disabled");
this.#descriptionControl.classList.remove("accounting-clickable");
this.#accountControl.dataset.bsToggle = "";
this.#accountControl.dataset.bsTarget = "";
this.#accountControl.classList.add("accounting-disabled");

View File

@ -17,8 +17,8 @@
"""The template filters.
"""
import datetime as dt
import typing as t
from datetime import date, timedelta
from decimal import Decimal
from flask_babel import get_locale
@ -41,24 +41,24 @@ def format_amount(value: Decimal | None) -> str | None:
return "{:,}".format(whole) + str(abs(frac))[1:]
def format_date(value: date) -> str:
def format_date(value: dt.date) -> str:
"""Formats a date to be human-friendly.
:param value: The date.
:return: The human-friendly date text.
"""
today: date = date.today()
today: dt.date = dt.date.today()
if value == today:
return gettext("Today")
if value == today - timedelta(days=1):
if value == today - dt.timedelta(days=1):
return gettext("Yesterday")
if value == today + timedelta(days=1):
if value == today + dt.timedelta(days=1):
return gettext("Tomorrow")
locale = str(get_locale())
if locale == "zh" or locale.startswith("zh_"):
if value == today - timedelta(days=2):
if value == today - dt.timedelta(days=2):
return gettext("The day before yesterday")
if value == today + timedelta(days=2):
if value == today + dt.timedelta(days=2):
return gettext("The day after tomorrow")
if locale == "zh" or locale.startswith("zh_"):
weekdays = ["", "", "", "", "", "", ""]

View File

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

View File

@ -36,7 +36,7 @@ First written: 2023/2/26
{{ A_("Edit") }}
</a>
{% endif %}
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.order", journal_entry_date=obj.date)|accounting_append_next }}">
<a class="btn btn-primary" role="button" href="{{ url_for("accounting.journal-entry.order", date=obj.date)|accounting_append_next }}">
<i class="fa-solid fa-bars-staggered"></i>
<span class="d-none d-md-inline">{{ A_("Order") }}</span>
</a>

View File

@ -38,7 +38,7 @@ First written: 2023/2/26
</div>
{% if list|length > 1 and accounting_can_edit() %}
<form action="{{ url_for("accounting.journal-entry.sort", journal_entry_date=date) }}" method="post">
<form action="{{ url_for("accounting.journal-entry.sort", date=date) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if request.args.next %}
<input type="hidden" name="next" value="{{ request.args.next }}">

View File

@ -22,18 +22,6 @@ This module should not import any other module from the application.
"""
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.

View File

@ -17,8 +17,8 @@
"""The test for the account management.
"""
import datetime as dt
import unittest
from datetime import timedelta, date
import httpx
from flask import Flask
@ -461,7 +461,7 @@ class AccountTestCase(unittest.TestCase):
account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account)
account.created_at \
= account.created_at - timedelta(seconds=5)
= account.created_at - dt.timedelta(seconds=5)
account.updated_at = account.created_at
db.session.commit()
@ -592,7 +592,7 @@ class AccountTestCase(unittest.TestCase):
add_journal_entry(self.client,
form={"csrf_token": self.csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"date": dt.date.today().isoformat(),
"currency-1-code": "USD",
"currency-1-credit-1-account_code": BANK.code,
"currency-1-credit-1-amount": "20"})

View File

@ -17,8 +17,8 @@
"""The test for the currency management.
"""
import datetime as dt
import unittest
from datetime import timedelta, date
import httpx
from flask import Flask
@ -384,7 +384,7 @@ class CurrencyTestCase(unittest.TestCase):
currency = db.session.get(Currency, USD.code)
self.assertIsNotNone(currency)
currency.created_at \
= currency.created_at - timedelta(seconds=5)
= currency.created_at - dt.timedelta(seconds=5)
currency.updated_at = currency.created_at
db.session.commit()
@ -534,7 +534,7 @@ class CurrencyTestCase(unittest.TestCase):
add_journal_entry(self.client,
form={"csrf_token": self.csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"date": dt.date.today().isoformat(),
"currency-1-code": EUR.code,
"currency-1-credit-1-account_code": "1111-001",
"currency-1-credit-1-amount": "20"})

View File

@ -17,8 +17,8 @@
"""The test for the description editor.
"""
import datetime as dt
import unittest
from datetime import date
from flask import Flask
@ -149,7 +149,7 @@ def get_form_data(csrf_token: str) -> list[dict[str, str]]:
:param csrf_token: The CSRF token.
:return: A list of the form data.
"""
journal_entry_date: str = date.today().isoformat()
journal_entry_date: str = dt.date.today().isoformat()
return [{"csrf_token": csrf_token,
"next": NEXT_URI,
"date": journal_entry_date,

View File

@ -17,8 +17,8 @@
"""The test for the journal entry management.
"""
import datetime as dt
import unittest
from datetime import date, timedelta
from decimal import Decimal
import httpx
@ -500,7 +500,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
journal_entry = db.session.get(JournalEntry, journal_entry_id)
self.assertIsNotNone(journal_entry)
journal_entry.created_at \
= journal_entry.created_at - timedelta(seconds=5)
= journal_entry.created_at - dt.timedelta(seconds=5)
journal_entry.updated_at = journal_entry.created_at
db.session.commit()
@ -576,7 +576,7 @@ class CashReceiptJournalEntryTestCase(unittest.TestCase):
self.client,
form={"csrf_token": self.csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"date": dt.date.today().isoformat(),
"currency-1-code": line_item.currency_code,
"currency-1-debit-1-original_line_item_id": line_item.id,
"currency-1-debit-1-account_code": line_item.account_code,
@ -1112,7 +1112,7 @@ class CashDisbursementJournalEntryTestCase(unittest.TestCase):
journal_entry = db.session.get(JournalEntry, journal_entry_id)
self.assertIsNotNone(journal_entry)
journal_entry.created_at \
= journal_entry.created_at - timedelta(seconds=5)
= journal_entry.created_at - dt.timedelta(seconds=5)
journal_entry.updated_at = journal_entry.created_at
db.session.commit()
@ -1773,7 +1773,7 @@ class TransferJournalEntryTestCase(unittest.TestCase):
journal_entry = db.session.get(JournalEntry, journal_entry_id)
self.assertIsNotNone(journal_entry)
journal_entry.created_at \
= journal_entry.created_at - timedelta(seconds=5)
= journal_entry.created_at - dt.timedelta(seconds=5)
journal_entry.updated_at = journal_entry.created_at
db.session.commit()
@ -2124,9 +2124,9 @@ class JournalEntryReorderTestCase(unittest.TestCase):
with self.app.app_context():
journal_entry_1: JournalEntry = db.session.get(JournalEntry, id_1)
journal_entry_date_2: date = journal_entry_1.date
journal_entry_date_1: date \
= journal_entry_date_2 - timedelta(days=1)
journal_entry_date_2: dt.date = journal_entry_1.date
journal_entry_date_1: dt.date \
= journal_entry_date_2 - dt.timedelta(days=1)
journal_entry_1.date = journal_entry_date_1
journal_entry_1.no = 3
journal_entry_2: JournalEntry = db.session.get(JournalEntry, id_2)
@ -2176,10 +2176,10 @@ class JournalEntryReorderTestCase(unittest.TestCase):
self.__get_add_disbursement_form())
with self.app.app_context():
journal_entry_date: date = db.session.get(JournalEntry, id_1).date
date: dt.date = db.session.get(JournalEntry, id_1).date
response = self.client.post(
f"{PREFIX}/dates/{journal_entry_date.isoformat()}",
f"{PREFIX}/dates/{date.isoformat()}",
data={"csrf_token": self.csrf_token,
"next": "/next",
f"{id_1}-no": "4",
@ -2207,7 +2207,7 @@ class JournalEntryReorderTestCase(unittest.TestCase):
db.session.commit()
response = self.client.post(
f"{PREFIX}/dates/{journal_entry_date.isoformat()}",
f"{PREFIX}/dates/{date.isoformat()}",
data={"csrf_token": self.csrf_token,
"next": "/next",
f"{id_2}-no": "3a",

View File

@ -17,8 +17,8 @@
"""The test for the options.
"""
import datetime as dt
import unittest
from datetime import datetime, timedelta
import httpx
from flask import Flask
@ -286,7 +286,8 @@ class OptionTestCase(unittest.TestCase):
with self.app.app_context():
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
timestamp: datetime = option.created_at - timedelta(seconds=5)
timestamp: dt.datetime \
= option.created_at - dt.timedelta(seconds=5)
option.created_at = timestamp
option.updated_at = timestamp
db.session.commit()

View File

@ -17,8 +17,8 @@
"""The test for the reports.
"""
import datetime as dt
import unittest
from datetime import date
import httpx
from flask import Flask
@ -446,15 +446,15 @@ class ReportTestData(BaseTestData):
"""The report test data."""
def _init_data(self) -> None:
today: date = date.today()
today: dt.date = dt.date.today()
year: int = today.year - 5
month: int = today.month
while True:
j_date: date = date(year, month, 5)
if j_date > today:
date: dt.date = dt.date(year, month, 5)
if date > today:
break
self._add_simple_journal_entry(
(j_date - today).days, "USD",
(date - today).days, "USD",
"Salary薪水", "1200", Accounts.BANK, Accounts.SERVICE)
month = month + 1
if month > 12:

View File

@ -21,20 +21,21 @@ import typing as t
from flask import Blueprint, render_template, Flask, redirect, url_for, \
session, request, g, Response, abort
from sqlalchemy.orm import Mapped, mapped_column
from . import db
bp: Blueprint = Blueprint("auth", __name__, url_prefix="/")
"""The authentication blueprint."""
class User(db.Model):
"""A user."""
__tablename__ = "users"
"""The table name."""
id = db.Column(db.Integer, nullable=False, primary_key=True,
autoincrement=True)
"""The ID"""
username = db.Column(db.String, nullable=False, unique=True)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
"""The ID."""
username: Mapped[str] = mapped_column(unique=True)
"""The username."""
def __str__(self) -> str:

View File

@ -19,9 +19,9 @@
"""
from __future__ import annotations
import datetime as dt
import typing as t
from abc import ABC, abstractmethod
from datetime import date, timedelta
from decimal import Decimal
from secrets import randbelow
@ -167,10 +167,10 @@ class JournalEntryData:
:param is_update: True for an update operation, or False otherwise
:return: The journal entry as a form.
"""
journal_entry_date: date = date.today() - timedelta(days=self.days)
date: dt.date = dt.date.today() - dt.timedelta(days=self.days)
form: dict[str, str] = {"csrf_token": csrf_token,
"next": next_uri,
"date": journal_entry_date.isoformat()}
"date": date.isoformat()}
for i in range(len(self.currencies)):
form.update(self.currencies[i].form(i + 1, is_update))
if self.note is not None:
@ -240,11 +240,12 @@ class BaseTestData(ABC):
existing_j_id: set[int] = {x["id"] for x in self.__journal_entries}
existing_l_id: set[int] = {x["id"] for x in self.__line_items}
journal_entry_data.id = self.__new_id(existing_j_id)
j_date: date = date.today() - timedelta(days=journal_entry_data.days)
date: dt.date \
= dt.date.today() - dt.timedelta(days=journal_entry_data.days)
self.__journal_entries.append(
{"id": journal_entry_data.id,
"date": j_date,
"no": self.__next_j_no(j_date),
"date": date,
"no": self.__next_j_no(date),
"note": journal_entry_data.note,
"created_by_id": self.__current_user_id,
"updated_by_id": self.__current_user_id})
@ -303,14 +304,14 @@ class BaseTestData(ABC):
existing_id.add(obj_id)
return obj_id
def __next_j_no(self, j_date: date) -> int:
def __next_j_no(self, date: dt.date) -> int:
"""Returns the next journal entry number in a day.
:param j_date: The journal entry date.
:param date: The journal entry date.
:return: The next journal entry number.
"""
existing: set[int] = {x["no"] for x in self.__journal_entries
if x["date"] == j_date}
if x["date"] == date}
return 1 if len(existing) == 0 else max(existing) + 1
def _add_simple_journal_entry(

View File

@ -17,7 +17,7 @@
"""The data reset for the Mia! Accounting demonstration website.
"""
from datetime import date, timedelta
import datetime as dt
from flask import Flask, Blueprint, url_for, flash, redirect, session, \
render_template, current_app
@ -116,15 +116,15 @@ class SampleData(BaseTestData):
:return: None.
"""
today: date = date.today()
today: dt.date = dt.date.today()
days: int
year: int
month: int
# Recurring in USD
j_date: date = date(today.year - 5, today.month, today.day)
j_date = j_date + timedelta(days=(4 - j_date.weekday()))
days = (today - j_date).days
date: dt.date = dt.date(today.year - 5, today.month, today.day)
date = date + dt.timedelta(days=(4 - date.weekday()))
days = (today - date).days
while True:
if days < 0:
break
@ -147,7 +147,7 @@ class SampleData(BaseTestData):
if month > 12:
year = year + 1
month = 1
days = (today - date(year, month, 1)).days
days = (today - dt.date(year, month, 1)).days
if days < 0:
break
self.__add_journal_entry(
@ -159,12 +159,12 @@ class SampleData(BaseTestData):
:return: None.
"""
today: date = date.today()
today: dt.date = dt.date.today()
year: int = today.year - 5
month: int = today.month
while True:
days: int = (today - date(year, month, 5)).days
days: int = (today - dt.date(year, month, 5)).days
if days < 0:
break
self.__add_journal_entry(

View File

@ -17,8 +17,8 @@
"""The common test libraries for the journal entry test cases.
"""
import datetime as dt
import re
from datetime import date
from decimal import Decimal
from secrets import randbelow
@ -41,7 +41,7 @@ def get_add_form(csrf_token: str) -> dict[str, str]:
"""
return {"csrf_token": csrf_token,
"next": NEXT_URI,
"date": date.today().isoformat(),
"date": dt.date.today().isoformat(),
"currency-0-code": "USD",
"currency-0-debit-0-no": "16",
"currency-0-debit-0-account_code": Accounts.CASH,